diff --git a/backend/main.py b/backend/main.py index 9edf59b..636b05f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -66,6 +66,7 @@ def on_startup(): key TEXT PRIMARY KEY, value TEXT NOT NULL )""", + "ALTER TABLE user_settings ADD COLUMN cookies_file TEXT DEFAULT ''", "ALTER TABLE user_settings ADD COLUMN feed_weight_recency REAL DEFAULT 5.0", "ALTER TABLE user_settings ADD COLUMN feed_weight_affinity REAL DEFAULT 5.0", "ALTER TABLE user_settings ADD COLUMN feed_weight_channel REAL DEFAULT 5.0", @@ -142,6 +143,7 @@ def on_startup(): if first_user_settings: ytdlp_service.set_max_concurrent(first_user_settings.max_concurrent_downloads) ytdlp_service.set_cookies_browser(first_user_settings.cookies_browser or "") + ytdlp_service.set_cookies_file(first_user_settings.cookies_file or "") finally: db.close() diff --git a/backend/models.py b/backend/models.py index 6eb414b..fbf87de 100644 --- a/backend/models.py +++ b/backend/models.py @@ -109,6 +109,7 @@ class UserSettings(Base): mark_watched_at_percent = Column(Integer, default=90) # 50–100 auto_download_on_sync = Column(Boolean, default=False) cookies_browser = Column(String, default="") # chrome / firefox / etc., "" = disabled + cookies_file = Column(String, default="") # path to Netscape cookies.txt, "" = disabled theater_mode = Column(Boolean, default=False) discovery_regions = Column(String, default="US,SE") # comma-separated ISO country codes calm_mode = Column(Boolean, default=False) diff --git a/backend/routers/settings.py b/backend/routers/settings.py index 038a2a9..db5352b 100644 --- a/backend/routers/settings.py +++ b/backend/routers/settings.py @@ -22,6 +22,7 @@ class SettingsOut(BaseModel): mark_watched_at_percent: int auto_download_on_sync: bool cookies_browser: str = "" + cookies_file: str = "" theater_mode: bool = False discovery_regions: str = "US,SE" calm_mode: bool = False @@ -41,6 +42,7 @@ class SettingsPatch(BaseModel): mark_watched_at_percent: Optional[int] = Field(None, ge=50, le=100) auto_download_on_sync: Optional[bool] = None cookies_browser: Optional[str] = None + cookies_file: Optional[str] = None theater_mode: Optional[bool] = None discovery_regions: Optional[str] = None calm_mode: Optional[bool] = None @@ -91,6 +93,9 @@ def update_settings( if body.cookies_browser is not None and body.cookies_browser in VALID_BROWSERS: s.cookies_browser = body.cookies_browser ytdlp.set_cookies_browser(body.cookies_browser) + if body.cookies_file is not None: + s.cookies_file = body.cookies_file.strip() + ytdlp.set_cookies_file(body.cookies_file) if body.theater_mode is not None: s.theater_mode = body.theater_mode if body.discovery_regions is not None: diff --git a/backend/services/ytdlp.py b/backend/services/ytdlp.py index 79eebaf..a51906f 100644 --- a/backend/services/ytdlp.py +++ b/backend/services/ytdlp.py @@ -401,8 +401,11 @@ def predicted_file_path(video_id: str) -> Path: _SEMAPHORE = threading.Semaphore(3) _semaphore_lock = threading.Lock() _cookies_browser: str = "" +_cookies_file: str = "" _cookies_lock = threading.Lock() +_AUTO_COOKIES_PATHS = ["/data/cookies.txt"] + def set_max_concurrent(n: int) -> None: global _SEMAPHORE @@ -416,10 +419,27 @@ def set_cookies_browser(browser: str) -> None: _cookies_browser = browser.strip().lower() +def set_cookies_file(path: str) -> None: + global _cookies_file + with _cookies_lock: + _cookies_file = path.strip() + + def _cookie_args() -> list[str]: with _cookies_lock: + cf = _cookies_file b = _cookies_browser - return ["--cookies-from-browser", b] if b else [] + # Prefer explicit cookies file + if cf and Path(cf).exists(): + return ["--cookies", cf] + # Auto-detect cookies.txt in well-known Docker locations + for candidate in _AUTO_COOKIES_PATHS: + if Path(candidate).exists(): + return ["--cookies", candidate] + # Fall back to browser (works in local dev, not in Docker) + if b: + return ["--cookies-from-browser", b] + return [] def start_download( diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index bd3bb89..92e0ca1 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -241,9 +241,27 @@ export default function SettingsPage() { {/* YouTube authentication */}
+
+
+

Cookies file

+

+ Recommended for Docker. Export your YouTube cookies as cookies.txt using + the "Get cookies.txt LOCALLY" browser extension, then + place the file at /data/cookies.txt inside + the data volume — it will be picked up automatically. Or enter a custom path below. +

+
+ set({ cookies_file: e.target.value })} + className="bg-zinc-800 text-zinc-200 text-sm rounded-lg px-3 py-2 border border-zinc-700 focus:outline-none focus:border-accent font-mono placeholder:text-zinc-600 placeholder:font-sans" + /> +