<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) {
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
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'),
}
# misc
+_MAX_PING_AGE_S = 2
_HEADER_CONTENT_TYPE = 'Content-Type'
_HEADER_APP_JSON = 'application/json'
*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)
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')
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)
f'data: {json_dumps(data)}\n\n'.encode())
self.wfile.flush()
except BrokenPipeError:
- return
+ break
sleep(0.25)