diff --git a/src/components/DoomScrollView.tsx b/src/components/DoomScrollView.tsx index d677ed8..e9c4c38 100644 --- a/src/components/DoomScrollView.tsx +++ b/src/components/DoomScrollView.tsx @@ -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(() => { diff --git a/src/components/tv/TvView.tsx b/src/components/tv/TvView.tsx index b862b1f..c5fb257 100644 --- a/src/components/tv/TvView.tsx +++ b/src/components/tv/TvView.tsx @@ -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) + ) + ) + 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)