feat: durations, clean subtitles, icon buttons
Subtitles rendered pinned to the left edge and clipped, in half-grey karaoke text: YouTube's auto-captions carry align:start position:0% on every cue plus inline <00:00:12.480><c>word</c> timing tags. Both are now stripped after download, and existing downloads are tidied the first time they are listed. Thumbnails show video length. The Atom feed carries no duration, so it is read from the watch page and cached — but only a trickle. The first version fetched 24 pages every 12 seconds at six concurrent, roughly two requests a second sustained, and YouTube answered by challenging the whole IP: 'Sign in to confirm you are not a bot', which broke streaming and downloads too. It is now four pages every five minutes, one at a time, and stops for the session the moment a batch is refused. A subtitle chosen in the player becomes the stored preference, so the next video matches without going back to Settings. The back and download buttons are square icon buttons; the back glyph was off-centre because px-2 beat the p-0 meant to clear it.
This commit is contained in:
+32
-1
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
cancelDownload, deleteAllDownloads, deleteDownload, downloadVideo,
|
||||
onRefreshProgress, refreshFeeds,
|
||||
fetchDurations, onRefreshProgress, refreshFeeds,
|
||||
} from "./api";
|
||||
import Player from "./components/Player";
|
||||
import Settings from "./components/Settings";
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
const TOAST_MS = 2400;
|
||||
/** How often to pull new videos while online, so the feed stays live. */
|
||||
const AUTO_REFRESH_MS = 10 * 60 * 1000;
|
||||
/**
|
||||
* Video lengths trickle in. This was once every 12s and it got the whole IP
|
||||
* challenged by YouTube, which broke playback and downloads too — the feed
|
||||
* being fully annotated is not worth that.
|
||||
*/
|
||||
const DURATION_FILL_MS = 5 * 60 * 1000;
|
||||
|
||||
function remembered(key: string): boolean {
|
||||
try {
|
||||
@@ -218,6 +224,30 @@ export default function App() {
|
||||
return () => clearInterval(id);
|
||||
}, [online, refreshing, playingIndex, doRefresh]);
|
||||
|
||||
// The Atom feed carries no duration, so lengths are looked up a batch at a
|
||||
// time in the background and cached. Paused while the player is open.
|
||||
useEffect(() => {
|
||||
if (!online) return;
|
||||
let stop = false;
|
||||
// Set when the backend reports it is being refused, so we stop for the
|
||||
// rest of the session rather than making the block worse.
|
||||
let refused = false;
|
||||
const tick = async () => {
|
||||
if (stop || refused || playingIndex != null) return;
|
||||
try {
|
||||
if ((await fetchDurations()) > 0 && !stop) await reload();
|
||||
} catch {
|
||||
refused = true;
|
||||
}
|
||||
};
|
||||
void tick();
|
||||
const id = setInterval(tick, DURATION_FILL_MS);
|
||||
return () => {
|
||||
stop = true;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [online, playingIndex, reload]);
|
||||
|
||||
// Refresh once on launch, as soon as there is a connection and something to
|
||||
// refresh, so the feed is current without anyone pressing anything.
|
||||
const launched = useRef(false);
|
||||
@@ -378,6 +408,7 @@ export default function App() {
|
||||
total={items.length}
|
||||
maxHeight={streamQuality === "best" ? null : Number(streamQuality)}
|
||||
subLang={subLang}
|
||||
onSubLang={setSubLang}
|
||||
titleBarInset={titleBarInset}
|
||||
onPrev={
|
||||
stepFrom(playingIndex, -1) != null
|
||||
|
||||
Reference in New Issue
Block a user