From fe66926335dfd5e9a582453fb6eb88f30aea3c7f Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Fri, 12 Jun 2026 16:20:17 +0000 Subject: [PATCH] fix(playback): add offline fallback for next-up episode resolution When offline, getNextUpPlayableMedias() only tried the Jellyfin API which silently returns an empty list on failure. This broke the queue population for offline playback, meaning no playlist was built and autoplay-next could never fire. Add an offline fallback path that queries the Room database for the current episode's series, finds the next episodes ordered by (seasonIndex, index), and resolves them as downloaded PlayableMedia via getPlayableMedia(). --- .../DefaultPlayableMediaRepository.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/data/src/main/java/hu/bbara/purefin/data/jellyfin/DefaultPlayableMediaRepository.kt b/data/src/main/java/hu/bbara/purefin/data/jellyfin/DefaultPlayableMediaRepository.kt index fdf68507..bb456585 100644 --- a/data/src/main/java/hu/bbara/purefin/data/jellyfin/DefaultPlayableMediaRepository.kt +++ b/data/src/main/java/hu/bbara/purefin/data/jellyfin/DefaultPlayableMediaRepository.kt @@ -8,6 +8,7 @@ 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.OfflineMediaManager import hu.bbara.purefin.core.data.PlayableMediaRepository import hu.bbara.purefin.core.data.PlaybackMediaItemTag import hu.bbara.purefin.core.data.PlaybackReportContext @@ -50,6 +51,7 @@ class DefaultPlayableMediaRepository @Inject constructor( private val mediaDownloadController: MediaDownloadController, private val networkMonitor: NetworkMonitor, @param:Offline private val offlineMediaRepository: LocalMediaRepository, + private val offlineMediaManager: OfflineMediaManager, ) : PlayableMediaRepository { override suspend fun getPlayableMedia(mediaId: UUID): PlayableMedia? = withContext(Dispatchers.IO) { @@ -239,7 +241,8 @@ class DefaultPlayableMediaRepository @Inject constructor( existingIds: Set, count: Int, ): List = withContext(Dispatchers.IO) { - runCatching { + // First try the online (Jellyfin API) path + val onlineResult = runCatching { val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count) episodes.mapNotNull { episode -> val id = episode.id ?: return@mapNotNull null @@ -249,9 +252,27 @@ class DefaultPlayableMediaRepository @Inject constructor( getPlayableMedia(id) } }.getOrElse { error -> - Timber.tag(TAG).w(error, "Unable to load next-up items for $episodeId") - emptyList() + Timber.tag(TAG).w(error, "Unable to load next-up items from API for $episodeId") + null // Signal that we should try the offline fallback } + if (onlineResult != null) return@withContext onlineResult + + // Offline fallback: find next episodes from the local database + Timber.tag(TAG).d("Falling back to offline next-up resolution for $episodeId") + val currentEpisode = offlineMediaRepository.getEpisode(episodeId).first() ?: return@withContext emptyList() + val seriesEpisodes = offlineMediaManager.getEpisodesBySeries(currentEpisode.seriesId) + .sortedWith(compareBy({ it.seasonIndex }, { it.index })) + + val currentIndex = seriesEpisodes.indexOfFirst { it.id == episodeId } + if (currentIndex == -1) return@withContext emptyList() + + seriesEpisodes + .drop(currentIndex + 1) + .take(count) + .filter { it.id !in existingIds } + .mapNotNull { nextEpisode -> + getPlayableMedia(nextEpisode.id) + } } @OptIn(UnstableApi::class)