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.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
22
app/main.py
22
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")
|
||||
|
||||
@@ -255,9 +255,23 @@
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><span>apoena</span> transcript</h1>
|
||||
<span id="device-badge" class="badge badge-loading">Loading...</span>
|
||||
<div style="display:flex;align-items:center;gap:0.6rem">
|
||||
<span id="device-badge" class="badge badge-loading">Loading...</span>
|
||||
<button id="settings-toggle" style="background:transparent;border:1px solid #2a2a38;color:#64748b;padding:0.3rem 0.6rem;font-size:0.8rem;font-weight:400">⚙ Settings</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Settings panel -->
|
||||
<div id="settings-panel" class="card" style="display:none;margin-bottom:1rem">
|
||||
<h2>YouTube Cookies</h2>
|
||||
<p style="font-size:0.82rem;color:#64748b;margin-bottom:0.8rem">Upload a <code>cookies.txt</code> (Netscape format) to bypass YouTube bot detection. Export it from Chrome using the <em>Get cookies.txt LOCALLY</em> extension while logged in to YouTube.</p>
|
||||
<div style="display:flex;gap:0.6rem;align-items:center;flex-wrap:wrap">
|
||||
<input type="file" id="cookies-input" accept=".txt,text/plain" style="font-size:0.85rem;color:#94a3b8" />
|
||||
<button id="cookies-btn" disabled>Upload</button>
|
||||
</div>
|
||||
<div id="cookies-status" style="font-size:0.82rem;color:#64748b;margin-top:0.6rem"></div>
|
||||
</div>
|
||||
|
||||
<!-- Model status -->
|
||||
<div id="model-status" class="model-status">
|
||||
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;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user