feat: sign in to YouTube with browser cookies

Answers the bot challenge from inside the app. Settings gains a browser
picker listing only the browsers actually installed; choosing one passes
--cookies-from-browser to every yt-dlp call, so YouTube sees an
authenticated session. Verified against a live block: refused without
cookies, resolved with them.

Arc is Chromium underneath but is not one of yt-dlp's known names, so it
is addressed by its profile directory instead.

yt-dlp's errors are translated into something actionable. The stock bot
message points at command-line flags a user cannot type; it now names
the setting that fixes it, and Safari's protected cookie store gets its
own message about Full Disk Access rather than a bare 'Operation not
permitted'.

A Check connection button reports whether YouTube is reachable, testing
against the newest video in the feed — the hardcoded id it used at first
had been taken down, so it reported a dead video rather than the
connection.
This commit is contained in:
vincent
2026-08-29 14:21:35 +02:00
parent 7aae080e46
commit 22c7a3a5ec
5 changed files with 257 additions and 15 deletions
+20 -2
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
cancelDownload, deleteAllDownloads, deleteDownload, downloadVideo,
fetchDurations, onRefreshProgress, refreshFeeds,
fetchDurations, onRefreshProgress, refreshFeeds, setCookieSource,
} from "./api";
import Player from "./components/Player";
import Settings from "./components/Settings";
@@ -55,6 +55,13 @@ export default function App() {
const [showSettings, setShowSettings] = useState(false);
// Index into the current feed, so the player can step through it.
const [playingIndex, setPlayingIndex] = useState<number | null>(null);
const [browser, setBrowser] = useState(() => {
try {
return localStorage.getItem("flighttube.browser") ?? "";
} catch {
return "";
}
});
const [subLang, setSubLang] = useState(() => {
try {
const stored = localStorage.getItem("flighttube.subLang");
@@ -84,6 +91,14 @@ export default function App() {
const [toast, setToast] = useState<string | null>(null);
const [failure, setFailure] = useState<string | null>(null);
// The backend holds no state across launches, so the stored choice has to be
// handed back to it before the first yt-dlp call.
useEffect(() => {
setCookieSource(browser).catch(() => {
/* falls back to no cookies */
});
}, [browser]);
const { mode, setMode } = useAppearance();
const windowFullscreen = useWindowFullscreen();
const { online, reachable, forcedOffline, setForcedOffline, probe } = useConnectivity();
@@ -104,13 +119,14 @@ export default function App() {
localStorage.setItem("flighttube.quality", quality);
localStorage.setItem("flighttube.streamQuality", streamQuality);
localStorage.setItem("flighttube.subLang", subLang);
localStorage.setItem("flighttube.browser", browser);
localStorage.setItem("flighttube.downloadedOnly", downloadedOnly ? "1" : "0");
localStorage.setItem("flighttube.hideShorts", hideShorts ? "1" : "0");
localStorage.setItem("flighttube.sidebarHidden", sidebarHidden ? "1" : "0");
} catch {
/* storage blocked */
}
}, [view, quality, streamQuality, subLang, downloadedOnly, hideShorts, sidebarHidden]);
}, [view, quality, streamQuality, subLang, browser, downloadedOnly, hideShorts, sidebarHidden]);
// Offline, the only videos that can be played are the ones already on disk,
// so the feed collapses to those regardless of the toggle.
@@ -459,6 +475,8 @@ export default function App() {
onStreamQuality={setStreamQuality}
subLang={subLang}
onSubLang={setSubLang}
browser={browser}
onBrowser={setBrowser}
onError={setFailure}
onImported={(n) => {
reload();