Files @ f066d6e874db
Branch filter:

Location: light9/bin/effecteval

drewp@bigasterisk.com
2to3 with these fixers: all idioms set_literal
Ignore-this: cbd28518218c2f0ddce8c4f92d3b8b33
#!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, subprocess, json, itertools
from rdflib import URIRef, Literal

sys.path.append('/usr/lib/pymodules/python2.7/')  # for numpy, on rpi
sys.path.append('/usr/lib/python2.7/dist-packages')  # For numpy
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.greplin_cyclone import StatsForCyclone
from light9.namespaces import L9
from rdfdb.patch import Patch
from rdfdb.syncedgraph import SyncedGraph
from greplin import scales

from lib.cycloneerr import PrettyErrorHandler


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)})
            out[-1]['effects'] = [{
                'uri': uri,
                'label': self.graph.label(uri)
            } for uri in sorted(self.graph.objects(s, L9['effect']))]
        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)
        raise NotImplementedError
        self.write(maxDict(effectDmxDict(e) for e in effects))
        # 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)

        self.stats = scales.collection(
            '/',
            scales.PmfStat('sendLevels'),
            scales.PmfStat('getMusic'),
            scales.PmfStat('evals'),
            scales.PmfStat('sendOutput'),
            scales.IntStat('errors'),
        )

    def launch(self, *args):
        log.info('launch')
        if self.outputWhere:
            self.loop = makeEffectLoop(self.graph, self.stats, 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),
            (r'/stats', StatsForCyclone),
        ],
                                                  debug=True,
                                                  graph=self.graph,
                                                  stats=self.stats)
        reactor.listenTCP(networking.effectEval.port, self.cycloneApp)
        log.info("listening on %s" % networking.effectEval.port)


class StaticCoffee(PrettyErrorHandler, cyclone.web.RequestHandler):

    def initialize(self, src):
        super(StaticCoffee, self).initialize()
        self.src = src

    def get(self):
        self.set_header('Content-Type', 'application/javascript')
        self.write(
            subprocess.check_output(
                ['/usr/bin/coffee', '--compile', '--print', self.src]))


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