Add cookies.txt upload UI — drag/drop or click to upload, stored in data volume

This commit is contained in:
inputnoise
2026-05-25 21:01:02 +02:00
parent 56dd5f8360
commit 5f5ca52b95
3 changed files with 155 additions and 37 deletions

View File

@@ -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 (
<Section title="YouTube authentication">
<div className="px-5 py-4 flex flex-col gap-3">
<div>
<p className="text-sm font-medium text-zinc-200">Cookies file</p>
<p className="text-xs text-zinc-500 mt-0.5">
Export your YouTube cookies as <span className="font-mono text-zinc-400 text-[11px]">cookies.txt</span> using
the <span className="text-zinc-400">"Get cookies.txt LOCALLY"</span> browser extension,
then upload it here. Required for age-restricted or bot-detected videos.
</p>
</div>
{hasFile ? (
<div className="flex items-center justify-between gap-3 bg-zinc-800 rounded-xl px-4 py-3">
<div className="flex items-center gap-2 min-w-0">
<svg className="w-4 h-4 text-green-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-xs text-zinc-300 font-mono truncate">{s.cookies_file}</span>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => fileRef.current?.click()}
className="text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
>
replace
</button>
<button
onClick={() => deleteMut.mutate()}
disabled={deleteMut.isPending}
className="text-xs text-red-400 hover:text-red-300 transition-colors"
>
remove
</button>
</div>
</div>
) : (
<div
onDragOver={(e) => { 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 ? (
<div className="w-5 h-5 border-2 border-accent border-t-transparent rounded-full animate-spin" />
) : (
<>
<svg className="w-6 h-6 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
<p className="text-xs text-zinc-500">Drop <span className="font-mono text-zinc-400">cookies.txt</span> here or click to upload</p>
</>
)}
{uploadMut.isError && (
<p className="text-xs text-red-400">{uploadMut.error?.response?.data?.detail ?? "Upload failed"}</p>
)}
</div>
)}
<input ref={fileRef} type="file" accept=".txt" className="hidden" onChange={(e) => handleFile(e.target.files[0])} />
</div>
<Row
label="Browser cookies"
hint="Only works outside Docker. Pass cookies from a local browser install."
>
<select
value={s?.cookies_browser ?? ""}
onChange={(e) => set({ cookies_browser: e.target.value })}
className="bg-zinc-800 text-zinc-200 text-sm rounded-lg px-3 py-1.5 border border-zinc-700 focus:outline-none focus:border-accent"
>
{BROWSER_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</Row>
</Section>
);
}
export default function SettingsPage() {
const { user } = useAuth();
const qc = useQueryClient();
@@ -240,40 +340,7 @@ export default function SettingsPage() {
</Section>
{/* YouTube authentication */}
<Section title="YouTube authentication">
<div className="px-5 py-4 flex flex-col gap-3">
<div>
<p className="text-sm font-medium text-zinc-200">Cookies file</p>
<p className="text-xs text-zinc-500 mt-0.5">
Recommended for Docker. Export your YouTube cookies as <span className="text-zinc-400 font-mono text-[11px]">cookies.txt</span> using
the <span className="text-zinc-400">"Get cookies.txt LOCALLY"</span> browser extension, then
place the file at <span className="text-zinc-400 font-mono text-[11px]">/data/cookies.txt</span> inside
the data volume it will be picked up automatically. Or enter a custom path below.
</p>
</div>
<input
type="text"
placeholder="e.g. /data/cookies.txt"
value={s?.cookies_file ?? ""}
onChange={(e) => 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"
/>
</div>
<Row
label="Browser cookies"
hint="Only works outside Docker. Pass cookies from a local browser install to bypass bot detection."
>
<select
value={s?.cookies_browser ?? ""}
onChange={(e) => set({ cookies_browser: e.target.value })}
className="bg-zinc-800 text-zinc-200 text-sm rounded-lg px-3 py-1.5 border border-zinc-700 focus:outline-none focus:border-accent"
>
{BROWSER_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</Row>
</Section>
<CookiesSection s={s} qc={qc} set={set} />
{/* Download quality */}
<Section title="Download quality">