From 0c3860cf9b6cccb143a7ef13cf77d9891353ba93 Mon Sep 17 00:00:00 2001 From: TonyBlu <101348912+TonyBlur@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:11:06 +0800 Subject: [PATCH] Make PUBLIC_MODE per-visit isolated sessions like reclip --- app/main.py | 75 +++++++++++++------- ui/src/app/services/downloads.service.ts | 2 +- ui/src/app/services/metube-socket.service.ts | 9 ++- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/app/main.py b/app/main.py index 5c22898..6a17bc2 100644 --- a/app/main.py +++ b/app/main.py @@ -446,50 +446,59 @@ def _migrate_legacy_request(post: dict) -> dict: return post -public_client_downloads: dict[str, set[str]] = {} +public_visit_downloads: dict[str, set[str]] = {} public_download_owner: dict[str, str] = {} public_download_owner_by_id: dict[str, str] = {} +public_visit_sids: dict[str, set[str]] = {} +public_sid_visit: dict[str, str] = {} -def _register_public_download(sid: str, url: str): - if not sid or not url: +def _register_public_download(visit_id: str, url: str): + if not visit_id or not url: return - public_client_downloads.setdefault(sid, set()).add(url) - public_download_owner[url] = sid + public_visit_downloads.setdefault(visit_id, set()).add(url) + public_download_owner[url] = visit_id def _forget_public_download(url: str | None = None, *, dl_id: str | None = None): - sid = None + visit_id = None if dl_id: - sid = public_download_owner_by_id.pop(dl_id, None) - if sid is None and url: - sid = public_download_owner.pop(url, None) - if not sid: + visit_id = public_download_owner_by_id.pop(dl_id, None) + if visit_id is None and url: + visit_id = public_download_owner.pop(url, None) + if not visit_id: return - urls = public_client_downloads.get(sid) + urls = public_visit_downloads.get(visit_id) if not urls: return - urls.discard(url) + if url: + urls.discard(url) if not urls: - public_client_downloads.pop(sid, None) + public_visit_downloads.pop(visit_id, None) async def _emit_download_event(event: str, payload, *, url: str | None = None, dl_id: str | None = None): if not config.PUBLIC_MODE: await sio.emit(event, payload) return - if not url: + visit_id = (public_download_owner_by_id.get(dl_id) if dl_id else None) or (public_download_owner.get(url) if url else None) + if not visit_id: return - sid = (public_download_owner_by_id.get(dl_id) if dl_id else None) or (public_download_owner.get(url) if url else None) - if sid: + for sid in list(public_visit_sids.get(visit_id, set())): await sio.emit(event, payload, to=sid) + +def _public_visit_from_environ(environ: dict) -> str: + qs = parse_qs(environ.get('QUERY_STRING', '')) + return (qs.get('visit_id', [''])[0] or '').strip() + + class Notifier(DownloadQueueNotifier): async def added(self, dl): log.info(f"Notifier: Download added - {dl.title}") - sid = public_download_owner.get(dl.url) - if sid: - public_download_owner_by_id[dl.id] = sid + visit_id = public_download_owner.get(dl.url) + if visit_id: + public_download_owner_by_id[dl.id] = visit_id await _emit_download_event('added', serializer.encode(dl), url=dl.url, dl_id=dl.id) async def updated(self, dl): @@ -770,10 +779,10 @@ async def add(request): o['auto_start'], ) if config.PUBLIC_MODE: - sid = request.headers.get('X-Client-Id', '').strip() - if not sid: - raise web.HTTPBadRequest(reason='missing X-Client-Id in public mode') - _register_public_download(sid, o['url']) + visit_id = request.headers.get('X-Visit-Id', '').strip() + if not visit_id: + raise web.HTTPBadRequest(reason='missing X-Visit-Id in public mode') + _register_public_download(visit_id, o['url']) status = await dqueue.add( o['url'], o['download_type'], @@ -1016,6 +1025,11 @@ async def history(request): @sio.event async def connect(sid, environ): log.info(f"Client connected: {sid}") + if config.PUBLIC_MODE: + visit_id = _public_visit_from_environ(environ) + if visit_id: + public_sid_visit[sid] = visit_id + public_visit_sids.setdefault(visit_id, set()).add(sid) if config.PUBLIC_MODE: await sio.emit('all', serializer.encode(([], [])), to=sid) else: @@ -1032,10 +1046,21 @@ async def connect(sid, environ): async def disconnect(sid): if not config.PUBLIC_MODE: return - owned = list(public_client_downloads.pop(sid, set())) + visit_id = public_sid_visit.pop(sid, '') + if not visit_id: + return + sids = public_visit_sids.get(visit_id, set()) + sids.discard(sid) + if sids: + return + public_visit_sids.pop(visit_id, None) + owned = list(public_visit_downloads.pop(visit_id, set())) for url in owned: public_download_owner.pop(url, None) - public_download_owner_by_id.pop(url, None) + # remove any lingering id ownership for this visit + for dl_id, owner in list(public_download_owner_by_id.items()): + if owner == visit_id: + public_download_owner_by_id.pop(dl_id, None) if owned: await dqueue.cancel(owned) diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts index 8d1f9b3..2110d0e 100644 --- a/ui/src/app/services/downloads.service.ts +++ b/ui/src/app/services/downloads.service.ts @@ -152,7 +152,7 @@ export class DownloadsService { const ce = payload.clipEnd?.trim(); if (cs) body['clip_start'] = cs; if (ce) body['clip_end'] = ce; - const headers = this.socket.ioSocket?.id ? { 'X-Client-Id': this.socket.ioSocket.id } : undefined; + const headers = { 'X-Visit-Id': this.socket.getVisitId() }; return this.http.post('add', body, { headers }).pipe( catchError(this.handleHTTPError) ); diff --git a/ui/src/app/services/metube-socket.service.ts b/ui/src/app/services/metube-socket.service.ts index a6c49bf..b68853c 100644 --- a/ui/src/app/services/metube-socket.service.ts +++ b/ui/src/app/services/metube-socket.service.ts @@ -6,12 +6,19 @@ import { Socket } from 'ngx-socket-io'; { providedIn: 'root' } ) export class MeTubeSocket extends Socket { + private readonly visitId: string; + + getVisitId(): string { + return this.visitId; + } constructor() { const appRef = inject(ApplicationRef); const path = document.location.pathname.replace(/share-target/, '') + 'socket.io'; - super({ url: '', options: { path } }, appRef); + const visitId = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2); + super({ url: '', options: { path, query: { visit_id: visitId } } }, appRef); + this.visitId = visitId; } }