mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
feat: enhance media playback handling with offline support and download metadata integration
This commit is contained in:
@@ -68,8 +68,8 @@ class CompositeLocalMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||||
val repository = onlineRepository
|
onlineRepository.updateWatchProgress(mediaId, positionMs, durationMs)
|
||||||
repository.updateWatchProgress(mediaId, positionMs, durationMs)
|
offlineRepository.updateWatchProgress(mediaId, positionMs, durationMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun markAsWatched(mediaId: UUID, watched: Boolean) {
|
override suspend fun markAsWatched(mediaId: UUID, watched: Boolean) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package hu.bbara.purefin.core.download
|
package hu.bbara.purefin.core.download
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -7,6 +8,7 @@ import java.util.UUID
|
|||||||
interface MediaDownloadController {
|
interface MediaDownloadController {
|
||||||
fun observeActiveDownloads(): Flow<Map<String, Float>>
|
fun observeActiveDownloads(): Flow<Map<String, Float>>
|
||||||
fun observeDownloadState(contentId: String): StateFlow<DownloadState>
|
fun observeDownloadState(contentId: String): StateFlow<DownloadState>
|
||||||
|
fun getCompletedDownloadMediaItem(contentId: String): MediaItem?
|
||||||
suspend fun downloadMovie(movieId: UUID)
|
suspend fun downloadMovie(movieId: UUID)
|
||||||
suspend fun cancelDownload(movieId: UUID)
|
suspend fun cancelDownload(movieId: UUID)
|
||||||
suspend fun downloadEpisode(episodeId: UUID)
|
suspend fun downloadEpisode(episodeId: UUID)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.offline.Download
|
import androidx.media3.exoplayer.offline.Download
|
||||||
import androidx.media3.exoplayer.offline.DownloadManager
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
@@ -109,6 +110,12 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
return downloadManager.downloadIndex.getDownload(contentId)?.state == Download.STATE_COMPLETED
|
return downloadManager.downloadIndex.getDownload(contentId)?.state == Download.STATE_COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCompletedDownloadMediaItem(contentId: String): MediaItem? {
|
||||||
|
val download = downloadManager.downloadIndex.getDownload(contentId) ?: return null
|
||||||
|
if (download.state != Download.STATE_COMPLETED) return null
|
||||||
|
return download.request.toMediaItem()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun downloadMovie(movieId: UUID) {
|
override suspend fun downloadMovie(movieId: UUID) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val episode = episodesState.value[id] ?: return flowOf(null)
|
val episode = episodesState.value[id] ?: return flowOf(null)
|
||||||
loadSeasons(seriesId = episode.seriesId)
|
preloadSeasonsForEpisode(episode.seriesId)
|
||||||
return episodesState.map { it[id] }.distinctUntilChanged()
|
return episodesState.map { it[id] }.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,14 +106,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun loadSeasons(seriesId: UUID) {
|
override suspend fun loadSeasons(seriesId: UUID) {
|
||||||
try {
|
try {
|
||||||
seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
|
loadSeasonsInternal(seriesId)
|
||||||
|
|
||||||
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
|
|
||||||
|
|
||||||
val updatedSeries = series.copy(
|
|
||||||
seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
|
|
||||||
)
|
|
||||||
seriesState.update { it + (updatedSeries.id to updatedSeries) }
|
|
||||||
} catch (error: CancellationException) {
|
} catch (error: CancellationException) {
|
||||||
throw error
|
throw error
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
@@ -122,6 +115,27 @@ class InMemoryLocalMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun preloadSeasonsForEpisode(seriesId: UUID) {
|
||||||
|
try {
|
||||||
|
loadSeasonsInternal(seriesId)
|
||||||
|
} catch (error: CancellationException) {
|
||||||
|
throw error
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Log.w("InMemoryMediaRepository", "Unable to preload seasons for episode series $seriesId", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadSeasonsInternal(seriesId: UUID) {
|
||||||
|
seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
|
||||||
|
|
||||||
|
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
|
||||||
|
|
||||||
|
val updatedSeries = series.copy(
|
||||||
|
seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
|
||||||
|
)
|
||||||
|
seriesState.update { it + (updatedSeries.id to updatedSeries) }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) {
|
override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) {
|
||||||
loadSeasons(seriesId)
|
loadSeasons(seriesId)
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ import androidx.core.net.toUri
|
|||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import hu.bbara.purefin.core.Offline
|
||||||
|
import hu.bbara.purefin.core.data.LocalMediaRepository
|
||||||
|
import hu.bbara.purefin.core.data.NetworkMonitor
|
||||||
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.UserSessionRepository
|
import hu.bbara.purefin.core.data.UserSessionRepository
|
||||||
|
import hu.bbara.purefin.core.download.MediaDownloadController
|
||||||
import hu.bbara.purefin.core.image.ArtworkKind
|
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.player.preference.TrackPreferencesRepository
|
import hu.bbara.purefin.core.player.preference.TrackPreferencesRepository
|
||||||
@@ -16,9 +20,11 @@ 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 hu.bbara.purefin.model.Episode
|
||||||
import hu.bbara.purefin.model.MediaSegment
|
import hu.bbara.purefin.model.MediaSegment
|
||||||
import hu.bbara.purefin.model.PlayableMedia
|
import hu.bbara.purefin.model.PlayableMedia
|
||||||
import hu.bbara.purefin.model.SegmentType
|
import hu.bbara.purefin.model.SegmentType
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
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
|
||||||
@@ -40,13 +46,53 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
private val jellyfinPlaybackResolver: JellyfinPlaybackResolver,
|
private val jellyfinPlaybackResolver: JellyfinPlaybackResolver,
|
||||||
private val trackPreferencesRepository: TrackPreferencesRepository,
|
private val trackPreferencesRepository: TrackPreferencesRepository,
|
||||||
private val userSessionRepository: UserSessionRepository,
|
private val userSessionRepository: UserSessionRepository,
|
||||||
|
private val mediaDownloadController: MediaDownloadController,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
@param:Offline private val offlineMediaRepository: LocalMediaRepository,
|
||||||
) : PlayableMediaRepository {
|
) : PlayableMediaRepository {
|
||||||
|
|
||||||
override suspend fun getPlayableMedia(mediaId: UUID): PlayableMedia? = withContext(Dispatchers.IO) {
|
override suspend fun getPlayableMedia(mediaId: UUID): PlayableMedia? = withContext(Dispatchers.IO) {
|
||||||
val baseItem = jellyfinApiClient.getItemInfo(mediaId) ?: return@withContext null
|
val downloadedMediaItem = mediaDownloadController.getCompletedDownloadMediaItem(mediaId.toString())
|
||||||
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(mediaId) ?: return@withContext null
|
if (downloadedMediaItem != null) {
|
||||||
|
return@withContext getDownloadedPlayableMedia(mediaId, downloadedMediaItem)
|
||||||
|
}
|
||||||
|
getStreamingPlayableMedia(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
val mediaItem = getMediaItem(baseItem, playbackDecision)
|
private suspend fun getDownloadedPlayableMedia(
|
||||||
|
mediaId: UUID,
|
||||||
|
downloadedMediaItem: MediaItem,
|
||||||
|
): PlayableMedia? {
|
||||||
|
if (!networkMonitor.isOnline.first()) {
|
||||||
|
return getOfflineDownloadedPlayableMedia(mediaId, downloadedMediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
getOnlinePlayableMedia(mediaId, downloadedMediaItem)
|
||||||
|
?: getOfflineDownloadedPlayableMedia(mediaId, downloadedMediaItem)
|
||||||
|
}.getOrElse { error ->
|
||||||
|
if (error is CancellationException) throw error
|
||||||
|
Log.w("PlayableMediaRepo", "Unable to load online metadata for downloaded media $mediaId", error)
|
||||||
|
getOfflineDownloadedPlayableMedia(mediaId, downloadedMediaItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getStreamingPlayableMedia(mediaId: UUID): PlayableMedia? {
|
||||||
|
return getOnlinePlayableMedia(mediaId, downloadedMediaItem = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getOnlinePlayableMedia(
|
||||||
|
mediaId: UUID,
|
||||||
|
downloadedMediaItem: MediaItem?,
|
||||||
|
): PlayableMedia? {
|
||||||
|
val baseItem = jellyfinApiClient.getItemInfo(mediaId) ?: return null
|
||||||
|
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(mediaId) ?: return null
|
||||||
|
|
||||||
|
val mediaItem = if (downloadedMediaItem == null) {
|
||||||
|
getMediaItem(baseItem, playbackDecision)
|
||||||
|
} else {
|
||||||
|
getDownloadedMediaItem(baseItem, playbackDecision, downloadedMediaItem)
|
||||||
|
}
|
||||||
val resumePositionMs = calculateResumePosition(baseItem, playbackDecision.mediaSource)
|
val resumePositionMs = calculateResumePosition(baseItem, playbackDecision.mediaSource)
|
||||||
val preferenceMediaId = when (baseItem.type) {
|
val preferenceMediaId = when (baseItem.type) {
|
||||||
BaseItemKind.EPISODE -> baseItem.seriesId ?: mediaId
|
BaseItemKind.EPISODE -> baseItem.seriesId ?: mediaId
|
||||||
@@ -54,7 +100,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
val mediaTrackPreferences = trackPreferencesRepository.getMediaPreferences(preferenceMediaId.toString()).first()
|
val mediaTrackPreferences = trackPreferencesRepository.getMediaPreferences(preferenceMediaId.toString()).first()
|
||||||
val mediaSegments = getMediaSegments(mediaId)
|
val mediaSegments = getMediaSegments(mediaId)
|
||||||
when (baseItem.type) {
|
return when (baseItem.type) {
|
||||||
BaseItemKind.MOVIE -> PlayableMedia.Movie(
|
BaseItemKind.MOVIE -> PlayableMedia.Movie(
|
||||||
id = mediaId,
|
id = mediaId,
|
||||||
preferenceMediaId = preferenceMediaId,
|
preferenceMediaId = preferenceMediaId,
|
||||||
@@ -83,6 +129,49 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getOfflineDownloadedPlayableMedia(
|
||||||
|
mediaId: UUID,
|
||||||
|
downloadedMediaItem: MediaItem,
|
||||||
|
): PlayableMedia? {
|
||||||
|
offlineMediaRepository.getMovie(mediaId).first()?.let { movie ->
|
||||||
|
val preferences = trackPreferencesRepository.getMediaPreferences(mediaId.toString()).first()
|
||||||
|
return PlayableMedia.Movie(
|
||||||
|
id = mediaId,
|
||||||
|
preferenceMediaId = mediaId,
|
||||||
|
mediaItem = downloadedMediaItem.withMetadata(
|
||||||
|
mediaId = mediaId.toString(),
|
||||||
|
title = movie.title,
|
||||||
|
subtitle = null,
|
||||||
|
artworkUrl = ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
|
playbackReportContext = null,
|
||||||
|
),
|
||||||
|
resumePositionMs = 0L,
|
||||||
|
preferences = preferences,
|
||||||
|
mediaSegments = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
offlineMediaRepository.getEpisode(mediaId).first()?.let { episode ->
|
||||||
|
val preferences = trackPreferencesRepository.getMediaPreferences(episode.seriesId.toString()).first()
|
||||||
|
return PlayableMedia.Episode(
|
||||||
|
id = mediaId,
|
||||||
|
preferenceMediaId = episode.seriesId,
|
||||||
|
mediaItem = downloadedMediaItem.withMetadata(
|
||||||
|
mediaId = mediaId.toString(),
|
||||||
|
title = episode.title,
|
||||||
|
subtitle = episode.seasonEpisodeLabel(),
|
||||||
|
artworkUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
|
playbackReportContext = null,
|
||||||
|
),
|
||||||
|
resumePositionMs = 0L,
|
||||||
|
preferences = preferences,
|
||||||
|
mediaSegments = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getMediaItem(baseItem: BaseItemDto, playbackDecision: PlaybackDecision): MediaItem = withContext(Dispatchers.IO) {
|
private suspend fun getMediaItem(baseItem: BaseItemDto, playbackDecision: PlaybackDecision): MediaItem = withContext(Dispatchers.IO) {
|
||||||
val mediaId = baseItem.id
|
val mediaId = baseItem.id
|
||||||
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
||||||
@@ -102,6 +191,25 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
return@withContext mediaItem
|
return@withContext mediaItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getDownloadedMediaItem(
|
||||||
|
baseItem: BaseItemDto,
|
||||||
|
playbackDecision: PlaybackDecision,
|
||||||
|
downloadedMediaItem: MediaItem,
|
||||||
|
): MediaItem = withContext(Dispatchers.IO) {
|
||||||
|
val mediaId = baseItem.id
|
||||||
|
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
||||||
|
val serverUrl = userSessionRepository.serverUrl.first()
|
||||||
|
val artworkUrl = ImageUrlBuilder.toImageUrl(serverUrl, mediaId, ArtworkKind.PRIMARY)
|
||||||
|
|
||||||
|
return@withContext downloadedMediaItem.withMetadata(
|
||||||
|
mediaId = mediaId.toString(),
|
||||||
|
title = baseItem?.name ?: playbackDecision.mediaSource.name ?: "Unknown",
|
||||||
|
subtitle = seasonEpisodeLabel(baseItem),
|
||||||
|
artworkUrl = artworkUrl,
|
||||||
|
playbackReportContext = playbackDecision.reportContext,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getMediaSegments(mediaId: UUID): List<MediaSegment> {
|
private suspend fun getMediaSegments(mediaId: UUID): List<MediaSegment> {
|
||||||
val mediaSegments = jellyfinApiClient.getMediaSegments(mediaId)
|
val mediaSegments = jellyfinApiClient.getMediaSegments(mediaId)
|
||||||
return mediaSegments.mapNotNull {
|
return mediaSegments.mapNotNull {
|
||||||
@@ -180,6 +288,28 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
|||||||
return "S$seasonNumber:E$episodeNumber"
|
return "S$seasonNumber:E$episodeNumber"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Episode.seasonEpisodeLabel(): String = "S$seasonIndex:E$index"
|
||||||
|
|
||||||
|
private fun MediaItem.withMetadata(
|
||||||
|
mediaId: String,
|
||||||
|
title: String,
|
||||||
|
subtitle: String?,
|
||||||
|
artworkUrl: String,
|
||||||
|
playbackReportContext: PlaybackReportContext?,
|
||||||
|
): MediaItem {
|
||||||
|
val metadataBuilder = MediaMetadata.Builder()
|
||||||
|
.setTitle(title)
|
||||||
|
.setSubtitle(subtitle)
|
||||||
|
if (artworkUrl.isNotBlank()) {
|
||||||
|
metadataBuilder.setArtworkUri(artworkUrl.toUri())
|
||||||
|
}
|
||||||
|
return buildUpon()
|
||||||
|
.setMediaId(mediaId)
|
||||||
|
.setMediaMetadata(metadataBuilder.build())
|
||||||
|
.setTag(playbackReportContext)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
private fun MediaSegmentDto.toMediaSegment(): MediaSegment {
|
private fun MediaSegmentDto.toMediaSegment(): MediaSegment {
|
||||||
val segmentType = when (type) {
|
val segmentType = when (type) {
|
||||||
INTRO -> SegmentType.INTRO
|
INTRO -> SegmentType.INTRO
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import hu.bbara.purefin.data.converter.toMovie
|
|||||||
import hu.bbara.purefin.data.converter.toSeason
|
import hu.bbara.purefin.data.converter.toSeason
|
||||||
import hu.bbara.purefin.data.converter.toSeries
|
import hu.bbara.purefin.data.converter.toSeries
|
||||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecisionResolver
|
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 hu.bbara.purefin.data.jellyfin.playback.playbackCustomCacheKey
|
||||||
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 org.jellyfin.sdk.model.api.MediaSourceInfo
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -24,6 +24,7 @@ import javax.inject.Singleton
|
|||||||
class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
private val userSessionRepository: UserSessionRepository,
|
private val userSessionRepository: UserSessionRepository,
|
||||||
|
private val jellyfinPlaybackResolver: JellyfinPlaybackResolver,
|
||||||
) : DownloadMediaSourceResolver {
|
) : DownloadMediaSourceResolver {
|
||||||
override suspend fun resolveMovieDownload(movieId: UUID): MovieDownloadSource? = withContext(Dispatchers.IO) {
|
override suspend fun resolveMovieDownload(movieId: UUID): MovieDownloadSource? = withContext(Dispatchers.IO) {
|
||||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||||
@@ -31,14 +32,13 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
|||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaSource = jellyfinApiClient.getMediaSources(movieId).firstOrNull() ?: return@withContext null
|
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(movieId) ?: return@withContext null
|
||||||
val playbackUrl = resolvePlaybackUrl(movieId, mediaSource, serverUrl) ?: return@withContext null
|
|
||||||
val itemInfo = jellyfinApiClient.getItemInfo(movieId) ?: return@withContext null
|
val itemInfo = jellyfinApiClient.getItemInfo(movieId) ?: return@withContext null
|
||||||
|
|
||||||
MovieDownloadSource(
|
MovieDownloadSource(
|
||||||
movie = itemInfo.toMovie(serverUrl),
|
movie = itemInfo.toMovie(serverUrl),
|
||||||
playbackUrl = playbackUrl,
|
playbackUrl = playbackDecision.url,
|
||||||
customCacheKey = mediaSource.downloadCustomCacheKey(movieId, playbackUrl),
|
customCacheKey = playbackDecision.downloadCustomCacheKey(movieId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +48,7 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
|||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaSource = jellyfinApiClient.getMediaSources(episodeId).firstOrNull() ?: return@withContext null
|
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(episodeId) ?: return@withContext null
|
||||||
val playbackUrl = resolvePlaybackUrl(episodeId, mediaSource, serverUrl) ?: return@withContext null
|
|
||||||
val episodeDto = jellyfinApiClient.getItemInfo(episodeId) ?: return@withContext null
|
val episodeDto = jellyfinApiClient.getItemInfo(episodeId) ?: return@withContext null
|
||||||
val episode = episodeDto.toEpisode(serverUrl)
|
val episode = episodeDto.toEpisode(serverUrl)
|
||||||
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: return@withContext null
|
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: return@withContext null
|
||||||
@@ -59,8 +58,8 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
|||||||
episode = episode,
|
episode = episode,
|
||||||
series = series,
|
series = series,
|
||||||
season = season,
|
season = season,
|
||||||
playbackUrl = playbackUrl,
|
playbackUrl = playbackDecision.url,
|
||||||
customCacheKey = mediaSource.downloadCustomCacheKey(episodeId, playbackUrl),
|
customCacheKey = playbackDecision.downloadCustomCacheKey(episodeId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,37 +91,14 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
|||||||
.map { it.id }
|
.map { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolvePlaybackUrl(
|
private fun PlaybackDecision.downloadCustomCacheKey(mediaId: UUID): String? {
|
||||||
mediaId: UUID,
|
if (reportContext.playMethod != PlaybackMethod.DIRECT_PLAY) {
|
||||||
mediaSource: MediaSourceInfo,
|
|
||||||
serverUrl: String,
|
|
||||||
): String? {
|
|
||||||
val shouldTranscode = mediaSource.supportsTranscoding == true &&
|
|
||||||
(mediaSource.supportsDirectPlay == false || mediaSource.transcodingUrl != null)
|
|
||||||
|
|
||||||
return if (shouldTranscode && !mediaSource.transcodingUrl.isNullOrBlank()) {
|
|
||||||
PlaybackDecisionResolver.absolutePlaybackUrl(serverUrl, requireNotNull(mediaSource.transcodingUrl))
|
|
||||||
} else {
|
|
||||||
jellyfinApiClient.getVideoStreamUrl(
|
|
||||||
itemId = mediaId,
|
|
||||||
mediaSourceId = mediaSource.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MediaSourceInfo.downloadCustomCacheKey(
|
|
||||||
mediaId: UUID,
|
|
||||||
playbackUrl: String,
|
|
||||||
): String? {
|
|
||||||
val shouldTranscode = supportsTranscoding == true &&
|
|
||||||
(supportsDirectPlay == false || transcodingUrl != null)
|
|
||||||
if (shouldTranscode) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return playbackCustomCacheKey(
|
return playbackCustomCacheKey(
|
||||||
mediaId = mediaId.toString(),
|
mediaId = mediaId.toString(),
|
||||||
playbackUrl = playbackUrl,
|
playbackUrl = url,
|
||||||
playMethod = PlaybackMethod.DIRECT_PLAY,
|
playMethod = reportContext.playMethod,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user