feat: add artwork thumbnails to player queue

- Inject UserSessionRepository into MediaRepository to access server URL
- Build artwork URLs using JellyfinImageHelper for both initial and next-up episodes
- Add artworkUrl parameter to MediaItem metadata via setArtworkUri()
- Fix PlayerQueuePanel thumbnail display with proper 4:3 aspect ratio
- Increase next-up queue count from 2 to 5 episodes
This commit is contained in:
2026-02-09 20:32:59 +01:00
parent 57a9f4f236
commit c1f733d1f3
2 changed files with 28 additions and 11 deletions

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.player.data package hu.bbara.purefin.player.data
import android.net.Uri
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
@@ -9,10 +8,16 @@ import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.MediaSourceInfo
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import androidx.core.net.toUri
import hu.bbara.purefin.image.JellyfinImageHelper
import hu.bbara.purefin.session.UserSessionRepository
import kotlinx.coroutines.flow.first
import org.jellyfin.sdk.model.api.ImageType
@ViewModelScoped @ViewModelScoped
class MediaRepository @Inject constructor( class MediaRepository @Inject constructor(
private val jellyfinApiClient: JellyfinApiClient private val jellyfinApiClient: JellyfinApiClient,
private val userSessionRepository: UserSessionRepository
) { ) {
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? { suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? {
@@ -27,11 +32,15 @@ class MediaRepository @Inject constructor(
// Calculate resume position // Calculate resume position
val resumePositionMs = calculateResumePosition(baseItem, selectedMediaSource) val resumePositionMs = calculateResumePosition(baseItem, selectedMediaSource)
val serverUrl = userSessionRepository.serverUrl.first()
val artworkUrl = JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY)
val mediaItem = createMediaItem( val mediaItem = createMediaItem(
mediaId = mediaId.toString(), mediaId = mediaId.toString(),
playbackUrl = playbackUrl, playbackUrl = playbackUrl,
title = baseItem?.name ?: selectedMediaSource.name, title = baseItem?.name ?: selectedMediaSource.name!!,
subtitle = "S${baseItem!!.parentIndexNumber}:E${baseItem.indexNumber}" subtitle = "S${baseItem!!.parentIndexNumber}:E${baseItem.indexNumber}",
artworkUrl = artworkUrl
) )
return Pair(mediaItem, resumePositionMs) return Pair(mediaItem, resumePositionMs)
@@ -61,7 +70,8 @@ class MediaRepository @Inject constructor(
return if (percentage in 5.0..95.0) positionMs else null return if (percentage in 5.0..95.0) positionMs else null
} }
suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set<String>, count: Int = 2): List<MediaItem> { suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set<String>, count: Int = 5): List<MediaItem> {
val serverUrl = userSessionRepository.serverUrl.first()
val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count) val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count)
return episodes.mapNotNull { episode -> return episodes.mapNotNull { episode ->
val id = episode.id ?: return@mapNotNull null val id = episode.id ?: return@mapNotNull null
@@ -75,11 +85,13 @@ class MediaRepository @Inject constructor(
mediaId = id, mediaId = id,
mediaSourceId = selectedMediaSource.id mediaSourceId = selectedMediaSource.id
) ?: return@mapNotNull null ) ?: return@mapNotNull null
val artworkUrl = JellyfinImageHelper.toImageUrl(serverUrl, id, ImageType.PRIMARY)
createMediaItem( createMediaItem(
mediaId = stringId, mediaId = stringId,
playbackUrl = playbackUrl, playbackUrl = playbackUrl,
title = episode.name ?: selectedMediaSource.name, title = episode.name ?: selectedMediaSource.name!!,
subtitle = "S${episode.parentIndexNumber}:E${episode.indexNumber}" subtitle = "S${episode.parentIndexNumber}:E${episode.indexNumber}",
artworkUrl = artworkUrl
) )
} }
} }
@@ -87,15 +99,17 @@ class MediaRepository @Inject constructor(
private fun createMediaItem( private fun createMediaItem(
mediaId: String, mediaId: String,
playbackUrl: String, playbackUrl: String,
title: String?, title: String,
subtitle: String? subtitle: String?,
artworkUrl: String
): MediaItem { ): MediaItem {
val metadata = MediaMetadata.Builder() val metadata = MediaMetadata.Builder()
.setTitle(title) .setTitle(title)
.setSubtitle(subtitle) .setSubtitle(subtitle)
.setArtworkUri(artworkUrl.toUri())
.build() .build()
return MediaItem.Builder() return MediaItem.Builder()
.setUri(Uri.parse(playbackUrl)) .setUri(playbackUrl.toUri())
.setMediaId(mediaId) .setMediaId(mediaId)
.setMediaMetadata(metadata) .setMediaMetadata(metadata)
.build() .build()

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -134,7 +135,9 @@ private fun QueueRow(
PurefinAsyncImage( PurefinAsyncImage(
model = artworkUrl, model = artworkUrl,
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.aspectRatio(4f / 3f)
) )
} }
} }