mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
ref: adjust episode release logic
This commit is contained in:
parent
bd84bd9b56
commit
410c1d48d8
4 changed files with 105 additions and 41 deletions
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue