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:
+20
-2
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user