performance-stability #14

Merged
gpatti merged 2 commits from performance-stability into main 2026-04-07 00:22:00 +00:00
2 changed files with 39 additions and 10 deletions
Showing only changes of commit f08950f456 - Show all commits

View File

@@ -73,9 +73,20 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
setTimeout(() => { cooldownRef.current = false }, 300) setTimeout(() => { cooldownRef.current = false }, 300)
}, [goNext, goPrev]) }, [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(() => { useEffect(() => {
setIsPaused(false) 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]) }, [current?.url])
// Sync muted imperatively — React's muted prop is not reliable // 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 if (videoRef.current) videoRef.current.muted = localMuted
}, [localMuted, current?.url]) }, [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(() => { useEffect(() => {
if (!videoRef.current) return if (!videoRef.current) return
if (isPaused) { if (isPaused) {
@@ -91,7 +103,7 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
} else { } else {
videoRef.current.play().catch(() => {}) videoRef.current.play().catch(() => {})
} }
}, [isPaused, current?.url]) }, [isPaused])
// Auto-play timer — resets on each new item, pause, enable/disable, or interval change // Auto-play timer — resets on each new item, pause, enable/disable, or interval change
useEffect(() => { useEffect(() => {

View File

@@ -2,7 +2,7 @@
import { useEffect, useRef, useState, useCallback } from 'react' import { useEffect, useRef, useState, useCallback } from 'react'
import type { TvSeries, TvSeason, TvEpisode } from '@/types' import type { TvSeries, TvSeason, TvEpisode } from '@/types'
import type { DirectoryListing } from '@/types'
import FilterPanel from '@/components/FilterPanel' import FilterPanel from '@/components/FilterPanel'
import VideoPlayerModal from '@/components/mixed/VideoPlayerModal' import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
import TagSelector from '@/components/tags/TagSelector' import TagSelector from '@/components/tags/TagSelector'
@@ -190,13 +190,30 @@ export default function TvView({ libraryId }: Props) {
mediaType: 'video' as const, mediaType: 'video' as const,
})) }))
} else { } else {
// No filters — use full recursive browse // No filters — fetch all episodes via the TV API hierarchy
const data: DirectoryListing = await fetch( const allSeries: TvSeries[] = await fetch(
`/api/browse?libraryId=${encodeURIComponent(libraryId)}&path=&recursive=true` `/api/tv?libraryId=${encodeURIComponent(libraryId)}`
).then((r) => r.json()) ).then((r) => r.json())
items = data.entries const episodeLists = await Promise.all(
.filter((e) => e.type === 'file' && e.mediaType === 'video' && e.url) allSeries.map(async (s) => {
.map((e) => ({ url: e.url!, name: e.name, mediaType: 'video' as const })) 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) setDoomScrollItems(items)
setDoomScrollActive(true) setDoomScrollActive(true)