# HG changeset patch # User drewp@bigasterisk.com # Date 2024-05-25 22:44:11 # Node ID cc69faa87c27a050cb0c433d641b73e69a619aed # Parent 90792e984249df059e9ed8717b6d74ef4fa2c8d3 tear up and rewrite ascoltami to emit player state into the graph. web ui works but displays nothing but songs diff --git a/src/light9/ascoltami/main.py b/src/light9/ascoltami/main.py --- a/src/light9/ascoltami/main.py +++ b/src/light9/ascoltami/main.py @@ -1,36 +1,62 @@ #!bin/python import logging -import optparse -import sys from typing import cast -import gi -from rdflib import URIRef +import louie +from rdfdb.syncedgraph.syncedgraph import SyncedGraph +from rdflib import ConjunctiveGraph, Literal, URIRef +from rdflib.graph import _ContextType 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 - from light9 import networking, showconfig from light9.ascoltami import webapp -from light9.ascoltami.player import Player from light9.ascoltami.import_gst import Gst +from light9.ascoltami.player import Player, PlayerState from light9.ascoltami.playlist import NoSuchSong, Playlist - -reactor = cast(IReactorCore, reactor) - +from light9.namespaces import L9 +from light9.newtypes import decimalLiteral +from light9.run_local import log class Ascoltami: - def __init__(self, graph, show): + def __init__(self, graph: SyncedGraph, show: URIRef): self.graph = graph - self.player = Player(onEOS=self.onEOS, autoStopOffset=0) + self.player = Player(onStateChange=self.onStateChange, autoStopOffset=0) self.show = show - self.playlist = Playlist.fromShow(graph, show) + self.ctx = cast(_ContextType, (URIRef(self.show + '/ascoltami'))) + + self.playerState = PlayerState() + self.playlist = Playlist(graph, show) + + def onStateChange(self, s: PlayerState): + log.info('louie send') + louie.send(webapp.OnStateChange, s=s) + g = self.stateAsGraph(s) + + self.graph.patchSubgraph(newGraph=g, context=self.ctx) + self.playerState = s + + def stateAsGraph(self, s): + g = ConjunctiveGraph() + asc = L9['ascoltami'] + if s.song: + try: + g.add((asc, L9['song'], self.playlist.songUri(s.song), self.ctx)) + except KeyError: + pass + g.add((asc, L9['duration'], decimalLiteral(s.duration), self.ctx)) + g.add((asc, L9['playing'], Literal(s.playing), self.ctx)) + if s.wallStartTime is not None: + g.add((asc, L9['wallStartTime'], decimalLiteral(s.wallStartTime), self.ctx)) + if s.pausedSongTime is not None: + g.add((asc, L9['pausedSongTime'], decimalLiteral(s.pausedSongTime), self.ctx)) + g.add((asc, L9['endOfSong'], Literal(s.endOfSong), self.ctx)) + return g + + def getPlayerState(self) -> PlayerState: + return self.playerState def onEOS(self, song): self.player.pause() @@ -50,31 +76,28 @@ def main(): logging.getLogger('sse_starlette.sse').setLevel(logging.INFO) Gst.init(None) - graph = showconfig.getGraph() + graph = SyncedGraph(networking.rdfdb.url, "ascoltami") asco = Ascoltami(graph, showconfig.showUri()) + h = webapp.WebHandlers(graph, asco.show, asco.player, asco.playlist, asco.getPlayerState) 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"]), + Route("/config", h.get_config), + Route("/time", h.get_time, methods=["GET"]), + Route("/time", h.post_time, methods=["POST"]), + Route("/time/stream", h.timeStream), + Route("/song", h.post_song, methods=["POST"]), + Route("/songs", h.get_songs), + Route("/seekPlayOrPause", h.post_seekPlayOrPause), + Route("/output", h.post_output, methods=["POST"]), + Route("/go", h.post_goButton, methods=["POST"]), ], ) 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 diff --git a/src/light9/ascoltami/player.py b/src/light9/ascoltami/player.py --- a/src/light9/ascoltami/player.py +++ b/src/light9/ascoltami/player.py @@ -1,60 +1,89 @@ """ -alternate to the mpd music player, for ascoltami +alternate to the mpd music player, for ascoltami. + +This module doesn't know URIRef or graph. It does use +GstFileUri (str) for filesystem paths. """ +import asyncio import logging import time import traceback from dataclasses import dataclass -from pathlib import Path +from typing import NewType -from rdflib import URIRef -from twisted.internet import task from light9.ascoltami.import_gst import Gst # type: ignore from light9.metrics import metrics + log = logging.getLogger() +GstFileUri = NewType('GstFileUri', str) + + +@dataclass +class PlayerState: + song: GstFileUri | None = None + duration: float = 0 + playing: bool = False # time is advancing, and song time is now()-wallStartTime + # wall time of song-time 0 (this is adjusted when you seek) + wallStartTime: float = 0 + pausedSongTime: float | None = None # if we're paused, this has the song time + endOfSong: bool = False # True if we're in the stopped state due to EOS class Player: - def __init__(self, autoStopOffset=4, onEOS=None): + def __init__(self, autoStopOffset=4, onStateChange=lambda ps: None): """autoStopOffset is the number of seconds before the end of song before automatically stopping (which is really pausing). - onEOS is an optional function to be called when we reach the - end of a stream (for example, can be used to advance the song). - It is called with one argument which is the URI of the song that - just finished.""" + """ self.autoStopOffset = autoStopOffset - self.playbin = self.pipeline = Gst.ElementFactory.make('playbin', None) + self.onStateChange = onStateChange + self._playbin = self.pipeline = Gst.ElementFactory.make('playbin', None) - self._playStartTime = 0 + self.playerState: PlayerState = PlayerState() + self._lastWatchTime = 0 self._autoStopTime = 0 - self._lastSetSongUri = None - self._onEOS = onEOS + self._lastSetSongUri: GstFileUri | None = None - task.LoopingCall(self.watchTime).start(.050) + # task.LoopingCall(self._watchTime).start(.050) + asyncio.create_task(self._watchTime()) #bus = self.pipeline.get_bus() # not working- see notes in pollForMessages - #self.watchForMessages(bus) + #self._watchForMessages(bus) - def watchTime(self): - try: - self.pollForMessages() + async def _watchTime(self): + while True: + now = time.time() + try: + self._pollForMessages() + t = self.currentTime() + # log.debug("watch %s < %s < %s", self._lastWatchTime, self._autoStopTime, t) + if self._lastWatchTime < self._autoStopTime < t: + log.info("autostop") + self.pause() - t = self.currentTime() - log.debug("watch %s < %s < %s", self._lastWatchTime, self._autoStopTime, t) - if self._lastWatchTime < self._autoStopTime < t: - log.info("autostop") - self.pause() + ps = PlayerState( + song=self._getSongFileUri(), + duration=round(self.duration(), 2), + wallStartTime=round(now - t, 2) if self.isPlaying() else None, + playing=self.isPlaying(), + pausedSongTime=None if self.isPlaying() else t, + endOfSong=t >= self.duration() - .1, #todo + ) - self._lastWatchTime = t - except Exception: - traceback.print_exc() + if self.playerState != ps: + self.onStateChange(ps) + self.playerState = ps - def watchForMessages(self, bus): + self._lastWatchTime = t + except Exception: + traceback.print_exc() + await asyncio.sleep(0.5) + + def _watchForMessages(self, bus): """this would be nicer than pollForMessages but it's not working for me. It's like add_signal_watch isn't running.""" bus.add_signal_watch() @@ -62,7 +91,7 @@ class Player: def onEos(*args): print("onEos", args) if self._onEOS is not None: - self._onEOS(self.getSong()) + self._onEOS(self._getSongFileUri()) bus.connect('message::eos', onEos) @@ -74,7 +103,7 @@ class Player: bus.connect('message::stream-status', onStreamStatus) - def pollForMessages(self): + def _pollForMessages(self): """bus.add_signal_watch seems to be having no effect, but this works""" bus = self.pipeline.get_bus() mt = Gst.MessageType @@ -90,26 +119,25 @@ class Player: if msg.type == mt.ERROR: log.error(repr(msg.parse_error())) if msg.type == mt.EOS: - if self._onEOS is not None: - self._onEOS(self.getSong()) + log.info("EOS msg: todo") if msg.type == mt.STREAM_STATUS: (statusType, _elem) = msg.parse_stream_status() if statusType == Gst.StreamStatusType.ENTER: self.setupAutostop() def seek(self, t): - isSeekable = self.playbin.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE | Gst.SeekFlags.SKIP, t * Gst.SECOND) + isSeekable = self._playbin.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE | Gst.SeekFlags.SKIP, t * Gst.SECOND) if not isSeekable: raise ValueError('seek_simple failed') self._playStartTime = time.time() - def setSong(self, songLoc, play=True): + def setSong(self, songLoc: GstFileUri, play=True): """ uri like file:///my/proj/light9/show/dance2010/music/07.wav """ log.info("set song to %r" % songLoc) self.pipeline.set_state(Gst.State.READY) - self.preload(songLoc) + self._preload(songLoc) self.pipeline.set_property("uri", songLoc) self._lastSetSongUri = songLoc # todo: don't have any error report yet if the uri can't be read @@ -117,12 +145,15 @@ class Player: self.pipeline.set_state(Gst.State.PLAYING) self._playStartTime = time.time() - def getSong(self): - """Returns the URI of the current song.""" + def _getSongFileUri(self) -> GstFileUri | None: + """Returns the file URI of the current song.""" # even the 'uri' that I just set isn't readable yet - return self.playbin.get_property("uri") or self._lastSetSongUri + return GstFileUri(self._playbin.get_property("uri")) or self._lastSetSongUri - def preload(self, songPath): + def getSong(self) -> GstFileUri | None: + return self.playerState.song + + def _preload(self, songPath): """ to avoid disk seek stutters, which happened sometimes (in 2007) with the non-gst version of this program, we read the whole file to get @@ -140,13 +171,13 @@ class Player: @metrics('current_time').time() def currentTime(self): - success, cur = self.playbin.query_position(Gst.Format.TIME) + success, cur = self._playbin.query_position(Gst.Format.TIME) if not success: return 0 return cur / Gst.SECOND def duration(self): - success, dur = self.playbin.query_duration(Gst.Format.TIME) + success, dur = self._playbin.query_duration(Gst.Format.TIME) if not success: return 0 return dur / Gst.SECOND @@ -154,7 +185,7 @@ class Player: def states(self): """json-friendly object describing the interesting states of the player nodes""" - success, state, pending = self.playbin.get_state(timeout=0) + success, state, pending = self._playbin.get_state(timeout=0) return {"current": {"name": state.value_nick}, "pending": {"name": state.value_nick}} def pause(self): diff --git a/src/light9/ascoltami/playlist.py b/src/light9/ascoltami/playlist.py --- a/src/light9/ascoltami/playlist.py +++ b/src/light9/ascoltami/playlist.py @@ -1,5 +1,13 @@ +import logging + +from rdfdb.syncedgraph.syncedgraph import SyncedGraph +from rdflib import RDFS, URIRef + +from light9.ascoltami.player import GstFileUri +from light9.namespaces import L9 from light9.showconfig import songOnDisk -from light9.namespaces import L9 + +log = logging.getLogger('list') class NoSuchSong(ValueError): @@ -8,48 +16,65 @@ class NoSuchSong(ValueError): class Playlist: + """ + maintains a view of the playlist in the rdf graph. Also produces GstFileUri strs + for Player + """ + _songs: list[URIRef] + _labels: dict[URIRef, str] + _nextSong: dict[URIRef, URIRef] + _songUri: dict[GstFileUri, URIRef] + _fileUri: dict[URIRef, GstFileUri] - def __init__(self, graph, playlistUri): + def __init__(self, graph: SyncedGraph, show: URIRef): self.graph = graph - self.playlistUri = playlistUri - self.songs = list(graph.items(playlistUri)) + self.show = show + self.graph.addHandler(self._readSongs) - def nextSong(self, currentSong): + def _readSongs(self): + self._songs = [] + self._labels = {} + self._songUri = {} + self._fileUri = {} + playlistUri = self.graph.value(self.show, L9['playList'], default=None) + if not playlistUri: + # expected at startup that we'd have no data + return + prevSong = None + for s in self.graph.items(playlistUri): + f = GstFileUri('file://' + str(songOnDisk(self.graph, s))) + self._songs.append(s) + self._labels[s] = self.graph.value(s, RDFS.label, default="").toPython() + self._songUri[f] = s + self._fileUri[s] = f + if prevSong is not None: + self._nextSong[prevSong] = s + log.info(f'read {len(self._songs)} songs') + + def nextSong(self, currentSong: URIRef) -> URIRef: """Returns the next song in the playlist or raises NoSuchSong if we are at the end of the playlist.""" try: - currentIndex = self.songs.index(currentSong) - except IndexError: - raise ValueError("%r is not in the current playlist (%r)." % - (currentSong, self.playlistUri)) + return self._nextSong[currentSong] + except KeyError: + raise NoSuchSong() - try: - nextSong = self.songs[currentIndex + 1] - except IndexError: - raise NoSuchSong("%r is the last item in the playlist." % - currentSong) + def fileUri(self, song: URIRef) -> GstFileUri: + return self._fileUri[song] - return nextSong + def songUri(self, fileUri: GstFileUri) -> URIRef: + return self._songUri[fileUri] + + def label(self, song: URIRef) -> str: + return self._labels[song] def allSongs(self): """Returns a list of all song URIs in order.""" - return self.songs - - def allSongPaths(self): - """Returns a list of the filesystem paths to all songs in order.""" - paths = [] - for song in self.songs: - paths.append(songOnDisk(song)) - return paths + return self._songs - def songPath(self, uri): - """filesystem path to a song""" - raise NotImplementedError("see showconfig.songOnDisk") - # maybe that function should be moved to this method - - @classmethod - def fromShow(cls, graph, show): - playlistUri = graph.value(show, L9['playList']) - if not playlistUri: - raise ValueError("%r has no l9:playList" % show) - return cls(graph, playlistUri) + # def allSongPaths(self) -> list[Path]: + # """Returns a list of the filesystem paths to all songs in order.""" + # paths = [] + # for row in self._songs: + # paths.append(row.songPath) + # return paths diff --git a/src/light9/ascoltami/webapp.py b/src/light9/ascoltami/webapp.py --- a/src/light9/ascoltami/webapp.py +++ b/src/light9/ascoltami/webapp.py @@ -1,189 +1,187 @@ +""" +this module shouldn't be necessary for playback to work +""" import asyncio import json import logging import socket import subprocess +from dataclasses import dataclass, field import time -from typing import cast +from typing import Callable +from typing import Literal as Lit -from rdflib import RDFS, Graph, URIRef +import louie +from rdfdb.syncedgraph.syncedgraph import SyncedGraph +from rdflib import URIRef from sse_starlette.sse import EventSourceResponse from starlette.requests import Request from starlette.responses import JSONResponse, PlainTextResponse -from light9.ascoltami.player import Player -from light9.namespaces import L9 -from light9.showconfig import getSongsFromShow, showUri, songOnDisk - -log = logging.getLogger() -_songUris = {} # locationUri : song - - -def songLocation(graph, songUri): - loc = URIRef("file://%s" % songOnDisk(songUri)) - _songUris[loc] = songUri - return loc - - -def songUri(graph, locationUri): - return _songUris[locationUri] - +from light9.ascoltami.player import Player, PlayerState +from light9.ascoltami.playlist import Playlist +from light9.showconfig import showUri -async def get_config(request: Request) -> JSONResponse: - return JSONResponse( - dict( - host=socket.gethostname(), - show=str(showUri()), - times={ - # these are just for the web display. True values are on Player.__init__ - 'intro': 4, - 'post': 0 - })) - - -def playerSongUri(graph, player): - """or None""" - - playingLocation = player.getSong() - if playingLocation: - return songUri(graph, URIRef(playingLocation)) - else: - return None +log = logging.getLogger("web") -def currentState(graph, player): - if player.isAutostopped(): - nextAction = 'finish' - elif player.isPlaying(): - nextAction = 'disabled' - else: - nextAction = 'play' - - return { - "song": playerSongUri(graph, player), - "started": player.playStartTime, - "duration": player.duration(), - "playing": player.isPlaying(), - "t": player.currentTime(), - "state": player.states(), - "next": nextAction, - } +class OnStateChange: + pass -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)) - - -async def post_time(request: Request) -> PlainTextResponse: - """ - post a json object with {pause: true} or {resume: true} if you - want those actions. Use {t: } to seek, optionally - with a pause/resume command too. - """ - 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']) - return PlainTextResponse("ok") +@dataclass +class PlayerState2(PlayerState): + song2: URIRef | None = None + nextAction: Lit["finish"] | Lit['disabled'] | Lit['play'] = 'disabled' -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 +@dataclass +class WebHandlers: + graph: SyncedGraph + show: URIRef + player: Player + playlist: Playlist + getPlayerState: Callable[[], PlayerState] - while True: - now = time.time() - 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 + # _keep:list=field(default_factory=list) + async def get_config(self, request: Request) -> JSONResponse: + return JSONResponse( + dict( + host=socket.gethostname(), + show=str(showUri()), + times={ + # these are just for the web display. True values are on Player.__init__ + 'intro': 4, + 'post': 0 + })) - await asyncio.sleep(0.1) - - return EventSourceResponse(event_generator()) - + def currentState(self, player: Player, playlist: Playlist) -> PlayerState2: + if player.isAutostopped(): + nextAction = 'finish' + elif player.isPlaying(): + nextAction = 'disabled' + else: + nextAction = 'play' -async def get_songs(request: Request) -> JSONResponse: - graph = cast(Graph, request.app.state.graph) + ps = self.getPlayerState() + return PlayerState2( + song2=playlist.songUri((ps.song)) if ps.song else None, + duration=ps.duration, + playing=ps.playing, + # state= player.states(), + nextAction=nextAction, + ) - songs = getSongsFromShow(graph, request.app.state.show) + async def get_time(self, request: Request) -> JSONResponse: + return JSONResponse({'t': self.player.currentTime()}) - songs_data = [ - { # - "uri": s, - "path": graph.value(s, L9['songFilename']), - "label": graph.value(s, RDFS.label) - } for s in songs - ] + async def post_time(self, request: Request) -> PlainTextResponse: + """ + post a json object with {pause: true} or {resume: true} if you + want those actions. Use {t: } to seek, optionally + with a pause/resume command too. + """ + params = await request.json() + if params.get('pause', False): + self.player.pause() + if params.get('resume', False): + self.player.resume() + if 't' in params: + self.player.seek(params['t']) + return PlainTextResponse("ok") - return JSONResponse({"songs": songs_data}) - + async def timeStream(self, request: Request): -async def post_song(request: Request) -> PlainTextResponse: - """post a uri of song to switch to (and start playing)""" - 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 - song_uri = URIRef((await request.body()).decode('utf8')) - player.setSong(songLocation(graph, song_uri)) + def onStateChange(s: PlayerState2): + log.info('ws heanndlerr gets state') + + louie.connect(onStateChange, OnStateChange, weak=False) - return PlainTextResponse("ok") - + try: + while True: + now = time.time() + msg = self.currentState(self.player, self.playlist) + if msg != last_sent or now > last_sent_time + 2: + event_data = json.dumps({ + # obsolete- watch the graph for these + 'duration': msg.duration, + 'playing': msg.playing, + 'song': self.playlist.songUri(msg.song) if msg.song else None, + 'state': {}, + }) + yield event_data + last_sent = msg + last_sent_time = now -async def post_seekPlayOrPause(request: Request) -> PlainTextResponse: - """curveCalc's ctrl-p or a vidref scrub""" - player = cast(Player, request.app.state.player) + await asyncio.sleep(0.1) + finally: + log.info(f'bye listnner {event_generator}') + louie.disconnect(onStateChange, OnStateChange, weak=False) - data = await request.json() - if 'scrub' in data: - player.pause() - player.seek(data['scrub']) - return PlainTextResponse("ok") - if 'action' in data: - if data['action'] == 'play': - player.resume() - elif data['action'] == 'pause': - player.pause() - else: - raise NotImplementedError + return EventSourceResponse(event_generator()) + + async def get_songs(self, request: Request) -> JSONResponse: + + songs_data = [ + { # + "uri": s, + "path": self.playlist.fileUri(s), + "label": self.playlist.label(s), + } for s in self.playlist.allSongs() + ] + + return JSONResponse({"songs": songs_data}) + + async def post_song(self, request: Request) -> PlainTextResponse: + """post a uri of song to switch to (and start playing)""" + + song_uri = URIRef((await request.body()).decode('utf8')) + self.player.setSong(self.playlist.fileUri(song_uri)) + return PlainTextResponse("ok") - if player.isPlaying(): - player.pause() - else: - player.seek(data['t']) - player.resume() - return PlainTextResponse("ok") - - -async def post_output(request: Request) -> PlainTextResponse: - d = await request.json() - subprocess.check_call(["bin/movesinks", str(d['sink'])]) - return PlainTextResponse("ok") + async def post_seekPlayOrPause(self, request: Request) -> PlainTextResponse: + """curveCalc's ctrl-p or a vidref scrub""" - -async def post_goButton(request: Request) -> PlainTextResponse: - """ - if music is playing, this silently does nothing. - """ - player = cast(Player, request.app.state.player) + data = await request.json() + if 'scrub' in data: + self.player.pause() + self.player.seek(data['scrub']) + return PlainTextResponse("ok") + if 'action' in data: + if data['action'] == 'play': + self.player.resume() + elif data['action'] == 'pause': + self.player.pause() + else: + raise NotImplementedError + return PlainTextResponse("ok") + if self.player.isPlaying(): + self.player.pause() + else: + self.player.seek(data['t']) + self.player.resume() - if player.isAutostopped(): - player.resume() - elif player.isPlaying(): - pass - else: - player.resume() - return PlainTextResponse("ok") + return PlainTextResponse("ok") + + async def post_output(self, request: Request) -> PlainTextResponse: + d = await request.json() + subprocess.check_call(["bin/movesinks", str(d['sink'])]) + return PlainTextResponse("ok") + + async def post_goButton(self, request: Request) -> PlainTextResponse: + """ + if music is playing, this silently does nothing. + """ + + if self.player.isAutostopped(): + self.player.resume() + elif self.player.isPlaying(): + pass + else: + self.player.resume() + return PlainTextResponse("ok") diff --git a/src/light9/showconfig.py b/src/light9/showconfig.py --- a/src/light9/showconfig.py +++ b/src/light9/showconfig.py @@ -1,28 +1,31 @@ -import logging, warnings +import logging +import os +import warnings +from os import getenv +from pathlib import Path +from typing import cast + +from rdfdb.syncedgraph.syncedgraph import SyncedGraph +from rdflib import Graph, Literal, URIRef from twisted.python.filepath import FilePath -from os import path, getenv -from rdflib import Graph -from rdflib import URIRef, Literal + from .namespaces import L9 -from typing import List, cast + log = logging.getLogger('showconfig') _config = None # graph def getGraph() -> Graph: - warnings.warn( - "code that's using showconfig.getGraph should be " - "converted to use the sync graph", - stacklevel=2) + warnings.warn("code that's using showconfig.getGraph should be " + "converted to use the sync graph", stacklevel=2) global _config if _config is None: graph = Graph() # note that logging is probably not configured the first time # we're in here warnings.warn("reading n3 files around %r" % root()) - for f in FilePath(root()).globChildren("*.n3") + FilePath( - root()).globChildren("build/*.n3"): + for f in FilePath(root()).globChildren("*.n3") + FilePath(root()).globChildren("build/*.n3"): graph.parse(location=f.path, format='n3') _config = graph return _config @@ -31,8 +34,7 @@ def getGraph() -> Graph: def root() -> bytes: r = getenv("LIGHT9_SHOW") if r is None: - raise OSError( - "LIGHT9_SHOW env variable has not been set to the show root") + raise OSError("LIGHT9_SHOW env variable has not been set to the show root") return r.encode('ascii') @@ -43,13 +45,12 @@ def showUri() -> URIRef: """Return the show URI associated with $LIGHT9_SHOW.""" global _showUri if _showUri is None: - _showUri = URIRef(open(path.join(root(), b'URI')).read().strip()) + _showUri = URIRef(open(os.path.join(root(), b'URI')).read().strip()) return _showUri -def songOnDisk(song: URIRef) -> bytes: +def songOnDisk(graph: SyncedGraph, song: URIRef) -> Path: """given a song URI, where's the on-disk file that mpd would read?""" - graph = getGraph() root = graph.value(showUri(), L9['musicRoot']) if not root: raise ValueError("%s has no :musicRoot" % showUri()) @@ -58,10 +59,7 @@ def songOnDisk(song: URIRef) -> bytes: if not name: raise ValueError("Song %r has no :songFilename" % song) - return path.abspath( - path.join( - cast(Literal, root).toPython(), - cast(Literal, name).toPython())) + return (Path(cast(Literal, root).toPython()) / cast(Literal, name).toPython()).absolute() def songFilenameFromURI(uri: URIRef) -> bytes: @@ -74,7 +72,7 @@ def songFilenameFromURI(uri: URIRef) -> return str(uri).split('/')[-1].encode('ascii') -def getSongsFromShow(graph: Graph, show: URIRef) -> List[URIRef]: +def getSongsFromShow(graph: Graph, show: URIRef) -> list[URIRef]: playList = graph.value(show, L9['playList']) if not playList: raise ValueError("%r has no l9:playList" % show) @@ -82,16 +80,16 @@ def getSongsFromShow(graph: Graph, show: # serious bug here. songs = list(graph.items(playList)) - return songs + return cast(list[URIRef], songs) def curvesDir(): - return path.join(root(), b"curves") + return os.path.join(root(), b"curves") def subFile(subname): - return path.join(root(), b"subs", subname) + return os.path.join(root(), b"subs", subname) def subsDir(): - return path.join(root(), b'subs') + return os.path.join(root(), b'subs') diff --git a/web/ascoltami/main.ts b/web/ascoltami/main.ts --- a/web/ascoltami/main.ts +++ b/web/ascoltami/main.ts @@ -2,6 +2,7 @@ function byId(id: string): HTMLElement { return document.getElementById(id)!; } +// obsolete: watch the graph instead export interface TimingUpdate { // GET /ascoltami/time response duration: number; @@ -18,7 +19,7 @@ export interface TimingUpdate { // let lastPlaying = false; - const events = new EventSource("../service/ascoltami/time/stream"); + const events = new EventSource("/service/ascoltami/time/stream"); events.addEventListener("message", (m)=>{ const update = JSON.parse(m.data) as TimingUpdate updateCurrent(update)