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()