diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt index 15874f7a3..e8d6624db 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsModels.kt @@ -38,6 +38,7 @@ data class MetaDetails( val trailers: List = emptyList(), val links: List = emptyList(), val videos: List = emptyList(), + val customSeasons: List = emptyList(), ) enum class MoreLikeThisSource { @@ -82,6 +83,11 @@ data class MetaLink( val url: String, ) +data class MetaSeason( + val season: Int, + val poster: String? = null, +) + data class MetaVideo( val id: String, val title: String, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt index 21e8b4aa9..027e25830 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsParser.kt @@ -55,6 +55,7 @@ internal object MetaDetailsParser { trailers = meta.trailers(), links = links, videos = meta.videos(), + customSeasons = meta.customSeasons(), ) } @@ -219,6 +220,16 @@ internal object MetaDetailsParser { return merged.values.toList() } + private fun JsonObject.customSeasons(): List = + array("seasons").mapNotNull { element -> + val obj = element as? JsonObject ?: return@mapNotNull null + val season = obj.int("season") ?: return@mapNotNull null + MetaSeason( + season = season, + poster = obj.string("poster") ?: obj.string("poster_path"), + ) + } + private fun JsonObject.videos(): List = array("videos").mapNotNull { element -> val video = element as? JsonObject ?: return@mapNotNull null diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt index 71ba2eded..3c50763fe 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt @@ -189,9 +189,10 @@ fun DetailSeriesContent( ) { if (seasons.size > 1) { val hasSeasonPosters = seasons.any { season -> - groupedEpisodes[season] - .orEmpty() - .any { !it.seasonPoster.isNullOrBlank() } + meta.customSeasons.any { it.season == season && !it.poster.isNullOrBlank() } || + groupedEpisodes[season] + .orEmpty() + .any { !it.seasonPoster.isNullOrBlank() } } Column( modifier = Modifier.animateContentSize(animationSpec = tween(280)), @@ -495,9 +496,11 @@ private fun SeasonPosterScrollRow( items(seasons, key = { season -> season }) { season -> SeasonPosterButton( label = season.label(), - imageUrl = groupedEpisodes[season] - .orEmpty() - .firstNotNullOfOrNull { episode -> episode.seasonPoster } + imageUrl = meta.customSeasons.find { it.season == season }?.poster + ?.takeIf { it.isNotBlank() } + ?: groupedEpisodes[season] + .orEmpty() + .firstNotNullOfOrNull { episode -> episode.seasonPoster } ?: meta.poster ?: meta.background, isSelected = season == currentSeason, diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/MetaDetailsParserSeasonsTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/MetaDetailsParserSeasonsTest.kt new file mode 100644 index 000000000..4cbd33fc0 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/MetaDetailsParserSeasonsTest.kt @@ -0,0 +1,52 @@ +package com.nuvio.app.features.details + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MetaDetailsParserSeasonsTest { + + @Test + fun `parse reads custom seasons with poster and poster_path fallback`() { + val result = MetaDetailsParser.parse( + """ + { + "meta": { + "id": "custom:show", + "type": "series", + "name": "Show", + "seasons": [ + { "season": 1, "poster": "https://example.com/s1.jpg" }, + { "season": 2, "poster_path": "https://example.com/s2.jpg" }, + { "poster": "https://example.com/orphan.jpg" } + ] + } + } + """.trimIndent(), + ) + + // The entry without a season number is skipped. + assertEquals(2, result.customSeasons.size) + assertEquals(1, result.customSeasons[0].season) + assertEquals("https://example.com/s1.jpg", result.customSeasons[0].poster) + assertEquals(2, result.customSeasons[1].season) + assertEquals("https://example.com/s2.jpg", result.customSeasons[1].poster) + } + + @Test + fun `parse defaults custom seasons to empty when absent`() { + val result = MetaDetailsParser.parse( + """ + { + "meta": { + "id": "custom:show", + "type": "series", + "name": "Show" + } + } + """.trimIndent(), + ) + + assertTrue(result.customSeasons.isEmpty()) + } +}