diff --git a/app/main.py b/app/main.py
index 61798f0..d7a6caf 100644
--- a/app/main.py
+++ b/app/main.py
@@ -445,26 +445,62 @@ def _migrate_legacy_request(post: dict) -> dict:
return post
+
+public_client_downloads: dict[str, set[str]] = {}
+public_download_owner: dict[str, str] = {}
+
+
+def _register_public_download(sid: str, url: str):
+ if not sid or not url:
+ return
+ public_client_downloads.setdefault(sid, set()).add(url)
+ public_download_owner[url] = sid
+
+
+def _forget_public_download(url: str):
+ sid = public_download_owner.pop(url, None)
+ if not sid:
+ return
+ urls = public_client_downloads.get(sid)
+ if not urls:
+ return
+ urls.discard(url)
+ if not urls:
+ public_client_downloads.pop(sid, None)
+
+
+async def _emit_download_event(event: str, payload, *, url: str | None = None):
+ if not config.PUBLIC_MODE:
+ await sio.emit(event, payload)
+ return
+ if not url:
+ return
+ sid = public_download_owner.get(url)
+ if sid:
+ await sio.emit(event, payload, to=sid)
+
class Notifier(DownloadQueueNotifier):
async def added(self, dl):
log.info(f"Notifier: Download added - {dl.title}")
- await sio.emit('added', serializer.encode(dl))
+ await _emit_download_event('added', serializer.encode(dl), url=dl.url)
async def updated(self, dl):
log.debug(f"Notifier: Download updated - {dl.title}")
- await sio.emit('updated', serializer.encode(dl))
+ await _emit_download_event('updated', serializer.encode(dl), url=dl.url)
async def completed(self, dl):
log.info(f"Notifier: Download completed - {dl.title}")
- await sio.emit('completed', serializer.encode(dl))
+ await _emit_download_event('completed', serializer.encode(dl), url=dl.url)
+ _forget_public_download(dl.url)
async def canceled(self, id):
log.info(f"Notifier: Download canceled - {id}")
- await sio.emit('canceled', serializer.encode(id))
+ await _emit_download_event('canceled', serializer.encode(id), url=id)
+ _forget_public_download(id)
async def cleared(self, id):
log.info(f"Notifier: Download cleared - {id}")
- await sio.emit('cleared', serializer.encode(id))
+ await _emit_download_event('cleared', serializer.encode(id), url=id)
dqueue = DownloadQueue(config, Notifier())
app.on_startup.append(lambda app: dqueue.initialize())
@@ -725,6 +761,11 @@ async def add(request):
bool(o.get('folder')),
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'])
status = await dqueue.add(
o['url'],
o['download_type'],
@@ -979,6 +1020,17 @@ async def connect(sid, environ):
if config.YTDL_OPTIONS_FILE:
await sio.emit('ytdl_options_changed', serializer.encode(get_options_update_time()), to=sid)
+@sio.event
+async def disconnect(sid):
+ if not config.PUBLIC_MODE:
+ return
+ owned = list(public_client_downloads.pop(sid, set()))
+ for url in owned:
+ public_download_owner.pop(url, None)
+ if owned:
+ await dqueue.cancel(owned)
+
+
def get_custom_dirs():
cache_ttl_seconds = 5
now = asyncio.get_running_loop().time()
diff --git a/ui/src/app/app.html b/ui/src/app/app.html
index f5eb602..3b595a8 100644
--- a/ui/src/app/app.html
+++ b/ui/src/app/app.html
@@ -121,11 +121,13 @@
[disabled]="downloads.loading">
Download
+ @if (!isPublicMode()) {
+ }
}
diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts
index 60c3df6..8d1f9b3 100644
--- a/ui/src/app/services/downloads.service.ts
+++ b/ui/src/app/services/downloads.service.ts
@@ -152,7 +152,8 @@ export class DownloadsService {
const ce = payload.clipEnd?.trim();
if (cs) body['clip_start'] = cs;
if (ce) body['clip_end'] = ce;
- return this.http.post('add', body).pipe(
+ const headers = this.socket.ioSocket?.id ? { 'X-Client-Id': this.socket.ioSocket.id } : undefined;
+ return this.http.post('add', body, { headers }).pipe(
catchError(this.handleHTTPError)
);
}