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 duration: Option<f64>,
|
||||
pub primary_image_tag: Option<String>,
|
||||
/// Neutral image identifier — replaces `primary_image_tag` (same value).
|
||||
pub image_id: Option<String>,
|
||||
pub media_type: String,
|
||||
}
|
||||
|
||||
@@ -120,6 +122,7 @@ impl From<&crate::player::MediaItem> for MergedMediaItem {
|
||||
album_id: item.album_id.clone(),
|
||||
duration: item.duration,
|
||||
primary_image_tag: item.primary_image_tag.clone(),
|
||||
image_id: item.primary_image_tag.clone(),
|
||||
media_type: match item.media_type {
|
||||
crate::player::MediaType::Audio => "audio".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(),
|
||||
duration: item.run_time_ticks.map(|ticks| ticks as f64 / 10_000_000.0),
|
||||
primary_image_tag: item.primary_image_tag.clone(),
|
||||
image_id: item.primary_image_tag.clone(),
|
||||
media_type: item
|
||||
.item_type
|
||||
.clone()
|
||||
@@ -361,10 +365,11 @@ pub(super) async fn create_media_item(
|
||||
artist_items: 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
|
||||
item_type: None, // Not available from video-only request
|
||||
playlist_id: None, // Not available from video-only request
|
||||
duration: None, // Not available from video-only request
|
||||
artwork_url: None, // Not available from video-only request
|
||||
image_id: None,
|
||||
item_type: None, // Not available from video-only request
|
||||
playlist_id: None, // Not available from video-only request
|
||||
duration: None, // Not available from video-only request
|
||||
artwork_url: None, // Not available from video-only request
|
||||
media_type: crate::player::MediaType::Video, // Video-only request
|
||||
source,
|
||||
video_codec: Some(req.video_codec),
|
||||
@@ -595,6 +600,7 @@ pub async fn player_enter_background_audio(
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: item.primary_image_tag.clone(),
|
||||
image_id: item.primary_image_tag.clone(),
|
||||
item_type: None,
|
||||
playlist_id: None,
|
||||
// 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
|
||||
artists: track.artists.clone(), // Fallback artist info
|
||||
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
|
||||
playlist_id: None,
|
||||
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
|
||||
artists: track.artists.clone(), // Fallback artist info
|
||||
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
|
||||
playlist_id: None, // Set based on context below
|
||||
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
||||
@@ -2428,6 +2436,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: None,
|
||||
playlist_id: 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
|
||||
artists: track.artists.clone(), // Fallback artist info
|
||||
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
|
||||
playlist_id: None,
|
||||
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
|
||||
artists: track.artists.clone(), // Fallback artist info
|
||||
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
|
||||
playlist_id: None,
|
||||
duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
|
||||
|
||||
@@ -119,7 +119,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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("CollectionFolder", true),
|
||||
|
||||
@@ -948,6 +948,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Test Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
@@ -979,6 +980,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Test Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
|
||||
@@ -380,6 +380,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Test Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
@@ -438,6 +439,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Test Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
@@ -490,6 +492,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Test Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
|
||||
@@ -67,9 +67,16 @@ pub struct MediaItem {
|
||||
/// Artists as array of strings (fallback when artist_items not available)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
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")]
|
||||
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.)
|
||||
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
|
||||
pub item_type: Option<String>,
|
||||
@@ -345,6 +352,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: None,
|
||||
playlist_id: None,
|
||||
duration: None,
|
||||
@@ -380,6 +388,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: None,
|
||||
playlist_id: None,
|
||||
duration: None,
|
||||
@@ -414,6 +423,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: None,
|
||||
playlist_id: None,
|
||||
duration: None,
|
||||
@@ -448,6 +458,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: None,
|
||||
playlist_id: None,
|
||||
duration: None,
|
||||
@@ -489,6 +500,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: None,
|
||||
playlist_id: None,
|
||||
duration: None,
|
||||
@@ -523,6 +535,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Movie".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(120.0),
|
||||
|
||||
@@ -1397,6 +1397,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Test Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
|
||||
@@ -551,6 +551,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
|
||||
@@ -242,6 +242,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: Some(vec!["Test Artist".to_string()]),
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Audio".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(180.0),
|
||||
@@ -272,6 +273,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Movie".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(7200.0),
|
||||
|
||||
@@ -323,6 +323,7 @@ mod tests {
|
||||
artist_items: None,
|
||||
artists: None,
|
||||
primary_image_tag: None,
|
||||
image_id: None,
|
||||
item_type: Some("Video".to_string()),
|
||||
playlist_id: None,
|
||||
duration: Some(100.0),
|
||||
|
||||
@@ -128,8 +128,7 @@ impl OfflineRepository {
|
||||
let playback_position_ticks: Option<i64> = row.get(0).ok();
|
||||
Ok(UserData {
|
||||
playback_position_ticks,
|
||||
playback_position_ms: playback_position_ticks
|
||||
.map(crate::domain::ticks_to_ms),
|
||||
playback_position_ms: playback_position_ticks.map(crate::domain::ticks_to_ms),
|
||||
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),
|
||||
play_count: row.get(3).ok(),
|
||||
|
||||
Reference in New Issue
Block a user