From 5f5ca52b951443fee6ac88e145b166e54c4efd48 Mon Sep 17 00:00:00 2001 From: inputnoise Date: Mon, 25 May 2026 21:01:02 +0200 Subject: [PATCH] =?UTF-8?q?Add=20cookies.txt=20upload=20UI=20=E2=80=94=20d?= =?UTF-8?q?rag/drop=20or=20click=20to=20upload,=20stored=20in=20data=20vol?= =?UTF-8?q?ume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routers/settings.py | 47 ++++++++++- frontend/src/api/index.js | 6 ++ frontend/src/pages/Settings.jsx | 139 +++++++++++++++++++++++--------- 3 files changed, 155 insertions(+), 37 deletions(-) diff --git a/backend/routers/settings.py b/backend/routers/settings.py index db5352b..af6fd17 100644 --- a/backend/routers/settings.py +++ b/backend/routers/settings.py @@ -1,9 +1,11 @@ +from pathlib import Path from typing import Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, UploadFile, File, HTTPException from pydantic import BaseModel, Field from sqlalchemy.orm import Session from ..auth_utils import get_current_user +from ..config import settings from ..database import get_db from ..models import User, UserSettings from ..services import ytdlp @@ -120,3 +122,46 @@ def update_settings( db.commit() db.refresh(s) return s + + +def _cookies_path() -> Path: + db_file = settings.database_url.replace("sqlite:///", "") + return Path(db_file).parent / "cookies.txt" + + +@router.post("/cookies-file", response_model=SettingsOut) +async def upload_cookies_file( + file: UploadFile = File(...), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + if not file.filename or not file.filename.endswith(".txt"): + raise HTTPException(status_code=400, detail="Upload a .txt cookies file") + content = await file.read() + if len(content) > 5 * 1024 * 1024: + raise HTTPException(status_code=400, detail="File too large (max 5 MB)") + path = _cookies_path() + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + s = _get_or_create(db, current_user.id) + s.cookies_file = str(path) + ytdlp.set_cookies_file(str(path)) + db.commit() + db.refresh(s) + return s + + +@router.delete("/cookies-file", response_model=SettingsOut) +def delete_cookies_file( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + path = _cookies_path() + if path.exists(): + path.unlink() + s = _get_or_create(db, current_user.id) + s.cookies_file = "" + ytdlp.set_cookies_file("") + db.commit() + db.refresh(s) + return s diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index ce0d805..d13001c 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -101,6 +101,12 @@ export const exportData = () => api.get("/export", { responseType: "blob" }); // Settings export const getSettings = () => api.get("/settings"); export const updateSettings = (data) => api.patch("/settings", data); +export const uploadCookiesFile = (file) => { + const form = new FormData(); + form.append("file", file); + return api.post("/settings/cookies-file", form); +}; +export const deleteCookiesFile = () => api.delete("/settings/cookies-file"); // Discovery export const getDiscovery = (offset = 0, limit = 50) => diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 92e0ca1..38016a5 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -1,6 +1,6 @@ -import { useState } from "react"; +import { useState, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { getSettings, updateSettings, exportData, getAdminUsers, deleteAdminUser, getAdminConfig, updateAdminConfig } from "../api"; +import { getSettings, updateSettings, exportData, uploadCookiesFile, deleteCookiesFile, getAdminUsers, deleteAdminUser, getAdminConfig, updateAdminConfig } from "../api"; import { useAuth } from "../hooks/useAuth"; const REGION_OPTIONS = [ @@ -195,6 +195,106 @@ function AdminSection() { ); } +function CookiesSection({ s, qc, set }) { + const fileRef = useRef(null); + const [dragOver, setDragOver] = useState(false); + + const uploadMut = useMutation({ + mutationFn: (file) => uploadCookiesFile(file), + onSuccess: (res) => qc.setQueryData(["settings"], res.data), + }); + + const deleteMut = useMutation({ + mutationFn: deleteCookiesFile, + onSuccess: (res) => qc.setQueryData(["settings"], res.data), + }); + + const handleFile = (file) => { + if (file) uploadMut.mutate(file); + }; + + const hasFile = !!s?.cookies_file; + + return ( +
+
+
+

Cookies file

+

+ Export your YouTube cookies as cookies.txt using + the "Get cookies.txt LOCALLY" browser extension, + then upload it here. Required for age-restricted or bot-detected videos. +

+
+ {hasFile ? ( +
+
+ + + + {s.cookies_file} +
+
+ + +
+
+ ) : ( +
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFile(e.dataTransfer.files[0]); }} + onClick={() => fileRef.current?.click()} + className={`flex flex-col items-center justify-center gap-2 border-2 border-dashed rounded-xl px-4 py-6 cursor-pointer transition-colors ${ + dragOver ? "border-accent bg-accent/5" : "border-zinc-700 hover:border-zinc-500" + }`} + > + {uploadMut.isPending ? ( +
+ ) : ( + <> + + + +

Drop cookies.txt here or click to upload

+ + )} + {uploadMut.isError && ( +

{uploadMut.error?.response?.data?.detail ?? "Upload failed"}

+ )} +
+ )} + handleFile(e.target.files[0])} /> +
+ + + +
+ ); +} + export default function SettingsPage() { const { user } = useAuth(); const qc = useQueryClient(); @@ -240,40 +340,7 @@ 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" - /> -
- - - -
+ {/* Download quality */}