From 57910462e45dc84c30b480cc7340158512fecfbc Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Mon, 23 Mar 2026 19:32:51 +0100 Subject: [PATCH] feat: add YouTube cookies upload via web UI Adds a Settings panel to upload a cookies.txt file directly from the browser, persisted in a named Docker volume. yt-dlp uses the file when present to bypass YouTube bot detection. --- app/config.py | 1 + app/downloader.py | 7 ++++- app/main.py | 22 +++++++++++++++- app/static/index.html | 59 ++++++++++++++++++++++++++++++++++++++++++- docker-compose.yml | 5 ++++ 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/app/config.py b/app/config.py index 4c9f011..36674b0 100644 --- a/app/config.py +++ b/app/config.py @@ -3,6 +3,7 @@ from pydantic_settings import BaseSettings class Settings(BaseSettings): max_upload_size_mb: int = 500 + yt_dlp_cookies_file: str = "" @property def max_upload_size_bytes(self) -> int: diff --git a/app/downloader.py b/app/downloader.py index 24d2cbb..04ce646 100644 --- a/app/downloader.py +++ b/app/downloader.py @@ -2,6 +2,7 @@ import asyncio import uuid import os from pathlib import Path +from app.config import settings AUDIO_TMP_DIR = "/tmp/apoena-audio" @@ -21,9 +22,13 @@ async def extract_audio(url: str) -> Path: "--audio-quality", "128K", "--format", "bestaudio/best", "--output", outtmpl, - url, ] + if settings.yt_dlp_cookies_file: + cmd += ["--cookies", settings.yt_dlp_cookies_file] + + cmd.append(url) + proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, diff --git a/app/main.py b/app/main.py index 704373f..3e587ad 100644 --- a/app/main.py +++ b/app/main.py @@ -2,11 +2,12 @@ import os from contextlib import asynccontextmanager from pathlib import Path -from fastapi import FastAPI, HTTPException, BackgroundTasks +from fastapi import FastAPI, HTTPException, BackgroundTasks, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel from app import downloader +from app.config import settings STATIC_DIR = Path(__file__).parent / "static" @@ -74,6 +75,25 @@ def _delete_file(path): pass +@app.post("/admin/cookies") +async def upload_cookies(file: UploadFile): + if not settings.yt_dlp_cookies_file: + raise HTTPException(status_code=500, detail="YT_DLP_COOKIES_FILE not configured") + cookies_path = Path(settings.yt_dlp_cookies_file) + cookies_path.parent.mkdir(parents=True, exist_ok=True) + content = await file.read() + cookies_path.write_bytes(content) + return {"status": "ok", "path": str(cookies_path)} + + +@app.get("/admin/cookies/status") +async def cookies_status(): + if not settings.yt_dlp_cookies_file: + return {"present": False} + path = Path(settings.yt_dlp_cookies_file) + return {"present": path.exists(), "size": path.stat().st_size if path.exists() else 0} + + @app.get("/") async def index(): return FileResponse(STATIC_DIR / "index.html") diff --git a/app/static/index.html b/app/static/index.html index 437ba01..55aaaa6 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -255,9 +255,23 @@

apoena transcript

- Loading... +
+ Loading... + +
+ + +
Loading model — first visit downloads ~100 MB, then it's cached locally. @@ -684,6 +698,49 @@ function pad(n, len = 2) { return String(Math.floor(n)).padStart(len, '0'); } if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } + +// ── Settings (cookies upload) ─────────────────────────────────────────────── +const settingsToggle = document.getElementById('settings-toggle'); +const settingsPanel = document.getElementById('settings-panel'); +const cookiesInput = document.getElementById('cookies-input'); +const cookiesBtn = document.getElementById('cookies-btn'); +const cookiesStatus = document.getElementById('cookies-status'); + +settingsToggle.addEventListener('click', async () => { + const open = settingsPanel.style.display !== 'none'; + settingsPanel.style.display = open ? 'none' : 'block'; + if (!open) { + const res = await fetch('/admin/cookies/status').then(r => r.json()).catch(() => null); + if (res) { + cookiesStatus.textContent = res.present + ? `✓ Cookies file present (${(res.size / 1024).toFixed(1)} KB)` + : 'No cookies file uploaded yet.'; + } + } +}); + +cookiesInput.addEventListener('change', () => { + cookiesBtn.disabled = !cookiesInput.files.length; +}); + +cookiesBtn.addEventListener('click', async () => { + const file = cookiesInput.files[0]; + if (!file) return; + const form = new FormData(); + form.append('file', file); + cookiesBtn.disabled = true; + cookiesStatus.textContent = 'Uploading…'; + try { + const res = await fetch('/admin/cookies', { method: 'POST', body: form }); + if (!res.ok) throw new Error((await res.json()).detail || res.statusText); + cookiesStatus.textContent = '✓ Cookies uploaded successfully.'; + cookiesInput.value = ''; + } catch (err) { + cookiesStatus.textContent = 'Error: ' + err.message; + } finally { + cookiesBtn.disabled = false; + } +}); diff --git a/docker-compose.yml b/docker-compose.yml index d5ad63d..085cec3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,13 @@ services: build: . environment: - MAX_UPLOAD_SIZE_MB=500 + - YT_DLP_COOKIES_FILE=/data/yt-cookies.txt volumes: - /tmp/apoena-audio:/tmp/apoena-audio + - apoena-data:/data + +volumes: + apoena-data: labels: - 'traefik.http.middlewares.myauth.basicauth.users=julien:$$2y$$05$$1AWK6NyxNNnAX4opcBftnutdMEPTH422uKkNs/NoB3S8i9Bze4GSS' - 'traefik.http.routers.http-0-p10gcndwy2krf8zu7swn3i2w-app.middlewares=myauth'