From 8b8e38386dc3bb19d5f5e8d10343a977f611c4ce Mon Sep 17 00:00:00 2001 From: Christian Heller <c.heller@plomlompom.de> Date: Wed, 18 Dec 2024 17:49:56 +0100 Subject: [PATCH] Simplify keeping alive of events stream. --- src/templates/_base.tmpl | 16 ++++----- src/ytplom/http.py | 70 ++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/src/templates/_base.tmpl b/src/templates/_base.tmpl index 53ed389..62483a3 100644 --- a/src/templates/_base.tmpl +++ b/src/templates/_base.tmpl @@ -6,30 +6,28 @@ <script> const RETRY_INTERVAL_S = 5; -const PING_INTERVAL_S = 1; const PATH_EVENTS = "/{{page_names.events}}"; -const PATH_EVENTS_PING = "/{{page_names.events_ping}}"; const PATH_PLAYER = "/{{page_names.player}}"; const PATH_PLAYLIST = "/{{page_names.playlist}}"; const PATH_PREFIX_FILE = "/{{page_names.file}}/"; var event_handlers = []; var events_params = ""; -var client_id = null; +var events_stream = null; -setInterval(function() {if (client_id) { send_to({client_id: [client_id]}, PATH_EVENTS_PING); }}, - PING_INTERVAL_S * 1000); +window.addEventListener( + "beforeunload", function() { + if (events_stream) { + events_stream.close(); } }); function connect_events() { - const events_stream = new EventSource(`${PATH_EVENTS}?${events_params}`); + events_stream = new EventSource(`${PATH_EVENTS}?${events_params}`); events_stream.onmessage = function(event) { const data = JSON.parse(event.data); - if (data.your_id) { - client_id = data.your_id; + if (data.ping) { return; } for (let i = 0; i < event_handlers.length; i++) { event_handlers[i](data); }} events_stream.onerror = function(error) { - client_id = null; const while_connecting = events_stream.readyState == events_stream.CONNECTING; console.log(`Error on ${PATH_EVENTS} connection:`, error); events_stream.close(); diff --git a/src/ytplom/http.py b/src/ytplom/http.py index d283c3d..6054dc9 100644 --- a/src/ytplom/http.py +++ b/src/ytplom/http.py @@ -2,12 +2,11 @@ from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler from json import dumps as json_dumps, loads as json_loads from pathlib import Path -from time import time, sleep +from time import sleep, time from typing import Any, Generator, Optional from urllib.parse import parse_qs, urlparse from urllib.request import urlretrieve from urllib.error import HTTPError -from uuid import uuid4 from jinja2 import ( # type: ignore Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader) from ytplom.db import Hash, DbConn @@ -37,7 +36,6 @@ _NAME_TEMPLATE_PLAYLIST = Path('playlist.tmpl') PAGE_NAMES: dict[str, Path] = { 'download': Path('dl'), 'events': Path('events'), - 'events_ping': Path('events_ping'), 'file': Path('file'), 'files': Path('files'), 'missing': Path('missing'), @@ -50,7 +48,8 @@ PAGE_NAMES: dict[str, Path] = { } # misc -_MAX_PING_AGE_S = 2 +_PING_INTERVAL_S = 1 +_EVENTS_UPDATE_INTERVAL_S = 0.1 _HEADER_CONTENT_TYPE = 'Content-Type' _HEADER_APP_JSON = 'application/json' @@ -97,7 +96,6 @@ class Server(ThreadingHTTPServer): *args, **kwargs) self.config = config self.jinja = JinjaEnv(loader=JinjaFSLoader(_PATH_TEMPLATES)) - self.event_pings: dict[str, float] = {} self.player = Player(config.whitelist_tags_display, config.whitelist_tags_prefilter, config.needed_tags_prefilter) @@ -143,15 +141,6 @@ class _TaskHandler(BaseHTTPRequestHandler): self._receive_yt_query(QueryText(postvars.first_for('query'))) elif PAGE_NAMES['player'] == page_name: self._receive_player_command(postvars) - elif PAGE_NAMES['events_ping'] == page_name: - self._receive_events_ping(postvars.first_for('client_id')) - - def _receive_events_ping(self, client_id: str) -> None: - if client_id not in self.server.event_pings: - self._send_http('unknown client ID', code=400) - else: - self.server.event_pings[client_id] = time() - self._send_http('OK') def _receive_player_command(self, postvars: _ReqMap) -> None: command = postvars.first_for('command') @@ -381,49 +370,46 @@ class _TaskHandler(BaseHTTPRequestHandler): self._send_http(headers=[(_HEADER_CONTENT_TYPE, 'text/event-stream'), ('Cache-Control', 'no-cache'), ('Connection', 'keep-alive')]) - client_id = str(uuid4()) - self.server.event_pings[client_id] = time() playing: Optional[VideoFile] = None last_sent = '' - init_msg = {'your_id': client_id} - self.wfile.write(f'data: {json_dumps(init_msg)}\n\n'.encode()) - self.wfile.flush() + payload: dict[str, Any] = {} + time_last_write = 0.0 while True: - min_ping_time = time() - _MAX_PING_AGE_S - if (client_id in self.server.event_pings - and self.server.event_pings[client_id] < min_ping_time): - del self.server.event_pings[client_id] - break + if not payload and time_last_write < time() - _PING_INTERVAL_S: + payload['ping'] = True + if payload: + payload_encoded = f'data: {json_dumps(payload)}\n\n'.encode() + try: + self.wfile.write(payload_encoded) + self.wfile.flush() + except BrokenPipeError: + break + time_last_write = time() + payload.clear() if not self.server.player.current_digest: playing = None elif ((not playing) or (playing.digest != self.server.player.current_digest)): with DbConn() as conn: playing = VideoFile.get_one_with_whitelist_tags_display( - conn, - self.server.player.current_digest, - self.server.config.whitelist_tags_display) + conn, + self.server.player.current_digest, + self.server.config.whitelist_tags_display) if last_sent < self.server.player.last_update: last_sent = self.server.player.last_update title, tags = '', '' if playing: tags = playing.tags_showable.joined title = str(playing.rel_path) - data = { - 'last_update': self.server.player.last_update, - 'running': self.server.player.is_running, - 'paused': self.server.player.is_paused, - 'idx': self.server.player.idx, - 'title_tags': tags, - 'title': title} + payload['last_update'] = self.server.player.last_update + payload['running'] = self.server.player.is_running + payload['paused'] = self.server.player.is_paused + payload['idx'] = self.server.player.idx + payload['title_tags'] = tags + payload['title'] = title if 'playlist' in params.as_dict: - data['playlist_files'] = [ + payload['playlist_files'] = [ {'rel_path': str(f.rel_path), 'digest': f.digest.b64} for f in self.server.player.playlist] - try: - self.wfile.write( - f'data: {json_dumps(data)}\n\n'.encode()) - self.wfile.flush() - except BrokenPipeError: - break - sleep(0.25) + else: + sleep(_EVENTS_UPDATE_INTERVAL_S) -- 2.30.2