Skip to content
Merged
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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import java.util.Properties
import org.gradle.api.provider.Property
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

val appVersionCode = 143
val appVersionName = "3.4.7"
val appVersionCode = 144
val appVersionName = "3.4.8"

plugins {
alias(libs.plugins.android.application)
Expand Down
100 changes: 75 additions & 25 deletions app/src/main/java/com/example/xtreamplayer/MainActivityUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1125,16 +1125,28 @@ fun RootScreen(
}
}

fun syncPlaybackStateToQueueIndex(index: Int): Boolean {
val queue = activePlaybackQueue ?: return false
if (index !in queue.items.indices) return false
activePlaybackItems.getOrNull(index)?.let { activePlaybackItem = it }
activePlaybackTitle = queue.items[index].title
activePlaybackSubtitleState = null
resumePositionMs = null
return true
}

fun startPlayback(
item: ContentItem,
items: List<ContentItem>,
config: AuthConfig,
parentItem: ContentItem? = null,
playbackResumePositionMs: Long? = null
) {
val playableItems = items.filter(::isPlayableContent)
val playableItems = playableContentItemsForQueue(items, item)
val queue = buildPlaybackQueue(playableItems, item, config)
val startItem = playableItems.getOrNull(queue.startIndex) ?: item
activePlaybackItems = playableItems
activePlaybackItem = item
activePlaybackItem = startItem
activePlaybackSeriesParent =
if (item.contentType == ContentType.SERIES) {
parentItem
Expand All @@ -1149,9 +1161,8 @@ fun RootScreen(
BufferProfile.VOD
}
playbackEngine.setBufferProfile(profile)
val queue = buildPlaybackQueue(items, item, config)
activePlaybackQueue = queue
activePlaybackTitle = queue.items.getOrNull(queue.startIndex)?.title ?: item.title
activePlaybackTitle = queue.items.getOrNull(queue.startIndex)?.title ?: startItem.title
}

val handlePlayItem: (ContentItem, List<ContentItem>) -> Unit = { item, items ->
Expand All @@ -1160,12 +1171,7 @@ fun RootScreen(
resumeFocusId = resolveResumeFocusTarget(item)
val shouldResolveSeriesQueue =
item.contentType == ContentType.SERIES &&
!item.containerExtension.isNullOrBlank() &&
(items.size <= 1 ||
items.any {
it.contentType != ContentType.SERIES ||
it.containerExtension.isNullOrBlank()
})
!item.containerExtension.isNullOrBlank()
if (shouldResolveSeriesQueue) {
coroutineScope.launch {
val resolved =
Expand Down Expand Up @@ -1274,12 +1280,7 @@ fun RootScreen(
resumeFocusId = resolveResumeFocusTarget(item)
val shouldResolveSeriesQueue =
item.contentType == ContentType.SERIES &&
!item.containerExtension.isNullOrBlank() &&
(items.size <= 1 ||
items.any {
it.contentType != ContentType.SERIES ||
it.containerExtension.isNullOrBlank()
})
!item.containerExtension.isNullOrBlank()
if (shouldResolveSeriesQueue) {
coroutineScope.launch {
val resolved =
Expand Down Expand Up @@ -1387,9 +1388,16 @@ fun RootScreen(
completionThresholdPercent = CONTINUE_WATCHING_MAX_PROGRESS_PERCENT
)
val subtitleState = activePlaybackSubtitleState
val hasFollowingSeriesEpisode =
item.contentType == ContentType.SERIES &&
activePlaybackQueue?.items
?.drop(safePlayer.currentMediaItemIndex + 1)
?.any { it.type == ContentType.SERIES } == true
coroutineScope.launch {
if (!shouldStore) {
continueWatchingRepository.removeEntry(config, item)
if (!hasFollowingSeriesEpisode) {
continueWatchingRepository.removeEntry(config, item)
}
vodPlaybackStateRepository.removeEntry(config, queueMediaIdFor(item))
} else {
val parentItem =
Expand Down Expand Up @@ -1619,6 +1627,14 @@ fun RootScreen(
syncPlaybackStateFromPlayer(mediaItem)
}

override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
syncPlaybackStateFromPlayer(newPosition.mediaItem)
}

override fun onPlaybackStateChanged(playbackState: Int) {
syncPlaybackStateFromPlayer()
if (playbackState == Player.STATE_READY) {
Expand Down Expand Up @@ -2294,8 +2310,15 @@ fun RootScreen(
resumePositionMs = null
},
onPlayNextEpisode = {
playbackEngine.player.seekToNextMediaItem()
playbackEngine.player.playWhenReady = true
val player = playbackEngine.player
savePlaybackProgress()
val nextIndex = player.currentMediaItemIndex + 1
if (syncPlaybackStateToQueueIndex(nextIndex)) {
player.seekTo(nextIndex, 0L)
} else {
player.seekToNextMediaItem()
}
player.playWhenReady = true
},
onMatchFrameRateChange = { enabled ->
playbackEngine.applySettings(settings.copy(matchFrameRateEnabled = enabled))
Expand Down Expand Up @@ -2718,6 +2741,17 @@ private fun isPlayableContent(item: ContentItem): Boolean {
return item.contentType != ContentType.SERIES || !item.containerExtension.isNullOrBlank()
}

private fun playableContentItemsForQueue(
items: List<ContentItem>,
current: ContentItem
): List<ContentItem> {
val playableItems = items.filter(::isPlayableContent).toMutableList()
if (playableItems.none { isSameContentIdentity(it, current) }) {
playableItems.add(current)
}
return playableItems
}

private fun isSameContentIdentity(first: ContentItem, second: ContentItem): Boolean {
if (first.contentType != second.contentType) return false
val firstStreamId = first.streamId?.takeUnless { it.isBlank() }
Expand All @@ -2739,10 +2773,7 @@ private fun buildPlaybackQueue(
current: ContentItem,
authConfig: AuthConfig
): PlaybackQueue {
val playableItems = items.filter(::isPlayableContent).toMutableList()
if (playableItems.none { isSameContentIdentity(it, current) }) {
playableItems.add(current)
}
val playableItems = playableContentItemsForQueue(items, current)
val startIndex =
playableItems.indexOfFirst { isSameContentIdentity(it, current) }.let { index ->
if (index >= 0) index else 0
Expand Down Expand Up @@ -8746,10 +8777,13 @@ fun ContinueWatchingScreen(
}
val columns = rememberReflowColumns(baseColumns, navLayoutExpanded)
val resolvedParents = remember { androidx.compose.runtime.mutableStateMapOf<String, ContentItem>() }
val parentResolutionAttempted =
remember { androidx.compose.runtime.mutableStateMapOf<String, Boolean>() }
val posterFontScale = remember(columns) { 4f / columns.toFloat() }
val resolvedParentsSnapshot = resolvedParents.toMap()
val parentResolutionAttemptedSnapshot = parentResolutionAttempted.toMap()
val displayEntries =
remember(continueWatchingItems, resolvedParentsSnapshot) {
remember(continueWatchingItems, resolvedParentsSnapshot, parentResolutionAttemptedSnapshot) {
fun displayGroupingKey(entry: ContinueWatchingEntry): String {
val canonicalSeriesId =
entry.parentItem?.streamId?.takeUnless { it.isBlank() }
Expand Down Expand Up @@ -8786,6 +8820,16 @@ fun ContinueWatchingScreen(
}
.maxByOrNull { it.first }
?.second
val waitingForParentResolution =
group.any { entry ->
entry.item.contentType == ContentType.SERIES &&
entry.parentItem == null &&
!resolvedParentsSnapshot.containsKey(entry.key) &&
parentResolutionAttemptedSnapshot[entry.key] != true
}
if (latestSeriesParent == null && waitingForParentResolution) {
return@mapNotNull null
}
val displayItem = latestSeriesParent ?: latest.item
val resumeLabel =
if (latest.item.contentType == ContentType.SERIES) {
Expand Down Expand Up @@ -8856,16 +8900,22 @@ fun ContinueWatchingScreen(
.filterNot { it in currentKeys }
.toList()
.forEach { resolvedParents.remove(it) }
parentResolutionAttempted.keys
.filterNot { it in currentKeys }
.toList()
.forEach { parentResolutionAttempted.remove(it) }
continueWatchingItems
.filter { entry ->
entry.item.contentType == ContentType.SERIES && entry.parentItem == null &&
!resolvedParents.containsKey(entry.key)
!resolvedParents.containsKey(entry.key) &&
parentResolutionAttempted[entry.key] != true
}
.forEach { entry ->
val resolved = resolveSeriesParent(entry.item)
if (resolved != null) {
resolvedParents[entry.key] = resolved
}
parentResolutionAttempted[entry.key] = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry parent resolution after transient lookup failures

This marks every unresolved series entry as attempted even when resolveSeriesParent returns null, and future passes explicitly skip any key with parentResolutionAttempted == true. In a transient failure scenario (e.g., network/cache miss during first render), that permanently disables parent re-resolution for that entry, so the continue-watching card can remain stuck without the canonical series parent until the entry key changes.

Useful? React with 👍 / 👎.

}
}

Expand Down
20 changes: 19 additions & 1 deletion app/src/main/java/com/example/xtreamplayer/PlayerScreen.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.example.xtreamplayer

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.media3.common.Player
import com.example.xtreamplayer.content.CategoryItem
import com.example.xtreamplayer.content.ContentItem
import com.example.xtreamplayer.content.ContentType
Expand Down Expand Up @@ -32,7 +38,19 @@ internal fun PlayerScreen(
loadLiveCategoryThumbnail: suspend (CategoryItem) -> Result<String?>
) {
val queue = activePlaybackQueue ?: return
val currentIndex = playbackEngine.player.currentMediaItemIndex
val player = playbackEngine.player
var currentIndex by remember(queue, player) { mutableIntStateOf(player.currentMediaItemIndex) }
DisposableEffect(queue, player) {
currentIndex = player.currentMediaItemIndex
val listener =
object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
currentIndex = player.currentMediaItemIndex
}
}
player.addListener(listener)
onDispose { player.removeListener(listener) }
}
val queueItems = queue.items
val hasNextEpisode = currentIndex >= 0 && currentIndex < queueItems.size - 1
val nextEpisodeTitle = queueItems.getOrNull(currentIndex + 1)?.title
Expand Down
Loading