fix: update global index fallback logic

fixes #1027
This commit is contained in:
tapframe 2026-05-12 12:38:30 +05:30
parent ee66440bf5
commit 9056716c06
4 changed files with 63 additions and 6 deletions

View file

@ -98,11 +98,14 @@ internal fun MetaDetails.nextReleasedEpisodeAfter(
// Fallback: if the seed wasn't found by season+episode (anime with absolute // Fallback: if the seed wasn't found by season+episode (anime with absolute
// numbering on Trakt vs multi-season on addon), try global index matching. // numbering on Trakt vs multi-season on addon), try global index matching.
if (watchedIndex < 0 && seasonNumber != null && episodeNumber != null) { if (watchedIndex < 0 && seasonNumber != null && episodeNumber != null) {
val addonSeasons = sortedEpisodes.mapTo(mutableSetOf()) { it.season } val mainEpisodes = sortedEpisodes.filter { episode -> normalizeSeasonNumber(episode.season) > 0 }
val addonSeasons = mainEpisodes.mapTo(mutableSetOf()) { episode ->
normalizeSeasonNumber(episode.season)
}
if (seasonNumber == 1 && addonSeasons.size > 1 && episodeNumber > 0) { if (seasonNumber == 1 && addonSeasons.size > 1 && episodeNumber > 0) {
val globalIndex = episodeNumber - 1 val globalIndex = episodeNumber - 1
if (globalIndex in sortedEpisodes.indices) { if (globalIndex in mainEpisodes.indices) {
watchedIndex = globalIndex watchedIndex = sortedEpisodes.indexOf(mainEpisodes[globalIndex])
} }
} }
} }

View file

@ -53,11 +53,14 @@ fun nextReleasedEpisodeAfter(
// Fallback: if the seed wasn't found by season+episode (anime with absolute // Fallback: if the seed wasn't found by season+episode (anime with absolute
// numbering on Trakt vs multi-season on addon), try global index matching. // numbering on Trakt vs multi-season on addon), try global index matching.
if (watchedIndex < 0 && seasonNumber != null && episodeNumber != null) { if (watchedIndex < 0 && seasonNumber != null && episodeNumber != null) {
val addonSeasons = sortedEpisodes.mapTo(mutableSetOf()) { it.seasonNumber } val mainEpisodes = sortedEpisodes.filter { episode -> normalizeSeasonNumber(episode.seasonNumber) > 0 }
val addonSeasons = mainEpisodes.mapTo(mutableSetOf()) { episode ->
normalizeSeasonNumber(episode.seasonNumber)
}
if (seasonNumber == 1 && addonSeasons.size > 1 && episodeNumber > 0) { if (seasonNumber == 1 && addonSeasons.size > 1 && episodeNumber > 0) {
val globalIndex = episodeNumber - 1 val globalIndex = episodeNumber - 1
if (globalIndex in sortedEpisodes.indices) { if (globalIndex in mainEpisodes.indices) {
watchedIndex = globalIndex watchedIndex = sortedEpisodes.indexOf(mainEpisodes[globalIndex])
} }
} }
} }

View file

@ -88,4 +88,31 @@ class SeriesPlaybackResolverTest {
assertEquals("Up Next • S1E3", action.label) assertEquals("Up Next • S1E3", action.label)
assertEquals("show:1:3", action.videoId) assertEquals("show:1:3", action.videoId)
} }
@Test
fun nextReleasedEpisodeAfter_global_index_fallback_ignores_specials() {
val meta = MetaDetails(
id = "show",
type = "series",
name = "Show",
videos = listOf(
MetaVideo(id = "sp1", title = "Special 1", season = 0, episode = 1, released = "2026-01-01"),
MetaVideo(id = "s1e1", title = "Episode 1", season = 1, episode = 1, released = "2026-01-08"),
MetaVideo(id = "s1e2", title = "Episode 2", season = 1, episode = 2, released = "2026-01-15"),
MetaVideo(id = "s2e1", title = "Episode 3", season = 2, episode = 1, released = "2026-01-22"),
MetaVideo(id = "s2e2", title = "Episode 4", season = 2, episode = 2, released = "2026-01-29"),
),
)
val nextEpisode = meta.nextReleasedEpisodeAfter(
seasonNumber = 1,
episodeNumber = 3,
todayIsoDate = "2026-02-01",
)
assertNotNull(nextEpisode)
assertEquals(2, nextEpisode.season)
assertEquals(2, nextEpisode.episode)
assertEquals("s2e2", nextEpisode.id)
}
} }

View file

@ -97,6 +97,30 @@ class SeriesContinuityTest {
assertEquals("show:1:1", action.videoId) assertEquals("show:1:1", action.videoId)
} }
@Test
fun nextReleasedEpisodeAfter_global_index_fallback_ignores_specials() {
val episodesWithSpecials = listOf(
WatchingReleasedEpisode(videoId = "sp1", seasonNumber = 0, episodeNumber = 1, title = "Special 1", releasedDate = "2026-01-01"),
WatchingReleasedEpisode(videoId = "s1e1", seasonNumber = 1, episodeNumber = 1, title = "Episode 1", releasedDate = "2026-01-08"),
WatchingReleasedEpisode(videoId = "s1e2", seasonNumber = 1, episodeNumber = 2, title = "Episode 2", releasedDate = "2026-01-15"),
WatchingReleasedEpisode(videoId = "s2e1", seasonNumber = 2, episodeNumber = 1, title = "Episode 3", releasedDate = "2026-01-22"),
WatchingReleasedEpisode(videoId = "s2e2", seasonNumber = 2, episodeNumber = 2, title = "Episode 4", releasedDate = "2026-01-29"),
)
val nextEpisode = nextReleasedEpisodeAfter(
content = show,
episodes = episodesWithSpecials,
seasonNumber = 1,
episodeNumber = 3,
todayIsoDate = "2026-02-01",
)
assertNotNull(nextEpisode)
assertEquals(2, nextEpisode.seasonNumber)
assertEquals(2, nextEpisode.episodeNumber)
assertEquals("s2e2", nextEpisode.videoId)
}
@Test @Test
fun decideSeriesPrimaryAction_falls_back_to_specials_when_no_main_season() { fun decideSeriesPrimaryAction_falls_back_to_specials_when_no_main_season() {
val specialsOnly = listOf( val specialsOnly = listOf(