Enorme refactoring de PMO control step 2

This commit is contained in:
2025-12-30 16:50:26 +01:00
parent a4301140d8
commit 9c90e706f1
45 changed files with 4404 additions and 5089 deletions

View File

@@ -28,103 +28,7 @@
//! - This identity is used by the sync helpers to preserve the current
//! track across queue rebuilds when the MediaServer content changes.
use crate::DeviceId;
use crate::errors::ControlPointError;
// ADAPTE ces imports aux modules existants dans pmocontrol.
// Exemple probable :
// use crate::model::MediaServerId;
// use crate::model::TrackMetadata;
use crate::model::TrackMetadata;
/// Canonical representation of a track in a renderer queue.
///
/// This type is the bridge between:
/// - the UPnP MediaServer (DIDL-Lite items),
/// - the ControlPoint runtime,
/// - and the different queue backends (internal / OpenHome).
///
/// It is intentionally DIDL-centric: every item in a queue comes from
/// a UPnP ContentDirectory and carries its MediaServer identity.
#[derive(Clone, Debug)]
pub struct PlaybackItem {
/// Identifier of the UPnP MediaServer that owns this content.
///
/// Typically this is the UDN of the MediaServer device, or an
/// equivalent logical identifier.
pub media_server_id: DeviceId,
/// L'ID interne de l'item dans la queue.
/// Cet ID n'a de sens que lors du retour d'un snapshot.
/// Dans une queue interne, il peut avoir n'importe quelle valeur,
/// l'ID qui compte et la position dans le vecteur.
/// Par principe, on la peut la mettre égale à usize::MAX.
pub backend_id: usize,
/// DIDL-Lite `id` attribute of the `item` in the ContentDirectory.
///
/// This, combined with `media_server_id`, is the logical identity
/// of the track across refreshes of the MediaServer state.
pub didl_id: String,
/// Main resource URI to be used for playback.
///
/// This is usually the first `<res>` element (or a selected one)
/// from the DIDL-Lite item.
pub uri: String,
/// UPnP protocolInfo string for the resource (e.g., "http-get:*:audio/flac:*").
///
/// This string describes the protocol, network, MIME type, and additional
/// info about the media resource. It's required for proper UPnP/OpenHome
/// renderer compatibility.
pub protocol_info: String,
/// Optional rich metadata for the track (title, artist, album, cover,
/// duration, …).
///
/// The exact structure is defined in `TrackMetadata` and may
/// aggregate information from DIDL, tags, or additional sources.
pub metadata: Option<TrackMetadata>,
}
impl PlaybackItem {
/// Returns a stable, backend-agnostic logical identifier for this item.
///
/// By default this is the concatenation of the MediaServer identifier
/// and the DIDL `id`. Backends and higher-level logic should use this
/// when they need to match items across queue rebuilds.
pub fn unique_id(&self) -> String {
// ADAPTE si MediaServerId n'implémente pas Display : utilise
// un champ string interne ou une méthode as_str().
format!("{}::{}", self.media_server_id.0, self.didl_id)
}
}
/// Logical snapshot of a renderer queue.
///
/// This is the canonical view used by the ControlPoint and the REST/API
/// layer. It is independent of how the queue is actually stored (local
/// in-memory queue, OpenHome playlist, …).
#[derive(Clone, Debug)]
pub struct QueueSnapshot {
/// All items currently in the queue, in play order.
pub items: Vec<PlaybackItem>,
/// Index (0-based) of the current item in `items`, or `None` if
/// no item is currently selected.
pub current_index: Option<usize>,
}
impl QueueSnapshot {
/// Returns the number of items in the snapshot.
pub fn len(&self) -> usize {
self.items.len()
}
/// Returns `true` if the snapshot contains no items.
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
use crate::{PlaybackItem, QueueSnapshot, errors::ControlPointError};
/// High-level enqueue mode.
///
@@ -164,6 +68,24 @@ pub trait QueueBackend {
// BACKEND PRIMITIVES (must be implemented)
// =====================================================================
/// Returns the length of this queue.
fn len(&self) -> Result<usize, ControlPointError>;
/// Lists all the items in this queue.
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError>;
/// Converts a track ID to its position in the queue.
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError>;
/// Converts a track ID to its position in the queue.
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError>;
/// Return the current playing track identifier
fn current_track(&self) -> Result<Option<u32>, ControlPointError>;
/// Returns the current playing track index in the queue
fn current_index(&self) -> Result<Option<usize>, ControlPointError>;
/// Returns the full snapshot (items + current index) of this queue.
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError>;
@@ -181,12 +103,28 @@ pub trait QueueBackend {
current_index: Option<usize>,
) -> Result<(), ControlPointError>;
/// Updates the queue while adjusting so that the new current_index
/// corresponds to the old current index track.
/// If the old current index track is absent from the new queue,
/// it is kept as the first item and the new items are appended after it.
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError>;
/// Returns the item at `index`, if it exists.
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError>;
/// Replaces the item at `index` with `item`.
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError>;
/// Enqueues items according to the selected `EnqueueMode`.
///
/// This method only manipulates the queue structure; it does not
/// start playback.
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError>;
// =====================================================================
// DEFAULT HELPERS (backend-agnostic logic)
// =====================================================================
@@ -196,41 +134,11 @@ pub trait QueueBackend {
self.replace_queue(Vec::new(), None)
}
/// Alias for `clear_queue`, semantic name for “empty before rebuild”.
fn empty_queue(&mut self) -> Result<(), ControlPointError> {
self.clear_queue()
}
/// Returns the current index, if any.
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
Ok(self.queue_snapshot()?.current_index)
}
/// Returns the number of items in the queue.
fn len(&self) -> Result<usize, ControlPointError> {
Ok(self.queue_snapshot()?.len())
}
/// Returns `true` if the queue is empty.
fn is_empty(&self) -> Result<bool, ControlPointError> {
Ok(self.queue_snapshot()?.is_empty())
}
/// Returns a full snapshot of the queue.
fn full_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
self.queue_snapshot()
}
/// Returns an iterator over all items in the queue.
///
/// The default implementation:
/// - takes a snapshot,
/// - returns a boxed iterator owning the underlying `Vec`.
fn iter_items(&self) -> Result<Box<dyn Iterator<Item = PlaybackItem>>, ControlPointError> {
let snapshot = self.queue_snapshot()?;
Ok(Box::new(snapshot.items.into_iter()))
}
/// Returns the list of items that come strictly after the current index.
fn upcoming_items(&self) -> Result<Vec<PlaybackItem>, ControlPointError> {
let snapshot = self.queue_snapshot()?;
@@ -243,7 +151,12 @@ pub trait QueueBackend {
/// Returns how many items remain in the queue after the current index.
fn upcoming_len(&self) -> Result<usize, ControlPointError> {
Ok(self.upcoming_items()?.len())
let snapshot = self.queue_snapshot()?;
let len = snapshot.items.len();
match snapshot.current_index {
None => Ok(len),
Some(idx) => Ok(len.saturating_sub(idx + 1)),
}
}
/// Returns the current item (or the first pending item if no index is set)
@@ -312,36 +225,6 @@ pub trait QueueBackend {
Ok(Some((item, remaining)))
}
/// Enqueues items according to the selected `EnqueueMode`.
///
/// This method only manipulates the queue structure; it does not
/// start playback.
fn enqueue_items(&mut self, items: Vec<PlaybackItem>, mode: EnqueueMode) -> Result<(), ControlPointError> {
let mut snapshot = self.queue_snapshot()?;
match mode {
EnqueueMode::AppendToEnd => {
snapshot.items.extend(items);
}
EnqueueMode::InsertAfterCurrent => {
let insert_pos = snapshot
.current_index
.map(|i| (i + 1).min(snapshot.items.len()))
.unwrap_or(0);
for (offset, it) in items.into_iter().enumerate() {
snapshot.items.insert(insert_pos + offset, it);
}
}
EnqueueMode::ReplaceAll => {
snapshot.items = items;
snapshot.current_index = None;
}
}
self.replace_queue(snapshot.items, snapshot.current_index)
}
/// Replaces the queue with `items` and sets a default index.
fn replace_all(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
if items.is_empty() {
@@ -410,7 +293,7 @@ pub trait QueueBackend {
}
}
/// Convenience helper to update an item in place at the given index.
/// Convenience helper to update an item "in place" at the given index.
fn update_item(
&mut self,
index: usize,
@@ -420,34 +303,10 @@ pub trait QueueBackend {
let new_item = update(item);
self.replace_item(index, new_item)
} else {
Err(ControlPointError::QueueError(format!("Queue index {} out of range", index)))
}
}
/// Synchronizes the queue with a new list of items coming from an
/// external MediaServer, trying to preserve the current track.
fn sync_from_external_preserve_current(&mut self, new_items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
let snapshot = self.queue_snapshot()?;
let current = snapshot
.current_index
.and_then(|i| snapshot.items.get(i).cloned());
let Some(current) = current else {
return self.replace_all(new_items);
};
let current_uid = current.unique_id();
if let Some(new_idx) = new_items
.iter()
.position(|it| it.unique_id() == current_uid)
{
self.replace_queue(new_items, Some(new_idx))
} else {
let mut items = Vec::with_capacity(new_items.len() + 1);
items.push(current);
items.extend(new_items);
self.replace_queue(items, Some(0))
Err(ControlPointError::QueueError(format!(
"Queue index {} out of range",
index
)))
}
}
}

View File

@@ -15,18 +15,11 @@
//! - maintains a `current_index`,
//! - never starts playback (transport control is handled elsewhere).
use std::sync::{Arc, Mutex};
use anyhow::Result;
use crate::{
DeviceId, RendererInfo,
DeviceIdentity,
DeviceId, DeviceIdentity, RendererInfo,
errors::ControlPointError,
queue::{
MusicQueue,
backend::{PlaybackItem, QueueBackend, QueueSnapshot},
},
queue::{MusicQueue, PlaybackItem, QueueBackend, QueueFromRendererInfo, QueueSnapshot},
};
/// Internal/local queue implementation.
@@ -54,27 +47,59 @@ impl InternalQueue {
}
}
pub fn from_renderer_info(
info: &RendererInfo,
) -> Result<InternalQueue, ControlPointError> {
Ok(InternalQueue::new(
info.id(),
),
)
pub fn from_renderer_info(info: &RendererInfo) -> Result<InternalQueue, ControlPointError> {
Ok(InternalQueue::new(info.id()))
}
/// Exposes a read-only view of the underlying items.
pub fn items(&self) -> &[PlaybackItem] {
&self.items
}
/// Exposes the current index (read-only).
pub fn current_index(&self) -> Option<usize> {
self.current_index
}
}
impl QueueBackend for InternalQueue {
fn len(&self) -> Result<usize, ControlPointError> {
Ok(self.items.len())
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
let ids: Vec<u32> = (0..self.len()?).map(|i| i as u32).collect();
Ok(ids)
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
Ok(id as usize)
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
u32::try_from(id).map_err(|_| {
ControlPointError::QueueError(format!(
"Position {} exceeds u32::MAX",
id
))
})
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
match self.current_index {
None => Ok(None),
Some(i) => {
u32::try_from(i)
.map(Some)
.map_err(|_| {
ControlPointError::QueueError(format!(
"Current index {} exceeds u32::MAX",
i
))
})
}
}
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
Ok(self.current_index)
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
let mut items = self.items.clone();
for (i, item) in items.iter_mut().enumerate() {
@@ -96,7 +121,11 @@ impl QueueBackend for InternalQueue {
if i < self.items.len() {
self.current_index = Some(i);
} else {
self.current_index = None;
return Err(ControlPointError::QueueError(format!(
"Index out of bound {} >= {}",
i,
self.items.len()
)));
}
}
}
@@ -113,14 +142,187 @@ impl QueueBackend for InternalQueue {
Ok(())
}
fn sync_queue(
&mut self,
items: Vec<PlaybackItem>
) -> Result<(), ControlPointError> {
if items.is_empty() {
return self.replace_queue(Vec::new(), None);
}
// Récupérer l'item actuel
let current = self.current_index
.and_then(|idx| self.items.get(idx).map(|item| (idx, item.uri.clone())));
if let Some((_current_idx, current_uri)) = current {
// Chercher l'item actuel dans la nouvelle liste (par URI)
let new_idx = items.iter().position(|item| item.uri == current_uri);
if let Some(new_idx) = new_idx {
// Item trouvé dans la nouvelle liste
self.replace_queue(items, Some(new_idx))
} else {
// Item pas trouvé, le garder comme premier
let current_item = self.items[self.current_index.unwrap()].clone();
let mut new_items = Vec::with_capacity(items.len() + 1);
new_items.push(current_item);
new_items.extend(items);
self.replace_queue(new_items, Some(0))
}
} else {
// Pas d'item actuel
self.replace_queue(items, None)
}
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: crate::queue::EnqueueMode,
) -> Result<(), ControlPointError> {
use crate::queue::EnqueueMode;
match mode {
EnqueueMode::AppendToEnd => {
self.items.extend(items);
}
EnqueueMode::InsertAfterCurrent => {
let insert_pos = self.current_index
.map(|i| (i + 1).min(self.items.len()))
.unwrap_or(0);
for (offset, item) in items.into_iter().enumerate() {
self.items.insert(insert_pos + offset, item);
}
}
EnqueueMode::ReplaceAll => {
self.items = items;
self.current_index = None;
}
}
Ok(())
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
Ok(self.items.get(index).cloned())
if index < self.items.len() {
Ok(self.items.get(index).cloned())
} else {
Err(ControlPointError::QueueError(format!(
"get_item index out of bound {} >= {}",
index,
self.items.len()
)))
}
}
// Optimized helpers for InternalQueue
fn clear_queue(&mut self) -> Result<(), ControlPointError> {
self.items.clear();
self.current_index = None;
Ok(())
}
fn is_empty(&self) -> Result<bool, ControlPointError> {
Ok(self.items.is_empty())
}
fn upcoming_len(&self) -> Result<usize, ControlPointError> {
let len = self.items.len();
match self.current_index {
None => Ok(len),
Some(idx) => Ok(len.saturating_sub(idx + 1)),
}
}
fn upcoming_items(&self) -> Result<Vec<PlaybackItem>, ControlPointError> {
let items = match self.current_index {
None => self.items.clone(),
Some(idx) => self.items.iter().skip(idx + 1).cloned().collect(),
};
Ok(items)
}
fn peek_current(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
if self.items.is_empty() {
return Ok(None);
}
let len = self.items.len();
let (item, resolved_index) = match self.current_index {
Some(idx) if idx < len => (self.items.get(idx).cloned(), Some(idx)),
_ => (self.items.first().cloned(), None),
};
let item = match item {
Some(item) => item,
None => return Ok(None),
};
let remaining = match resolved_index {
Some(idx) => len.saturating_sub(idx + 1),
None => len,
};
Ok(Some((item, remaining)))
}
fn dequeue_next(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
if self.items.is_empty() {
return Ok(None);
}
let len = self.items.len();
let next_index = match self.current_index {
None => 0,
Some(idx) => {
let candidate = idx + 1;
if candidate >= len {
return Ok(None);
}
candidate
}
};
let Some(item) = self.items.get(next_index).cloned() else {
return Ok(None);
};
let remaining = len.saturating_sub(next_index + 1);
self.current_index = Some(next_index);
Ok(Some((item, remaining)))
}
fn append_or_init_index(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
let was_empty = self.items.is_empty();
self.items.extend(items);
if was_empty && !self.items.is_empty() {
self.current_index = Some(0);
}
Ok(())
}
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
if index < self.items.len() {
self.items[index] = item;
Ok(())
} else {
Err(ControlPointError::QueueError(format!(
"Index out of bound {} >= {}",
index,
self.items.len()
)))
}
Ok(())
}
}
impl QueueFromRendererInfo for InternalQueue {
fn from_renderer_info(renderer: &RendererInfo) -> Result<Self, ControlPointError> {
InternalQueue::from_renderer_info(renderer)
}
fn to_backend(self) -> MusicQueue {
MusicQueue::Internal(self)
}
}

View File

@@ -1,7 +1,45 @@
mod music_queue;
mod openhome;
mod backend;
mod snapshot;
mod openhome;
mod interne;
pub use music_queue::MusicQueue;
pub use backend::{PlaybackItem, QueueBackend, QueueSnapshot, EnqueueMode};
use std::sync::{Arc, Mutex};
pub use music_queue::MusicQueue;
pub use backend::{QueueBackend, EnqueueMode};
pub use snapshot::{PlaybackItem, QueueSnapshot};
// Internal queue implementations - not part of the public API
pub(crate) use openhome::OpenHomeQueue;
pub(crate) use interne::InternalQueue;
use crate::{RendererInfo, errors::ControlPointError};
pub trait QueueFromRendererInfo {
fn from_renderer_info(renderer: &RendererInfo) -> Result<Self, ControlPointError>
where
Self: Sized;
fn to_backend(self) -> MusicQueue;
fn build_from_renderer_info(
renderer: &RendererInfo,
) -> Result<MusicQueue, ControlPointError>
where
Self: Sized,
{
let instance = Self::from_renderer_info(renderer)?;
Ok(instance.to_backend())
}
fn make_from_renderer_info(
renderer: &RendererInfo,
) -> Result<Arc<Mutex<MusicQueue>>, ControlPointError>
where
Self: Sized,
{
let backend = Self::build_from_renderer_info(renderer)?;
Ok(Arc::new(Mutex::new(backend)))
}
}

View File

@@ -1,9 +1,8 @@
use crate::RendererInfo;
use crate::control_point::openhome_queue::OpenHomeQueue;
use crate::errors::ControlPointError;
use crate::openhome_playlist::OpenHomePlaylistSnapshot;
use crate::queue::backend::{PlaybackItem, QueueBackend, QueueSnapshot};
use crate::queue::interne::InternalQueue;
use crate::queue::{
EnqueueMode, InternalQueue, OpenHomeQueue, QueueBackend, QueueFromRendererInfo,
};
use crate::{PlaybackItem, QueueSnapshot, RendererInfo};
#[derive(Debug, Clone)]
pub enum MusicQueue {
@@ -12,39 +11,65 @@ pub enum MusicQueue {
}
impl MusicQueue {
/// Creates a queue appropriate for the given renderer.
/// This is the factory method used by QueueFromRendererInfo trait.
pub fn from_renderer_info(info: &RendererInfo) -> Result<MusicQueue, ControlPointError> {
if info.capabilities().has_oh_playlist() {
Ok(MusicQueue::OpenHome(OpenHomeQueue::from_renderer_info(info)?))
Ok(MusicQueue::OpenHome(OpenHomeQueue::from_renderer_info(
info,
)?))
} else {
Ok(MusicQueue::Internal(InternalQueue::from_renderer_info(info)?))
}
}
pub fn openhome_playlist_snapshot(&self) -> Result<OpenHomePlaylistSnapshot, ControlPointError> {
match self {
MusicQueue::OpenHome(queue) => queue.openhome_playlist_snapshot(),
_ => Err(ControlPointError::QueueError(format!(
"OpenHome playlist snapshot is only available for OpenHome queues"
))),
}
}
pub fn replace_with_attached_playlist(
&mut self,
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
match self {
MusicQueue::OpenHome(queue) => queue.replace_entire_playlist(items, current_index),
MusicQueue::Internal(queue) => queue.replace_queue(items, current_index),
Ok(MusicQueue::Internal(InternalQueue::from_renderer_info(
info,
)?))
}
}
}
impl QueueBackend for MusicQueue {
// Primitives
fn len(&self) -> Result<usize, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.len(),
MusicQueue::OpenHome(q) => q.len(),
}
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.track_ids(),
MusicQueue::OpenHome(q) => q.track_ids(),
}
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.id_to_position(id),
MusicQueue::OpenHome(q) => q.id_to_position(id),
}
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.position_to_id(id),
MusicQueue::OpenHome(q) => q.position_to_id(id),
}
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.current_track(),
MusicQueue::OpenHome(q) => q.current_track(),
}
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.current_index(),
MusicQueue::OpenHome(q) => q.current_index(),
}
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.queue_snapshot(),
@@ -70,6 +95,13 @@ impl QueueBackend for MusicQueue {
}
}
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
match self {
MusicQueue::Internal(q) => q.sync_queue(items),
MusicQueue::OpenHome(q) => q.sync_queue(items),
}
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.get_item(index),
@@ -83,4 +115,75 @@ impl QueueBackend for MusicQueue {
MusicQueue::OpenHome(q) => q.replace_item(index, item),
}
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
match self {
MusicQueue::Internal(q) => q.enqueue_items(items, mode),
MusicQueue::OpenHome(q) => q.enqueue_items(items, mode),
}
}
// Optimized helpers
fn clear_queue(&mut self) -> Result<(), ControlPointError> {
match self {
MusicQueue::Internal(q) => q.clear_queue(),
MusicQueue::OpenHome(q) => q.clear_queue(),
}
}
fn is_empty(&self) -> Result<bool, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.is_empty(),
MusicQueue::OpenHome(q) => q.is_empty(),
}
}
fn upcoming_len(&self) -> Result<usize, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.upcoming_len(),
MusicQueue::OpenHome(q) => q.upcoming_len(),
}
}
fn upcoming_items(&self) -> Result<Vec<PlaybackItem>, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.upcoming_items(),
MusicQueue::OpenHome(q) => q.upcoming_items(),
}
}
fn peek_current(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.peek_current(),
MusicQueue::OpenHome(q) => q.peek_current(),
}
}
fn dequeue_next(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
match self {
MusicQueue::Internal(q) => q.dequeue_next(),
MusicQueue::OpenHome(q) => q.dequeue_next(),
}
}
fn append_or_init_index(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
match self {
MusicQueue::Internal(q) => q.append_or_init_index(items),
MusicQueue::OpenHome(q) => q.append_or_init_index(items),
}
}
}
impl QueueFromRendererInfo for MusicQueue {
fn from_renderer_info(renderer: &RendererInfo) -> Result<Self, ControlPointError> {
MusicQueue::from_renderer_info(renderer)
}
fn to_backend(self) -> MusicQueue {
self
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
use crate::{DeviceId, model::TrackMetadata};
/// Canonical representation of a track in a renderer queue.
///
/// This type is the bridge between:
/// - the UPnP MediaServer (DIDL-Lite items),
/// - the ControlPoint runtime,
/// - and the different queue backends (internal / OpenHome).
///
/// It is intentionally DIDL-centric: every item in a queue comes from
/// a UPnP ContentDirectory and carries its MediaServer identity.
#[derive(Clone, Debug)]
pub struct PlaybackItem {
/// Identifier of the UPnP MediaServer that owns this content.
///
/// Typically this is the UDN of the MediaServer device, or an
/// equivalent logical identifier.
pub media_server_id: DeviceId,
/// Internal ID of the item in the queue.
/// This ID only has meaning when returning a snapshot.
/// In an internal queue, it can have any value; the position in the vector is what matters.
/// In principle, it can be set to usize::MAX.
pub backend_id: usize,
/// DIDL-Lite `id` attribute of the `item` in the ContentDirectory.
///
/// This, combined with `media_server_id`, is the logical identity
/// of the track across refreshes of the MediaServer state.
pub didl_id: String,
/// Main resource URI to be used for playback.
///
/// This is usually the first `<res>` element (or a selected one)
/// from the DIDL-Lite item.
pub uri: String,
/// UPnP protocolInfo string for the resource (e.g., "http-get:*:audio/flac:*").
///
/// This string describes the protocol, network, MIME type, and additional
/// info about the media resource. It's required for proper UPnP/OpenHome
/// renderer compatibility.
pub protocol_info: String,
/// Optional rich metadata for the track (title, artist, album, cover,
/// duration, …).
///
/// The exact structure is defined in `TrackMetadata` and may
/// aggregate information from DIDL, tags, or additional sources.
pub metadata: Option<TrackMetadata>,
}
impl PlaybackItem {
/// Returns a stable, backend-agnostic logical identifier for this item.
///
/// By default this is the concatenation of the MediaServer identifier
/// and the DIDL `id`. Backends and higher-level logic should use this
/// when they need to match items across queue rebuilds.
pub fn unique_id(&self) -> String {
// ADAPTE si MediaServerId n'implémente pas Display : utilise
// un champ string interne ou une méthode as_str().
format!("{}::{}", self.media_server_id.0, self.didl_id)
}
}
/// Logical snapshot of a renderer queue.
///
/// This is the canonical view used by the ControlPoint and the REST/API
/// layer. It is independent of how the queue is actually stored (local
/// in-memory queue, OpenHome playlist, …).
#[derive(Clone, Debug)]
pub struct QueueSnapshot {
/// All items currently in the queue, in play order.
pub items: Vec<PlaybackItem>,
/// Index (0-based) of the current item in `items`, or `None` if
/// no item is currently selected.
pub current_index: Option<usize>,
}
impl QueueSnapshot {
/// Returns the number of items in the snapshot.
pub fn len(&self) -> usize {
self.items.len()
}
/// Returns `true` if the snapshot contains no items.
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}