feat: enhance media item retrieval to include resume position calculation

This commit is contained in:
2026-02-05 20:13:06 +01:00
parent 3fed91aa27
commit 6977acc60f
3 changed files with 46 additions and 8 deletions

View File

@@ -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.authenticateUserByName
import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi 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.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.userViewsApi 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.api.client.extensions.videosApi
import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.createJellyfin
import org.jellyfin.sdk.model.ClientInfo 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.DeviceProfile
import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.MediaSourceInfo 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.PlaybackInfoDto
import org.jellyfin.sdk.model.api.PlaybackOrder import org.jellyfin.sdk.model.api.PlaybackOrder
import org.jellyfin.sdk.model.api.PlaybackProgressInfo import org.jellyfin.sdk.model.api.PlaybackProgressInfo
import org.jellyfin.sdk.model.api.PlaybackStartInfo import org.jellyfin.sdk.model.api.PlaybackStartInfo
import org.jellyfin.sdk.model.api.PlaybackStopInfo 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.RepeatMode
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.SubtitleProfile import org.jellyfin.sdk.model.api.SubtitleProfile
@@ -189,7 +189,7 @@ class JellyfinApiClient @Inject constructor(
} }
val result = api.userLibraryApi.getItem( val result = api.userLibraryApi.getItem(
itemId = mediaId, itemId = mediaId,
userId = getUserId(), userId = getUserId()
) )
Log.d("getItemInfo", result.content.toString()) Log.d("getItemInfo", result.content.toString())
return result.content return result.content

View File

@@ -5,6 +5,8 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
import hu.bbara.purefin.client.JellyfinApiClient 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 java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@@ -13,7 +15,7 @@ class MediaRepository @Inject constructor(
private val jellyfinApiClient: JellyfinApiClient private val jellyfinApiClient: JellyfinApiClient
) { ) {
suspend fun getMediaItem(mediaId: UUID): MediaItem? { suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? {
val mediaSources = jellyfinApiClient.getMediaSources(mediaId) val mediaSources = jellyfinApiClient.getMediaSources(mediaId)
val selectedMediaSource = mediaSources.firstOrNull() ?: return null val selectedMediaSource = mediaSources.firstOrNull() ?: return null
val playbackUrl = jellyfinApiClient.getMediaPlaybackUrl( val playbackUrl = jellyfinApiClient.getMediaPlaybackUrl(
@@ -21,12 +23,42 @@ class MediaRepository @Inject constructor(
mediaSourceId = selectedMediaSource.id mediaSourceId = selectedMediaSource.id
) ?: return null ) ?: return null
val baseItem = jellyfinApiClient.getItemInfo(mediaId) val baseItem = jellyfinApiClient.getItemInfo(mediaId)
return createMediaItem(
// Calculate resume position
val resumePositionMs = calculateResumePosition(baseItem, selectedMediaSource)
val mediaItem = createMediaItem(
mediaId = mediaId.toString(), mediaId = mediaId.toString(),
playbackUrl = playbackUrl, playbackUrl = playbackUrl,
title = baseItem?.name ?: selectedMediaSource.name, title = baseItem?.name ?: selectedMediaSource.name,
subtitle = "S${baseItem!!.parentIndexNumber}:E${baseItem.indexNumber}" 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<String>, count: Int = 2): List<MediaItem> { suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set<String>, count: Int = 2): List<MediaItem> {

View File

@@ -9,7 +9,6 @@ import hu.bbara.purefin.player.manager.PlayerManager
import hu.bbara.purefin.player.manager.ProgressManager import hu.bbara.purefin.player.manager.ProgressManager
import hu.bbara.purefin.player.model.PlayerUiState import hu.bbara.purefin.player.model.PlayerUiState
import hu.bbara.purefin.player.model.TrackOption import hu.bbara.purefin.player.model.TrackOption
import javax.inject.Inject
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,6 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PlayerViewModel @Inject constructor( class PlayerViewModel @Inject constructor(
@@ -133,9 +133,15 @@ class PlayerViewModel @Inject constructor(
return return
} }
viewModelScope.launch { viewModelScope.launch {
val mediaItem = mediaRepository.getMediaItem(uuid) val result = mediaRepository.getMediaItem(uuid)
if (mediaItem != null) { if (result != null) {
val (mediaItem, resumePositionMs) = result
playerManager.play(mediaItem) playerManager.play(mediaItem)
// Seek to resume position after play() is called
resumePositionMs?.let { playerManager.seekTo(it) }
if (dataErrorMessage != null) { if (dataErrorMessage != null) {
dataErrorMessage = null dataErrorMessage = null
_uiState.update { it.copy(error = null) } _uiState.update { it.copy(error = null) }