home · contact · privacy
Simplify keeping alive of events stream.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 18 Dec 2024 16:49:56 +0000 (17:49 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 18 Dec 2024 16:49:56 +0000 (17:49 +0100)
src/templates/_base.tmpl
src/ytplom/http.py

index 53ed3895ec1679cc2bb7e9c9c5de201474e64f31..62483a3d3596292da9804fd928a4624b6d523675 100644 (file)
@@ -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();
index d283c3d4ab38167b510434cd329ec3d659d7481c..6054dc91b296312e1ec49e2feeeb9ef23e7150d9 100644 (file)
@@ -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)