ref: adjust episode release logic

This commit is contained in:
tapframe 2026-04-19 12:55:19 +05:30
parent bd84bd9b56
commit 410c1d48d8
4 changed files with 105 additions and 41 deletions

View file

@ -14,6 +14,7 @@ import com.nuvio.app.features.watching.domain.isReleasedBy
import com.nuvio.app.features.watching.domain.latestCompletedSeriesEpisode import com.nuvio.app.features.watching.domain.latestCompletedSeriesEpisode
import com.nuvio.app.features.watching.domain.playLabel import com.nuvio.app.features.watching.domain.playLabel
import com.nuvio.app.features.watching.domain.resumeLabel import com.nuvio.app.features.watching.domain.resumeLabel
import com.nuvio.app.features.watching.domain.shouldSurfaceNextEpisode
import com.nuvio.app.features.watching.domain.upNextLabel import com.nuvio.app.features.watching.domain.upNextLabel
internal fun MetaDetails.sortedPlayableEpisodes(): List<MetaVideo> = internal fun MetaDetails.sortedPlayableEpisodes(): List<MetaVideo> =
@ -63,6 +64,20 @@ internal fun MetaDetails.nextReleasedEpisodeAfter(
seasonNumber: Int?, seasonNumber: Int?,
episodeNumber: Int?, episodeNumber: Int?,
todayIsoDate: String, todayIsoDate: String,
): MetaVideo? {
return nextReleasedEpisodeAfter(
seasonNumber = seasonNumber,
episodeNumber = episodeNumber,
todayIsoDate = todayIsoDate,
showUnairedNextUp = false,
)
}
internal fun MetaDetails.nextReleasedEpisodeAfter(
seasonNumber: Int?,
episodeNumber: Int?,
todayIsoDate: String,
showUnairedNextUp: Boolean,
): MetaVideo? { ): MetaVideo? {
val sortedEpisodes = sortedPlayableEpisodes() val sortedEpisodes = sortedPlayableEpisodes()
val watchedVideoId = buildPlaybackVideoId( val watchedVideoId = buildPlaybackVideoId(
@ -81,7 +96,13 @@ internal fun MetaDetails.nextReleasedEpisodeAfter(
} }
.drop(1) .drop(1)
.filter { episode -> .filter { episode ->
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.released) shouldSurfaceNextEpisode(
watchedSeasonNumber = seasonNumber,
candidateSeasonNumber = episode.season,
todayIsoDate = todayIsoDate,
releasedDate = episode.released,
showUnairedNextUp = showUnairedNextUp,
)
} }
return candidates.firstOrNull { normalizeSeasonNumber(it.season) > 0 } return candidates.firstOrNull { normalizeSeasonNumber(it.season) > 0 }
} }

View file

@ -20,8 +20,7 @@ import com.nuvio.app.core.ui.NuvioScreen
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.details.MetaDetailsRepository
import com.nuvio.app.features.details.filterUnavailableFutureSeasons import com.nuvio.app.features.details.nextReleasedEpisodeAfter
import com.nuvio.app.features.details.sortedPlayableEpisodes
import com.nuvio.app.features.home.components.HomeCatalogRowSection import com.nuvio.app.features.home.components.HomeCatalogRowSection
import com.nuvio.app.features.home.components.HomeContinueWatchingSection import com.nuvio.app.features.home.components.HomeContinueWatchingSection
import com.nuvio.app.features.home.components.HomeEmptyStateCard import com.nuvio.app.features.home.components.HomeEmptyStateCard
@ -45,10 +44,8 @@ import com.nuvio.app.features.watchprogress.toContinueWatchingItem
import com.nuvio.app.features.watchprogress.toUpNextContinueWatchingItem import com.nuvio.app.features.watchprogress.toUpNextContinueWatchingItem
import com.nuvio.app.features.watching.application.WatchingState import com.nuvio.app.features.watching.application.WatchingState
import com.nuvio.app.features.watching.domain.WatchingContentRef import com.nuvio.app.features.watching.domain.WatchingContentRef
import com.nuvio.app.features.watching.domain.buildPlaybackVideoId
import com.nuvio.app.features.collection.CollectionRepository import com.nuvio.app.features.collection.CollectionRepository
import com.nuvio.app.features.home.components.HomeCollectionRowSection import com.nuvio.app.features.home.components.HomeCollectionRowSection
import com.nuvio.app.features.watching.domain.isReleasedBy
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -605,41 +602,6 @@ private fun CompletedSeriesCandidate.toContinueWatchingSeed(meta: com.nuvio.app.
isCompleted = true, isCompleted = true,
) )
private fun com.nuvio.app.features.details.MetaDetails.nextReleasedEpisodeAfter(
seasonNumber: Int?,
episodeNumber: Int?,
todayIsoDate: String,
showUnairedNextUp: Boolean,
): com.nuvio.app.features.details.MetaVideo? {
val content = WatchingContentRef(type = type, id = id)
val watchedVideoId = buildPlaybackVideoId(
content = content,
seasonNumber = seasonNumber,
episodeNumber = episodeNumber,
)
val ordered = sortedPlayableEpisodes()
.dropWhile { episode ->
buildPlaybackVideoId(
content = content,
seasonNumber = episode.season,
episodeNumber = episode.episode,
fallbackVideoId = episode.id,
) != watchedVideoId
}
.drop(1)
.filter { episode -> (episode.season ?: 0) > 0 }
.filterUnavailableFutureSeasons(todayIsoDate = todayIsoDate)
if (showUnairedNextUp) {
return ordered.firstOrNull()
}
return ordered.firstOrNull { episode ->
isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.released)
}
}
private fun ContinueWatchingItem.shouldDisplayInContinueWatching(): Boolean = private fun ContinueWatchingItem.shouldDisplayInContinueWatching(): Boolean =
isNextUp || progressFraction < 0.995f isNextUp || progressFraction < 0.995f

View file

@ -45,7 +45,15 @@ fun nextReleasedEpisodeAfter(
val candidates = sortedEpisodes val candidates = sortedEpisodes
.dropWhile { episode -> buildPlaybackVideoId(content, episode.seasonNumber, episode.episodeNumber, episode.videoId) != watchedVideoId } .dropWhile { episode -> buildPlaybackVideoId(content, episode.seasonNumber, episode.episodeNumber, episode.videoId) != watchedVideoId }
.drop(1) .drop(1)
.filter { episode -> isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = episode.releasedDate) } .filter { episode ->
shouldSurfaceNextEpisode(
watchedSeasonNumber = seasonNumber,
candidateSeasonNumber = episode.seasonNumber,
todayIsoDate = todayIsoDate,
releasedDate = episode.releasedDate,
showUnairedNextUp = false,
)
}
return candidates.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 } return candidates.firstOrNull { normalizeSeasonNumber(it.seasonNumber) > 0 }
} }

View file

@ -3,6 +3,7 @@ package com.nuvio.app.features.watching.domain
private const val InProgressStartThresholdFraction = 0.02f private const val InProgressStartThresholdFraction = 0.02f
private const val CompletionThresholdFraction = 0.85 private const val CompletionThresholdFraction = 0.85
private const val InProgressStartThresholdMinMs = 30_000L private const val InProgressStartThresholdMinMs = 30_000L
private const val UpcomingNextSeasonWindowDays = 7
fun watchedKey( fun watchedKey(
content: WatchingContentRef, content: WatchingContentRef,
@ -48,6 +49,78 @@ fun isReleasedBy(
return isoDate <= todayIsoDate return isoDate <= todayIsoDate
} }
internal fun shouldSurfaceNextEpisode(
watchedSeasonNumber: Int?,
candidateSeasonNumber: Int?,
todayIsoDate: String,
releasedDate: String?,
showUnairedNextUp: Boolean,
): Boolean {
val isSeasonRollover = normalizeSeasonNumber(candidateSeasonNumber) != normalizeSeasonNumber(watchedSeasonNumber)
if (!isSeasonRollover) {
if (showUnairedNextUp) return true
return isReleasedBy(todayIsoDate = todayIsoDate, releasedDate = releasedDate)
}
if (isExplicitlyReleasedBy(todayIsoDate = todayIsoDate, releasedDate = releasedDate)) {
return true
}
if (!showUnairedNextUp) {
return false
}
val daysUntilRelease = daysUntilExplicitRelease(
todayIsoDate = todayIsoDate,
releasedDate = releasedDate,
) ?: return false
return daysUntilRelease in 0..UpcomingNextSeasonWindowDays
}
private fun isExplicitlyReleasedBy(
todayIsoDate: String,
releasedDate: String?,
): Boolean {
val isoDate = isoCalendarDateOrNull(releasedDate) ?: return false
return isoDate <= todayIsoDate
}
private fun daysUntilExplicitRelease(
todayIsoDate: String,
releasedDate: String?,
): Int? {
val startDate = isoCalendarDateOrNull(todayIsoDate) ?: return null
val targetDate = isoCalendarDateOrNull(releasedDate) ?: return null
return (isoEpochDay(targetDate) - isoEpochDay(startDate)).toInt()
}
private fun isoCalendarDateOrNull(value: String?): String? {
val datePart = value
?.trim()
?.substringBefore('T')
?.takeIf { it.length == 10 }
?: return null
val parts = datePart.split('-')
if (parts.size != 3) return null
val year = parts[0].toIntOrNull() ?: return null
val month = parts[1].toIntOrNull()?.takeIf { it in 1..12 } ?: return null
val day = parts[2].toIntOrNull()?.takeIf { it in 1..31 } ?: return null
return "%04d-%02d-%02d".format(year, month, day)
}
private fun isoEpochDay(date: String): Long {
val year = date.substring(0, 4).toLong()
val month = date.substring(5, 7).toLong()
val day = date.substring(8, 10).toLong()
val adjustedYear = year - if (month <= 2L) 1L else 0L
val era = if (adjustedYear >= 0L) adjustedYear / 400L else (adjustedYear - 399L) / 400L
val yearOfEra = adjustedYear - era * 400L
val adjustedMonth = month + if (month > 2L) -3L else 9L
val dayOfYear = (153L * adjustedMonth + 2L) / 5L + day - 1L
val dayOfEra = yearOfEra * 365L + yearOfEra / 4L - yearOfEra / 100L + dayOfYear
return era * 146_097L + dayOfEra - 719_468L
}
fun releasedEpisodes( fun releasedEpisodes(
episodes: List<WatchingReleasedEpisode>, episodes: List<WatchingReleasedEpisode>,
todayIsoDate: String, todayIsoDate: String,