Mercurial > code > home > repos > light9
comparison bin/attic/effecteval @ 2376:4556eebe5d73
topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author | drewp@bigasterisk.com |
---|---|
date | Sun, 12 May 2024 19:02:10 -0700 |
parents | bin/effecteval@9aa046cc9b33 |
children |
comparison
equal
deleted
inserted
replaced
2375:623836db99af | 2376:4556eebe5d73 |
---|---|
1 #!bin/python | |
2 | |
3 from run_local import log | |
4 from twisted.internet import reactor | |
5 from twisted.internet.defer import inlineCallbacks, returnValue | |
6 import cyclone.web, cyclone.websocket, cyclone.httpclient | |
7 import sys, optparse, logging, json, itertools | |
8 from rdflib import URIRef, Literal | |
9 | |
10 from light9 import networking, showconfig | |
11 from light9.effecteval.effect import EffectNode | |
12 from light9.effect.edit import getMusicStatus, songNotePatch | |
13 from light9.effecteval.effectloop import makeEffectLoop | |
14 from light9.metrics import metrics, metricsRoute | |
15 from light9.namespaces import L9 | |
16 from rdfdb.patch import Patch | |
17 from rdfdb.syncedgraph import SyncedGraph | |
18 | |
19 from cycloneerr import PrettyErrorHandler | |
20 from light9.coffee import StaticCoffee | |
21 | |
22 | |
23 | |
24 class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler): | |
25 | |
26 def get(self): | |
27 self.set_header('Content-Type', 'text/html') | |
28 self.write(open("light9/effecteval/effect.html").read()) | |
29 | |
30 def delete(self): | |
31 graph = self.settings.graph | |
32 uri = URIRef(self.get_argument('uri')) | |
33 with graph.currentState(tripleFilter=(None, L9['effect'], uri)) as g: | |
34 song = ctx = list(g.subjects(L9['effect'], uri))[0] | |
35 self.settings.graph.patch( | |
36 Patch(delQuads=[ | |
37 (song, L9['effect'], uri, ctx), | |
38 ])) | |
39 | |
40 | |
41 @inlineCallbacks | |
42 def currentSong(): | |
43 s = (yield getMusicStatus())['song'] | |
44 if s is None: | |
45 raise ValueError("no current song") | |
46 returnValue(URIRef(s)) | |
47 | |
48 | |
49 class SongEffects(PrettyErrorHandler, cyclone.web.RequestHandler): | |
50 | |
51 def wideOpenCors(self): | |
52 self.set_header('Access-Control-Allow-Origin', '*') | |
53 self.set_header('Access-Control-Allow-Methods', | |
54 'GET, PUT, POST, DELETE, OPTIONS') | |
55 self.set_header('Access-Control-Max-Age', '1000') | |
56 self.set_header('Access-Control-Allow-Headers', | |
57 'Content-Type, Authorization, X-Requested-With') | |
58 | |
59 def options(self): | |
60 self.wideOpenCors() | |
61 self.write('') | |
62 | |
63 @inlineCallbacks | |
64 def post(self): | |
65 self.wideOpenCors() | |
66 dropped = URIRef(self.get_argument('drop')) | |
67 | |
68 try: | |
69 song = URIRef(self.get_argument('uri')) | |
70 except Exception: # which? | |
71 song = yield currentSong() | |
72 | |
73 event = self.get_argument('event', default='default') | |
74 | |
75 note = self.get_argument('note', default=None) | |
76 if note is not None: | |
77 note = URIRef(note) | |
78 | |
79 log.info("adding to %s", song) | |
80 note, p = yield songNotePatch(self.settings.graph, | |
81 dropped, | |
82 song, | |
83 event, | |
84 ctx=song, | |
85 note=note) | |
86 | |
87 self.settings.graph.patch(p) | |
88 self.settings.graph.suggestPrefixes(song, {'song': URIRef(song + '/')}) | |
89 self.write(json.dumps({'note': note})) | |
90 | |
91 | |
92 class SongEffectsUpdates(cyclone.websocket.WebSocketHandler): | |
93 | |
94 def connectionMade(self, *args, **kwargs): | |
95 self.graph = self.settings.graph | |
96 self.graph.addHandler(self.updateClient) | |
97 | |
98 def updateClient(self): | |
99 # todo: abort if client is gone | |
100 playlist = self.graph.value(showconfig.showUri(), L9['playList']) | |
101 songs = list(self.graph.items(playlist)) | |
102 out = [] | |
103 for s in songs: | |
104 out.append({'uri': s, 'label': self.graph.label(s), 'effects': []}) | |
105 seen = set() | |
106 for n in self.graph.objects(s, L9['note']): | |
107 for uri in self.graph.objects(n, L9['effectClass']): | |
108 if uri in seen: | |
109 continue | |
110 seen.add(uri) | |
111 out[-1]['effects'].append({ | |
112 'uri': uri, | |
113 'label': self.graph.label(uri) | |
114 }) | |
115 out[-1]['effects'].sort(key=lambda e: e['uri']) | |
116 | |
117 self.sendMessage({'songs': out}) | |
118 | |
119 | |
120 class EffectUpdates(cyclone.websocket.WebSocketHandler): | |
121 """ | |
122 stays alive for the life of the effect page | |
123 """ | |
124 | |
125 def connectionMade(self, *args, **kwargs): | |
126 log.info("websocket opened") | |
127 self.uri = URIRef(self.get_argument('uri')) | |
128 self.sendMessage({'hello': repr(self)}) | |
129 | |
130 self.graph = self.settings.graph | |
131 self.graph.addHandler(self.updateClient) | |
132 | |
133 def updateClient(self): | |
134 # todo: if client has dropped, abort and don't get any more | |
135 # graph updates | |
136 | |
137 # EffectNode knows how to put them in order. Somehow this is | |
138 # not triggering an update when the order changes. | |
139 en = EffectNode(self.graph, self.uri) | |
140 codeLines = [c.code for c in en.codes] | |
141 self.sendMessage({'codeLines': codeLines}) | |
142 | |
143 def connectionLost(self, reason): | |
144 log.info("websocket closed") | |
145 | |
146 def messageReceived(self, message): | |
147 log.info("got message %s" % message) | |
148 # write a patch back to the graph | |
149 | |
150 | |
151 def replaceObjects(graph, c, s, p, newObjs): | |
152 patch = graph.getObjectPatch(context=c, | |
153 subject=s, | |
154 predicate=p, | |
155 newObject=newObjs[0]) | |
156 | |
157 moreAdds = [] | |
158 for line in newObjs[1:]: | |
159 moreAdds.append((s, p, line, c)) | |
160 fullPatch = Patch(delQuads=patch.delQuads, | |
161 addQuads=patch.addQuads + moreAdds) | |
162 graph.patch(fullPatch) | |
163 | |
164 | |
165 class Code(PrettyErrorHandler, cyclone.web.RequestHandler): | |
166 | |
167 def put(self): | |
168 effect = URIRef(self.get_argument('uri')) | |
169 codeLines = [] | |
170 for i in itertools.count(0): | |
171 k = 'codeLines[%s][text]' % i | |
172 v = self.get_argument(k, None) | |
173 if v is not None: | |
174 codeLines.append(Literal(v)) | |
175 else: | |
176 break | |
177 if not codeLines: | |
178 log.info("no codelines received on PUT /code") | |
179 return | |
180 with self.settings.graph.currentState(tripleFilter=(None, L9['effect'], | |
181 effect)) as g: | |
182 song = next(g.subjects(L9['effect'], effect)) | |
183 | |
184 replaceObjects(self.settings.graph, song, effect, L9['code'], codeLines) | |
185 | |
186 # right here we could tell if the code has a python error and return it | |
187 self.send_error(202) | |
188 | |
189 | |
190 class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler): | |
191 | |
192 @inlineCallbacks | |
193 def get(self): | |
194 # return dmx list for that effect | |
195 uri = URIRef(self.get_argument('uri')) | |
196 response = yield cyclone.httpclient.fetch( | |
197 networking.musicPlayer.path('time')) | |
198 songTime = json.loads(response.body)['t'] | |
199 | |
200 node = EffectNode(self.settings.graph, uri) | |
201 outSub = node.eval(songTime) | |
202 self.write(json.dumps(outSub.get_dmx_list())) | |
203 | |
204 | |
205 # Completely not sure where the effect background loop should | |
206 # go. Another process could own it, and get this request repeatedly: | |
207 class SongEffectsEval(PrettyErrorHandler, cyclone.web.RequestHandler): | |
208 | |
209 def get(self): | |
210 song = URIRef(self.get_argument('song')) | |
211 effects = effectsForSong(self.settings.graph, song) # noqa | |
212 raise NotImplementedError | |
213 self.write(maxDict(effectDmxDict(e) for e in effects)) # noqa | |
214 # return dmx dict for all effects in the song, already combined | |
215 | |
216 | |
217 class App(object): | |
218 | |
219 def __init__(self, show, outputWhere): | |
220 self.show = show | |
221 self.outputWhere = outputWhere | |
222 self.graph = SyncedGraph(networking.rdfdb.url, "effectEval") | |
223 self.graph.initiallySynced.addCallback(self.launch).addErrback( | |
224 log.error) | |
225 | |
226 def launch(self, *args): | |
227 log.info('launch') | |
228 if self.outputWhere: | |
229 self.loop = makeEffectLoop(self.graph, self.outputWhere) | |
230 self.loop.startLoop() | |
231 | |
232 SFH = cyclone.web.StaticFileHandler | |
233 self.cycloneApp = cyclone.web.Application(handlers=[ | |
234 (r'/()', SFH, { | |
235 'path': 'light9/effecteval', | |
236 'default_filename': 'index.html' | |
237 }), | |
238 (r'/effect', EffectEdit), | |
239 (r'/effect\.js', StaticCoffee, { | |
240 'src': 'light9/effecteval/effect.coffee' | |
241 }), | |
242 (r'/(effect-components\.html)', SFH, { | |
243 'path': 'light9/effecteval' | |
244 }), | |
245 (r'/effectUpdates', EffectUpdates), | |
246 (r'/code', Code), | |
247 (r'/songEffectsUpdates', SongEffectsUpdates), | |
248 (r'/effect/eval', EffectEval), | |
249 (r'/songEffects', SongEffects), | |
250 (r'/songEffects/eval', SongEffectsEval), | |
251 metricsRoute(), | |
252 ], | |
253 debug=True, | |
254 graph=self.graph) | |
255 reactor.listenTCP(networking.effectEval.port, self.cycloneApp) | |
256 log.info("listening on %s" % networking.effectEval.port) | |
257 | |
258 | |
259 if __name__ == "__main__": | |
260 parser = optparse.OptionParser() | |
261 parser.add_option( | |
262 '--show', | |
263 help='show URI, like http://light9.bigasterisk.com/show/dance2008', | |
264 default=showconfig.showUri()) | |
265 parser.add_option("-v", | |
266 "--verbose", | |
267 action="store_true", | |
268 help="logging.DEBUG") | |
269 parser.add_option("--twistedlog", | |
270 action="store_true", | |
271 help="twisted logging") | |
272 parser.add_option("--output", metavar="WHERE", help="dmx or leds") | |
273 (options, args) = parser.parse_args() | |
274 log.setLevel(logging.DEBUG if options.verbose else logging.INFO) | |
275 | |
276 if not options.show: | |
277 raise ValueError("missing --show http://...") | |
278 | |
279 app = App(URIRef(options.show), options.output) | |
280 if options.twistedlog: | |
281 from twisted.python import log as twlog | |
282 twlog.startLogging(sys.stderr) | |
283 reactor.run() |