From 44785331bbd78f5b24cf67f4b41707b8f2a5203a Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:58:27 +0530 Subject: [PATCH] feat: Introduce season support utilities and refactor episode sorting logic --- .../details/SeriesPlaybackResolver.kt | 10 +----- .../features/details/SeriesSeasonSupport.kt | 25 ++++++++++++++ .../details/components/DetailSeriesContent.kt | 16 ++++----- .../details/SeriesSeasonSupportTest.kt | 34 +++++++++++++++++++ 4 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesSeasonSupport.kt create mode 100644 composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/SeriesSeasonSupportTest.kt diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt index cdc83138..eec926b9 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesPlaybackResolver.kt @@ -7,14 +7,7 @@ import com.nuvio.app.features.watchprogress.resumeEntryForSeries internal fun MetaDetails.sortedPlayableEpisodes(): List = videos .filter { it.season != null || it.episode != null } - .sortedWith( - compareBy( - { it.season ?: Int.MAX_VALUE }, - { it.episode ?: Int.MAX_VALUE }, - { it.released ?: "" }, - { it.title }, - ), - ) + .sortedWith(metaVideoSeasonEpisodeComparator) internal fun MetaDetails.firstPlayableEpisode(): MetaVideo? = sortedPlayableEpisodes().firstOrNull() @@ -126,4 +119,3 @@ private fun MetaVideo.isReleasedBy(todayIsoDate: String): Boolean { ?: return true return releaseDate <= todayIsoDate } - diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesSeasonSupport.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesSeasonSupport.kt new file mode 100644 index 00000000..e9c39b92 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/SeriesSeasonSupport.kt @@ -0,0 +1,25 @@ +package com.nuvio.app.features.details + +internal const val SPECIALS_SEASON_NUMBER = 0 + +internal val metaVideoSeasonEpisodeComparator: Comparator = + compareBy( + { seasonSortKey(it.season) }, + { it.episode ?: Int.MAX_VALUE }, + { it.released ?: "" }, + { it.title }, + ) + +internal fun normalizeSeasonNumber(seasonNumber: Int?): Int = + if (seasonNumber == null || seasonNumber <= SPECIALS_SEASON_NUMBER) { + SPECIALS_SEASON_NUMBER + } else { + seasonNumber + } + +internal fun seasonSortKey(seasonNumber: Int?): Int = + if (seasonNumber == null || seasonNumber <= SPECIALS_SEASON_NUMBER) { + Int.MAX_VALUE + } else { + seasonNumber + } 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 ac5003ad..4f4d7942 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 @@ -42,6 +42,9 @@ import com.nuvio.app.core.ui.NuvioAnimatedWatchedBadge import com.nuvio.app.core.ui.NuvioProgressBar import com.nuvio.app.features.details.MetaDetails import com.nuvio.app.features.details.MetaVideo +import com.nuvio.app.features.details.metaVideoSeasonEpisodeComparator +import com.nuvio.app.features.details.normalizeSeasonNumber +import com.nuvio.app.features.details.seasonSortKey import com.nuvio.app.features.watchprogress.WatchProgressEntry import com.nuvio.app.features.watchprogress.buildPlaybackVideoId @@ -62,20 +65,13 @@ fun DetailSeriesContent( log.w { "All videos lack season/episode fields! First: ${meta.videos.first()}" } } withSeasonOrEp - .sortedWith( - compareBy( - { it.season ?: Int.MAX_VALUE }, - { it.episode ?: Int.MAX_VALUE }, - { it.released ?: "" }, - { it.title }, - ), - ) - .groupBy { it.season ?: 1 } + .sortedWith(metaVideoSeasonEpisodeComparator) + .groupBy { normalizeSeasonNumber(it.season) } } if (groupedEpisodes.isEmpty()) return - val seasons = groupedEpisodes.keys.sorted() + val seasons = groupedEpisodes.keys.sortedBy(::seasonSortKey) val defaultSeason = seasons.first() var selectedSeason by rememberSaveable(meta.id) { mutableStateOf(defaultSeason) } val currentSeason = selectedSeason.takeIf { it in groupedEpisodes } ?: defaultSeason diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/SeriesSeasonSupportTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/SeriesSeasonSupportTest.kt new file mode 100644 index 00000000..0b1a0db5 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/details/SeriesSeasonSupportTest.kt @@ -0,0 +1,34 @@ +package com.nuvio.app.features.details + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SeriesSeasonSupportTest { + + @Test + fun `normalize season number maps zero and null to specials`() { + assertEquals(SPECIALS_SEASON_NUMBER, normalizeSeasonNumber(0)) + assertEquals(SPECIALS_SEASON_NUMBER, normalizeSeasonNumber(null)) + assertEquals(2, normalizeSeasonNumber(2)) + } + + @Test + fun `sorted playable episodes place specials after numbered seasons`() { + val meta = MetaDetails( + id = "show", + type = "series", + name = "Show", + videos = listOf( + MetaVideo(id = "special-1", title = "Special 1", season = 0, episode = 1), + MetaVideo(id = "season-2", title = "Episode 1", season = 2, episode = 1), + MetaVideo(id = "season-1", title = "Episode 1", season = 1, episode = 1), + MetaVideo(id = "special-2", title = "Special 2", season = 0, episode = 2), + ), + ) + + assertEquals( + listOf("season-1", "season-2", "special-1", "special-2"), + meta.sortedPlayableEpisodes().map(MetaVideo::id), + ) + } +}