Fix Doom Scroll mode bugs in TV libraries and video autoplay
TV library fix: the unfiltered Doom Scroll path was calling /api/browse which explicitly rejects non-mixed libraries with a 400 error, leaving the item list empty. Replace it with the same TV API hierarchy fetch already used by the filtered path (series → seasons → episodes), matching how the rest of the TV library is loaded. Autoplay fix (all library types): two interacting bugs caused videos to silently stall on navigation. First, the play/pause effect had current?.url in its deps, so navigating while paused would call pause() on the freshly-mounted video element before the isPaused reset could take effect. Second, browser autoplay policy blocks unmuted play() calls and the rejection was silently swallowed with no recovery. Fix by merging the isPaused reset and the play() call into one navigation effect, and adding a muted fallback on rejection so playback always starts — the user can unmute manually afterward. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -73,9 +73,20 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
|
||||
setTimeout(() => { cooldownRef.current = false }, 300)
|
||||
}, [goNext, goPrev])
|
||||
|
||||
// Reset pause when switching items
|
||||
// On navigation to a new item: reset pause state and start playing.
|
||||
// Merging the reset + play() into one effect prevents the old isPaused=true
|
||||
// value from calling pause() on the freshly-mounted video element before the
|
||||
// reset fires. If autoplay is blocked by browser policy (common when unmuted),
|
||||
// fall back to muted and retry — the user can unmute manually afterward.
|
||||
useEffect(() => {
|
||||
setIsPaused(false)
|
||||
if (!videoRef.current) return
|
||||
videoRef.current.play().catch(() => {
|
||||
if (!videoRef.current) return
|
||||
videoRef.current.muted = true
|
||||
setLocalMuted(true)
|
||||
videoRef.current.play().catch(() => {})
|
||||
})
|
||||
}, [current?.url])
|
||||
|
||||
// Sync muted imperatively — React's muted prop is not reliable
|
||||
@@ -83,7 +94,8 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
|
||||
if (videoRef.current) videoRef.current.muted = localMuted
|
||||
}, [localMuted, current?.url])
|
||||
|
||||
// Sync play/pause imperatively
|
||||
// Sync play/pause imperatively for user-initiated pause/unpause only.
|
||||
// current?.url is intentionally excluded: navigation is handled above.
|
||||
useEffect(() => {
|
||||
if (!videoRef.current) return
|
||||
if (isPaused) {
|
||||
@@ -91,7 +103,7 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
|
||||
} else {
|
||||
videoRef.current.play().catch(() => {})
|
||||
}
|
||||
}, [isPaused, current?.url])
|
||||
}, [isPaused])
|
||||
|
||||
// Auto-play timer — resets on each new item, pause, enable/disable, or interval change
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import type { TvSeries, TvSeason, TvEpisode } from '@/types'
|
||||
import type { DirectoryListing } from '@/types'
|
||||
|
||||
import FilterPanel from '@/components/FilterPanel'
|
||||
import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
|
||||
import TagSelector from '@/components/tags/TagSelector'
|
||||
@@ -190,13 +190,30 @@ export default function TvView({ libraryId }: Props) {
|
||||
mediaType: 'video' as const,
|
||||
}))
|
||||
} else {
|
||||
// No filters — use full recursive browse
|
||||
const data: DirectoryListing = await fetch(
|
||||
`/api/browse?libraryId=${encodeURIComponent(libraryId)}&path=&recursive=true`
|
||||
// No filters — fetch all episodes via the TV API hierarchy
|
||||
const allSeries: TvSeries[] = await fetch(
|
||||
`/api/tv?libraryId=${encodeURIComponent(libraryId)}`
|
||||
).then((r) => r.json())
|
||||
items = data.entries
|
||||
.filter((e) => e.type === 'file' && e.mediaType === 'video' && e.url)
|
||||
.map((e) => ({ url: e.url!, name: e.name, mediaType: 'video' as const }))
|
||||
const episodeLists = await Promise.all(
|
||||
allSeries.map(async (s) => {
|
||||
const seasons: TvSeason[] = await fetch(
|
||||
`/api/tv?libraryId=${encodeURIComponent(libraryId)}&seriesId=${encodeURIComponent(s.id)}`
|
||||
).then((r) => r.json())
|
||||
const seasonEps = await Promise.all(
|
||||
seasons.map((season) =>
|
||||
fetch(
|
||||
`/api/tv?libraryId=${encodeURIComponent(libraryId)}&seriesId=${encodeURIComponent(s.id)}&seasonId=${encodeURIComponent(season.id)}`
|
||||
).then((r) => r.json() as Promise<TvEpisode[]>)
|
||||
)
|
||||
)
|
||||
return seasonEps.flat()
|
||||
})
|
||||
)
|
||||
items = episodeLists.flat().map((ep) => ({
|
||||
url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(ep.videoPath)}`,
|
||||
name: ep.title,
|
||||
mediaType: 'video' as const,
|
||||
}))
|
||||
}
|
||||
setDoomScrollItems(items)
|
||||
setDoomScrollActive(true)
|
||||
|
||||
Reference in New Issue
Block a user