From 280db357861af3649369246512e9f70fc1010c08 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 5 May 2024 14:38:30 +0200 Subject: [PATCH] Refactor QueueEntry design --- .../playback/rewrite/RewriteMediaManager.kt | 9 +-- .../src/main/kotlin/element/ElementKey.kt | 8 +++ .../main/kotlin/element/ElementsContainer.kt | 26 +++++++ .../core/src/main/kotlin/element/delegates.kt | 40 +++++++++++ .../kotlin/mediasession/MediaSessionPlayer.kt | 1 + .../mediasession/MediaSessionService.kt | 3 +- .../kotlin/mediasession/MetadataExtensions.kt | 2 +- .../main/kotlin/mediastream/MediaStream.kt | 2 +- .../kotlin/mediastream/MediaStreamResolver.kt | 2 +- .../core/src/main/kotlin/queue/EmptyQueue.kt | 2 - .../core/src/main/kotlin/queue/PagedQueue.kt | 2 - .../src/main/kotlin/queue/PlayerQueueState.kt | 1 - playback/core/src/main/kotlin/queue/Queue.kt | 2 - .../core/src/main/kotlin/queue/QueueEntry.kt | 9 +++ .../queue/{item => }/QueueEntryMetadata.kt | 11 ++- .../src/main/kotlin/queue/SequenceQueue.kt | 2 - .../src/main/kotlin/queue/item/QueueEntry.kt | 36 ---------- .../mediastream/AudioMediaStreamResolver.kt | 16 ++--- .../UniversalAudioMediaStreamResolver.kt | 12 ++-- .../kotlin/playsession/PlaySessionService.kt | 18 ++--- .../src/main/kotlin/queue/AudioAlbumQueue.kt | 5 +- .../main/kotlin/queue/AudioInstantMixQueue.kt | 5 +- .../src/main/kotlin/queue/AudioTrackQueue.kt | 5 +- .../src/main/kotlin/queue/EpisodeQueue.kt | 5 +- .../src/main/kotlin/queue/baseItemElement.kt | 13 ++++ .../kotlin/queue/createBaseItemQueueEntry.kt | 59 +++++++++++++++ .../queue/item/BaseItemDtoUserQueueEntry.kt | 72 ------------------- 27 files changed, 204 insertions(+), 164 deletions(-) create mode 100644 playback/core/src/main/kotlin/element/ElementKey.kt create mode 100644 playback/core/src/main/kotlin/element/ElementsContainer.kt create mode 100644 playback/core/src/main/kotlin/element/delegates.kt create mode 100644 playback/core/src/main/kotlin/queue/QueueEntry.kt rename playback/core/src/main/kotlin/queue/{item => }/QueueEntryMetadata.kt (68%) delete mode 100644 playback/core/src/main/kotlin/queue/item/QueueEntry.kt create mode 100644 playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt create mode 100644 playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt delete mode 100644 playback/jellyfin/src/main/kotlin/queue/item/BaseItemDtoUserQueueEntry.kt diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt index 6c70e66591..696b6f25d8 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/rewrite/RewriteMediaManager.kt @@ -25,8 +25,9 @@ import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode import org.jellyfin.playback.core.queue.Queue -import org.jellyfin.playback.core.queue.item.QueueEntry -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.jellyfin.queue.baseItem +import org.jellyfin.playback.jellyfin.queue.createBaseItemQueueEntry import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemDto import kotlin.math.max @@ -59,7 +60,7 @@ class RewriteMediaManager( ?: currentAudioQueue.size()).toString() override val currentAudioItem: BaseItemDto? - get() = (playbackManager.state.queue.entry.value as? BaseItemDtoUserQueueEntry)?.baseItem + get() = playbackManager.state.queue.entry.value?.baseItem override fun toggleRepeat(): Boolean { val newMode = when (playbackManager.state.repeatMode.value) { @@ -277,7 +278,7 @@ class RewriteMediaManager( override suspend fun getItem(index: Int): QueueEntry? { val item = items.getOrNull(index) ?: return null - return BaseItemDtoUserQueueEntry.build(api, item) + return createBaseItemQueueEntry(api, item) } } } diff --git a/playback/core/src/main/kotlin/element/ElementKey.kt b/playback/core/src/main/kotlin/element/ElementKey.kt new file mode 100644 index 0000000000..5bc3ce747e --- /dev/null +++ b/playback/core/src/main/kotlin/element/ElementKey.kt @@ -0,0 +1,8 @@ +package org.jellyfin.playback.core.element + +/** + * A key to identify the type of an element. + */ +class ElementKey(val name: String) { + override fun toString(): String = "ElementKey $name" +} diff --git a/playback/core/src/main/kotlin/element/ElementsContainer.kt b/playback/core/src/main/kotlin/element/ElementsContainer.kt new file mode 100644 index 0000000000..b5740de950 --- /dev/null +++ b/playback/core/src/main/kotlin/element/ElementsContainer.kt @@ -0,0 +1,26 @@ +package org.jellyfin.playback.core.element + +import java.util.concurrent.ConcurrentHashMap + +/** + * Container to hold elements identified with an [ElementKey]. + */ +open class ElementsContainer { + private val elements = ConcurrentHashMap, Any?>() + + fun get(key: ElementKey): T = getOrNull(key) + ?: error("No element found for key $key.") + + @Suppress("UNCHECKED_CAST") + fun getOrNull(key: ElementKey): T? = elements[key] as T? + + operator fun contains(key: ElementKey): Boolean = elements.containsKey(key) + + fun put(key: ElementKey, value: T) { + elements[key] = value + } + + fun remove(key: ElementKey) { + elements.remove(key) + } +} diff --git a/playback/core/src/main/kotlin/element/delegates.kt b/playback/core/src/main/kotlin/element/delegates.kt new file mode 100644 index 0000000000..967d9a797c --- /dev/null +++ b/playback/core/src/main/kotlin/element/delegates.kt @@ -0,0 +1,40 @@ +package org.jellyfin.playback.core.element + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * Delegate for an optional element. + */ +fun element( + key: ElementKey +) = object : ReadWriteProperty { + override fun getValue(thisRef: ElementsContainer, property: KProperty<*>): T? = + thisRef.getOrNull(key) + + override fun setValue(thisRef: ElementsContainer, property: KProperty<*>, value: T?) { + if (value == null) thisRef.remove(key) + else thisRef.put(key, value) + } +} + +/** + * Delegate for an required element. + */ +fun requiredElement( + key: ElementKey, + computeDefault: () -> T, +) = object : ReadWriteProperty { + override fun getValue(thisRef: ElementsContainer, property: KProperty<*>): T { + val value = thisRef.getOrNull(key) + if (value != null) return value + + val default = computeDefault() + thisRef.put(key, default) + return default + } + + override fun setValue(thisRef: ElementsContainer, property: KProperty<*>, value: T) { + thisRef.put(key, value) + } +} diff --git a/playback/core/src/main/kotlin/mediasession/MediaSessionPlayer.kt b/playback/core/src/main/kotlin/mediasession/MediaSessionPlayer.kt index d4842cba3e..df734e32d4 100644 --- a/playback/core/src/main/kotlin/mediasession/MediaSessionPlayer.kt +++ b/playback/core/src/main/kotlin/mediasession/MediaSessionPlayer.kt @@ -22,6 +22,7 @@ import org.jellyfin.playback.core.PlaybackManager import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode +import org.jellyfin.playback.core.queue.metadata import timber.log.Timber import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds diff --git a/playback/core/src/main/kotlin/mediasession/MediaSessionService.kt b/playback/core/src/main/kotlin/mediasession/MediaSessionService.kt index 40b507e426..6172a02294 100644 --- a/playback/core/src/main/kotlin/mediasession/MediaSessionService.kt +++ b/playback/core/src/main/kotlin/mediasession/MediaSessionService.kt @@ -12,7 +12,8 @@ import androidx.media3.session.MediaStyleNotificationHelper import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.jellyfin.playback.core.plugin.PlayerService -import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.metadata class MediaSessionService( private val androidContext: Context, diff --git a/playback/core/src/main/kotlin/mediasession/MetadataExtensions.kt b/playback/core/src/main/kotlin/mediasession/MetadataExtensions.kt index 58b43846b8..310f6ed72a 100644 --- a/playback/core/src/main/kotlin/mediasession/MetadataExtensions.kt +++ b/playback/core/src/main/kotlin/mediasession/MetadataExtensions.kt @@ -3,7 +3,7 @@ package org.jellyfin.playback.core.mediasession import androidx.core.net.toUri import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import org.jellyfin.playback.core.queue.item.QueueEntryMetadata +import org.jellyfin.playback.core.queue.QueueEntryMetadata fun QueueEntryMetadata.toMediaItem() = MediaItem.Builder().apply { if (mediaId != null) setMediaId(mediaId) diff --git a/playback/core/src/main/kotlin/mediastream/MediaStream.kt b/playback/core/src/main/kotlin/mediastream/MediaStream.kt index 55d0c264b2..4e21e02a30 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStream.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStream.kt @@ -1,6 +1,6 @@ package org.jellyfin.playback.core.mediastream -import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.queue.QueueEntry interface MediaStream { val identifier: String diff --git a/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt b/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt index a4acda3dcb..999bc0b8b9 100644 --- a/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt +++ b/playback/core/src/main/kotlin/mediastream/MediaStreamResolver.kt @@ -1,6 +1,6 @@ package org.jellyfin.playback.core.mediastream -import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.playback.core.support.PlaySupportReport /** diff --git a/playback/core/src/main/kotlin/queue/EmptyQueue.kt b/playback/core/src/main/kotlin/queue/EmptyQueue.kt index 2171e992d5..4eccc929a5 100644 --- a/playback/core/src/main/kotlin/queue/EmptyQueue.kt +++ b/playback/core/src/main/kotlin/queue/EmptyQueue.kt @@ -1,7 +1,5 @@ package org.jellyfin.playback.core.queue -import org.jellyfin.playback.core.queue.item.QueueEntry - data object EmptyQueue : Queue { override val size: Int = 0 override suspend fun getItem(index: Int): QueueEntry? = null diff --git a/playback/core/src/main/kotlin/queue/PagedQueue.kt b/playback/core/src/main/kotlin/queue/PagedQueue.kt index ab9401fcfb..2ddb97062a 100644 --- a/playback/core/src/main/kotlin/queue/PagedQueue.kt +++ b/playback/core/src/main/kotlin/queue/PagedQueue.kt @@ -1,7 +1,5 @@ package org.jellyfin.playback.core.queue -import org.jellyfin.playback.core.queue.item.QueueEntry - abstract class PagedQueue( private val pageSize: Int = 10, ) : Queue { diff --git a/playback/core/src/main/kotlin/queue/PlayerQueueState.kt b/playback/core/src/main/kotlin/queue/PlayerQueueState.kt index 9b3ac76359..690199824f 100644 --- a/playback/core/src/main/kotlin/queue/PlayerQueueState.kt +++ b/playback/core/src/main/kotlin/queue/PlayerQueueState.kt @@ -14,7 +14,6 @@ import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.PlaybackOrder import org.jellyfin.playback.core.model.RepeatMode -import org.jellyfin.playback.core.queue.item.QueueEntry import org.jellyfin.playback.core.queue.order.DefaultOrderIndexProvider import org.jellyfin.playback.core.queue.order.OrderIndexProvider import org.jellyfin.playback.core.queue.order.RandomOrderIndexProvider diff --git a/playback/core/src/main/kotlin/queue/Queue.kt b/playback/core/src/main/kotlin/queue/Queue.kt index 5f43ff045c..bda05e217f 100644 --- a/playback/core/src/main/kotlin/queue/Queue.kt +++ b/playback/core/src/main/kotlin/queue/Queue.kt @@ -1,7 +1,5 @@ package org.jellyfin.playback.core.queue -import org.jellyfin.playback.core.queue.item.QueueEntry - /** * A queue contains all items in the current playback session. This includes already played items, * the currently playing item and future items. diff --git a/playback/core/src/main/kotlin/queue/QueueEntry.kt b/playback/core/src/main/kotlin/queue/QueueEntry.kt new file mode 100644 index 0000000000..d6da4ac9bd --- /dev/null +++ b/playback/core/src/main/kotlin/queue/QueueEntry.kt @@ -0,0 +1,9 @@ +package org.jellyfin.playback.core.queue + +import org.jellyfin.playback.core.element.ElementsContainer + +/** + * The QueueEntry is a single item in a queue and can represent any supported media type. + * All related data is stored in elements via the [ElementsContainer]. + */ +class QueueEntry : ElementsContainer() diff --git a/playback/core/src/main/kotlin/queue/item/QueueEntryMetadata.kt b/playback/core/src/main/kotlin/queue/QueueEntryMetadata.kt similarity index 68% rename from playback/core/src/main/kotlin/queue/item/QueueEntryMetadata.kt rename to playback/core/src/main/kotlin/queue/QueueEntryMetadata.kt index 12780fde87..6380c47f12 100644 --- a/playback/core/src/main/kotlin/queue/item/QueueEntryMetadata.kt +++ b/playback/core/src/main/kotlin/queue/QueueEntryMetadata.kt @@ -1,5 +1,7 @@ -package org.jellyfin.playback.core.queue.item +package org.jellyfin.playback.core.queue +import org.jellyfin.playback.core.element.ElementKey +import org.jellyfin.playback.core.element.requiredElement import java.time.LocalDate import kotlin.time.Duration @@ -33,3 +35,10 @@ data class QueueEntryMetadata( val Empty = QueueEntryMetadata() } } + +private val metadataKey = ElementKey("QueueEntryMetadata") + +/** + * Get or set the [QueueEntryMetadata] for this [QueueEntry]. Defaults to [QueueEntryMetadata.Empty]. + */ +var QueueEntry.metadata by requiredElement(metadataKey) { QueueEntryMetadata.Empty } diff --git a/playback/core/src/main/kotlin/queue/SequenceQueue.kt b/playback/core/src/main/kotlin/queue/SequenceQueue.kt index 0eb01ee3be..b66f37cd12 100644 --- a/playback/core/src/main/kotlin/queue/SequenceQueue.kt +++ b/playback/core/src/main/kotlin/queue/SequenceQueue.kt @@ -1,7 +1,5 @@ package org.jellyfin.playback.core.queue -import org.jellyfin.playback.core.queue.item.QueueEntry - abstract class SequenceQueue : Queue { companion object { const val MAX_SIZE = 100 diff --git a/playback/core/src/main/kotlin/queue/item/QueueEntry.kt b/playback/core/src/main/kotlin/queue/item/QueueEntry.kt deleted file mode 100644 index 625c1e3248..0000000000 --- a/playback/core/src/main/kotlin/queue/item/QueueEntry.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.jellyfin.playback.core.queue.item - -/** - * The QueueEntry is a single entry in a queue. It can represent any supported media type. - * Implementations normally extend one of it's subclasses [BrandedQueueEntry] or [UserQueueEntry]. - */ -interface QueueEntry { - /** - * Whether this entry can be skipped and seeked or not. Normally set to `true`. - */ - val skippable: Boolean - - /** - * The metadata for this item. - */ - val metadata: QueueEntryMetadata -} - -/** - * Branded queue entries are used for intros, trailers, advertisements and other related videos. It - * is not possible to seek or skip these items and are normally invisible when showing a queue's - * contents. - */ -open class BrandedQueueEntry : QueueEntry { - override val skippable = false - override val metadata = QueueEntryMetadata.Empty -} - -/** - * User queue entries are used for regular media like music tracks, movies and series episodes. This - * is the entry type used most often. - */ -open class UserQueueEntry : QueueEntry { - override val skippable = true - override val metadata = QueueEntryMetadata.Empty -} diff --git a/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt b/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt index 4d6cd90ac3..e9fd4181b4 100644 --- a/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt +++ b/playback/jellyfin/src/main/kotlin/mediastream/AudioMediaStreamResolver.kt @@ -5,9 +5,9 @@ import org.jellyfin.playback.core.mediastream.MediaConversionMethod import org.jellyfin.playback.core.mediastream.MediaStream import org.jellyfin.playback.core.mediastream.MediaStreamContainer import org.jellyfin.playback.core.mediastream.PlayableMediaStream -import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.playback.core.support.PlaySupportReport -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.jellyfin.queue.baseItem import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.audioApi import org.jellyfin.sdk.api.client.extensions.dynamicHlsApi @@ -51,10 +51,10 @@ class AudioMediaStreamResolver( queueEntry: QueueEntry, testStream: (stream: MediaStream) -> PlaySupportReport, ): PlayableMediaStream? { - if (queueEntry !is BaseItemDtoUserQueueEntry) return null - if (queueEntry.baseItem.type != BaseItemKind.AUDIO) return null + val baseItem = queueEntry.baseItem + if (baseItem == null || baseItem.type != BaseItemKind.AUDIO) return null - val mediaInfo = getPlaybackInfo(queueEntry.baseItem) + val mediaInfo = getPlaybackInfo(baseItem) // Test for direct play support val directPlayStream = mediaInfo.getDirectPlayStream() @@ -62,7 +62,7 @@ class AudioMediaStreamResolver( return directPlayStream.toPlayableMediaStream( queueEntry = queueEntry, url = api.audioApi.getAudioStreamUrl( - itemId = queueEntry.baseItem.id, + itemId = baseItem.id, mediaSourceId = mediaInfo.mediaSource.id, playSessionId = mediaInfo.playSessionId, static = true, @@ -78,7 +78,7 @@ class AudioMediaStreamResolver( return remuxStream.toPlayableMediaStream( queueEntry = queueEntry, url = api.audioApi.getAudioStreamByContainerUrl( - itemId = queueEntry.baseItem.id, + itemId = baseItem.id, mediaSourceId = mediaInfo.mediaSource.id, playSessionId = mediaInfo.playSessionId, container = container, @@ -96,7 +96,7 @@ class AudioMediaStreamResolver( return transcodeStream.toPlayableMediaStream( queueEntry = queueEntry, url = api.dynamicHlsApi.getMasterHlsAudioPlaylistUrl( - itemId = queueEntry.baseItem.id, + itemId = baseItem.id, mediaSourceId = requireNotNull(mediaInfo.mediaSource.id), playSessionId = mediaInfo.playSessionId, tag = mediaInfo.mediaSource.eTag, diff --git a/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt b/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt index 031642193a..b8c32015ba 100644 --- a/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt +++ b/playback/jellyfin/src/main/kotlin/mediastream/UniversalAudioMediaStreamResolver.kt @@ -3,9 +3,9 @@ package org.jellyfin.playback.jellyfin.mediastream import org.jellyfin.playback.core.mediastream.MediaConversionMethod import org.jellyfin.playback.core.mediastream.MediaStream import org.jellyfin.playback.core.mediastream.PlayableMediaStream -import org.jellyfin.playback.core.queue.item.QueueEntry +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.playback.core.support.PlaySupportReport -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.jellyfin.queue.baseItem import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.universalAudioApi import org.jellyfin.sdk.model.api.BaseItemKind @@ -19,13 +19,13 @@ class UniversalAudioMediaStreamResolver( queueEntry: QueueEntry, testStream: (stream: MediaStream) -> PlaySupportReport, ): PlayableMediaStream? { - if (queueEntry !is BaseItemDtoUserQueueEntry) return null - if (queueEntry.baseItem.type != BaseItemKind.AUDIO) return null + val baseItem = queueEntry.baseItem + if (baseItem == null || baseItem.type != BaseItemKind.AUDIO) return null - val mediaInfo = getPlaybackInfo(queueEntry.baseItem) + val mediaInfo = getPlaybackInfo(baseItem) val url = api.universalAudioApi.getUniversalAudioStreamUrl( - itemId = queueEntry.baseItem.id, + itemId = baseItem.id, mediaSourceId = mediaInfo.mediaSource.id, enableRedirection = false, enableRemoteMedia = false, diff --git a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt index 9932ec0b0c..beed3ac925 100644 --- a/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt +++ b/playback/jellyfin/src/main/kotlin/playsession/PlaySessionService.kt @@ -10,7 +10,7 @@ import org.jellyfin.playback.core.mediastream.PlayableMediaStream import org.jellyfin.playback.core.model.PlayState import org.jellyfin.playback.core.model.RepeatMode import org.jellyfin.playback.core.plugin.PlayerService -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.jellyfin.queue.baseItem import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.playStateApi import org.jellyfin.sdk.model.api.PlayMethod @@ -40,12 +40,6 @@ class PlaySessionService( }.launchIn(coroutineScope) } - private val PlayableMediaStream.baseItem - get() = when (val entry = queueEntry) { - is BaseItemDtoUserQueueEntry -> entry.baseItem - else -> null - } - private val MediaConversionMethod.playMethod get() = when (this) { MediaConversionMethod.None -> PlayMethod.DIRECT_PLAY @@ -94,12 +88,12 @@ class PlaySessionService( // backend. return state.queue .peekNext(15) - .filterIsInstance() - .map { QueueItem(id = it.baseItem.id, playlistItemId = it.baseItem.playlistItemId) } + .mapNotNull { it.baseItem } + .map { QueueItem(id = it.id, playlistItemId = it.playlistItemId) } } private suspend fun sendStreamStart(stream: PlayableMediaStream) { - val item = stream.baseItem ?: return + val item = stream.queueEntry.baseItem ?: return api.playStateApi.reportPlaybackStart(PlaybackStartInfo( itemId = item.id, playSessionId = stream.identifier, @@ -117,7 +111,7 @@ class PlaySessionService( } private suspend fun sendStreamUpdate(stream: PlayableMediaStream) { - val item = stream.baseItem ?: return + val item = stream.queueEntry.baseItem ?: return api.playStateApi.reportPlaybackProgress(PlaybackProgressInfo( itemId = item.id, playSessionId = stream.identifier, @@ -135,7 +129,7 @@ class PlaySessionService( } private suspend fun sendStreamStop(stream: PlayableMediaStream) { - val item = stream.baseItem ?: return + val item = stream.queueEntry.baseItem ?: return api.playStateApi.reportPlaybackStopped(PlaybackStopInfo( itemId = item.id, playSessionId = stream.identifier, diff --git a/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueue.kt b/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueue.kt index 456dcfc66f..aa93e2236f 100644 --- a/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/AudioAlbumQueue.kt @@ -1,8 +1,7 @@ package org.jellyfin.playback.jellyfin.queue import org.jellyfin.playback.core.queue.PagedQueue -import org.jellyfin.playback.core.queue.item.QueueEntry -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemDto @@ -35,6 +34,6 @@ class AudioAlbumQueue( enableTotalRecordCount = true, ) this.size = result.totalRecordCount - return result.items.orEmpty().map { BaseItemDtoUserQueueEntry.build(api, it) } + return result.items.orEmpty().map { createBaseItemQueueEntry(api, it) } } } diff --git a/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueue.kt b/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueue.kt index 645a13ee6c..05ab4d6333 100644 --- a/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/AudioInstantMixQueue.kt @@ -1,8 +1,7 @@ package org.jellyfin.playback.jellyfin.queue import org.jellyfin.playback.core.queue.PagedQueue -import org.jellyfin.playback.core.queue.item.QueueEntry -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.instantMixApi import org.jellyfin.sdk.model.api.BaseItemDto @@ -43,6 +42,6 @@ class AudioInstantMixQueue( limit = size, ) this.size = result.totalRecordCount - return result.items.orEmpty().map { BaseItemDtoUserQueueEntry.build(api, it) } + return result.items.orEmpty().map { createBaseItemQueueEntry(api, it) } } } diff --git a/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueue.kt b/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueue.kt index 1a093830d0..fed1c53875 100644 --- a/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/AudioTrackQueue.kt @@ -1,8 +1,7 @@ package org.jellyfin.playback.jellyfin.queue import org.jellyfin.playback.core.queue.PagedQueue -import org.jellyfin.playback.core.queue.item.QueueEntry -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDto @@ -23,6 +22,6 @@ class AudioTrackQueue( if (offset > 0) return emptyList() val item by api.userLibraryApi.getItem(itemId = item.id) - return listOf(BaseItemDtoUserQueueEntry.build(api, item)) + return listOf(createBaseItemQueueEntry(api, item)) } } diff --git a/playback/jellyfin/src/main/kotlin/queue/EpisodeQueue.kt b/playback/jellyfin/src/main/kotlin/queue/EpisodeQueue.kt index a2a6103aa4..0bd09aadb0 100644 --- a/playback/jellyfin/src/main/kotlin/queue/EpisodeQueue.kt +++ b/playback/jellyfin/src/main/kotlin/queue/EpisodeQueue.kt @@ -1,8 +1,7 @@ package org.jellyfin.playback.jellyfin.queue import org.jellyfin.playback.core.queue.PagedQueue -import org.jellyfin.playback.core.queue.item.QueueEntry -import org.jellyfin.playback.jellyfin.queue.item.BaseItemDtoUserQueueEntry +import org.jellyfin.playback.core.queue.QueueEntry import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.model.api.BaseItemDto @@ -35,6 +34,6 @@ class EpisodeQueue( limit = size, ) this.size = result.totalRecordCount - return result.items.orEmpty().map { BaseItemDtoUserQueueEntry.build(api, it) } + return result.items.orEmpty().map { createBaseItemQueueEntry(api, it) } } } diff --git a/playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt b/playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt new file mode 100644 index 0000000000..4d43226b86 --- /dev/null +++ b/playback/jellyfin/src/main/kotlin/queue/baseItemElement.kt @@ -0,0 +1,13 @@ +package org.jellyfin.playback.jellyfin.queue + +import org.jellyfin.playback.core.element.ElementKey +import org.jellyfin.playback.core.element.element +import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.sdk.model.api.BaseItemDto + +private val baseItemKey = ElementKey("BaseItemDto") + +/** + * Get or set the [BaseItemDto] for this [QueueEntry]. + */ +var QueueEntry.baseItem by element(baseItemKey) diff --git a/playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt b/playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt new file mode 100644 index 0000000000..6be035f662 --- /dev/null +++ b/playback/jellyfin/src/main/kotlin/queue/createBaseItemQueueEntry.kt @@ -0,0 +1,59 @@ +package org.jellyfin.playback.jellyfin.queue + +import org.jellyfin.playback.core.queue.QueueEntry +import org.jellyfin.playback.core.queue.QueueEntryMetadata +import org.jellyfin.playback.core.queue.metadata +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.imageApi +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.ImageType +import org.jellyfin.sdk.model.extensions.ticks +import java.util.UUID + +/** + * Create a [QueueEntry] from a [BaseItemDto]. + */ +fun createBaseItemQueueEntry(api: ApiClient, baseItem: BaseItemDto): QueueEntry { + val entry = QueueEntry() + entry.metadata = QueueEntryMetadata( + mediaId = baseItem.id.toString(), + duration = baseItem.runTimeTicks?.ticks, + title = baseItem.name, + artist = baseItem.albumArtist, + albumTitle = baseItem.album, + albumArtist = baseItem.albumArtists + ?.mapNotNull { it.name } + ?.joinToString(", "), + displayTitle = baseItem.name, + description = baseItem.overview, + artworkUri = when { + baseItem.imageTags?.containsKey(ImageType.PRIMARY) == true -> api.getImageUri( + itemId = baseItem.id, + tag = baseItem.imageTags!![ImageType.PRIMARY] + ) + + baseItem.albumPrimaryImageTag != null -> api.getImageUri( + itemId = baseItem.albumId ?: baseItem.id, + tag = baseItem.albumPrimaryImageTag, + ) + + else -> null + }, + trackNumber = baseItem.indexNumber, + releaseDate = baseItem.premiereDate?.toLocalDate(), + genre = baseItem.genres?.joinToString(", "), + ) + entry.baseItem = baseItem + return entry +} + +private fun ApiClient.getImageUri(itemId: UUID?, tag: String?): String? = when { + // Invalid item id / tag + itemId == null || tag == null -> null + // Valid item id & tag + else -> imageApi.getItemImageUrl( + itemId = itemId, + imageType = ImageType.PRIMARY, + tag = tag, + ) +} diff --git a/playback/jellyfin/src/main/kotlin/queue/item/BaseItemDtoUserQueueEntry.kt b/playback/jellyfin/src/main/kotlin/queue/item/BaseItemDtoUserQueueEntry.kt deleted file mode 100644 index 6d06a85c1f..0000000000 --- a/playback/jellyfin/src/main/kotlin/queue/item/BaseItemDtoUserQueueEntry.kt +++ /dev/null @@ -1,72 +0,0 @@ -package org.jellyfin.playback.jellyfin.queue.item - -import org.jellyfin.playback.core.queue.item.QueueEntry -import org.jellyfin.playback.core.queue.item.QueueEntryMetadata -import org.jellyfin.playback.core.queue.item.UserQueueEntry -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.imageApi -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.ImageType -import org.jellyfin.sdk.model.extensions.ticks -import java.util.UUID - -class BaseItemDtoUserQueueEntry private constructor( - val baseItem: BaseItemDto, - override val metadata: QueueEntryMetadata, - base: QueueEntry, -) : QueueEntry by base { - companion object { - fun build(api: ApiClient, baseItem: BaseItemDto): BaseItemDtoUserQueueEntry { - // Create metadata - val metadata = QueueEntryMetadata( - mediaId = baseItem.id.toString(), - duration = baseItem.runTimeTicks?.ticks, - title = baseItem.name, - artist = baseItem.albumArtist, - albumTitle = baseItem.album, - albumArtist = baseItem.albumArtists - ?.mapNotNull { it.name } - ?.joinToString(", "), - displayTitle = baseItem.name, - description = baseItem.overview, - artworkUri = when { - baseItem.imageTags?.containsKey(ImageType.PRIMARY) == true -> api.getImageUri( - itemId = baseItem.id, - tag = baseItem.imageTags!![ImageType.PRIMARY] - ) - - baseItem.albumPrimaryImageTag != null -> api.getImageUri( - itemId = baseItem.albumId ?: baseItem.id, - tag = baseItem.albumPrimaryImageTag, - ) - - else -> null - }, - trackNumber = baseItem.indexNumber, - releaseDate = baseItem.premiereDate?.toLocalDate(), - genre = baseItem.genres?.joinToString(", "), - ) - - // Return entry - return BaseItemDtoUserQueueEntry( - baseItem = baseItem, - metadata = metadata, - base = UserQueueEntry() - ) - } - - /** - * Helper extension function to get the URL of the primary image by item id and tag. - */ - private fun ApiClient.getImageUri(itemId: UUID?, tag: String?): String? = when { - // Invalid item id / tag - itemId == null || tag == null -> null - // Valid item id & tag - else -> imageApi.getItemImageUrl( - itemId = itemId, - imageType = ImageType.PRIMARY, - tag = tag, - ) - } - } -}