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:
@@ -106,6 +106,8 @@ pub struct MergedMediaItem {
|
|||||||
pub album_id: Option<String>,
|
pub album_id: Option<String>,
|
||||||
pub duration: Option<f64>,
|
pub duration: Option<f64>,
|
||||||
pub primary_image_tag: Option<String>,
|
pub primary_image_tag: Option<String>,
|
||||||
|
/// Neutral image identifier — replaces `primary_image_tag` (same value).
|
||||||
|
pub image_id: Option<String>,
|
||||||
pub media_type: String,
|
pub media_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +122,7 @@ impl From<&crate::player::MediaItem> for MergedMediaItem {
|
|||||||
album_id: item.album_id.clone(),
|
album_id: item.album_id.clone(),
|
||||||
duration: item.duration,
|
duration: item.duration,
|
||||||
primary_image_tag: item.primary_image_tag.clone(),
|
primary_image_tag: item.primary_image_tag.clone(),
|
||||||
|
image_id: item.primary_image_tag.clone(),
|
||||||
media_type: match item.media_type {
|
media_type: match item.media_type {
|
||||||
crate::player::MediaType::Audio => "audio".to_string(),
|
crate::player::MediaType::Audio => "audio".to_string(),
|
||||||
crate::player::MediaType::Video => "video".to_string(),
|
crate::player::MediaType::Video => "video".to_string(),
|
||||||
@@ -142,6 +145,7 @@ impl From<&crate::jellyfin::client::NowPlayingItem> for MergedMediaItem {
|
|||||||
album_id: item.album_id.clone(),
|
album_id: item.album_id.clone(),
|
||||||
duration: item.run_time_ticks.map(|ticks| ticks as f64 / 10_000_000.0),
|
duration: item.run_time_ticks.map(|ticks| ticks as f64 / 10_000_000.0),
|
||||||
primary_image_tag: item.primary_image_tag.clone(),
|
primary_image_tag: item.primary_image_tag.clone(),
|
||||||
|
image_id: item.primary_image_tag.clone(),
|
||||||
media_type: item
|
media_type: item
|
||||||
.item_type
|
.item_type
|
||||||
.clone()
|
.clone()
|
||||||
@@ -361,6 +365,7 @@ pub(super) async fn create_media_item(
|
|||||||
artist_items: None, // Not available from video-only request
|
artist_items: None, // Not available from video-only request
|
||||||
artists: None, // Not available from video-only request
|
artists: None, // Not available from video-only request
|
||||||
primary_image_tag: None, // Not available from video-only request
|
primary_image_tag: None, // Not available from video-only request
|
||||||
|
image_id: None,
|
||||||
item_type: None, // Not available from video-only request
|
item_type: None, // Not available from video-only request
|
||||||
playlist_id: None, // Not available from video-only request
|
playlist_id: None, // Not available from video-only request
|
||||||
duration: None, // Not available from video-only request
|
duration: None, // Not available from video-only request
|
||||||
@@ -595,6 +600,7 @@ pub async fn player_enter_background_audio(
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: item.primary_image_tag.clone(),
|
primary_image_tag: item.primary_image_tag.clone(),
|
||||||
|
image_id: item.primary_image_tag.clone(),
|
||||||
item_type: None,
|
item_type: None,
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
// Carry the real duration so the lockscreen MediaSession can draw a scrubber.
|
// Carry the real duration so the lockscreen MediaSession can draw a scrubber.
|
||||||
@@ -1771,6 +1777,7 @@ pub async fn player_play_album_track(
|
|||||||
artist_items: track.artist_items.clone(), // For clickable artist links
|
artist_items: track.artist_items.clone(), // For clickable artist links
|
||||||
artists: track.artists.clone(), // Fallback artist info
|
artists: track.artists.clone(), // Fallback artist info
|
||||||
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
||||||
|
image_id: track.primary_image_tag.clone(),
|
||||||
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
||||||
@@ -1966,6 +1973,7 @@ pub async fn player_play_tracks(
|
|||||||
artist_items: track.artist_items.clone(), // For clickable artist links
|
artist_items: track.artist_items.clone(), // For clickable artist links
|
||||||
artists: track.artists.clone(), // Fallback artist info
|
artists: track.artists.clone(), // Fallback artist info
|
||||||
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
||||||
|
image_id: track.primary_image_tag.clone(),
|
||||||
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
||||||
playlist_id: None, // Set based on context below
|
playlist_id: None, // Set based on context below
|
||||||
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
||||||
@@ -2428,6 +2436,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: None,
|
item_type: None,
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: None,
|
duration: None,
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ pub async fn player_add_track_by_id(
|
|||||||
artist_items: track.artist_items.clone(), // For clickable artist links
|
artist_items: track.artist_items.clone(), // For clickable artist links
|
||||||
artists: track.artists.clone(), // Fallback artist info
|
artists: track.artists.clone(), // Fallback artist info
|
||||||
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
||||||
|
image_id: track.primary_image_tag.clone(),
|
||||||
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
||||||
@@ -329,6 +330,7 @@ pub async fn player_add_tracks_by_ids(
|
|||||||
artist_items: track.artist_items.clone(), // For clickable artist links
|
artist_items: track.artist_items.clone(), // For clickable artist links
|
||||||
artists: track.artists.clone(), // Fallback artist info
|
artists: track.artists.clone(), // Fallback artist info
|
||||||
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
primary_image_tag: track.primary_image_tag.clone(), // For frontend image display
|
||||||
|
image_id: track.primary_image_tag.clone(),
|
||||||
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
item_type: Some(track.item_type.clone()), // Frontend compatibility
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
||||||
|
|||||||
@@ -119,7 +119,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn channel_and_container_types_map() {
|
fn channel_and_container_types_map() {
|
||||||
assert_eq!(kind_from_jellyfin("TvChannel", false), MediaKind::LiveChannel);
|
assert_eq!(
|
||||||
|
kind_from_jellyfin("TvChannel", false),
|
||||||
|
MediaKind::LiveChannel
|
||||||
|
);
|
||||||
assert_eq!(kind_from_jellyfin("Channel", false), MediaKind::Channel);
|
assert_eq!(kind_from_jellyfin("Channel", false), MediaKind::Channel);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
kind_from_jellyfin("CollectionFolder", true),
|
kind_from_jellyfin("CollectionFolder", true),
|
||||||
|
|||||||
@@ -948,6 +948,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Test Artist".to_string()]),
|
artists: Some(vec!["Test Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
@@ -979,6 +980,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Test Artist".to_string()]),
|
artists: Some(vec!["Test Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Test Artist".to_string()]),
|
artists: Some(vec!["Test Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
@@ -438,6 +439,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Test Artist".to_string()]),
|
artists: Some(vec!["Test Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
@@ -490,6 +492,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Test Artist".to_string()]),
|
artists: Some(vec!["Test Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
|
|||||||
@@ -67,9 +67,16 @@ pub struct MediaItem {
|
|||||||
/// Artists as array of strings (fallback when artist_items not available)
|
/// Artists as array of strings (fallback when artist_items not available)
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub artists: Option<Vec<String>>,
|
pub artists: Option<Vec<String>>,
|
||||||
/// Primary image tag for artwork
|
/// Primary image tag for artwork.
|
||||||
|
///
|
||||||
|
/// Legacy Jellyfin name; being replaced by `image_id` (same value). Dual-carried
|
||||||
|
/// while the frontend migrates (docs/specs/frontend-domain-model.md).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub primary_image_tag: Option<String>,
|
pub primary_image_tag: Option<String>,
|
||||||
|
/// Neutral image identifier the frontend resolves to a URL — replaces
|
||||||
|
/// `primary_image_tag`.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_id: Option<String>,
|
||||||
/// Item type (Audio, Movie, Episode, etc.)
|
/// Item type (Audio, Movie, Episode, etc.)
|
||||||
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
|
||||||
pub item_type: Option<String>,
|
pub item_type: Option<String>,
|
||||||
@@ -345,6 +352,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: None,
|
item_type: None,
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: None,
|
duration: None,
|
||||||
@@ -380,6 +388,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: None,
|
item_type: None,
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: None,
|
duration: None,
|
||||||
@@ -414,6 +423,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: None,
|
item_type: None,
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: None,
|
duration: None,
|
||||||
@@ -448,6 +458,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: None,
|
item_type: None,
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: None,
|
duration: None,
|
||||||
@@ -489,6 +500,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: None,
|
item_type: None,
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: None,
|
duration: None,
|
||||||
@@ -523,6 +535,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Movie".to_string()),
|
item_type: Some("Movie".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(120.0),
|
duration: Some(120.0),
|
||||||
|
|||||||
@@ -1397,6 +1397,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Test Artist".to_string()]),
|
artists: Some(vec!["Test Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
|
|||||||
@@ -551,6 +551,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Artist".to_string()]),
|
artists: Some(vec!["Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: Some(vec!["Test Artist".to_string()]),
|
artists: Some(vec!["Test Artist".to_string()]),
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Audio".to_string()),
|
item_type: Some("Audio".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(180.0),
|
duration: Some(180.0),
|
||||||
@@ -272,6 +273,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Movie".to_string()),
|
item_type: Some("Movie".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(7200.0),
|
duration: Some(7200.0),
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ mod tests {
|
|||||||
artist_items: None,
|
artist_items: None,
|
||||||
artists: None,
|
artists: None,
|
||||||
primary_image_tag: None,
|
primary_image_tag: None,
|
||||||
|
image_id: None,
|
||||||
item_type: Some("Video".to_string()),
|
item_type: Some("Video".to_string()),
|
||||||
playlist_id: None,
|
playlist_id: None,
|
||||||
duration: Some(100.0),
|
duration: Some(100.0),
|
||||||
|
|||||||
@@ -128,8 +128,7 @@ impl OfflineRepository {
|
|||||||
let playback_position_ticks: Option<i64> = row.get(0).ok();
|
let playback_position_ticks: Option<i64> = row.get(0).ok();
|
||||||
Ok(UserData {
|
Ok(UserData {
|
||||||
playback_position_ticks,
|
playback_position_ticks,
|
||||||
playback_position_ms: playback_position_ticks
|
playback_position_ms: playback_position_ticks.map(crate::domain::ticks_to_ms),
|
||||||
.map(crate::domain::ticks_to_ms),
|
|
||||||
is_played: row.get::<_, Option<i32>>(1).ok().flatten().map(|v| v != 0),
|
is_played: row.get::<_, Option<i32>>(1).ok().flatten().map(|v| v != 0),
|
||||||
is_favorite: row.get::<_, Option<i32>>(2).ok().flatten().map(|v| v != 0),
|
is_favorite: row.get::<_, Option<i32>>(2).ok().flatten().map(|v| v != 0),
|
||||||
play_count: row.get(3).ok(),
|
play_count: row.get(3).ok(),
|
||||||
|
|||||||
+14
-2
@@ -1918,7 +1918,11 @@ export type MediaType = "audio" | "video"
|
|||||||
* Lightweight media item for merged playback state
|
* Lightweight media item for merged playback state
|
||||||
* Converts from both local MediaItem and remote NowPlayingItem
|
* Converts from both local MediaItem and remote NowPlayingItem
|
||||||
*/
|
*/
|
||||||
export type MergedMediaItem = { id: string; title: string; artist: string | null; album: string | null; albumId: string | null; duration: number | null; primaryImageTag: string | null; mediaType: string }
|
export type MergedMediaItem = { id: string; title: string; artist: string | null; album: string | null; albumId: string | null; duration: number | null; primaryImageTag: string | null;
|
||||||
|
/**
|
||||||
|
* Neutral image identifier — replaces `primary_image_tag` (same value).
|
||||||
|
*/
|
||||||
|
imageId: string | null; mediaType: string }
|
||||||
/**
|
/**
|
||||||
* Argument struct for [`set_network_state`].
|
* Argument struct for [`set_network_state`].
|
||||||
*
|
*
|
||||||
@@ -2110,9 +2114,17 @@ artistItems?: ArtistItem[] | null;
|
|||||||
*/
|
*/
|
||||||
artists?: string[] | null;
|
artists?: string[] | null;
|
||||||
/**
|
/**
|
||||||
* Primary image tag for artwork
|
* Primary image tag for artwork.
|
||||||
|
*
|
||||||
|
* Legacy Jellyfin name; being replaced by `image_id` (same value). Dual-carried
|
||||||
|
* while the frontend migrates (docs/specs/frontend-domain-model.md).
|
||||||
*/
|
*/
|
||||||
primaryImageTag?: string | null;
|
primaryImageTag?: string | null;
|
||||||
|
/**
|
||||||
|
* Neutral image identifier the frontend resolves to a URL — replaces
|
||||||
|
* `primary_image_tag`.
|
||||||
|
*/
|
||||||
|
imageId?: string | null;
|
||||||
/**
|
/**
|
||||||
* Item type (Audio, Movie, Episode, etc.)
|
* Item type (Audio, Movie, Episode, etc.)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -112,12 +112,12 @@
|
|||||||
<!-- Artist Info -->
|
<!-- Artist Info -->
|
||||||
<div class="flex flex-col items-center text-center py-12">
|
<div class="flex flex-col items-center text-center py-12">
|
||||||
<!-- Artist Image -->
|
<!-- Artist Image -->
|
||||||
{#if artist.primaryImageTag}
|
{#if artist.imageId}
|
||||||
<div class="mb-6 rounded-full overflow-hidden w-40 h-40 shadow-lg">
|
<div class="mb-6 rounded-full overflow-hidden w-40 h-40 shadow-lg">
|
||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={artist.id}
|
itemId={artist.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={artist.primaryImageTag}
|
tag={artist.imageId}
|
||||||
maxWidth={400}
|
maxWidth={400}
|
||||||
alt={artist.name}
|
alt={artist.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
@@ -160,11 +160,11 @@
|
|||||||
class="group cursor-pointer"
|
class="group cursor-pointer"
|
||||||
>
|
>
|
||||||
<div class="aspect-square bg-[var(--color-surface)] rounded-lg overflow-hidden mb-2 group-hover:opacity-80 transition-opacity">
|
<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
|
<CachedImage
|
||||||
itemId={album.id}
|
itemId={album.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={album.primaryImageTag}
|
tag={album.imageId}
|
||||||
maxWidth={200}
|
maxWidth={200}
|
||||||
alt={album.name}
|
alt={album.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
@@ -218,11 +218,11 @@
|
|||||||
class="group text-center"
|
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">
|
<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
|
<CachedImage
|
||||||
itemId={relatedArtist.id}
|
itemId={relatedArtist.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={relatedArtist.primaryImageTag}
|
tag={relatedArtist.imageId}
|
||||||
maxWidth={200}
|
maxWidth={200}
|
||||||
alt={relatedArtist.name}
|
alt={relatedArtist.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
if (episode.backdropImageTags?.[0]) {
|
if (episode.backdropImageTags?.[0]) {
|
||||||
return { itemId: episode.id, imageType: "Backdrop" as const, tag: episode.backdropImageTags[0] };
|
return { itemId: episode.id, imageType: "Backdrop" as const, tag: episode.backdropImageTags[0] };
|
||||||
}
|
}
|
||||||
if (episode.primaryImageTag) {
|
if (episode.imageId) {
|
||||||
return { itemId: episode.id, imageType: "Primary" as const, tag: episode.primaryImageTag };
|
return { itemId: episode.id, imageType: "Primary" as const, tag: episode.imageId };
|
||||||
}
|
}
|
||||||
if (series.backdropImageTags?.[0]) {
|
if (series.backdropImageTags?.[0]) {
|
||||||
return { itemId: series.id, imageType: "Backdrop" as const, tag: series.backdropImageTags[0] };
|
return { itemId: series.id, imageType: "Backdrop" as const, tag: series.backdropImageTags[0] };
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={ep.id}
|
itemId={ep.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={ep.primaryImageTag}
|
tag={ep.imageId}
|
||||||
maxWidth={400}
|
maxWidth={400}
|
||||||
alt={ep.name}
|
alt={ep.name}
|
||||||
class="w-full h-full object-cover transition-transform {isCurrent ? '' : 'group-hover/card:scale-105'}"
|
class="w-full h-full object-cover transition-transform {isCurrent ? '' : 'group-hover/card:scale-105'}"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={episode.id}
|
itemId={episode.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={episode.primaryImageTag}
|
tag={episode.imageId}
|
||||||
maxWidth={320}
|
maxWidth={320}
|
||||||
alt={episode.name}
|
alt={episode.name}
|
||||||
class="w-full h-full object-cover transition-transform group-hover/row:scale-105"
|
class="w-full h-full object-cover transition-transform group-hover/row:scale-105"
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={item.primaryImageTag}
|
tag={item.imageId}
|
||||||
maxWidth={300}
|
maxWidth={300}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getImageTag(item: MediaItem | Library): string | undefined {
|
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 {
|
function getSubtitle(item: MediaItem | Library): string {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const imageTag = $derived(
|
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);
|
const maxWidth = $derived(size === "large" ? 400 : size === "medium" ? 300 : 200);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={person.id}
|
itemId={person.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={person.primaryImageTag}
|
tag={person.imageId}
|
||||||
maxWidth={400}
|
maxWidth={400}
|
||||||
alt={person.name}
|
alt={person.name}
|
||||||
class="w-full rounded-lg shadow-lg"
|
class="w-full rounded-lg shadow-lg"
|
||||||
|
|||||||
@@ -146,11 +146,11 @@
|
|||||||
<div class="flex gap-6 pt-4">
|
<div class="flex gap-6 pt-4">
|
||||||
<!-- Playlist artwork -->
|
<!-- Playlist artwork -->
|
||||||
<div class="flex-shrink-0 w-48">
|
<div class="flex-shrink-0 w-48">
|
||||||
{#if playlist.primaryImageTag}
|
{#if playlist.imageId}
|
||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={playlist.id}
|
itemId={playlist.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={playlist.primaryImageTag}
|
tag={playlist.imageId}
|
||||||
maxWidth={400}
|
maxWidth={400}
|
||||||
alt={playlist.name}
|
alt={playlist.name}
|
||||||
class="w-full rounded-lg shadow-lg"
|
class="w-full rounded-lg shadow-lg"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={season.id}
|
itemId={season.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={season.primaryImageTag}
|
tag={season.imageId}
|
||||||
maxWidth={200}
|
maxWidth={200}
|
||||||
alt={seasonName}
|
alt={seasonName}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
|||||||
@@ -143,7 +143,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={artworkItemId}
|
itemId={artworkItemId}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={displayMedia?.primaryImageTag}
|
tag={displayMedia?.imageId}
|
||||||
maxWidth={800}
|
maxWidth={800}
|
||||||
alt=""
|
alt=""
|
||||||
class="w-full h-full object-cover blur-3xl opacity-30"
|
class="w-full h-full object-cover blur-3xl opacity-30"
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={artworkItemId}
|
itemId={artworkItemId}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={displayMedia?.primaryImageTag}
|
tag={displayMedia?.imageId}
|
||||||
maxWidth={500}
|
maxWidth={500}
|
||||||
alt={displayMedia?.name}
|
alt={displayMedia?.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
|||||||
@@ -326,7 +326,7 @@
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={displayMedia.albumId || displayMedia.id}
|
itemId={displayMedia.albumId || displayMedia.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={displayMedia.primaryImageTag}
|
tag={displayMedia.imageId}
|
||||||
maxWidth={100}
|
maxWidth={100}
|
||||||
alt={displayMedia?.name}
|
alt={displayMedia?.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
|||||||
@@ -78,11 +78,11 @@
|
|||||||
<div
|
<div
|
||||||
class="relative flex-shrink-0 w-28 h-16 rounded-lg overflow-hidden bg-gray-800"
|
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
|
<CachedImage
|
||||||
itemId={imageId}
|
itemId={imageId}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={$nextEpisodeItem.primaryImageTag}
|
tag={$nextEpisodeItem.imageId}
|
||||||
maxHeight={200}
|
maxHeight={200}
|
||||||
alt={$nextEpisodeItem.name}
|
alt={$nextEpisodeItem.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
|||||||
@@ -184,11 +184,11 @@
|
|||||||
|
|
||||||
<!-- Artwork -->
|
<!-- Artwork -->
|
||||||
<div class="w-10 h-10 rounded bg-gray-800 flex-shrink-0 overflow-hidden">
|
<div class="w-10 h-10 rounded bg-gray-800 flex-shrink-0 overflow-hidden">
|
||||||
{#if item.primaryImageTag}
|
{#if item.imageId}
|
||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={item.primaryImageTag}
|
tag={item.imageId}
|
||||||
maxWidth={80}
|
maxWidth={80}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
|||||||
@@ -1223,7 +1223,7 @@
|
|||||||
needsTranscoding: false,
|
needsTranscoding: false,
|
||||||
// Now-playing metadata so the lockscreen/miniplayer show the item.
|
// Now-playing metadata so the lockscreen/miniplayer show the item.
|
||||||
artist: media.seriesName ?? null,
|
artist: media.seriesName ?? null,
|
||||||
primaryImageTag: media.primaryImageTag ?? null,
|
primaryImageTag: media.imageId ?? null,
|
||||||
serverId: media.serverId ?? null,
|
serverId: media.serverId ?? null,
|
||||||
// Real duration so the lockscreen scrubber has a range to draw.
|
// Real duration so the lockscreen scrubber has a range to draw.
|
||||||
durationSeconds: duration > 0 ? duration : null,
|
durationSeconds: duration > 0 ? duration : null,
|
||||||
@@ -1626,11 +1626,11 @@
|
|||||||
{#if !isMediaReady}
|
{#if !isMediaReady}
|
||||||
<div class="absolute inset-0 flex items-center justify-center bg-black">
|
<div class="absolute inset-0 flex items-center justify-center bg-black">
|
||||||
<!-- Poster/Title Card -->
|
<!-- Poster/Title Card -->
|
||||||
{#if media?.primaryImageTag}
|
{#if media?.imageId}
|
||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={media.id}
|
itemId={media.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={media.primaryImageTag}
|
tag={media.imageId}
|
||||||
maxHeight={1080}
|
maxHeight={1080}
|
||||||
alt={media?.name || "Video"}
|
alt={media?.name || "Video"}
|
||||||
class="max-w-full max-h-full object-contain"
|
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"
|
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">
|
<div class="w-10 h-10 flex-shrink-0 rounded overflow-hidden">
|
||||||
{#if playlist.primaryImageTag}
|
{#if playlist.imageId}
|
||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={playlist.id}
|
itemId={playlist.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={playlist.primaryImageTag}
|
tag={playlist.imageId}
|
||||||
maxWidth={80}
|
maxWidth={80}
|
||||||
alt={playlist.name}
|
alt={playlist.name}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function createMoviesStore() {
|
|||||||
|
|
||||||
/** Artwork check for hero candidates: needs a backdrop or a primary image. */
|
/** Artwork check for hero candidates: needs a backdrop or a primary image. */
|
||||||
const hasArt = (i: MediaItem) =>
|
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) {
|
async function loadSections(libraryId: string) {
|
||||||
update(s => ({
|
update(s => ({
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function createMusicStore() {
|
|||||||
|
|
||||||
/** Artwork check for hero candidates: needs a primary image or backdrop. */
|
/** Artwork check for hero candidates: needs a primary image or backdrop. */
|
||||||
const hasArt = (i: MediaItem) =>
|
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) {
|
async function loadSections(libraryId: string) {
|
||||||
update(s => ({
|
update(s => ({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { writable, derived } from "svelte/store";
|
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 type { NowPlayingItem } from "$lib/api/bindings";
|
||||||
import { isRemoteMode } from "./playbackMode";
|
import { isRemoteMode } from "./playbackMode";
|
||||||
import { selectedSession } from "./sessions";
|
import { selectedSession } from "./sessions";
|
||||||
@@ -24,7 +24,7 @@ export interface MergedMediaItem {
|
|||||||
album: string | null;
|
album: string | null;
|
||||||
albumId: string | null;
|
albumId: string | null;
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
primaryImageTag: string | null;
|
imageId: string | null;
|
||||||
mediaType: "audio" | "video";
|
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.)
|
* falls back to the `artists` string list when `artistItems` is absent.)
|
||||||
*/
|
*/
|
||||||
function nowPlayingToMediaItem(npi: NowPlayingItem): MediaItem {
|
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 {
|
return {
|
||||||
id: npi.id ?? "",
|
id: npi.id ?? "",
|
||||||
name: npi.name ?? "",
|
name: npi.name ?? "",
|
||||||
type: (npi.Type ?? "Audio") as ItemType,
|
kind,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
albumName: npi.album,
|
albumName: npi.album,
|
||||||
albumId: npi.albumId,
|
albumId: npi.albumId,
|
||||||
artists: npi.artists,
|
artists: npi.artists,
|
||||||
primaryImageTag: npi.primaryImageTag ?? npi.albumPrimaryImageTag,
|
imageId: npi.primaryImageTag ?? npi.albumPrimaryImageTag,
|
||||||
runTimeTicks: npi.runTimeTicks,
|
durationMs: npi.runTimeTicks != null ? Math.floor(npi.runTimeTicks / 10000) : null,
|
||||||
} as MediaItem;
|
} as MediaItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function createTvStore() {
|
|||||||
const hasArt = (i: MediaItem) =>
|
const hasArt = (i: MediaItem) =>
|
||||||
!!(i.backdropImageTags && i.backdropImageTags.length > 0) ||
|
!!(i.backdropImageTags && i.backdropImageTags.length > 0) ||
|
||||||
!!(i.parentBackdropImageTags && i.parentBackdropImageTags.length > 0) ||
|
!!(i.parentBackdropImageTags && i.parentBackdropImageTags.length > 0) ||
|
||||||
!!i.primaryImageTag;
|
!!i.imageId;
|
||||||
|
|
||||||
async function loadSections(libraryId: string) {
|
async function loadSections(libraryId: string) {
|
||||||
update(s => ({
|
update(s => ({
|
||||||
|
|||||||
@@ -355,11 +355,11 @@
|
|||||||
<div class="flex gap-6">
|
<div class="flex gap-6">
|
||||||
<!-- Poster -->
|
<!-- Poster -->
|
||||||
<div class="flex-shrink-0 w-48">
|
<div class="flex-shrink-0 w-48">
|
||||||
{#if item.primaryImageTag}
|
{#if item.imageId}
|
||||||
<CachedImage
|
<CachedImage
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
imageType="Primary"
|
imageType="Primary"
|
||||||
tag={item.primaryImageTag}
|
tag={item.imageId}
|
||||||
maxWidth={400}
|
maxWidth={400}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
class="w-full {isMusicItem ? 'aspect-square' : ''} rounded-lg shadow-lg"
|
class="w-full {isMusicItem ? 'aspect-square' : ''} rounded-lg shadow-lg"
|
||||||
|
|||||||
@@ -355,8 +355,8 @@
|
|||||||
artist: t.artists?.join(", ") || null,
|
artist: t.artists?.join(", ") || null,
|
||||||
album: t.albumName || null,
|
album: t.albumName || null,
|
||||||
duration: t.runTimeTicks ? t.runTimeTicks / 10000000 : null,
|
duration: t.runTimeTicks ? t.runTimeTicks / 10000000 : null,
|
||||||
artworkUrl: t.primaryImageTag
|
artworkUrl: t.imageId
|
||||||
? repo.getImageUrl(t.albumId || t.id, "Primary", { maxWidth: 300, tag: t.primaryImageTag })
|
? repo.getImageUrl(t.albumId || t.id, "Primary", { maxWidth: 300, tag: t.imageId })
|
||||||
: null,
|
: null,
|
||||||
mediaType: "audio",
|
mediaType: "audio",
|
||||||
streamUrl: trackStreamUrl,
|
streamUrl: trackStreamUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user