home · contact · privacy
Close events stream earlier by checking for frequent client pings.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 15 Dec 2024 07:27:41 +0000 (08:27 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 15 Dec 2024 07:27:41 +0000 (08:27 +0100)
src/templates/_base.tmpl
src/ytplom/http.py

index cbda2ab2b37ce8767773034fd3b144ba9d25785d..0350ceda358c93e64ce0b194449b4eddaf585cec 100644 (file)
@@ -6,17 +6,26 @@
 <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;
+
+setInterval(function() {if (client_id) { send_to({client_id: [client_id]}, PATH_EVENTS_PING); }},
+            PING_INTERVAL_S * 1000);
 
 function connect_events() {
     const 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;
+            return; }
         for (let i = 0; i < event_handlers.length; i++) {
             event_handlers[i](data); }}
     events_stream.onerror = function(error) {
index de8950209bf040fddba1fec5fc98988d0e74d3ac..4dbe6514b6e0f34fe5be70e437b12700291ad713 100644 (file)
@@ -2,11 +2,12 @@
 from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
 from json import dumps as json_dumps, loads as json_loads
 from pathlib import Path
-from time import sleep
+from time import time, sleep
 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
@@ -36,6 +37,7 @@ _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'),
@@ -48,6 +50,7 @@ PAGE_NAMES: dict[str, Path] = {
 }
 
 # misc
+_MAX_PING_AGE_S = 2
 _HEADER_CONTENT_TYPE = 'Content-Type'
 _HEADER_APP_JSON = 'application/json'
 
@@ -94,6 +97,7 @@ 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)
@@ -139,6 +143,12 @@ 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:
+        self.server.event_pings[client_id] = time()
+        self._send_http('OK', code=200)
 
     def _receive_player_command(self, postvars: _ReqMap) -> None:
         command = postvars.first_for('command')
@@ -368,9 +378,18 @@ class _TaskHandler(BaseHTTPRequestHandler):
         self._send_http(headers=[(_HEADER_CONTENT_TYPE, 'text/event-stream'),
                                  ('Cache-Control', 'no-cache'),
                                  ('Connection', 'keep-alive')])
+        client_id = str(uuid4())
         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()
         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 self.server.player.current_digest:
                 playing = None
             elif ((not playing)
@@ -403,5 +422,5 @@ class _TaskHandler(BaseHTTPRequestHandler):
                             f'data: {json_dumps(data)}\n\n'.encode())
                     self.wfile.flush()
                 except BrokenPipeError:
-                    return
+                    break
             sleep(0.25)