From dfd9acf561b4f6779f22e5f49214657ec6e0fa3a Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:28:57 -0500 Subject: [PATCH] Add metadata to media session (#934) ## Description Adds some metadata about playing media for consumption in remote control apps such as Google Home or others. ### Related issues Partially address #889, doesn't add explicit controls yet, but Google Home works ### Testing Nvidia shield + Google Home app ## Screenshots iOS Google Home app: ![ios_google_home](https://github.com/user-attachments/assets/680ccae5-b66e-404a-a3a1-7c89991d841b) ## AI or LLM usage None --- .../wholphin/services/ImageUrlService.kt | 3 ++- .../wholphin/ui/playback/CurrentMediaInfo.kt | 24 +++++++++++++++++++ .../wholphin/ui/playback/PlaybackViewModel.kt | 13 +++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt index 3dfc24c1..f0de9d24 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ImageUrlService.kt @@ -159,13 +159,14 @@ class ImageUrlService imageType: ImageType, fillWidth: Int? = null, fillHeight: Int? = null, + useSeriesForPrimary: Boolean? = null, ): String? = if (item != null) { getItemImageUrl( itemId = item.id, itemType = item.type, seriesId = item.data.seriesId, - useSeriesForPrimary = item.useSeriesForPrimary, + useSeriesForPrimary = useSeriesForPrimary ?: item.useSeriesForPrimary, imageTags = item.data.imageTags.orEmpty(), imageType = imageType, parentThumbId = item.data.parentThumbItemId, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt index 2b7d61bc..e8dc7d1d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt @@ -1,7 +1,12 @@ package com.github.damontecres.wholphin.ui.playback +import androidx.core.net.toUri +import androidx.media3.common.MediaMetadata +import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter +import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.TrickplayInfo +import org.jellyfin.sdk.model.extensions.ticks data class CurrentMediaInfo( val sourceId: String?, @@ -15,3 +20,22 @@ data class CurrentMediaInfo( val EMPTY = CurrentMediaInfo(null, null, listOf(), listOf(), listOf(), null) } } + +fun BaseItem.toMediaMetadata(imageUrl: String?): MediaMetadata = + MediaMetadata + .Builder() + .setTitle(title) + .setSubtitle(subtitle) + .setReleaseYear(data.productionYear) + .setDescription(data.overview) + .setArtworkUri(imageUrl?.toUri()) + .setDurationMs(data.runTimeTicks?.ticks?.inWholeMilliseconds) + .setMediaType( + when (type) { + BaseItemKind.MOVIE -> MediaMetadata.MEDIA_TYPE_MOVIE + BaseItemKind.EPISODE -> MediaMetadata.MEDIA_TYPE_TV_SHOW + BaseItemKind.VIDEO -> MediaMetadata.MEDIA_TYPE_VIDEO + BaseItemKind.TV_CHANNEL, BaseItemKind.CHANNEL, BaseItemKind.LIVE_TV_CHANNEL -> MediaMetadata.MEDIA_TYPE_TV_CHANNEL + else -> MediaMetadata.MEDIA_TYPE_VIDEO + }, + ).build() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index b3a446d7..4d44ff80 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -41,6 +41,7 @@ import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.DatePlayedService import com.github.damontecres.wholphin.services.DeviceProfileService +import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PlayerFactory import com.github.damontecres.wholphin.services.PlaylistCreationResult @@ -95,6 +96,7 @@ import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.api.sockets.subscribe import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.MediaSegmentDto import org.jellyfin.sdk.model.api.MediaSegmentType import org.jellyfin.sdk.model.api.MediaStreamType @@ -137,6 +139,7 @@ class PlaybackViewModel private val refreshRateService: RefreshRateService, val streamChoiceService: StreamChoiceService, private val userPreferencesService: UserPreferencesService, + private val imageUrlService: ImageUrlService, @Assisted private val destination: Destination, ) : ViewModel(), Player.Listener, @@ -671,7 +674,15 @@ class PlaybackViewModel MediaItem .Builder() .setMediaId(itemId.toString()) - .setUri(mediaUrl.toUri()) + .setMediaMetadata( + item.toMediaMetadata( + imageUrlService.getItemImageUrl( + item, + ImageType.PRIMARY, + useSeriesForPrimary = true, + ), + ), + ).setUri(mediaUrl.toUri()) .setSubtitleConfigurations(listOfNotNull(externalSubtitle)) .apply { when (source.container) {