Fix stale preview reels when switching between series

Episode tiles were keyed by season/episode index only, so switching
series reused the DOM: text bindings updated but <video> elements were
kept, and browsers ignore patched <source> children without load() —
the previous series' reels kept playing.

- Key SeriesFullView by item id so a series switch fully remounts it.
- Include a seriesIdentity (id, else root_id+title) in episode tile
  keys so tiles/videos remount even on an in-place series swap.
- Reset episode cursor, audio owner and preview startup timers when
  the series identity changes, and re-gate video mounting.
This commit is contained in:
2026-09-23 21:35:03 +00:00
parent bb70279e61
commit c41504e70c
2 changed files with 23 additions and 2 deletions
+1
View File
@@ -2,6 +2,7 @@
<!-- Full screen view for series -->
<SeriesFullView
v-if="item.type === 'series'"
:key="item.id"
:series="item.data as Series"
:all-movies="allMovies"
:focus-episode="focusEpisode"
+22 -2
View File
@@ -112,7 +112,7 @@
<div class="episodes-grid">
<div
v-for="(episode, eIndex) in selectedSeason?.episodes || []"
:key="`${selectedSeasonIndex}-${episode.episode_number}`"
:key="`${seriesIdentity}-${selectedSeasonIndex}-${episode.episode_number}`"
class="episode-tile"
:class="{ 'episode-tile--ahead': isEpisodeAhead(eIndex) }"
v-bind="getEpisodeNavAttrs(selectedSeasonIndex, eIndex)"
@@ -258,7 +258,7 @@ import EpisodeReleaseMenu from "./EpisodeReleaseMenu.vue"
import { sortTorrentsByPreference } from "../composables/useSettings"
const props = defineProps<{
series: Series & { root_id?: string | null }
series: Series & { id?: string; root_id?: string | null }
allMovies: MovieUi[]
focusEpisode?: { seasonNumber: number; episodeNumber: number } | null
resumePoint?: SeriesResumePoint | null
@@ -297,6 +297,15 @@ const selectedSeason = computed<Season | null>(
() => props.series.seasons[selectedSeasonIndex.value] || null,
)
// Stable identity of the displayed series. Episode tiles carry it in their
// :key so swapping to another series remounts the tiles (and their <video>
// elements) instead of patching <source> children — which browsers ignore,
// leaving the previous series' preview reels playing.
const seriesIdentity = computed(() => {
const s = props.series
return s.id ?? `${s.root_id ?? ""}:${s.title ?? ""}`
})
function selectSeason(index: number) {
if (index < 0 || index >= props.series.seasons.length) return
if (selectedSeasonIndex.value === index) return
@@ -1409,6 +1418,17 @@ watch(
},
)
// Series swapped under a reused component instance: tiles remount via the
// :key, but per-episode state keyed by episode index alone (audio owner,
// cursor, startup timers) survives — reset it so the old series' playback
// and audio don't bleed into the new one.
watch(seriesIdentity, () => {
episodeCursorIndex.value = null
setAudioOwner(null)
stopEpisodePreviews()
scheduleEpisodeMediaReady()
})
// Episodes past the spoiler threshold (cursor or continue point) are faded;
// stop their playback too.
watch([episodeCursorIndex, resumePointGlobalIndex], () => {