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
+1 -1
View File
@@ -45,7 +45,7 @@ function createMoviesStore() {
/** Artwork check for hero candidates: needs a backdrop or a primary image. */
const hasArt = (i: MediaItem) =>
!!(i.backdropImageTags && i.backdropImageTags.length > 0) || !!i.primaryImageTag;
!!(i.backdropImageTags && i.backdropImageTags.length > 0) || !!i.imageId;
async function loadSections(libraryId: string) {
update(s => ({
+1 -1
View File
@@ -55,7 +55,7 @@ function createMusicStore() {
/** Artwork check for hero candidates: needs a primary image or backdrop. */
const hasArt = (i: MediaItem) =>
!!i.primaryImageTag || !!(i.backdropImageTags && i.backdropImageTags.length > 0);
!!i.imageId || !!(i.backdropImageTags && i.backdropImageTags.length > 0);
async function loadSections(libraryId: string) {
update(s => ({
+13 -5
View File
@@ -9,7 +9,7 @@
*/
import { writable, derived } from "svelte/store";
import type { MediaItem, ItemType } from "$lib/api/types";
import type { MediaItem, MediaKind } from "$lib/api/types";
import type { NowPlayingItem } from "$lib/api/bindings";
import { isRemoteMode } from "./playbackMode";
import { selectedSession } from "./sessions";
@@ -24,7 +24,7 @@ export interface MergedMediaItem {
album: string | null;
albumId: string | null;
duration: number | null;
primaryImageTag: string | null;
imageId: string | null;
mediaType: "audio" | "video";
}
@@ -170,16 +170,24 @@ export const isMuted = derived(player, ($p) => $p.muted);
* falls back to the `artists` string list when `artistItems` is absent.)
*/
function nowPlayingToMediaItem(npi: NowPlayingItem): MediaItem {
// NowPlayingItem is remote-session data still carrying Jellyfin field names
// (Type, runTimeTicks, primaryImageTag). Map it onto the neutral MediaItem the
// UI consumes. Only the coarse audio/video split matters here for display.
const kind: MediaKind =
npi.Type === "Movie" ? "movie" :
npi.Type === "Episode" ? "episode" :
npi.Type === "MusicAlbum" ? "album" :
"track";
return {
id: npi.id ?? "",
name: npi.name ?? "",
type: (npi.Type ?? "Audio") as ItemType,
kind,
serverId: "",
albumName: npi.album,
albumId: npi.albumId,
artists: npi.artists,
primaryImageTag: npi.primaryImageTag ?? npi.albumPrimaryImageTag,
runTimeTicks: npi.runTimeTicks,
imageId: npi.primaryImageTag ?? npi.albumPrimaryImageTag,
durationMs: npi.runTimeTicks != null ? Math.floor(npi.runTimeTicks / 10000) : null,
} as MediaItem;
}
+1 -1
View File
@@ -50,7 +50,7 @@ function createTvStore() {
const hasArt = (i: MediaItem) =>
!!(i.backdropImageTags && i.backdropImageTags.length > 0) ||
!!(i.parentBackdropImageTags && i.parentBackdropImageTags.length > 0) ||
!!i.primaryImageTag;
!!i.imageId;
async function loadSections(libraryId: string) {
update(s => ({