Mercurial > code > home > repos > light9
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/effecteval Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,283 @@ +#!bin/python + +from run_local import log +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, returnValue +import cyclone.web, cyclone.websocket, cyclone.httpclient +import sys, optparse, logging, json, itertools +from rdflib import URIRef, Literal + +from light9 import networking, showconfig +from light9.effecteval.effect import EffectNode +from light9.effect.edit import getMusicStatus, songNotePatch +from light9.effecteval.effectloop import makeEffectLoop +from light9.metrics import metrics, metricsRoute +from light9.namespaces import L9 +from rdfdb.patch import Patch +from rdfdb.syncedgraph import SyncedGraph + +from cycloneerr import PrettyErrorHandler +from light9.coffee import StaticCoffee + + + +class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + self.set_header('Content-Type', 'text/html') + self.write(open("light9/effecteval/effect.html").read()) + + def delete(self): + graph = self.settings.graph + uri = URIRef(self.get_argument('uri')) + with graph.currentState(tripleFilter=(None, L9['effect'], uri)) as g: + song = ctx = list(g.subjects(L9['effect'], uri))[0] + self.settings.graph.patch( + Patch(delQuads=[ + (song, L9['effect'], uri, ctx), + ])) + + +@inlineCallbacks +def currentSong(): + s = (yield getMusicStatus())['song'] + if s is None: + raise ValueError("no current song") + returnValue(URIRef(s)) + + +class SongEffects(PrettyErrorHandler, cyclone.web.RequestHandler): + + def wideOpenCors(self): + self.set_header('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Methods', + 'GET, PUT, POST, DELETE, OPTIONS') + self.set_header('Access-Control-Max-Age', '1000') + self.set_header('Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Requested-With') + + def options(self): + self.wideOpenCors() + self.write('') + + @inlineCallbacks + def post(self): + self.wideOpenCors() + dropped = URIRef(self.get_argument('drop')) + + try: + song = URIRef(self.get_argument('uri')) + except Exception: # which? + song = yield currentSong() + + event = self.get_argument('event', default='default') + + note = self.get_argument('note', default=None) + if note is not None: + note = URIRef(note) + + log.info("adding to %s", song) + note, p = yield songNotePatch(self.settings.graph, + dropped, + song, + event, + ctx=song, + note=note) + + self.settings.graph.patch(p) + self.settings.graph.suggestPrefixes(song, {'song': URIRef(song + '/')}) + self.write(json.dumps({'note': note})) + + +class SongEffectsUpdates(cyclone.websocket.WebSocketHandler): + + def connectionMade(self, *args, **kwargs): + self.graph = self.settings.graph + self.graph.addHandler(self.updateClient) + + def updateClient(self): + # todo: abort if client is gone + playlist = self.graph.value(showconfig.showUri(), L9['playList']) + songs = list(self.graph.items(playlist)) + out = [] + for s in songs: + out.append({'uri': s, 'label': self.graph.label(s), 'effects': []}) + seen = set() + for n in self.graph.objects(s, L9['note']): + for uri in self.graph.objects(n, L9['effectClass']): + if uri in seen: + continue + seen.add(uri) + out[-1]['effects'].append({ + 'uri': uri, + 'label': self.graph.label(uri) + }) + out[-1]['effects'].sort(key=lambda e: e['uri']) + + self.sendMessage({'songs': out}) + + +class EffectUpdates(cyclone.websocket.WebSocketHandler): + """ + stays alive for the life of the effect page + """ + + def connectionMade(self, *args, **kwargs): + log.info("websocket opened") + self.uri = URIRef(self.get_argument('uri')) + self.sendMessage({'hello': repr(self)}) + + self.graph = self.settings.graph + self.graph.addHandler(self.updateClient) + + def updateClient(self): + # todo: if client has dropped, abort and don't get any more + # graph updates + + # EffectNode knows how to put them in order. Somehow this is + # not triggering an update when the order changes. + en = EffectNode(self.graph, self.uri) + codeLines = [c.code for c in en.codes] + self.sendMessage({'codeLines': codeLines}) + + def connectionLost(self, reason): + log.info("websocket closed") + + def messageReceived(self, message): + log.info("got message %s" % message) + # write a patch back to the graph + + +def replaceObjects(graph, c, s, p, newObjs): + patch = graph.getObjectPatch(context=c, + subject=s, + predicate=p, + newObject=newObjs[0]) + + moreAdds = [] + for line in newObjs[1:]: + moreAdds.append((s, p, line, c)) + fullPatch = Patch(delQuads=patch.delQuads, + addQuads=patch.addQuads + moreAdds) + graph.patch(fullPatch) + + +class Code(PrettyErrorHandler, cyclone.web.RequestHandler): + + def put(self): + effect = URIRef(self.get_argument('uri')) + codeLines = [] + for i in itertools.count(0): + k = 'codeLines[%s][text]' % i + v = self.get_argument(k, None) + if v is not None: + codeLines.append(Literal(v)) + else: + break + if not codeLines: + log.info("no codelines received on PUT /code") + return + with self.settings.graph.currentState(tripleFilter=(None, L9['effect'], + effect)) as g: + song = next(g.subjects(L9['effect'], effect)) + + replaceObjects(self.settings.graph, song, effect, L9['code'], codeLines) + + # right here we could tell if the code has a python error and return it + self.send_error(202) + + +class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler): + + @inlineCallbacks + def get(self): + # return dmx list for that effect + uri = URIRef(self.get_argument('uri')) + response = yield cyclone.httpclient.fetch( + networking.musicPlayer.path('time')) + songTime = json.loads(response.body)['t'] + + node = EffectNode(self.settings.graph, uri) + outSub = node.eval(songTime) + self.write(json.dumps(outSub.get_dmx_list())) + + +# Completely not sure where the effect background loop should +# go. Another process could own it, and get this request repeatedly: +class SongEffectsEval(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + song = URIRef(self.get_argument('song')) + effects = effectsForSong(self.settings.graph, song) # noqa + raise NotImplementedError + self.write(maxDict(effectDmxDict(e) for e in effects)) # noqa + # return dmx dict for all effects in the song, already combined + + +class App(object): + + def __init__(self, show, outputWhere): + self.show = show + self.outputWhere = outputWhere + self.graph = SyncedGraph(networking.rdfdb.url, "effectEval") + self.graph.initiallySynced.addCallback(self.launch).addErrback( + log.error) + + def launch(self, *args): + log.info('launch') + if self.outputWhere: + self.loop = makeEffectLoop(self.graph, self.outputWhere) + self.loop.startLoop() + + SFH = cyclone.web.StaticFileHandler + self.cycloneApp = cyclone.web.Application(handlers=[ + (r'/()', SFH, { + 'path': 'light9/effecteval', + 'default_filename': 'index.html' + }), + (r'/effect', EffectEdit), + (r'/effect\.js', StaticCoffee, { + 'src': 'light9/effecteval/effect.coffee' + }), + (r'/(effect-components\.html)', SFH, { + 'path': 'light9/effecteval' + }), + (r'/effectUpdates', EffectUpdates), + (r'/code', Code), + (r'/songEffectsUpdates', SongEffectsUpdates), + (r'/effect/eval', EffectEval), + (r'/songEffects', SongEffects), + (r'/songEffects/eval', SongEffectsEval), + metricsRoute(), + ], + debug=True, + graph=self.graph) + reactor.listenTCP(networking.effectEval.port, self.cycloneApp) + log.info("listening on %s" % networking.effectEval.port) + + +if __name__ == "__main__": + parser = optparse.OptionParser() + parser.add_option( + '--show', + help='show URI, like http://light9.bigasterisk.com/show/dance2008', + default=showconfig.showUri()) + parser.add_option("-v", + "--verbose", + action="store_true", + help="logging.DEBUG") + parser.add_option("--twistedlog", + action="store_true", + help="twisted logging") + parser.add_option("--output", metavar="WHERE", help="dmx or leds") + (options, args) = parser.parse_args() + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + if not options.show: + raise ValueError("missing --show http://...") + + app = App(URIRef(options.show), options.output) + if options.twistedlog: + from twisted.python import log as twlog + twlog.startLogging(sys.stderr) + reactor.run()