use crate::media_server::ServerId; #[derive(Clone, Debug)] pub struct PlaybackItem { pub uri: String, pub title: Option, pub server_id: Option, pub object_id: Option, pub artist: Option, pub album: Option, pub genre: Option, pub album_art_uri: Option, pub date: Option, pub track_number: Option, pub creator: Option, pub protocol_info: Option, } impl PlaybackItem { pub fn new(uri: impl Into) -> Self { Self { uri: uri.into(), title: None, server_id: None, object_id: None, artist: None, album: None, genre: None, album_art_uri: None, date: None, track_number: None, creator: None, protocol_info: None, } } /// Convert PlaybackItem to DIDL-Lite XML metadata for SetAVTransportURI pub fn to_didl_metadata(&self) -> String { use quick_xml::escape::escape; use tracing::debug; let title = self.title.as_deref().unwrap_or("Unknown"); let escaped_uri = escape(&self.uri); let escaped_title = escape(title); let mut didl = String::from( r#""#, ); // Use proper ID from object_id if available, otherwise use "0" let item_id = self.object_id.as_deref().unwrap_or("0"); let escaped_item_id = escape(item_id); didl.push_str(&format!( r#""#, escaped_item_id )); didl.push_str(&format!("{}", escaped_title)); if let Some(artist) = &self.artist { let escaped_artist = escape(artist); didl.push_str(&format!("{}", escaped_artist)); didl.push_str(&format!("{}", escaped_artist)); } if let Some(album) = &self.album { let escaped_album = escape(album); didl.push_str(&format!("{}", escaped_album)); } if let Some(genre) = &self.genre { let escaped_genre = escape(genre); didl.push_str(&format!("{}", escaped_genre)); } if let Some(album_art) = &self.album_art_uri { let escaped_art = escape(album_art); debug!( title = title, album_art_uri = album_art.as_str(), "Including albumArtURI in DIDL metadata" ); didl.push_str(&format!( "{}", escaped_art )); } else { debug!( title = title, "No album_art_uri in PlaybackItem - skipping albumArtURI in DIDL" ); } if let Some(date) = &self.date { let escaped_date = escape(date); didl.push_str(&format!("{}", escaped_date)); } if let Some(track_num) = &self.track_number { let escaped_track = escape(track_num); didl.push_str(&format!( "{}", escaped_track )); } // Add resource with URI // Use the original protocolInfo if available, otherwise use a generic one let protocol_info = self .protocol_info .as_deref() .unwrap_or("http-get:*:audio/*:*"); // For protocolInfo, we only need to escape XML special chars, not ':' // We manually escape only the necessary characters to preserve the protocolInfo format let safe_protocol_info = protocol_info .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """); didl.push_str(&format!( r#"{}"#, safe_protocol_info, escaped_uri )); didl.push_str(r#"object.item.audioItem.musicTrack"#); didl.push_str(""); didl.push_str(""); didl } } #[derive(Clone, Debug, Default)] pub struct PlaybackQueue { items: Vec, current_index: Option, } impl PlaybackQueue { pub fn new() -> Self { Self { items: Vec::new(), current_index: None, } } pub fn len(&self) -> usize { self.items.len() } pub fn is_empty(&self) -> bool { self.items.is_empty() } pub fn clear(&mut self) { self.items.clear(); self.current_index = None; } pub fn enqueue(&mut self, item: PlaybackItem) { self.items.push(item); } pub fn enqueue_many>(&mut self, items: I) { for item in items { self.items.push(item); } } pub fn enqueue_front(&mut self, item: PlaybackItem) { let insert_at = match self.current_index { Some(idx) => { let next = idx.saturating_add(1); next.min(self.items.len()) } None => 0, }; self.items.insert(insert_at, item); // current_index remains unchanged; insertion happens after the cursor. } pub fn dequeue(&mut self) -> Option { if self.items.is_empty() { return None; } match self.current_index { None => { self.current_index = Some(0); self.items.get(0).cloned() } Some(idx) => { let next_idx = idx + 1; if next_idx >= self.items.len() { None } else { self.current_index = Some(next_idx); self.items.get(next_idx).cloned() } } } } pub fn peek(&self) -> Option<&PlaybackItem> { if let Some(idx) = self.current_index { self.items.get(idx) } else { self.items.first() } } pub fn snapshot(&self) -> Vec { match self.current_index { None => self.items.clone(), Some(idx) => self.items.iter().skip(idx + 1).cloned().collect(), } } pub fn upcoming_len(&self) -> usize { match self.current_index { None => self.items.len(), Some(idx) => self.items.len().saturating_sub(idx + 1), } } pub fn full_snapshot(&self) -> (Vec, Option) { (self.items.clone(), self.current_index) } pub fn set_current_index(&mut self, index: Option) { if let Some(idx) = index { if idx < self.items.len() { self.current_index = Some(idx); } else { self.current_index = None; } } else { self.current_index = None; } } }