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