Changeset - 3d58c1c78f1f
[Not reviewed]
default
0 3 1
drewp@bigasterisk.com - 20 months ago 2023-05-31 20:12:06
drewp@bigasterisk.com
port ascoltami from cyclone to starlette
4 files changed with 106 insertions and 115 deletions:
0 comments (0 inline, 0 general)
bin/ascoltami
Show inline comments
 
#!/bin/zsh
 
pnpm exec vite -c light9/ascoltami/vite.config.ts &
 
pdm run python light9/ascoltami/main.py
 
pdm run uvicorn light9.ascoltami.main:app --host 0.0.0.0 --port 8206 --no-access-log
 
wait
light9/ascoltami/main.py
Show inline comments
 
@@ -5,24 +5,29 @@ import sys
 
from typing import cast
 

	
 
import gi
 
from light9.run_local import log
 
from rdflib import URIRef
 
from starlette.applications import Starlette
 
from starlette.routing import Route
 
from starlette_exporter import PrometheusMiddleware, handle_metrics
 
from twisted.internet import reactor
 
from twisted.internet.interfaces import IReactorCore
 

	
 
from light9.run_local import log
 

	
 
gi.require_version('Gst', '1.0')
 
gi.require_version('Gtk', '3.0')
 

	
 
from gi.repository import Gst # type: ignore
 

	
 
from light9 import networking, showconfig
 
from light9.ascoltami import webapp
 
from light9.ascoltami.player import Player
 
from light9.ascoltami.playlist import NoSuchSong, Playlist
 
from light9.ascoltami.webapp import makeWebApp, songLocation, songUri
 

	
 
reactor = cast(IReactorCore, reactor)
 

	
 

	
 
class App:
 
class Ascoltami:
 

	
 
    def __init__(self, graph, show):
 
        self.graph = graph
 
@@ -34,35 +39,45 @@ class App:
 
        self.player.pause()
 
        self.player.seek(0)
 

	
 
        thisSongUri = songUri(graph, URIRef(song))
 
        thisSongUri = webapp.songUri(self.graph, URIRef(song))
 

	
 
        try:
 
            nextSong = self.playlist.nextSong(thisSongUri)
 
        except NoSuchSong:  # we're at the end of the playlist
 
            return
 

	
 
        self.player.setSong(songLocation(graph, nextSong), play=False)
 
        self.player.setSong(webapp.songLocation(self.graph, nextSong), play=False)
 

	
 

	
 
if __name__ == "__main__":
 
def main():
 
    Gst.init(None)
 

	
 
    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")
 
    (options, args) = parser.parse_args()
 

	
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 
    graph = showconfig.getGraph()
 
    asco = Ascoltami(graph, showconfig.showUri())
 

	
 
    if not options.show:
 
        raise ValueError("missing --show http://...")
 
    app = Starlette(
 
        debug=True,
 
        routes=[
 
            Route("/config", webapp.get_config),
 
            Route("/time", webapp.get_time, methods=["GET"]),
 
            Route("/time", webapp.post_time, methods=["POST"]),
 
            Route("/time/stream", webapp.timeStream),
 
            Route("/song", webapp.post_song, methods=["POST"]),
 
            Route("/songs", webapp.get_songs),
 
            Route("/seekPlayOrPause", webapp.post_seekPlayOrPause),
 
            Route("/output", webapp.post_output, methods=["POST"]),
 
            Route("/go", webapp.post_goButton, methods=["POST"]),
 
        ],
 
    )
 

	
 
    graph = showconfig.getGraph()
 
    app = App(graph, URIRef(options.show))
 
    if options.twistedlog:
 
        from twisted.python import log as twlog
 
        twlog.startLogging(sys.stderr)
 
    reactor.listenTCP(networking.musicPlayer.port, makeWebApp(app))
 
    log.info("listening on %s" % networking.musicPlayer.port)
 
    reactor.run()
 
    app.add_middleware(PrometheusMiddleware)
 
    app.add_route("/metrics", handle_metrics)
 

	
 
    app.state.graph = graph
 
    app.state.show = asco.show
 
    app.state.player = asco.player
 

	
 
    return app
 

	
 

	
 
app = main()
light9/ascoltami/main_test.py
Show inline comments
 
new file 100644
 

	
 
from light9.run_local import log
 

	
 

	
 
def test_import():
 
    import light9.ascoltami.main
 
    
 
\ No newline at end of file
light9/ascoltami/webapp.py
Show inline comments
 
import asyncio
 
import json
 
import logging
 
import socket
 
@@ -5,15 +6,14 @@ import subprocess
 
import time
 
from typing import cast
 

	
 
import cyclone.web
 
import cyclone.websocket
 
from cycloneerr import PrettyErrorHandler
 
from light9.metrics import metricsRoute
 
from rdflib import RDFS, Graph, URIRef
 
from light9.ascoltami.player import Player
 
from sse_starlette.sse import EventSourceResponse
 
from starlette.requests import Request
 
from starlette.responses import JSONResponse, PlainTextResponse
 

	
 
from light9.namespaces import L9
 
from light9.showconfig import getSongsFromShow, showUri, songOnDisk
 
from rdflib import RDFS, Graph, URIRef
 
from twisted.internet import reactor
 
from twisted.internet.interfaces import IReactorTime
 

	
 
log = logging.getLogger()
 
_songUris = {}  # locationUri : song
 
@@ -29,12 +29,8 @@ def songUri(graph, locationUri):
 
    return _songUris[locationUri]
 

	
 

	
 
class config(cyclone.web.RequestHandler):
 

	
 
    def get(self):
 
        self.set_header("Content-Type", "application/json")
 
        self.write(
 
            json.dumps(
 
async def get_config(request: Request) -> JSONResponse:
 
    return JSONResponse(
 
                dict(
 
                    host=socket.gethostname(),
 
                    show=str(showUri()),
 
@@ -42,7 +38,7 @@ class config(cyclone.web.RequestHandler)
 
                        # these are just for the web display. True values are on Player.__init__
 
                        'intro': 4,
 
                        'post': 0
 
                    })))
 
            }))
 

	
 

	
 
def playerSongUri(graph, player):
 
@@ -74,95 +70,86 @@ def currentState(graph, player):
 
    }
 

	
 

	
 
class timeResource(PrettyErrorHandler, cyclone.web.RequestHandler):
 
async def get_time(request: Request) -> JSONResponse:
 
    player = cast(Player, request.app.state.player)
 
    graph = cast(Graph, request.app.state.graph)
 
    return JSONResponse(currentState(graph, player))
 

	
 
    def get(self):
 
        player = self.settings.app.player
 
        graph = self.settings.app.graph
 
        self.set_header("Content-Type", "application/json")
 
        self.write(json.dumps(currentState(graph, player)))
 

	
 
    def post(self):
 
async def post_time(request: Request) -> PlainTextResponse:
 
        """
 
        post a json object with {pause: true} or {resume: true} if you
 
        want those actions. Use {t: <seconds>} to seek, optionally
 
        with a pause/resume command too.
 
        """
 
        params = json.loads(self.request.body)
 
        player = self.settings.app.player
 
    params = await request.json()
 
    player = cast(Player, request.app.state.player)
 
        if params.get('pause', False):
 
            player.pause()
 
        if params.get('resume', False):
 
            player.resume()
 
        if 't' in params:
 
            player.seek(params['t'])
 
        self.set_header("Content-Type", "text/plain")
 
        self.write("ok")
 
    return PlainTextResponse("ok")
 

	
 

	
 
class timeStreamResource(cyclone.websocket.WebSocketHandler):
 
async def timeStream(request: Request):
 
    graph = cast(Graph, request.app.state.graph)
 
    player = cast(Player, request.app.state.player)
 
    async def event_generator():
 
        last_sent = None
 
        last_sent_time = 0.0
 

	
 
    def connectionMade(self, *args, **kwargs) -> None:
 
        self.lastSent = None
 
        self.lastSentTime = 0.
 
        self.loop()
 

	
 
    def loop(self):
 
        while True:
 
        now = time.time()
 
        msg = currentState(self.settings.app.graph, self.settings.app.player)
 
        if msg != self.lastSent or now > self.lastSentTime + 2:
 
            # self.sendMessage(json.dumps(msg))
 
            self.lastSent = msg
 
            self.lastSentTime = now
 
            msg = currentState(graph, player)
 
            if msg != last_sent or now > last_sent_time + 2:
 
                event_data = json.dumps(msg)
 
                yield event_data
 
                last_sent = msg
 
                last_sent_time = now
 

	
 
        if self.transport.connected:
 
            cast(IReactorTime, reactor).callLater(.2, self.loop) # type: ignore
 
            await asyncio.sleep(0.2)
 

	
 
    def connectionLost(self, reason):
 
        log.info("bye ws client %r: %s", self, reason)
 
    return EventSourceResponse(event_generator())
 

	
 

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

	
 
    def get(self):
 
        graph = cast(Graph, self.settings.app.graph)
 
async def get_songs(request: Request) -> JSONResponse:
 
    graph = cast(Graph, request.app.state.graph)
 

	
 
        songs = getSongsFromShow(graph, self.settings.app.show)
 
    songs = getSongsFromShow(graph, request.app.state.show)
 

	
 
        self.set_header("Content-Type", "application/json")
 
        self.write(json.dumps({
 
            "songs": [
 
    songs_data = [
 
                {  #
 
                    "uri": s,
 
                    "path": graph.value(s, L9['songFilename']),
 
                    "label": graph.value(s, RDFS.label)
 
                } for s in songs
 
            ]
 
        }))
 

	
 
    return JSONResponse({"songs": songs_data})
 

	
 

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

	
 
    def post(self):
 
async def post_song(request: Request) -> PlainTextResponse:
 
        """post a uri of song to switch to (and start playing)"""
 
        graph = self.settings.app.graph
 
    graph = cast(Graph, request.app.state.graph)
 
    player = cast(Player, request.app.state.player)
 

	
 
        self.settings.app.player.setSong(songLocation(graph, URIRef(self.request.body.decode('utf8'))))
 
        self.set_header("Content-Type", "text/plain")
 
        self.write("ok")
 
    song_uri = URIRef((await request.body()).decode('utf8'))
 
    player.setSong(songLocation(graph, song_uri))
 

	
 
    return PlainTextResponse("ok")
 

	
 

	
 
class seekPlayOrPause(PrettyErrorHandler, cyclone.web.RequestHandler):
 
async def post_seekPlayOrPause(request: Request) -> PlainTextResponse:
 
    """curveCalc's ctrl-p or a vidref scrub"""
 
    player = cast(Player, request.app.state.player)
 

	
 
    def post(self):
 
        player = self.settings.app.player
 

	
 
        data = json.loads(self.request.body)
 
    data = await request.json()
 
        if 'scrub' in data:
 
            player.pause()
 
            player.seek(data['scrub'])
 
            return
 
        return PlainTextResponse("ok")
 
        if 'action' in data:
 
            if data['action'] == 'play':
 
                player.resume()
 
@@ -170,28 +157,27 @@ class seekPlayOrPause(PrettyErrorHandler
 
                player.pause()
 
            else:
 
                raise NotImplementedError
 
            return
 
        return PlainTextResponse("ok")
 
        if player.isPlaying():
 
            player.pause()
 
        else:
 
            player.seek(data['t'])
 
            player.resume()
 

	
 

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

	
 
    def post(self):
 
        d = json.loads(self.request.body)
 
        subprocess.check_call(["bin/movesinks", str(d['sink'])])
 
    return PlainTextResponse("ok")
 

	
 

	
 
class goButton(PrettyErrorHandler, cyclone.web.RequestHandler):
 
async def post_output(request: Request) -> PlainTextResponse:
 
    d = await request.json()
 
    subprocess.check_call(["bin/movesinks", str(d['sink'])])
 
    return PlainTextResponse("ok")
 

	
 
    def post(self):
 

	
 
async def post_goButton(request: Request) -> PlainTextResponse:
 
        """
 
        if music is playing, this silently does nothing.
 
        """
 
        player = self.settings.app.player
 
    player = cast(Player, request.app.state.player)
 

	
 
        if player.isAutostopped():
 
            player.resume()
 
@@ -199,21 +185,4 @@ class goButton(PrettyErrorHandler, cyclo
 
            pass
 
        else:
 
            player.resume()
 

	
 
        self.set_header("Content-Type", "text/plain")
 
        self.write("ok")
 

	
 

	
 
def makeWebApp(app):
 
    return cyclone.web.Application(handlers=[
 
        (r"/config", config),
 
        (r"/time", timeResource),
 
        (r"/time/stream", timeStreamResource),
 
        (r"/song", songResource),
 
        (r"/songs", songs),
 
        (r"/seekPlayOrPause", seekPlayOrPause),
 
        (r"/output", output),
 
        (r"/go", goButton),
 
        metricsRoute(),
 
    ],
 
                                   app=app)
 
    return PlainTextResponse("ok")
0 comments (0 inline, 0 general)