Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/src/main/kotlin/com/arflix/tv/data/model/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@ data class Episode(
val imdbRating: String = "",
val runtime: Int = 0,
val airDate: String = "",
val isWatched: Boolean = false
val isWatched: Boolean = false,
/** Canonical TMDB coordinates used by Trakt, history, ratings and metadata lookups. */
val tmdbSeasonNumber: Int = seasonNumber,
val tmdbEpisodeNumber: Int = episodeNumber,
/** Provider identity used when the anime-facing structure differs from TMDB. */
val kitsuId: Int? = null,
val kitsuEpisodeNumber: Int? = null
) : Serializable

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,7 @@ class StreamRepository @Inject constructor(
genreIds: List<Int> = emptyList(),
originalLanguage: String? = null,
title: String = "",
animeQueryOverride: String? = null,
airDate: String? = null
): List<StreamSource> {
val startedAt = System.currentTimeMillis()
Expand Down Expand Up @@ -1914,7 +1915,7 @@ class StreamRepository @Inject constructor(
ANIME_ID_LOOKUP_TIMEOUT_MS
}
val animeQuery = if (resolveAsAnime) {
resolveAnimeQuery(animeLookupTimeoutMs)
animeQueryOverride ?: resolveAnimeQuery(animeLookupTimeoutMs)
} else null

val seriesId = "$imdbId:$season:$episode"
Expand Down Expand Up @@ -2661,6 +2662,7 @@ class StreamRepository @Inject constructor(
originalLanguage: String? = null,
title: String = "",
forceRefresh: Boolean = false,
animeQueryOverride: String? = null,
airDate: String? = null
): Flow<ProgressiveStreamResult> = callbackFlow {
repositoryScope.launch {
Expand Down Expand Up @@ -2795,6 +2797,7 @@ class StreamRepository @Inject constructor(
genreIds = genreIds,
originalLanguage = originalLanguage,
title = title,
animeQueryOverride = animeQueryOverride,
airDate = airDate
)
} catch (e: Exception) {
Expand Down
28 changes: 25 additions & 3 deletions app/src/main/kotlin/com/arflix/tv/navigation/AppNavigation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ sealed class Screen(val route: String) {
}
}

data object Player : Screen("player/{mediaType}/{mediaId}?seasonNumber={seasonNumber}&episodeNumber={episodeNumber}&imdbId={imdbId}&streamUrl={streamUrl}&preferredAddonId={preferredAddonId}&preferredSourceName={preferredSourceName}&preferredBingeGroup={preferredBingeGroup}&startPositionMs={startPositionMs}&isLiveStream={isLiveStream}") {
data object Player : Screen("player/{mediaType}/{mediaId}?seasonNumber={seasonNumber}&episodeNumber={episodeNumber}&tmdbSeasonNumber={tmdbSeasonNumber}&tmdbEpisodeNumber={tmdbEpisodeNumber}&imdbId={imdbId}&streamUrl={streamUrl}&preferredAddonId={preferredAddonId}&preferredSourceName={preferredSourceName}&preferredBingeGroup={preferredBingeGroup}&startPositionMs={startPositionMs}&isLiveStream={isLiveStream}") {
fun createRoute(
mediaType: MediaType,
mediaId: Int,
seasonNumber: Int? = null,
episodeNumber: Int? = null,
tmdbSeasonNumber: Int? = seasonNumber,
tmdbEpisodeNumber: Int? = episodeNumber,
imdbId: String? = null,
streamUrl: String? = null,
preferredAddonId: String? = null,
Expand All @@ -99,6 +101,8 @@ sealed class Screen(val route: String) {
val params = mutableListOf<String>()
seasonNumber?.let { params.add("seasonNumber=$it") }
episodeNumber?.let { params.add("episodeNumber=$it") }
tmdbSeasonNumber?.let { params.add("tmdbSeasonNumber=$it") }
tmdbEpisodeNumber?.let { params.add("tmdbEpisodeNumber=$it") }
imdbId?.let { params.add("imdbId=${java.net.URLEncoder.encode(it, "UTF-8")}") }
streamUrl?.let { params.add("streamUrl=${java.net.URLEncoder.encode(it, "UTF-8")}") }
preferredAddonId?.let { params.add("preferredAddonId=${java.net.URLEncoder.encode(it, "UTF-8")}") }
Expand Down Expand Up @@ -424,13 +428,15 @@ fun AppNavigation(
initialSeason = initialSeason,
initialEpisode = initialEpisode,
currentProfile = currentProfile,
onNavigateToPlayer = { type, id, season, episode, imdbId, url, preferredAddonId, preferredSourceName, startPositionMs ->
onNavigateToPlayer = { type, id, season, episode, tmdbSeason, tmdbEpisode, imdbId, url, preferredAddonId, preferredSourceName, startPositionMs ->
navController.navigate(
Screen.Player.createRoute(
mediaType = type,
mediaId = id,
seasonNumber = season,
episodeNumber = episode,
tmdbSeasonNumber = tmdbSeason,
tmdbEpisodeNumber = tmdbEpisode,
imdbId = imdbId,
streamUrl = url,
preferredAddonId = preferredAddonId,
Expand Down Expand Up @@ -484,6 +490,14 @@ fun AppNavigation(
type = NavType.IntType
defaultValue = -1
},
navArgument("tmdbSeasonNumber") {
type = NavType.IntType
defaultValue = -1
},
navArgument("tmdbEpisodeNumber") {
type = NavType.IntType
defaultValue = -1
},
navArgument("imdbId") {
type = NavType.StringType
defaultValue = ""
Expand Down Expand Up @@ -518,6 +532,10 @@ fun AppNavigation(
val mediaId = backStackEntry.arguments?.getInt("mediaId") ?: 0
val seasonNumber = backStackEntry.arguments?.getInt("seasonNumber")?.takeIf { it >= 0 }
val episodeNumber = backStackEntry.arguments?.getInt("episodeNumber")?.takeIf { it >= 0 }
val tmdbSeasonNumber = backStackEntry.arguments?.getInt("tmdbSeasonNumber")?.takeIf { it >= 0 }
?: seasonNumber
val tmdbEpisodeNumber = backStackEntry.arguments?.getInt("tmdbEpisodeNumber")?.takeIf { it >= 0 }
?: episodeNumber
val imdbId = backStackEntry.arguments?.getString("imdbId")?.takeIf { it.isNotBlank() }
val streamUrl = backStackEntry.arguments?.getString("streamUrl")?.takeIf { it.isNotEmpty() }
val preferredAddonId = backStackEntry.arguments?.getString("preferredAddonId")?.takeIf { it.isNotBlank() }
Expand All @@ -532,6 +550,8 @@ fun AppNavigation(
mediaId = mediaId,
seasonNumber = seasonNumber,
episodeNumber = episodeNumber,
tmdbSeasonNumber = tmdbSeasonNumber,
tmdbEpisodeNumber = tmdbEpisodeNumber,
imdbId = imdbId,
streamUrl = streamUrl,
preferredAddonId = preferredAddonId,
Expand All @@ -540,14 +560,16 @@ fun AppNavigation(
startPositionMs = startPositionMs,
isLiveStream = isLiveStream,
onBack = { navController.popBackStack() },
onPlayNext = { nextSeason, nextEpisode, nextPreferredAddonId, nextPreferredSourceName, nextPreferredBingeGroup ->
onPlayNext = { nextSeason, nextEpisode, nextTmdbSeason, nextTmdbEpisode, nextPreferredAddonId, nextPreferredSourceName, nextPreferredBingeGroup ->
// Navigate to next episode
navController.navigate(
Screen.Player.createRoute(
mediaType = mediaType,
mediaId = mediaId,
seasonNumber = nextSeason,
episodeNumber = nextEpisode,
tmdbSeasonNumber = nextTmdbSeason,
tmdbEpisodeNumber = nextTmdbEpisode,
preferredAddonId = nextPreferredAddonId,
preferredSourceName = nextPreferredSourceName,
preferredBingeGroup = nextPreferredBingeGroup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ fun DetailsScreen(
initialEpisode: Int? = null,
viewModel: DetailsViewModel = hiltViewModel(),
currentProfile: com.arflix.tv.data.model.Profile? = null,
onNavigateToPlayer: (MediaType, Int, Int?, Int?, String?, String?, String?, String?, Long?) -> Unit,
onNavigateToPlayer: (MediaType, Int, Int?, Int?, Int?, Int?, String?, String?, String?, String?, Long?) -> Unit,
onNavigateToDetails: (MediaType, Int) -> Unit,
onNavigateToCollection: (String) -> Unit = {},
onNavigateToHome: () -> Unit = {},
Expand Down Expand Up @@ -254,13 +254,22 @@ fun DetailsScreen(
var seasonSelectDownAtMs by remember { mutableLongStateOf(0L) }
var ignoreFirstResumeRefresh by remember(mediaType, mediaId, initialSeason, initialEpisode) { mutableStateOf(true) }

fun requestFastAutoPlay(imdbId: String?, season: Int?, episode: Int?, startPositionMs: Long?) {
fun requestFastAutoPlay(
imdbId: String?,
displaySeason: Int?,
displayEpisode: Int?,
startPositionMs: Long?,
tmdbSeason: Int? = displaySeason,
tmdbEpisode: Int? = displayEpisode
) {
showStreamSelector = false
viewModel.loadStreams(imdbId, season, episode)
viewModel.loadStreams(imdbId, tmdbSeason, tmdbEpisode)
autoPlayWaitTick = 0
pendingAutoPlayRequest = PendingAutoPlayRequest(
season = season,
episode = episode,
displaySeason = displaySeason,
displayEpisode = displayEpisode,
tmdbSeason = tmdbSeason,
tmdbEpisode = tmdbEpisode,
startPositionMs = startPositionMs,
requestedAtMs = SystemClock.elapsedRealtime()
)
Expand Down Expand Up @@ -353,12 +362,14 @@ fun DetailsScreen(

when {
selectedStream != null && !shouldWaitForSources -> {
viewModel.recordPlayedEpisode(mediaId, request.season, request.episode)
viewModel.recordPlayedEpisode(mediaId, request.displaySeason, request.displayEpisode)
onNavigateToPlayer(
mediaType,
mediaId,
request.season,
request.episode,
request.displaySeason,
request.displayEpisode,
request.tmdbSeason,
request.tmdbEpisode,
uiState.imdbId,
selectedStream.url?.takeIf { it.isNotBlank() },
selectedStream.addonId.takeIf { it.isNotBlank() },
Expand Down Expand Up @@ -457,16 +468,24 @@ fun DetailsScreen(
if (!state.autoPlaySingleSource) {
// Autoplay OFF → open the source picker; never auto-play.
showStreamSelector = true
viewModel.loadStreams(state.imdbId, season, episode)
viewModel.loadStreams(
state.imdbId,
state.playTmdbSeason ?: season,
state.playTmdbEpisode ?: episode
)
} else {
// Autoplay ON → go straight to the player; PlayerScreen auto-picks.
requestFastAutoPlay(state.imdbId, season, episode, startPositionMs)
requestFastAutoPlay(
state.imdbId, season, episode, startPositionMs,
state.playTmdbSeason ?: season,
state.playTmdbEpisode ?: episode
)
}
}
1 -> { // Sources
showStreamSelector = true
val ep = state.episodes.getOrNull(currentEpIdx)
viewModel.loadStreams(state.imdbId, ep?.seasonNumber, ep?.episodeNumber)
viewModel.loadStreams(state.imdbId, ep?.tmdbSeasonNumber, ep?.tmdbEpisodeNumber)
}
2 -> { // Trailer
state.trailerKey?.let { showTrailerPlayer = true }
Expand Down Expand Up @@ -505,9 +524,12 @@ fun DetailsScreen(
episodeIndex = idx
if (isMobile || !state.autoPlaySingleSource) {
showStreamSelector = true
viewModel.loadStreams(state.imdbId, ep.seasonNumber, ep.episodeNumber)
viewModel.loadStreams(state.imdbId, ep.tmdbSeasonNumber, ep.tmdbEpisodeNumber)
} else {
requestFastAutoPlay(state.imdbId, ep.seasonNumber, ep.episodeNumber, null)
requestFastAutoPlay(
state.imdbId, ep.seasonNumber, ep.episodeNumber, null,
ep.tmdbSeasonNumber, ep.tmdbEpisodeNumber
)
}
}
}
Expand Down Expand Up @@ -814,17 +836,25 @@ fun DetailsScreen(
if (!uiState.autoPlaySingleSource) {
// Autoplay OFF → open the source picker; never auto-play.
showStreamSelector = true
viewModel.loadStreams(uiState.imdbId, season, episode)
viewModel.loadStreams(
uiState.imdbId,
uiState.playTmdbSeason ?: season,
uiState.playTmdbEpisode ?: episode
)
} else {
// Autoplay ON: pick a concrete stream first, then open PlayerScreen.
requestFastAutoPlay(uiState.imdbId, season, episode, startPositionMs)
requestFastAutoPlay(
uiState.imdbId, season, episode, startPositionMs,
uiState.playTmdbSeason ?: season,
uiState.playTmdbEpisode ?: episode
)
}
}
1 -> { // Sources - Show StreamSelector for manual selection
showStreamSelector = true
// Pass the currently focused episode for TV shows
val ep = uiState.episodes.getOrNull(episodeIndex)
viewModel.loadStreams(uiState.imdbId, ep?.seasonNumber, ep?.episodeNumber)
viewModel.loadStreams(uiState.imdbId, ep?.tmdbSeasonNumber, ep?.tmdbEpisodeNumber)
}
2 -> { // Trailer
uiState.trailerKey?.let {
Expand All @@ -844,9 +874,12 @@ fun DetailsScreen(
if (ep != null) {
if (!uiState.autoPlaySingleSource) {
showStreamSelector = true
viewModel.loadStreams(uiState.imdbId, ep.seasonNumber, ep.episodeNumber)
viewModel.loadStreams(uiState.imdbId, ep.tmdbSeasonNumber, ep.tmdbEpisodeNumber)
} else {
requestFastAutoPlay(uiState.imdbId, ep.seasonNumber, ep.episodeNumber, null)
requestFastAutoPlay(
uiState.imdbId, ep.seasonNumber, ep.episodeNumber, null,
ep.tmdbSeasonNumber, ep.tmdbEpisodeNumber
)
}
}
}
Expand Down Expand Up @@ -1064,6 +1097,7 @@ fun DetailsScreen(
onNavigateToPlayer(
mediaType, mediaId,
ep?.seasonNumber, ep?.episodeNumber,
ep?.tmdbSeasonNumber, ep?.tmdbEpisodeNumber,
uiState.imdbId,
stream.url?.takeIf { it.isNotBlank() },
stream.addonId.takeIf { it.isNotBlank() },
Expand All @@ -1083,12 +1117,15 @@ fun DetailsScreen(
isWatched = episode.isWatched,
onPlay = {
showEpisodeContextMenu = false
requestFastAutoPlay(uiState.imdbId, episode.seasonNumber, episode.episodeNumber, null)
requestFastAutoPlay(
uiState.imdbId, episode.seasonNumber, episode.episodeNumber, null,
episode.tmdbSeasonNumber, episode.tmdbEpisodeNumber
)
},
onSelectSource = {
showEpisodeContextMenu = false
showStreamSelector = true
viewModel.loadStreams(uiState.imdbId, episode.seasonNumber, episode.episodeNumber)
viewModel.loadStreams(uiState.imdbId, episode.tmdbSeasonNumber, episode.tmdbEpisodeNumber)
},
onToggleWatched = {
viewModel.markEpisodeWatched(
Expand Down Expand Up @@ -1140,8 +1177,10 @@ private enum class FocusSection {
}

private data class PendingAutoPlayRequest(
val season: Int?,
val episode: Int?,
val displaySeason: Int?,
val displayEpisode: Int?,
val tmdbSeason: Int?,
val tmdbEpisode: Int?,
val startPositionMs: Long?,
val requestedAtMs: Long
)
Expand Down
Loading
Loading