Fix download status handling and the blank page after download

- Never let a non-authoritative MeTube 'updated' event downgrade a
  terminal job (it refilled the progress bar after completion).
- Self-heal stale active jobs against MeTube history on status polls,
  so a missed event no longer leaves a job stuck in 'queued'.
- Build YT.Player on an imperatively created child div: React keeps
  owning the container, so switching to the local copy after a
  download no longer throws removeChild and blanks the page.
- Autoplay on open and seek controls; subtitle experiment reverted.
This commit is contained in:
vrubelroman 2026-09-27 23:36:09 +00:00
parent b1faeb3729
commit 43adec5224
9 changed files with 758 additions and 5 deletions

View file

@ -185,6 +185,7 @@ button, .button-primary, .button-secondary, .button-quiet, .button-danger { tran
.player-wrapper iframe, .player-wrapper video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: #000; }
.player-wrapper video { touch-action: manipulation; }
.yt-player-host { position: absolute; inset: 0; }
.yt-player-host > div { width: 100%; height: 100%; }
.video-page-meta-row { display: flex; align-items: center; justify-content: space-between; gap: 8px 16px; flex-wrap: wrap; margin: 11px 0 22px; }
.video-page-meta-row .video-page-source { margin: 0; }
.video-page-source { color: var(--muted); font-size: 12px; }

View file

@ -153,12 +153,15 @@ function Player({ video }: Props) {
}, [useLocal])
// YouTube-ветка: динамическая идемпотентная загрузка IFrame Player API,
// создание YT.Player в host-div с nocookie-хостом, фолбэк на plain-iframe по таймауту.
// создание YT.Player на императивном дочернем div в host-div (YT.Player заменяет
// переданный элемент iframe'ом, React управляет только контейнером), nocookie-хост,
// фолбэк на plain-iframe по таймауту.
useEffect(() => {
if (useLocal) return
let cancelled = false
let timedOut = false
let timeoutId: number | null = null
let child: HTMLDivElement | null = null
ytPlayerRef.current?.destroy()
ytPlayerRef.current = null
@ -167,6 +170,8 @@ function Player({ video }: Props) {
timedOut = true
ytPlayerRef.current?.destroy()
ytPlayerRef.current = null
if (child?.parentNode) child.remove()
child = null
if (!cancelled) setYtApiStatus('timeout')
}, YT_API_TIMEOUT_MS)
@ -175,7 +180,9 @@ function Player({ video }: Props) {
if (cancelled || timedOut || !window.YT?.Player) return
const host = ytHostRef.current
if (!host) return
const player = new window.YT.Player(host, {
child = document.createElement('div')
host.appendChild(child)
const player = new window.YT.Player(child, {
videoId: video.youtube_video_id,
host: 'https://www.youtube-nocookie.com',
playerVars: { playsinline: 1, autoplay: 1 },
@ -206,6 +213,8 @@ function Player({ video }: Props) {
if (timeoutId !== null) window.clearTimeout(timeoutId)
ytPlayerRef.current?.destroy()
ytPlayerRef.current = null
if (child?.parentNode) child.remove()
child = null
}
}, [useLocal, video.youtube_video_id])