domain: primaryImageTag -> imageId end-to-end (phase 4a/4b)

Rust: PlayerMediaItem and MergedMediaItem gain image_id (dual-carry),
populated from primary_image_tag at every construction/conversion site.
Regenerated bindings.

Frontend: all catalog + player + merged readers now use imageId. The
NowPlayingItem->MediaItem bridge (player.ts) properly maps the remote
session's Jellyfin fields (Type, runTimeTicks, primaryImageTag) onto the
neutral kind/durationMs/imageId. Types that are genuinely out of scope
(Person, NowPlayingItem, PlayItemRequest) keep primaryImageTag.

Rust 456, frontend 644, check clean.
This commit is contained in:
2026-07-23 21:40:58 +02:00
parent 2b42b74912
commit 93d198ce21
33 changed files with 107 additions and 51 deletions
@@ -112,12 +112,12 @@
<!-- Artist Info -->
<div class="flex flex-col items-center text-center py-12">
<!-- Artist Image -->
{#if artist.primaryImageTag}
{#if artist.imageId}
<div class="mb-6 rounded-full overflow-hidden w-40 h-40 shadow-lg">
<CachedImage
itemId={artist.id}
imageType="Primary"
tag={artist.primaryImageTag}
tag={artist.imageId}
maxWidth={400}
alt={artist.name}
class="w-full h-full object-cover"
@@ -160,11 +160,11 @@
class="group cursor-pointer"
>
<div class="aspect-square bg-[var(--color-surface)] rounded-lg overflow-hidden mb-2 group-hover:opacity-80 transition-opacity">
{#if album.primaryImageTag}
{#if album.imageId}
<CachedImage
itemId={album.id}
imageType="Primary"
tag={album.primaryImageTag}
tag={album.imageId}
maxWidth={200}
alt={album.name}
class="w-full h-full object-cover"
@@ -218,11 +218,11 @@
class="group text-center"
>
<div class="w-32 h-32 bg-[var(--color-surface)] rounded-full overflow-hidden mb-2 mx-auto group-hover:opacity-80 transition-opacity">
{#if relatedArtist.primaryImageTag}
{#if relatedArtist.imageId}
<CachedImage
itemId={relatedArtist.id}
imageType="Primary"
tag={relatedArtist.primaryImageTag}
tag={relatedArtist.imageId}
maxWidth={200}
alt={relatedArtist.name}
class="w-full h-full object-cover"
@@ -77,8 +77,8 @@
if (episode.backdropImageTags?.[0]) {
return { itemId: episode.id, imageType: "Backdrop" as const, tag: episode.backdropImageTags[0] };
}
if (episode.primaryImageTag) {
return { itemId: episode.id, imageType: "Primary" as const, tag: episode.primaryImageTag };
if (episode.imageId) {
return { itemId: episode.id, imageType: "Primary" as const, tag: episode.imageId };
}
if (series.backdropImageTags?.[0]) {
return { itemId: series.id, imageType: "Backdrop" as const, tag: series.backdropImageTags[0] };
@@ -246,7 +246,7 @@
<CachedImage
itemId={ep.id}
imageType="Primary"
tag={ep.primaryImageTag}
tag={ep.imageId}
maxWidth={400}
alt={ep.name}
class="w-full h-full object-cover transition-transform {isCurrent ? '' : 'group-hover/card:scale-105'}"
+1 -1
View File
@@ -59,7 +59,7 @@
<CachedImage
itemId={episode.id}
imageType="Primary"
tag={episode.primaryImageTag}
tag={episode.imageId}
maxWidth={320}
alt={episode.name}
class="w-full h-full object-cover transition-transform group-hover/row:scale-105"
@@ -234,7 +234,7 @@
<CachedImage
itemId={item.id}
imageType="Primary"
tag={item.primaryImageTag}
tag={item.imageId}
maxWidth={300}
alt={item.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform"
@@ -19,7 +19,7 @@
}
function getImageTag(item: MediaItem | Library): string | undefined {
return "primaryImageTag" in item ? (item.primaryImageTag ?? undefined) : ("imageTag" in item ? (item.imageTag ?? undefined) : undefined);
return "imageId" in item ? (item.imageId ?? undefined) : ("imageTag" in item ? (item.imageTag ?? undefined) : undefined);
}
function getSubtitle(item: MediaItem | Library): string {
+1 -1
View File
@@ -113,7 +113,7 @@
});
const imageTag = $derived(
"primaryImageTag" in item ? item.primaryImageTag : ("imageTag" in item ? item.imageTag : undefined)
"imageId" in item ? item.imageId : ("imageTag" in item ? item.imageTag : undefined)
);
const maxWidth = $derived(size === "large" ? 400 : size === "medium" ? 300 : 200);
@@ -53,7 +53,7 @@
<CachedImage
itemId={person.id}
imageType="Primary"
tag={person.primaryImageTag}
tag={person.imageId}
maxWidth={400}
alt={person.name}
class="w-full rounded-lg shadow-lg"
@@ -146,11 +146,11 @@
<div class="flex gap-6 pt-4">
<!-- Playlist artwork -->
<div class="flex-shrink-0 w-48">
{#if playlist.primaryImageTag}
{#if playlist.imageId}
<CachedImage
itemId={playlist.id}
imageType="Primary"
tag={playlist.primaryImageTag}
tag={playlist.imageId}
maxWidth={400}
alt={playlist.name}
class="w-full rounded-lg shadow-lg"
@@ -28,7 +28,7 @@
<CachedImage
itemId={season.id}
imageType="Primary"
tag={season.primaryImageTag}
tag={season.imageId}
maxWidth={200}
alt={seasonName}
class="w-full h-full object-cover"
+2 -2
View File
@@ -143,7 +143,7 @@
<CachedImage
itemId={artworkItemId}
imageType="Primary"
tag={displayMedia?.primaryImageTag}
tag={displayMedia?.imageId}
maxWidth={800}
alt=""
class="w-full h-full object-cover blur-3xl opacity-30"
@@ -223,7 +223,7 @@
<CachedImage
itemId={artworkItemId}
imageType="Primary"
tag={displayMedia?.primaryImageTag}
tag={displayMedia?.imageId}
maxWidth={500}
alt={displayMedia?.name}
class="w-full h-full object-cover"
+1 -1
View File
@@ -326,7 +326,7 @@
<CachedImage
itemId={displayMedia.albumId || displayMedia.id}
imageType="Primary"
tag={displayMedia.primaryImageTag}
tag={displayMedia.imageId}
maxWidth={100}
alt={displayMedia?.name}
class="w-full h-full object-cover"
@@ -78,11 +78,11 @@
<div
class="relative flex-shrink-0 w-28 h-16 rounded-lg overflow-hidden bg-gray-800"
>
{#if imageId && $nextEpisodeItem.primaryImageTag}
{#if imageId && $nextEpisodeItem.imageId}
<CachedImage
itemId={imageId}
imageType="Primary"
tag={$nextEpisodeItem.primaryImageTag}
tag={$nextEpisodeItem.imageId}
maxHeight={200}
alt={$nextEpisodeItem.name}
class="w-full h-full object-cover"
+2 -2
View File
@@ -184,11 +184,11 @@
<!-- Artwork -->
<div class="w-10 h-10 rounded bg-gray-800 flex-shrink-0 overflow-hidden">
{#if item.primaryImageTag}
{#if item.imageId}
<CachedImage
itemId={item.id}
imageType="Primary"
tag={item.primaryImageTag}
tag={item.imageId}
maxWidth={80}
alt={item.name}
class="w-full h-full object-cover"
+3 -3
View File
@@ -1223,7 +1223,7 @@
needsTranscoding: false,
// Now-playing metadata so the lockscreen/miniplayer show the item.
artist: media.seriesName ?? null,
primaryImageTag: media.primaryImageTag ?? null,
primaryImageTag: media.imageId ?? null,
serverId: media.serverId ?? null,
// Real duration so the lockscreen scrubber has a range to draw.
durationSeconds: duration > 0 ? duration : null,
@@ -1626,11 +1626,11 @@
{#if !isMediaReady}
<div class="absolute inset-0 flex items-center justify-center bg-black">
<!-- Poster/Title Card -->
{#if media?.primaryImageTag}
{#if media?.imageId}
<CachedImage
itemId={media.id}
imageType="Primary"
tag={media.primaryImageTag}
tag={media.imageId}
maxHeight={1080}
alt={media?.name || "Video"}
class="max-w-full max-h-full object-contain"
@@ -121,11 +121,11 @@
class="w-full flex items-center gap-3 p-3 hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors disabled:opacity-50"
>
<div class="w-10 h-10 flex-shrink-0 rounded overflow-hidden">
{#if playlist.primaryImageTag}
{#if playlist.imageId}
<CachedImage
itemId={playlist.id}
imageType="Primary"
tag={playlist.primaryImageTag}
tag={playlist.imageId}
maxWidth={80}
alt={playlist.name}
class="w-full h-full object-cover"