mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-07 03:49:15 +00:00
fix: ignore unavailable episodes in watched state
This commit is contained in:
parent
323aea2191
commit
94e1141bd1
13 changed files with 147 additions and 6 deletions
|
|
@ -85,6 +85,7 @@ data class MetaVideo(
|
|||
val id: String,
|
||||
val title: String,
|
||||
val released: String? = null,
|
||||
val available: Boolean = true,
|
||||
val thumbnail: String? = null,
|
||||
val seasonPoster: String? = null,
|
||||
val season: Int? = null,
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ internal object MetaDetailsParser {
|
|||
id = id,
|
||||
title = title,
|
||||
released = video.string("released"),
|
||||
available = video.boolean("available") ?: true,
|
||||
thumbnail = video.string("thumbnail"),
|
||||
seasonPoster = video.string("seasonPoster") ?: video.string("season_poster_path"),
|
||||
season = video.int("season"),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ internal fun List<MetaVideo>.filterUnavailableFutureSeasons(
|
|||
val firstEpisode = episodes.minWithOrNull(
|
||||
compareBy<MetaVideo>({ it.episode ?: Int.MAX_VALUE }, { it.released.orEmpty() }),
|
||||
) ?: return@filter false
|
||||
!isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = firstEpisode.released)
|
||||
!firstEpisode.isReleasedBy(todayIsoDate)
|
||||
}
|
||||
.keys
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ internal fun MetaDetails.firstPlayableEpisode(): MetaVideo? =
|
|||
|
||||
internal fun MetaDetails.firstReleasedPlayableEpisode(todayIsoDate: String): MetaVideo? =
|
||||
sortedPlayableEpisodes().firstOrNull { video ->
|
||||
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = video.released)
|
||||
video.isReleasedBy(todayIsoDate)
|
||||
}
|
||||
|
||||
internal fun MetaDetails.nextReleasedEpisodeAfter(
|
||||
|
|
@ -122,6 +122,7 @@ internal fun MetaDetails.nextReleasedEpisodeAfter(
|
|||
todayIsoDate = todayIsoDate,
|
||||
releasedDate = episode.released,
|
||||
showUnairedNextUp = showUnairedNextUp,
|
||||
available = episode.available,
|
||||
)
|
||||
}
|
||||
return candidates.firstOrNull { normalizeSeasonNumber(it.season) > 0 }
|
||||
|
|
@ -181,7 +182,11 @@ internal fun WatchProgressEntry.resumeLabel(): String =
|
|||
resumeLabel(seasonNumber = seasonNumber, episodeNumber = episodeNumber)
|
||||
|
||||
internal fun MetaVideo.isReleasedBy(todayIsoDate: String): Boolean =
|
||||
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = released)
|
||||
isReleasedBy(
|
||||
todayIsoDate = todayIsoDate,
|
||||
releasedDate = released,
|
||||
available = available,
|
||||
)
|
||||
|
||||
internal data class CompletedSeriesEpisode(
|
||||
val seasonNumber: Int,
|
||||
|
|
@ -209,6 +214,7 @@ private fun MetaVideo.toDomainReleasedEpisode(): WatchingReleasedEpisode =
|
|||
title = title,
|
||||
thumbnail = thumbnail,
|
||||
releasedDate = released,
|
||||
available = available,
|
||||
)
|
||||
|
||||
private fun WatchProgressEntry.toDomainProgressRecord(): WatchingProgressRecord =
|
||||
|
|
|
|||
|
|
@ -100,4 +100,5 @@ private fun MetaVideo.toDomainReleasedEpisode(): WatchingReleasedEpisode =
|
|||
title = title,
|
||||
thumbnail = thumbnail,
|
||||
releasedDate = released,
|
||||
available = available,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ fun nextReleasedEpisodeAfter(
|
|||
todayIsoDate = todayIsoDate,
|
||||
releasedDate = episode.releasedDate,
|
||||
showUnairedNextUp = showUnairedNextUp,
|
||||
available = episode.available,
|
||||
)
|
||||
}
|
||||
return candidates.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 }
|
||||
|
|
@ -119,7 +120,13 @@ fun decideSeriesPrimaryAction(
|
|||
} else {
|
||||
val sorted = episodes
|
||||
.sortedWith(compareBy<WatchingReleasedEpisode>({ normalizeSeasonNumber(it.seasonNumber) }, { it.episodeNumber ?: 0 }))
|
||||
val released = sorted.filter { episode -> isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.releasedDate) }
|
||||
val released = sorted.filter { episode ->
|
||||
isReleasedBy(
|
||||
todayIsoDate = todayIsoDate,
|
||||
releasedDate = episode.releasedDate,
|
||||
available = episode.available,
|
||||
)
|
||||
}
|
||||
released.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 } ?: released.firstOrNull()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ data class WatchingReleasedEpisode(
|
|||
val title: String? = null,
|
||||
val thumbnail: String? = null,
|
||||
val releasedDate: String? = null,
|
||||
val available: Boolean = true,
|
||||
)
|
||||
|
||||
data class WatchingCompletedEpisode(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ fun isProgressComplete(
|
|||
fun isReleasedBy(
|
||||
todayIsoDate: String,
|
||||
releasedDate: String?,
|
||||
available: Boolean = true,
|
||||
): Boolean {
|
||||
if (!available) return false
|
||||
val isoDate = releasedDate
|
||||
?.substringBefore('T')
|
||||
?.takeIf { it.length == 10 }
|
||||
|
|
@ -44,7 +46,9 @@ internal fun shouldSurfaceNextEpisode(
|
|||
todayIsoDate: String,
|
||||
releasedDate: String?,
|
||||
showUnairedNextUp: Boolean,
|
||||
available: Boolean = true,
|
||||
): Boolean {
|
||||
if (!available) return false
|
||||
val isSeasonRollover = normalizeSeasonNumber(candidateSeasonNumber) != normalizeSeasonNumber(watchedSeasonNumber)
|
||||
if (!isSeasonRollover) {
|
||||
if (showUnairedNextUp) return true
|
||||
|
|
@ -117,7 +121,11 @@ fun releasedEpisodes(
|
|||
episodes: List<WatchingReleasedEpisode>,
|
||||
todayIsoDate: String,
|
||||
): List<WatchingReleasedEpisode> = episodes.filter { episode ->
|
||||
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.releasedDate)
|
||||
isReleasedBy(
|
||||
todayIsoDate = todayIsoDate,
|
||||
releasedDate = episode.releasedDate,
|
||||
available = episode.available,
|
||||
)
|
||||
}
|
||||
|
||||
fun releasedMainSeasonEpisodes(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.nuvio.app.features.details
|
|||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class MetaDetailsParserTest {
|
||||
|
||||
|
|
@ -29,4 +31,38 @@ class MetaDetailsParserTest {
|
|||
assertEquals("series", result.type)
|
||||
assertEquals("The Fragrant Flower Blooms with Dignity", result.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse preserves explicit video availability`() {
|
||||
val result = MetaDetailsParser.parse(
|
||||
"""
|
||||
{
|
||||
"meta": {
|
||||
"id": "mal:52991",
|
||||
"type": "series",
|
||||
"name": "Show",
|
||||
"videos": [
|
||||
{
|
||||
"id": "show:3:1",
|
||||
"title": "Episode 1",
|
||||
"season": 3,
|
||||
"episode": 1,
|
||||
"released": null,
|
||||
"available": false
|
||||
},
|
||||
{
|
||||
"id": "show:1:1",
|
||||
"title": "Episode 1",
|
||||
"season": 1,
|
||||
"episode": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
assertFalse(result.videos[0].available)
|
||||
assertTrue(result.videos[1].available)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,4 +156,16 @@ class SeriesPlaybackResolverTest {
|
|||
assertEquals(2, nextEpisode.episode)
|
||||
assertEquals("s2e2", nextEpisode.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filterUnavailableFutureSeasons_removes_explicitly_unavailable_season_without_release_date() {
|
||||
val episodes = listOf(
|
||||
MetaVideo(id = "s1e1", title = "Episode 1", season = 1, episode = 1, released = "2026-01-01"),
|
||||
MetaVideo(id = "s3e1", title = "Episode 1", season = 3, episode = 1, released = null, available = false),
|
||||
)
|
||||
|
||||
val filtered = episodes.filterUnavailableFutureSeasons(todayIsoDate = "2026-07-05")
|
||||
|
||||
assertEquals(listOf("s1e1"), filtered.map(MetaVideo::id))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,30 @@ class WatchedRepositoryTest {
|
|||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullyWatchedSeries_ignores_explicitly_unavailable_main_episodes() {
|
||||
val meta = MetaDetails(
|
||||
id = "show",
|
||||
type = "series",
|
||||
name = "Show",
|
||||
videos = listOf(
|
||||
MetaVideo(id = "s1e1", title = "Episode 1", season = 1, episode = 1, released = "2026-03-01"),
|
||||
MetaVideo(id = "s3e1", title = "Episode 1", season = 3, episode = 1, released = null, available = false),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("s1e1"),
|
||||
meta.releasedMainSeasonEpisodes(todayIsoDate = "2026-07-05").map(MetaVideo::id),
|
||||
)
|
||||
|
||||
val result = meta.hasWatchedAllMainSeasonEpisodes(todayIsoDate = "2026-07-05") { episode ->
|
||||
episode.id == "s1e1"
|
||||
}
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mergeWatchedItemsPreservingUnsynced_keeps_local_items_marked_after_last_push() {
|
||||
val serverItem = WatchedItem(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.features.watching.domain
|
|||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class SeriesContinuityTest {
|
||||
private val show = WatchingContentRef(type = "series", id = "show")
|
||||
|
|
@ -121,6 +122,25 @@ class SeriesContinuityTest {
|
|||
assertEquals("s2e2", nextEpisode.videoId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextReleasedEpisodeAfter_skips_explicitly_unavailable_phantom_episode() {
|
||||
val episodesWithPhantom = listOf(
|
||||
WatchingReleasedEpisode(videoId = "s1e1", seasonNumber = 1, episodeNumber = 1, title = "Episode 1", releasedDate = "2026-01-01"),
|
||||
WatchingReleasedEpisode(videoId = "s3e1", seasonNumber = 3, episodeNumber = 1, title = "Episode 1", releasedDate = null, available = false),
|
||||
)
|
||||
|
||||
val nextEpisode = nextReleasedEpisodeAfter(
|
||||
content = show,
|
||||
episodes = episodesWithPhantom,
|
||||
seasonNumber = 1,
|
||||
episodeNumber = 1,
|
||||
todayIsoDate = "2026-07-05",
|
||||
showUnairedNextUp = true,
|
||||
)
|
||||
|
||||
assertNull(nextEpisode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decideSeriesPrimaryAction_falls_back_to_specials_when_no_main_season() {
|
||||
val specialsOnly = listOf(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,30 @@ class WatchingPoliciesTest {
|
|||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hasWatchedAllMainSeasonEpisodes_ignores_explicitly_unavailable_episodes() {
|
||||
val episodes = listOf(
|
||||
WatchingReleasedEpisode(videoId = "ep1", seasonNumber = 1, episodeNumber = 1, releasedDate = "2026-03-01"),
|
||||
WatchingReleasedEpisode(videoId = "phantom", seasonNumber = 3, episodeNumber = 1, releasedDate = null, available = false),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("ep1"),
|
||||
releasedMainSeasonEpisodes(
|
||||
episodes = episodes,
|
||||
todayIsoDate = "2026-07-05",
|
||||
).map(WatchingReleasedEpisode::videoId),
|
||||
)
|
||||
|
||||
val result = hasWatchedAllMainSeasonEpisodes(
|
||||
episodes = episodes,
|
||||
todayIsoDate = "2026-07-05",
|
||||
isEpisodeWatched = { episode -> episode.videoId == "ep1" },
|
||||
)
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun latestCompletedSeriesEpisode_prefers_newer_manual_watch_marker() {
|
||||
val latestCompleted = latestCompletedSeriesEpisode(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
CURRENT_PROJECT_VERSION=90
|
||||
MARKETING_VERSION=0.2.18
|
||||
MARKETING_VERSION=1.0.0
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue