From 6977acc60ff3996d841d40a115e587f7db905958 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Thu, 5 Feb 2026 20:13:06 +0100 Subject: [PATCH] feat: enhance media item retrieval to include resume position calculation --- .../bbara/purefin/client/JellyfinApiClient.kt | 6 ++-- .../purefin/player/data/MediaRepository.kt | 36 +++++++++++++++++-- .../player/viewmodel/PlayerViewModel.kt | 12 +++++-- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt index 14dd906..ee71a02 100644 --- a/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt +++ b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt @@ -9,11 +9,11 @@ import org.jellyfin.sdk.api.client.Response import org.jellyfin.sdk.api.client.extensions.authenticateUserByName import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.mediaInfoApi +import org.jellyfin.sdk.api.client.extensions.playStateApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userViewsApi -import org.jellyfin.sdk.api.client.extensions.playStateApi import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.model.ClientInfo @@ -24,12 +24,12 @@ import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.MediaSourceInfo +import org.jellyfin.sdk.model.api.PlayMethod import org.jellyfin.sdk.model.api.PlaybackInfoDto import org.jellyfin.sdk.model.api.PlaybackOrder import org.jellyfin.sdk.model.api.PlaybackProgressInfo import org.jellyfin.sdk.model.api.PlaybackStartInfo import org.jellyfin.sdk.model.api.PlaybackStopInfo -import org.jellyfin.sdk.model.api.PlayMethod import org.jellyfin.sdk.model.api.RepeatMode import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod import org.jellyfin.sdk.model.api.SubtitleProfile @@ -189,7 +189,7 @@ class JellyfinApiClient @Inject constructor( } val result = api.userLibraryApi.getItem( itemId = mediaId, - userId = getUserId(), + userId = getUserId() ) Log.d("getItemInfo", result.content.toString()) return result.content diff --git a/app/src/main/java/hu/bbara/purefin/player/data/MediaRepository.kt b/app/src/main/java/hu/bbara/purefin/player/data/MediaRepository.kt index 4ad339a..07b8783 100644 --- a/app/src/main/java/hu/bbara/purefin/player/data/MediaRepository.kt +++ b/app/src/main/java/hu/bbara/purefin/player/data/MediaRepository.kt @@ -5,6 +5,8 @@ import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import dagger.hilt.android.scopes.ViewModelScoped import hu.bbara.purefin.client.JellyfinApiClient +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.MediaSourceInfo import java.util.UUID import javax.inject.Inject @@ -13,7 +15,7 @@ class MediaRepository @Inject constructor( private val jellyfinApiClient: JellyfinApiClient ) { - suspend fun getMediaItem(mediaId: UUID): MediaItem? { + suspend fun getMediaItem(mediaId: UUID): Pair? { val mediaSources = jellyfinApiClient.getMediaSources(mediaId) val selectedMediaSource = mediaSources.firstOrNull() ?: return null val playbackUrl = jellyfinApiClient.getMediaPlaybackUrl( @@ -21,12 +23,42 @@ class MediaRepository @Inject constructor( mediaSourceId = selectedMediaSource.id ) ?: return null val baseItem = jellyfinApiClient.getItemInfo(mediaId) - return createMediaItem( + + // Calculate resume position + val resumePositionMs = calculateResumePosition(baseItem, selectedMediaSource) + + val mediaItem = createMediaItem( mediaId = mediaId.toString(), playbackUrl = playbackUrl, title = baseItem?.name ?: selectedMediaSource.name, subtitle = "S${baseItem!!.parentIndexNumber}:E${baseItem.indexNumber}" ) + + return Pair(mediaItem, resumePositionMs) + } + + private fun calculateResumePosition( + baseItem: BaseItemDto?, + mediaSource: MediaSourceInfo + ): Long? { + val userData = baseItem?.userData ?: return null + + // Get runtime in ticks + val runtimeTicks = mediaSource.runTimeTicks ?: baseItem.runTimeTicks ?: 0L + if (runtimeTicks == 0L) return null + + // Get saved playback position from userData + val playbackPositionTicks = userData.playbackPositionTicks ?: 0L + if (playbackPositionTicks == 0L) return null + + // Convert ticks to milliseconds + val positionMs = playbackPositionTicks / 10_000 + + // Calculate percentage for threshold check + val percentage = (playbackPositionTicks.toDouble() / runtimeTicks.toDouble()) * 100.0 + + // Apply thresholds: resume only if 5% ≤ progress ≤ 95% + return if (percentage in 5.0..95.0) positionMs else null } suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set, count: Int = 2): List { diff --git a/app/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt b/app/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt index d80c9c4..a71ec09 100644 --- a/app/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/hu/bbara/purefin/player/viewmodel/PlayerViewModel.kt @@ -9,7 +9,6 @@ import hu.bbara.purefin.player.manager.PlayerManager import hu.bbara.purefin.player.manager.ProgressManager import hu.bbara.purefin.player.model.PlayerUiState import hu.bbara.purefin.player.model.TrackOption -import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -18,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.UUID +import javax.inject.Inject @HiltViewModel class PlayerViewModel @Inject constructor( @@ -133,9 +133,15 @@ class PlayerViewModel @Inject constructor( return } viewModelScope.launch { - val mediaItem = mediaRepository.getMediaItem(uuid) - if (mediaItem != null) { + val result = mediaRepository.getMediaItem(uuid) + if (result != null) { + val (mediaItem, resumePositionMs) = result + playerManager.play(mediaItem) + + // Seek to resume position after play() is called + resumePositionMs?.let { playerManager.seekTo(it) } + if (dataErrorMessage != null) { dataErrorMessage = null _uiState.update { it.copy(error = null) }