mirror of
https://github.com/bbara04/Purefin.git
synced 2026-04-01 01:30:08 +02:00
feat: implement auto-play next episode and up-next queue
- Implement `getNextEpisodes` in `JellyfinApiClient` to fetch a list of upcoming episodes for a given series. - In `PlayerViewModel`, automatically load the next few episodes into the media queue when a new item starts playing. - Attach `MediaMetadata` (e.g., title) to `MediaItem` objects to ensure the correct title is displayed for queued items. - Trigger loading of the next episodes in the `onMediaItemTransition` player callback. - Refactor video playback and queueing methods (`playVideo`, `addVideoUri`) to accept and utilize `MediaMetadata`.
This commit is contained in:
@@ -227,6 +227,26 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
return result.content.mediaSources
|
return result.content.mediaSources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getNextEpisodes(episodeId: UUID, count: Int = 10): List<BaseItemDto> {
|
||||||
|
if (!ensureConfigured()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
// TODO pass complete Episode object not only an id
|
||||||
|
val episodeInfo = getItemInfo(episodeId) ?: return emptyList()
|
||||||
|
val seriesId = episodeInfo.seriesId ?: return emptyList()
|
||||||
|
val nextUpEpisodesResult = api.tvShowsApi.getEpisodes(
|
||||||
|
userId = getUserId(),
|
||||||
|
seriesId = seriesId,
|
||||||
|
enableUserData = true,
|
||||||
|
startItemId = episodeId,
|
||||||
|
limit = count + 1
|
||||||
|
)
|
||||||
|
//Remove first element as we need only the next episodes
|
||||||
|
val nextUpEpisodes = nextUpEpisodesResult.content.items.drop(1)
|
||||||
|
Log.d("getNextEpisodeMediaSources response: {}", nextUpEpisodes.toString())
|
||||||
|
return nextUpEpisodes
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getMediaPlaybackInfo(mediaId: UUID, mediaSourceId: String? = null): String? {
|
suspend fun getMediaPlaybackInfo(mediaId: UUID, mediaSourceId: String? = null): String? {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return null
|
return null
|
||||||
@@ -240,5 +260,4 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package hu.bbara.purefin.player.viewmodel
|
package hu.bbara.purefin.player.viewmodel
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -8,10 +9,12 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.TrackSelectionOverride
|
import androidx.media3.common.TrackSelectionOverride
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.client.JellyfinApiClient
|
import hu.bbara.purefin.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.player.model.PlayerUiState
|
import hu.bbara.purefin.player.model.PlayerUiState
|
||||||
@@ -81,8 +84,11 @@ class PlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
val mediaId = mediaItem?.mediaId
|
||||||
|
if (!mediaId.isNullOrEmpty()) {
|
||||||
updateMetadata(mediaItem)
|
updateMetadata(mediaItem)
|
||||||
updateQueue()
|
loadNextUpMedias(mediaId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,25 +106,65 @@ class PlayerViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val mediaSources: List<MediaSourceInfo> =
|
val mediaSources: List<MediaSourceInfo> =
|
||||||
jellyfinApiClient.getMediaSources(UUID.fromString(mediaId!!))
|
jellyfinApiClient.getMediaSources(UUID.fromString(mediaId!!))
|
||||||
|
val selectedMediaSource = mediaSources.first()
|
||||||
val contentUriString =
|
val contentUriString =
|
||||||
jellyfinApiClient.getMediaPlaybackInfo(
|
jellyfinApiClient.getMediaPlaybackInfo(
|
||||||
mediaId = UUID.fromString(mediaId),
|
mediaId = UUID.fromString(mediaId),
|
||||||
mediaSourceId = mediaSources.first().id
|
mediaSourceId = selectedMediaSource.id
|
||||||
)
|
)
|
||||||
|
val mediaMetadata = MediaMetadata.Builder()
|
||||||
|
.setTitle(selectedMediaSource.name)
|
||||||
|
.build()
|
||||||
contentUriString?.toUri()?.let {
|
contentUriString?.toUri()?.let {
|
||||||
playVideo(it)
|
playVideo(
|
||||||
|
uri = it,
|
||||||
|
metadata = mediaMetadata
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addVideoUri(contentUri: Uri) {
|
private fun loadNextUpMedias(mediaId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val episodes = jellyfinApiClient.getNextEpisodes(
|
||||||
|
episodeId = UUID.fromString(mediaId),
|
||||||
|
count = 2
|
||||||
|
)
|
||||||
|
for (episode in episodes) {
|
||||||
|
if (_uiState.value.queue.any { it.id == episode.id.toString() }) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val mediaSources = jellyfinApiClient.getMediaSources(episode.id)
|
||||||
|
val selectedMediaSource = mediaSources.first()
|
||||||
|
val contentUriString = jellyfinApiClient.getMediaPlaybackInfo(
|
||||||
|
mediaId = episode.id,
|
||||||
|
mediaSourceId = selectedMediaSource.id
|
||||||
|
)
|
||||||
|
val mediaMetadata = MediaMetadata.Builder()
|
||||||
|
.setTitle(selectedMediaSource.name)
|
||||||
|
.build()
|
||||||
|
contentUriString?.toUri()?.let {
|
||||||
|
addVideoUri(it, mediaMetadata, episode.id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addVideoUri(contentUri: Uri, metadata: MediaMetadata, mediaId: String? = null) {
|
||||||
savedStateHandle["videoUris"] = videoUris.value + contentUri
|
savedStateHandle["videoUris"] = videoUris.value + contentUri
|
||||||
player.addMediaItem(MediaItem.fromUri(contentUri))
|
val mediaItem = MediaItem.Builder()
|
||||||
|
.setUri(contentUri)
|
||||||
|
.setMediaMetadata(metadata)
|
||||||
|
.setMediaId(mediaId ?: contentUri.toString())
|
||||||
|
.build()
|
||||||
|
player.addMediaItem(mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playVideo(uri: Uri) {
|
fun playVideo(uri: Uri, metadata: MediaMetadata) {
|
||||||
val mediaItem = MediaItem.Builder()
|
val mediaItem = MediaItem.Builder()
|
||||||
.setUri(uri)
|
.setUri(uri)
|
||||||
|
.setMediaMetadata(metadata)
|
||||||
.setMediaId(mediaId ?: uri.toString())
|
.setMediaId(mediaId ?: uri.toString())
|
||||||
.build()
|
.build()
|
||||||
player.setMediaItem(mediaItem)
|
player.setMediaItem(mediaItem)
|
||||||
@@ -264,6 +310,7 @@ class PlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private fun updateTracks(tracks: Tracks = player.currentTracks) {
|
private fun updateTracks(tracks: Tracks = player.currentTracks) {
|
||||||
val audio = mutableListOf<TrackOption>()
|
val audio = mutableListOf<TrackOption>()
|
||||||
val text = mutableListOf<TrackOption>()
|
val text = mutableListOf<TrackOption>()
|
||||||
|
|||||||
Reference in New Issue
Block a user