Changeset - a10f0f0e4dae
[Not reviewed]
default
0 3 0
drewp@bigasterisk.com - 20 months ago 2023-05-31 20:34:06
drewp@bigasterisk.com
update web ui with one SSE, not repeated requests
3 files changed with 29 insertions and 49 deletions:
0 comments (0 inline, 0 general)
light9/ascoltami/main.py
Show inline comments
 
@@ -29,48 +29,49 @@ reactor = cast(IReactorCore, reactor)
 

	
 
class Ascoltami:
 

	
 
    def __init__(self, graph, show):
 
        self.graph = graph
 
        self.player = Player(onEOS=self.onEOS, autoStopOffset=0)
 
        self.show = show
 
        self.playlist = Playlist.fromShow(graph, show)
 

	
 
    def onEOS(self, song):
 
        self.player.pause()
 
        self.player.seek(0)
 

	
 
        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(webapp.songLocation(self.graph, nextSong), play=False)
 

	
 

	
 
def main():
 
    logging.getLogger('sse_starlette.sse').setLevel(logging.INFO)
 
    Gst.init(None)
 

	
 
    graph = showconfig.getGraph()
 
    asco = Ascoltami(graph, showconfig.showUri())
 

	
 
    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"]),
 
        ],
 
    )
 

	
 
    app.add_middleware(PrometheusMiddleware)
 
    app.add_route("/metrics", handle_metrics)
 

	
 
    app.state.graph = graph
light9/ascoltami/main.ts
Show inline comments
 
function byId(id: string): HTMLElement {
 
  return document.getElementById(id)!;
 
}
 

	
 
export interface TimingUpdate {
 
  // GET /ascoltami/time response
 
  duration: number;
 
  next: string; // e.g. 'play'
 
  playing: boolean;
 
  song: string;
 
  started: number; // unix sec
 
  t: number; // seconds into song
 
  state: { current: { name: string }; pending: { name: string } };
 
}
 

	
 
(window as any).finishOldStyleSetup = async (times: { intro: number; post: number }, updateFreq: number, timingUpdate: (data: TimingUpdate) => void) => {
 
  let currentHighlightedSong = "";
 
  // let lastPlaying = false;
 

	
 
  async function updateCurrent() {
 
    const data: TimingUpdate = await (await fetch("api/time")).json();
 
  
 
  const events = new EventSource("api/time/stream");
 
  events.addEventListener("message", (m)=>{
 
    const update = JSON.parse(m.data) as TimingUpdate
 
    updateCurrent(update)
 
    markUpdateTiming();
 
  })
 

	
 
  async function updateCurrent(data:TimingUpdate) {
 
    byId("currentSong").innerText = data.song;
 
    if (data.song != currentHighlightedSong) {
 
      showCurrentSong(data.song);
 
    }
 
    byId("currentTime").innerText = data.t.toFixed(1);
 
    byId("leftTime").innerText = (data.duration - data.t).toFixed(1);
 
    byId("leftAutoStopTime").innerText = Math.max(0, data.duration - times.post - data.t).toFixed(1);
 
    byId("states").innerText = JSON.stringify(data.state);
 
    //   document.querySelector("#timeSlider").slider({ value: data.t, max: data.duration });
 
    timingUpdate(data);
 
  }
 
  let recentUpdates: Array<number> = [];
 
  function markUpdateTiming() {
 
    recentUpdates.push(+new Date());
 
    recentUpdates = recentUpdates.slice(Math.max(recentUpdates.length - 5, 0));
 
  }
 

	
 
  function refreshUpdateFreqs() {
 
    if (recentUpdates.length > 1) {
 
      if (+new Date() - recentUpdates[recentUpdates.length - 1] > 1000) {
 
        byId("updateActual").innerText = "(stalled)";
 
        return;
 
      }
 

	
 
      var avgMs = (recentUpdates[recentUpdates.length - 1] - recentUpdates[0]) / (recentUpdates.length - 1);
 
      byId("updateActual").innerText = "" + Math.round(1000 / avgMs);
 
    }
 
  }
 
  setInterval(refreshUpdateFreqs, 2000);
 

	
 
  function showCurrentSong(uri: string) {
 
    document.querySelectorAll(".songs div").forEach((row: Element, i: number) => {
 
      if (row.querySelector("button")!.dataset.uri == uri) {
 
        row.classList.add("currentSong");
 
      } else {
 
        row.classList.remove("currentSong");
 
      }
 
    });
 
    currentHighlightedSong = uri;
 
  }
 

	
 
  const data = await (await fetch("api/songs")).json();
 
  data.songs.forEach((song: { uri: string; label: string }) => {
 
    const button = document.createElement("button");
 
    // link is just for dragging, not clicking
 
    const link = document.createElement("a");
 
    const n = document.createElement("span");
 
    n.classList.add("num");
 
    n.innerText = song.label.slice(0, 2);
 
    link.appendChild(n);
 

	
 
    const sn = document.createElement("span");
 
    sn.classList.add("song-name");
 
    sn.innerText = song.label.slice(2).trim();
 
    link.appendChild(sn);
 
    link.setAttribute("href", song.uri);
 
    link.addEventListener("click", (ev) => {
 
      ev.stopPropagation();
 
      button.click();
 
    });
 
    button.appendChild(link);
 
    button.dataset.uri = song.uri;
 
    button.addEventListener("click", async (ev) => {
 
      await fetch("api/song", { method: "POST", body: song.uri });
 
      showCurrentSong(song.uri);
 
    });
 
    const dv = document.createElement("div");
 
    dv.appendChild(button);
 
    document.querySelector(".songs")!.appendChild(dv);
 
  });
 

	
 
  //   var pendingSlide = false;
 
  //   $("#timeSlider").slider({
 
  //     step: 0.01,
 
  //     slide: function (event, ui) {
 
  //       if (pendingSlide) {
 
  //         return;
 
  //       }
 
  //       pendingSlide = true;
 
  //       $.post("time", '{"t" : ' + ui.value + "}", function (data, status, xhr) {
 
  //         pendingSlide = false;
 
  //       });
 
  //     },
 
  //   });
 

	
 
  let recentUpdates: Array<number> = [];
 
  function onUpdate() {
 
    recentUpdates.push(+new Date());
 
    recentUpdates = recentUpdates.slice(Math.max(recentUpdates.length - 5, 0));
 
    refreshUpdateFreqs();
 
  }
 

	
 
  function refreshUpdateFreqs() {
 
    if (recentUpdates.length > 1) {
 
      if (+new Date() - recentUpdates[recentUpdates.length - 1] > 1000) {
 
        byId("updateActual").innerText = "(stalled)";
 
        document.querySelectorAll(".dimStalled").forEach((el) => el.classList.add("stalled"));
 
        return;
 
      }
 

	
 
      var avgMs = (recentUpdates[recentUpdates.length - 1] - recentUpdates[0]) / (recentUpdates.length - 1);
 
      byId("updateActual").innerText = "" + Math.round(1000 / avgMs);
 
    }
 
  }
 
  setInterval(refreshUpdateFreqs, 2000);
 

	
 
  async function updateLoop() {
 
    var whenDone = function () {
 
      setTimeout(function () {
 
        requestAnimationFrame(updateLoop);
 
      }, 1000 / updateFreq);
 
    };
 
    onUpdate();
 
    await updateCurrent();
 
    whenDone();
 
  }
 
  updateLoop();
 
};
light9/ascoltami/webapp.py
Show inline comments
 
@@ -88,49 +88,49 @@ async def post_time(request: Request) ->
 
        player.pause()
 
    if params.get('resume', False):
 
        player.resume()
 
    if 't' in params:
 
        player.seek(params['t'])
 
    return PlainTextResponse("ok")
 

	
 

	
 
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
 

	
 
        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
 

	
 
            await asyncio.sleep(0.2)
 
            await asyncio.sleep(0.1)
 

	
 
    return EventSourceResponse(event_generator())
 

	
 

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

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

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

	
 

	
 
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)
0 comments (0 inline, 0 general)