mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor: Added MediaSegments to MediaContext to make it accesible during playback. Also refactored MediaTrackPreferences to load preference in the PlayerViewModel
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import androidx.media3.common.MediaItem
|
||||
import hu.bbara.purefin.core.model.MediaSegment
|
||||
import java.util.UUID
|
||||
|
||||
interface PlayableMediaRepository {
|
||||
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>?
|
||||
suspend fun getMediaSegments(mediaId: UUID): List<MediaSegment>
|
||||
suspend fun getNextUpMediaItems(
|
||||
episodeId: UUID,
|
||||
existingIds: Set<String>,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package hu.bbara.purefin.core.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
enum class SegmentType {
|
||||
INTRO,
|
||||
OUTRO,
|
||||
MAIN_CONTENT,
|
||||
PREVIEW,
|
||||
RECAP;
|
||||
}
|
||||
|
||||
data class MediaSegment(
|
||||
val id: UUID,
|
||||
val type: SegmentType,
|
||||
val startMs: Long,
|
||||
val endMs: Long
|
||||
)
|
||||
@@ -10,6 +10,10 @@ import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.player.model.MediaContext
|
||||
import hu.bbara.purefin.core.player.model.MetadataState
|
||||
import hu.bbara.purefin.core.player.model.PlaybackProgressSnapshot
|
||||
import hu.bbara.purefin.core.player.model.PlaybackStateSnapshot
|
||||
import hu.bbara.purefin.core.player.model.QueueItemUi
|
||||
import hu.bbara.purefin.core.player.model.TrackOption
|
||||
import hu.bbara.purefin.core.player.model.TrackType
|
||||
@@ -25,7 +29,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -231,7 +234,7 @@ class PlayerManager @Inject constructor(
|
||||
// Save track preference if media context is available
|
||||
currentMediaContext?.let { context ->
|
||||
scope.launch {
|
||||
saveTrackPreference(option, context.preferenceKey)
|
||||
saveTrackPreference(option, context.mediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,7 +266,7 @@ class PlayerManager @Inject constructor(
|
||||
|
||||
private suspend fun applyTrackPreferences() {
|
||||
val context = currentMediaContext ?: return
|
||||
val preferences = trackPreferencesRepository.getMediaPreferences(context.preferenceKey).firstOrNull() ?: return
|
||||
val preferences = context.preferences ?: return
|
||||
|
||||
val currentTrackState = _tracks.value
|
||||
|
||||
@@ -394,28 +397,3 @@ class PlayerManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
data class PlaybackStateSnapshot(
|
||||
val isPlaying: Boolean = false,
|
||||
val isBuffering: Boolean = false,
|
||||
val isEnded: Boolean = false,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
data class PlaybackProgressSnapshot(
|
||||
val durationMs: Long = 0L,
|
||||
val positionMs: Long = 0L,
|
||||
val bufferedMs: Long = 0L,
|
||||
val isLive: Boolean = false
|
||||
)
|
||||
|
||||
data class MetadataState(
|
||||
val mediaId: String? = null,
|
||||
val title: String? = null,
|
||||
val subtitle: String? = null,
|
||||
val playbackReportContext: PlaybackReportContext? = null,
|
||||
)
|
||||
|
||||
data class MediaContext(
|
||||
val mediaId: String,
|
||||
val preferenceKey: String
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.core.data.MediaProgressWriter
|
||||
import hu.bbara.purefin.core.data.PlaybackProgressReporter
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.player.model.MetadataState
|
||||
import hu.bbara.purefin.core.player.model.PlaybackProgressSnapshot
|
||||
import hu.bbara.purefin.core.player.model.PlaybackStateSnapshot
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package hu.bbara.purefin.core.player.model
|
||||
|
||||
import hu.bbara.purefin.core.model.MediaSegment
|
||||
import hu.bbara.purefin.core.player.preference.MediaTrackPreferences
|
||||
|
||||
data class MediaContext(
|
||||
val mediaId: String,
|
||||
val preferences: MediaTrackPreferences?,
|
||||
val mediaSegments: List<MediaSegment>
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package hu.bbara.purefin.core.player.model
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
|
||||
data class MetadataState(
|
||||
val mediaId: String? = null,
|
||||
val title: String? = null,
|
||||
val subtitle: String? = null,
|
||||
val playbackReportContext: PlaybackReportContext? = null,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package hu.bbara.purefin.core.player.model
|
||||
|
||||
data class PlaybackProgressSnapshot(
|
||||
val durationMs: Long = 0L,
|
||||
val positionMs: Long = 0L,
|
||||
val bufferedMs: Long = 0L,
|
||||
val isLive: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package hu.bbara.purefin.core.player.model
|
||||
|
||||
data class PlaybackStateSnapshot(
|
||||
val isPlaying: Boolean = false,
|
||||
val isBuffering: Boolean = false,
|
||||
val isEnded: Boolean = false,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package hu.bbara.purefin.core.player.preference
|
||||
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -6,18 +6,21 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.EpisodeSeriesLookup
|
||||
import hu.bbara.purefin.core.data.PlayableMediaRepository
|
||||
import hu.bbara.purefin.core.player.manager.MediaContext
|
||||
import hu.bbara.purefin.core.player.manager.PlayerManager
|
||||
import hu.bbara.purefin.core.player.manager.ProgressManager
|
||||
import hu.bbara.purefin.core.player.model.MediaContext
|
||||
import hu.bbara.purefin.core.player.model.PlayerUiState
|
||||
import hu.bbara.purefin.core.player.model.TrackOption
|
||||
import hu.bbara.purefin.core.player.preference.TrackPreferencesRepository
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -26,6 +29,7 @@ class PlayerViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val playerManager: PlayerManager,
|
||||
private val playableMediaRepository: PlayableMediaRepository,
|
||||
private val trackPreferencesRepository: TrackPreferencesRepository,
|
||||
private val episodeSeriesLookup: EpisodeSeriesLookup,
|
||||
private val progressManager: ProgressManager,
|
||||
) : ViewModel() {
|
||||
@@ -139,6 +143,7 @@ class PlayerViewModel @Inject constructor(
|
||||
loadMediaById(id)
|
||||
}
|
||||
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
private fun loadMediaById(id: String) {
|
||||
val uuid = id.toUuidOrNull()
|
||||
if (uuid == null) {
|
||||
@@ -152,7 +157,14 @@ class PlayerViewModel @Inject constructor(
|
||||
val (mediaItem, resumePositionMs) = result
|
||||
|
||||
val preferenceKey = episodeSeriesLookup.preferenceKeyFor(uuid)
|
||||
val mediaContext = MediaContext(mediaId = id, preferenceKey = preferenceKey)
|
||||
val preferences = trackPreferencesRepository.getMediaPreferences(preferenceKey).firstOrNull()
|
||||
val mediaSegments = playableMediaRepository.getMediaSegments(uuid)
|
||||
|
||||
val mediaContext = MediaContext(
|
||||
mediaId = id,
|
||||
preferences = preferences,
|
||||
mediaSegments = mediaSegments
|
||||
)
|
||||
|
||||
playerManager.play(mediaItem, mediaContext)
|
||||
|
||||
|
||||
@@ -9,20 +9,27 @@ import androidx.media3.common.util.UnstableApi
|
||||
import hu.bbara.purefin.core.data.PlayableMediaRepository
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.model.MediaSegment
|
||||
import hu.bbara.purefin.core.model.SegmentType
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.jellyfin.playback.JellyfinPlaybackResolver
|
||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecision
|
||||
import hu.bbara.purefin.data.jellyfin.playback.playbackCustomCacheKey
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentType.INTRO
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentType.OUTRO
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentType.PREVIEW
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentType.RECAP
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DefaultPlayableMediaRepository @Inject constructor(
|
||||
@@ -52,6 +59,13 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
Pair(mediaItem, resumePositionMs)
|
||||
}
|
||||
|
||||
override suspend fun getMediaSegments(mediaId: UUID): List<MediaSegment> {
|
||||
val mediaSegments = jellyfinApiClient.getMediaSegments(mediaId)
|
||||
return mediaSegments.mapNotNull {
|
||||
it.toMediaSegment()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNextUpMediaItems(
|
||||
episodeId: UUID,
|
||||
existingIds: Set<String>,
|
||||
@@ -133,4 +147,20 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
val episodeNumber = item.indexNumber ?: return null
|
||||
return "S$seasonNumber:E$episodeNumber"
|
||||
}
|
||||
|
||||
private fun MediaSegmentDto.toMediaSegment(): MediaSegment {
|
||||
val segmentType = when (type) {
|
||||
INTRO -> SegmentType.INTRO
|
||||
PREVIEW -> SegmentType.PREVIEW
|
||||
RECAP -> SegmentType.RECAP
|
||||
OUTRO -> SegmentType.OUTRO
|
||||
else -> SegmentType.MAIN_CONTENT
|
||||
}
|
||||
return MediaSegment(
|
||||
id = itemId,
|
||||
type = segmentType,
|
||||
startMs = startTicks,
|
||||
endMs = endTicks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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.mediaSegmentsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||
@@ -30,6 +31,8 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
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.MediaSegmentDto
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.PlayMethod
|
||||
@@ -264,6 +267,18 @@ class JellyfinApiClient @Inject constructor(
|
||||
result.content.mediaSources
|
||||
}
|
||||
|
||||
suspend fun getMediaSegments(mediaId: UUID) : List<MediaSegmentDto> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val result = api.mediaSegmentsApi.getItemSegments(
|
||||
itemId = mediaId,
|
||||
includeSegmentTypes = listOf(MediaSegmentType.INTRO)
|
||||
)
|
||||
Log.d("getMediaSegments", result.toString())
|
||||
result.content.items
|
||||
}
|
||||
|
||||
suspend fun getPlaybackInfo(
|
||||
mediaId: UUID,
|
||||
deviceProfile: DeviceProfile,
|
||||
|
||||
Reference in New Issue
Block a user