feat(details): support behaviorHints.defaultVideoId for series initial play

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-07-10 14:13:44 -06:00
parent dfc8accab1
commit 36f50fa4a5
6 changed files with 93 additions and 1 deletions

View file

@ -30,6 +30,7 @@ data class MetaDetails(
val language: String? = null,
val website: String? = null,
val hasScheduledVideos: Boolean = false,
val defaultVideoId: String? = null,
val moreLikeThis: List<MetaPreview> = emptyList(),
val moreLikeThisSource: MoreLikeThisSource? = null,
val collectionName: String? = null,

View file

@ -51,6 +51,7 @@ internal object MetaDetailsParser {
language = meta.string("language"),
website = meta.string("website"),
hasScheduledVideos = meta.behaviorHints().boolean("hasScheduledVideos") == true,
defaultVideoId = meta.behaviorHints().string("defaultVideoId"),
trailers = meta.trailers(),
links = links,
videos = meta.videos(),

View file

@ -170,6 +170,7 @@ internal fun MetaDetails.seriesPrimaryAction(
todayIsoDate = todayIsoDate,
preferFurthestEpisode = preferFurthestEpisode,
showUnairedNextUp = showUnairedNextUp,
defaultVideoId = defaultVideoId,
)?.toLegacySeriesPrimaryAction()
internal fun MetaVideo.playLabel(): String =

View file

@ -92,6 +92,7 @@ fun decideSeriesPrimaryAction(
todayIsoDate: String,
preferFurthestEpisode: Boolean = true,
showUnairedNextUp: Boolean = false,
defaultVideoId: String? = null,
): WatchingSeriesPrimaryAction? {
val resumeRecord = resumeProgressForSeries(
content = content,
@ -127,7 +128,9 @@ fun decideSeriesPrimaryAction(
available = episode.available,
)
}
released.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 } ?: released.firstOrNull()
defaultVideoId?.let { videoId -> released.firstOrNull { it.videoId == videoId } }
?: released.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 }
?: released.firstOrNull()
}
return nextEpisode?.let { episode ->

View file

@ -65,4 +65,24 @@ class MetaDetailsParserTest {
assertFalse(result.videos[0].available)
assertTrue(result.videos[1].available)
}
@Test
fun `parse reads defaultVideoId from behavior hints`() {
val result = MetaDetailsParser.parse(
"""
{
"meta": {
"id": "show",
"type": "series",
"name": "Show",
"behaviorHints": {
"defaultVideoId": "show:1:2"
}
}
}
""".trimIndent(),
)
assertEquals("show:1:2", result.defaultVideoId)
}
}

View file

@ -130,6 +130,72 @@ class SeriesPlaybackResolverTest {
assertEquals(15, action.episodeNumber)
}
@Test
fun seriesPrimaryAction_prefers_default_video_id_when_nothing_watched() {
val meta = MetaDetails(
id = "show",
type = "series",
name = "Show",
defaultVideoId = "show:1:2",
videos = listOf(
MetaVideo(id = "show:1:1", title = "Episode 1", season = 1, episode = 1, released = "2026-03-01"),
MetaVideo(id = "show:1:2", title = "Episode 2", season = 1, episode = 2, released = "2026-03-08"),
MetaVideo(id = "show:1:3", title = "Episode 3", season = 1, episode = 3, released = "2026-03-15"),
),
)
val action = meta.seriesPrimaryAction(
entries = emptyList(),
watchedItems = emptyList(),
todayIsoDate = "2026-03-30",
)
assertNotNull(action)
assertEquals("Play S1E2", action.label)
assertEquals("show:1:2", action.videoId)
assertEquals(1, action.seasonNumber)
assertEquals(2, action.episodeNumber)
}
@Test
fun seriesPrimaryAction_ignores_default_video_id_when_progress_exists() {
val meta = MetaDetails(
id = "show",
type = "series",
name = "Show",
defaultVideoId = "show:1:2",
videos = listOf(
MetaVideo(id = "show:1:1", title = "Episode 1", season = 1, episode = 1, released = "2026-03-01"),
MetaVideo(id = "show:1:2", title = "Episode 2", season = 1, episode = 2, released = "2026-03-08"),
MetaVideo(id = "show:1:3", title = "Episode 3", season = 1, episode = 3, released = "2026-03-15"),
),
)
val action = meta.seriesPrimaryAction(
entries = listOf(
WatchProgressEntry(
contentType = "series",
parentMetaId = "show",
parentMetaType = "series",
videoId = "show:1:1",
title = "Show",
seasonNumber = 1,
episodeNumber = 1,
lastPositionMs = 1_000L,
durationMs = 10_000L,
lastUpdatedEpochMs = 100L,
isCompleted = false,
),
),
watchedItems = emptyList(),
todayIsoDate = "2026-03-30",
)
assertNotNull(action)
assertEquals("Resume S1E1", action.label)
assertEquals("show:1:1", action.videoId)
}
@Test
fun nextReleasedEpisodeAfter_global_index_fallback_ignores_specials() {
val meta = MetaDetails(