From 8072d73725bac8f85cc164b24c0c764ffa0ce929 Mon Sep 17 00:00:00 2001 From: 6ip Date: Sun, 12 Jul 2026 23:35:48 +0300 Subject: [PATCH] feat: support custom seasons array for fallback posters --- .../app/features/details/MetaDetailsModels.kt | 6 +++ .../app/features/details/MetaDetailsParser.kt | 11 +++++ .../details/components/DetailSeriesContent.kt | 15 ++++--- .../features/details/MetaDetailsParserTest.kt | 44 +++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) 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 f61818fa6..6f5272e21 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 @@ -37,6 +37,7 @@ data class MetaDetails( val trailers: List = emptyList(), val links: List = emptyList(), val videos: List = emptyList(), + val customSeasons: List = emptyList(), ) enum class MoreLikeThisSource { @@ -81,6 +82,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 769b91566..fb238f1ba 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 @@ -54,6 +54,7 @@ internal object MetaDetailsParser { trailers = meta.trailers(), links = links, videos = meta.videos(), + customSeasons = meta.customSeasons(), ) } @@ -218,6 +219,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 039e10bd6..0b7f90afc 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 @@ -187,9 +187,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)), @@ -481,9 +482,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/MetaDetailsParserTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/MetaDetailsParserTest.kt index b46f29609..8a0d720eb 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/MetaDetailsParserTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/MetaDetailsParserTest.kt @@ -65,4 +65,48 @@ class MetaDetailsParserTest { assertFalse(result.videos[0].available) assertTrue(result.videos[1].available) } + + @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()) + } }