mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +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
|
package hu.bbara.purefin.core.data
|
||||||
|
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import hu.bbara.purefin.core.model.MediaSegment
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface PlayableMediaRepository {
|
interface PlayableMediaRepository {
|
||||||
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>?
|
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>?
|
||||||
|
suspend fun getMediaSegments(mediaId: UUID): List<MediaSegment>
|
||||||
suspend fun getNextUpMediaItems(
|
suspend fun getNextUpMediaItems(
|
||||||
episodeId: UUID,
|
episodeId: UUID,
|
||||||
existingIds: Set<String>,
|
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 androidx.media3.common.util.UnstableApi
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
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.QueueItemUi
|
||||||
import hu.bbara.purefin.core.player.model.TrackOption
|
import hu.bbara.purefin.core.player.model.TrackOption
|
||||||
import hu.bbara.purefin.core.player.model.TrackType
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -231,7 +234,7 @@ class PlayerManager @Inject constructor(
|
|||||||
// Save track preference if media context is available
|
// Save track preference if media context is available
|
||||||
currentMediaContext?.let { context ->
|
currentMediaContext?.let { context ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
saveTrackPreference(option, context.preferenceKey)
|
saveTrackPreference(option, context.mediaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,7 +266,7 @@ class PlayerManager @Inject constructor(
|
|||||||
|
|
||||||
private suspend fun applyTrackPreferences() {
|
private suspend fun applyTrackPreferences() {
|
||||||
val context = currentMediaContext ?: return
|
val context = currentMediaContext ?: return
|
||||||
val preferences = trackPreferencesRepository.getMediaPreferences(context.preferenceKey).firstOrNull() ?: return
|
val preferences = context.preferences ?: return
|
||||||
|
|
||||||
val currentTrackState = _tracks.value
|
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.MediaProgressWriter
|
||||||
import hu.bbara.purefin.core.data.PlaybackProgressReporter
|
import hu.bbara.purefin.core.data.PlaybackProgressReporter
|
||||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
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
|
package hu.bbara.purefin.core.player.preference
|
||||||
|
|
||||||
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -6,18 +6,21 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.core.data.EpisodeSeriesLookup
|
import hu.bbara.purefin.core.data.EpisodeSeriesLookup
|
||||||
import hu.bbara.purefin.core.data.PlayableMediaRepository
|
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.PlayerManager
|
||||||
import hu.bbara.purefin.core.player.manager.ProgressManager
|
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.PlayerUiState
|
||||||
import hu.bbara.purefin.core.player.model.TrackOption
|
import hu.bbara.purefin.core.player.model.TrackOption
|
||||||
|
import hu.bbara.purefin.core.player.preference.TrackPreferencesRepository
|
||||||
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
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -26,6 +29,7 @@ class PlayerViewModel @Inject constructor(
|
|||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val playerManager: PlayerManager,
|
private val playerManager: PlayerManager,
|
||||||
private val playableMediaRepository: PlayableMediaRepository,
|
private val playableMediaRepository: PlayableMediaRepository,
|
||||||
|
private val trackPreferencesRepository: TrackPreferencesRepository,
|
||||||
private val episodeSeriesLookup: EpisodeSeriesLookup,
|
private val episodeSeriesLookup: EpisodeSeriesLookup,
|
||||||
private val progressManager: ProgressManager,
|
private val progressManager: ProgressManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@@ -139,6 +143,7 @@ class PlayerViewModel @Inject constructor(
|
|||||||
loadMediaById(id)
|
loadMediaById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(InternalSerializationApi::class)
|
||||||
private fun loadMediaById(id: String) {
|
private fun loadMediaById(id: String) {
|
||||||
val uuid = id.toUuidOrNull()
|
val uuid = id.toUuidOrNull()
|
||||||
if (uuid == null) {
|
if (uuid == null) {
|
||||||
@@ -152,7 +157,14 @@ class PlayerViewModel @Inject constructor(
|
|||||||
val (mediaItem, resumePositionMs) = result
|
val (mediaItem, resumePositionMs) = result
|
||||||
|
|
||||||
val preferenceKey = episodeSeriesLookup.preferenceKeyFor(uuid)
|
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)
|
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.PlayableMediaRepository
|
||||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
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.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.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.data.jellyfin.playback.JellyfinPlaybackResolver
|
import hu.bbara.purefin.data.jellyfin.playback.JellyfinPlaybackResolver
|
||||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecision
|
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecision
|
||||||
import hu.bbara.purefin.data.jellyfin.playback.playbackCustomCacheKey
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import hu.bbara.purefin.core.image.ArtworkKind
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
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 org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class DefaultPlayableMediaRepository @Inject constructor(
|
class DefaultPlayableMediaRepository @Inject constructor(
|
||||||
@@ -52,6 +59,13 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
Pair(mediaItem, resumePositionMs)
|
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(
|
override suspend fun getNextUpMediaItems(
|
||||||
episodeId: UUID,
|
episodeId: UUID,
|
||||||
existingIds: Set<String>,
|
existingIds: Set<String>,
|
||||||
@@ -133,4 +147,20 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
val episodeNumber = item.indexNumber ?: return null
|
val episodeNumber = item.indexNumber ?: return null
|
||||||
return "S$seasonNumber:E$episodeNumber"
|
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.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.mediaSegmentsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
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.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.MediaSegmentDto
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.api.PlayMethod
|
import org.jellyfin.sdk.model.api.PlayMethod
|
||||||
@@ -264,6 +267,18 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
result.content.mediaSources
|
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(
|
suspend fun getPlaybackInfo(
|
||||||
mediaId: UUID,
|
mediaId: UUID,
|
||||||
deviceProfile: DeviceProfile,
|
deviceProfile: DeviceProfile,
|
||||||
|
|||||||
Reference in New Issue
Block a user