Remise au propre des abstractions de PMOcontrol.

This commit is contained in:
2025-12-27 22:15:10 +01:00
parent 0abe66f7fb
commit 0cbd0c9b30
3 changed files with 458 additions and 314 deletions

View File

@@ -52,7 +52,7 @@ use crate::openhome_client::parse_track_metadata_from_didl;
use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
use crate::openhome_renderer::{format_seconds, map_openhome_state};
use crate::provider::HttpXmlDescriptionProvider;
use crate::queue_backend::{EnqueueMode, PlaybackItem, QueueBackend};
use crate::queue_backend::{EnqueueMode, PlaybackItem, QueueBackend, QueueSnapshot};
use crate::queue_interne::InternalQueue;
use crate::registry::{DeviceRegistry, DeviceRegistryRead, DeviceUpdate};
use crate::upnp_renderer::UpnpRenderer;
@@ -941,17 +941,15 @@ impl ControlPoint {
// User-driven mutation: detach any playlist binding
self.detach_playlist_binding(renderer_id, "clear_queue");
if self.runtime.uses_openhome_playlist(renderer_id) {
let renderer = self.openhome_renderer(renderer_id)?;
renderer.openhome_playlist_clear()?;
self.sync_openhome_playlist_for(renderer_id)?;
debug!(
renderer = renderer_id.0.as_str(),
"Cleared OpenHome playlist"
);
return Ok(());
}
// Clear the queue on the backend (backend-agnostic)
let renderer = self.music_renderer_by_id(renderer_id)
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
renderer.clear_queue()?;
// Sync backend state to local cache (for OpenHome, this updates the cache)
renderer.sync_queue_state()?;
// Clear the local queue state
let removed = self.runtime.with_music_queue_mut(renderer_id, |queue| {
let removed = queue.upcoming_len()?;
queue.clear_queue()?;
@@ -998,11 +996,7 @@ impl ControlPoint {
// User-driven mutation: detach any playlist binding
self.detach_playlist_binding(renderer_id, "enqueue_items");
if self.runtime.uses_openhome_playlist(renderer_id) {
self.enqueue_items_openhome(renderer_id, items)?;
return Ok(());
}
// Enqueue items using QueueBackend abstraction (works for both backends)
let item_count = items.len();
let new_len = self.runtime.with_music_queue_mut(renderer_id, |queue| {
queue.enqueue_items(items, EnqueueMode::AppendToEnd)?;
@@ -1088,6 +1082,29 @@ impl ControlPoint {
self.sync_openhome_playlist_for(renderer_id)
}
/// Gets the backend queue snapshot for renderers with persistent queues.
///
/// Returns the queue snapshot if the renderer has a backend queue (e.g., OpenHome),
/// or None if it doesn't (e.g., AVTransport).
pub fn get_renderer_queue_snapshot(
&self,
renderer_id: &RendererId,
) -> anyhow::Result<Option<QueueSnapshot>> {
let renderer = self.music_renderer_by_id(renderer_id)
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
renderer.queue_snapshot()
}
/// Gets the length of the backend queue for renderers with persistent queues.
///
/// Returns the queue length if the renderer has a backend queue, or 0 if it doesn't.
pub fn get_renderer_queue_length(&self, renderer_id: &RendererId) -> anyhow::Result<usize> {
let snapshot = self.get_renderer_queue_snapshot(renderer_id)?;
Ok(snapshot.map(|s| s.len()).unwrap_or(0))
}
// Deprecated: Use get_renderer_queue_snapshot instead
#[deprecated(since = "0.1.0", note = "Use get_renderer_queue_snapshot instead")]
pub fn get_openhome_playlist_snapshot(
&self,
renderer_id: &RendererId,
@@ -1105,6 +1122,8 @@ impl ControlPoint {
self.runtime.openhome_snapshot_cached(&renderer, ttl)
}
// Deprecated: Use get_renderer_queue_length instead
#[deprecated(since = "0.1.0", note = "Use get_renderer_queue_length instead")]
pub fn get_openhome_playlist_len(&self, renderer_id: &RendererId) -> anyhow::Result<usize> {
let renderer = self.openhome_renderer(renderer_id)?;
renderer.openhome_playlist_len()
@@ -1121,123 +1140,31 @@ impl ControlPoint {
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
let info = renderer.info();
// MICRO-PATCH 5: Chemin OpenHome complètement découplé du miroir local
if self.runtime.uses_openhome_playlist(renderer_id) {
// Pour OpenHome: récupérer UNIQUEMENT runtime_snapshot pour volume/state/position
// Ne PAS utiliser queue_items/current_index du runtime (miroir local)
let (runtime_snapshot, _, _) = self.runtime.renderer_snapshot_bundle(renderer_id);
// Get runtime snapshot (needed for all backends: volume, state, position)
let (runtime_snapshot, runtime_queue_items, runtime_current_index) =
self.runtime.renderer_snapshot_bundle(renderer_id);
// OpenHome est la source de vérité pour la queue - pas de fallback au runtime
let snapshot = self.get_cached_openhome_playlist_snapshot(
renderer_id,
OPENHOME_SNAPSHOT_CACHE_TTL,
)?;
let queue_items: Vec<PlaybackItem> = snapshot
.tracks
.iter()
.map(|track| playback_item_from_openhome_track(renderer_id, track))
.collect();
let queue_len = snapshot.tracks.len();
// Pour OpenHome: current_index vient UNIQUEMENT d'OpenHome, pas d'heuristiques runtime
let queue_current_index = snapshot.current_index.or_else(|| {
snapshot.current_id.and_then(|id| {
snapshot.tracks.iter().position(|track| track.id == id)
})
});
// Try to get queue from backend (returns Some for OpenHome, None for others)
let backend_queue = renderer.queue_snapshot()?;
// Use backend queue if available (OpenHome), otherwise use runtime queue (AVTransport)
let (queue_items, mut queue_current_index, from_backend) = if let Some(snapshot) = backend_queue {
debug!(
renderer = renderer_id.0.as_str(),
current_id = ?snapshot.current_id,
current_index = ?queue_current_index,
track_count = snapshot.tracks.len(),
"renderer_full_snapshot: OpenHome snapshot retrieved"
current_index = ?snapshot.current_index,
track_count = snapshot.items.len(),
"renderer_full_snapshot: using backend queue (OpenHome)"
);
(snapshot.items, snapshot.current_index, true)
} else {
(runtime_queue_items, runtime_current_index, false)
};
let queue_view_items: Vec<QueueItem> = queue_items
.iter()
.enumerate()
.map(|(index, item)| QueueItem {
index,
uri: item.uri.clone(),
title: item.metadata.as_ref().and_then(|m| m.title.clone()),
artist: item.metadata.as_ref().and_then(|m| m.artist.clone()),
album: item.metadata.as_ref().and_then(|m| m.album.clone()),
album_art_uri: item.metadata.as_ref().and_then(|m| m.album_art_uri.clone()),
server_id: Some(item.media_server_id.0.clone()),
object_id: Some(item.didl_id.clone()),
})
.collect();
let queue_view = QueueSnapshotView {
renderer_id: renderer_id.0.clone(),
items: queue_view_items,
current_index: queue_current_index,
};
let binding = self.current_queue_playlist_binding(renderer_id).map(
|(server_id, container_id, has_seen_update)| RendererBindingView {
server_id: server_id.0,
container_id,
has_seen_update,
},
);
let (position_ms, duration_ms) =
convert_runtime_position(runtime_snapshot.position.as_ref());
let queue_current_metadata = queue_current_index
.and_then(|idx| queue_items.get(idx))
.map(current_track_from_playback_item);
// MICRO-PATCH 5: Pour OpenHome, préférer les métadonnées depuis le snapshot OpenHome
// car runtime_snapshot.last_metadata n'est jamais mis à jour pour OpenHome
let current_track = queue_current_metadata.or_else(|| {
runtime_snapshot
.last_metadata
.as_ref()
.map(|meta| CurrentTrackMetadata {
title: meta.title.clone(),
artist: meta.artist.clone(),
album: meta.album.clone(),
album_art_uri: meta.album_art_uri.clone(),
})
});
let state_view = RendererStateView {
id: renderer_id.0.clone(),
friendly_name: info.friendly_name.clone(),
transport_state: runtime_snapshot
.state
.as_ref()
.map(|state| state.as_str().to_string())
.unwrap_or_else(|| "UNKNOWN".to_string()),
position_ms,
duration_ms,
volume: runtime_snapshot
.last_volume
.and_then(|value| u8::try_from(value).ok()),
mute: runtime_snapshot.last_mute,
queue_len,
attached_playlist: binding.clone(),
current_track,
};
return Ok(FullRendererSnapshot {
state: state_view,
queue: queue_view,
binding,
});
}
// Chemin non-OpenHome (UPnP AV): continue d'utiliser le runtime
let (runtime_snapshot, queue_items, mut queue_current_index) =
self.runtime.renderer_snapshot_bundle(renderer_id);
let playback_source = self.runtime.playback_source(renderer_id);
let queue_len = queue_items.len();
if queue_current_index.is_none() {
// For non-backend queues (AVTransport), try heuristics to determine current_index
if !from_backend && queue_current_index.is_none() {
if let Some(position) = runtime_snapshot.position.as_ref() {
if let Some(uri) = position.track_uri.as_ref() {
if let Some(idx) = queue_items.iter().position(|item| item.uri == *uri) {
@@ -1250,18 +1177,19 @@ impl ControlPoint {
}
}
}
}
if queue_current_index.is_none()
&& matches!(playback_source, PlaybackSource::FromQueue)
&& runtime_snapshot
.state
.as_ref()
.map(|state| matches!(state, PlaybackState::Playing | PlaybackState::Paused))
.unwrap_or(false)
&& !queue_items.is_empty()
{
queue_current_index = Some(0);
// Final fallback: if playing from queue and no index, assume first track
if queue_current_index.is_none()
&& matches!(playback_source, PlaybackSource::FromQueue)
&& runtime_snapshot
.state
.as_ref()
.map(|state| matches!(state, PlaybackState::Playing | PlaybackState::Paused))
.unwrap_or(false)
&& !queue_items.is_empty()
{
queue_current_index = Some(0);
}
}
let queue_view_items: Vec<QueueItem> = queue_items
@@ -1299,16 +1227,32 @@ impl ControlPoint {
.and_then(|idx| queue_items.get(idx))
.map(current_track_from_playback_item);
let current_track = runtime_snapshot
.last_metadata
.as_ref()
.map(|meta| CurrentTrackMetadata {
title: meta.title.clone(),
artist: meta.artist.clone(),
album: meta.album.clone(),
album_art_uri: meta.album_art_uri.clone(),
// For backend queues (OpenHome), prefer queue metadata since runtime isn't updated
// For runtime queues (AVTransport), prefer runtime metadata which is fresher
let current_track = if from_backend {
queue_current_metadata.or_else(|| {
runtime_snapshot
.last_metadata
.as_ref()
.map(|meta| CurrentTrackMetadata {
title: meta.title.clone(),
artist: meta.artist.clone(),
album: meta.album.clone(),
album_art_uri: meta.album_art_uri.clone(),
})
})
.or(queue_current_metadata);
} else {
runtime_snapshot
.last_metadata
.as_ref()
.map(|meta| CurrentTrackMetadata {
title: meta.title.clone(),
artist: meta.artist.clone(),
album: meta.album.clone(),
album_art_uri: meta.album_art_uri.clone(),
})
.or(queue_current_metadata)
};
let state_view = RendererStateView {
id: renderer_id.0.clone(),
@@ -1336,12 +1280,63 @@ impl ControlPoint {
})
}
pub fn clear_openhome_playlist(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
let renderer = self.openhome_renderer(renderer_id)?;
renderer.openhome_playlist_clear()?;
self.sync_openhome_playlist_for(renderer_id)
/// Clears the renderer's backend queue.
///
/// For renderers with persistent queues (OpenHome), this clears the queue on the renderer.
/// For other renderers, this returns an error.
pub fn clear_renderer_queue(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
let renderer = self.music_renderer_by_id(renderer_id)
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
renderer.clear_queue()?;
renderer.sync_queue_state()
}
/// Adds a track to the renderer's backend queue.
///
/// For renderers with persistent queues (OpenHome), this adds the track to the queue.
/// For other renderers, this returns an error.
///
/// Returns the backend-specific track ID if applicable.
pub fn add_track_to_renderer(
&self,
renderer_id: &RendererId,
uri: &str,
metadata: &str,
after_id: Option<u32>,
play: bool,
) -> anyhow::Result<Option<u32>> {
let renderer = self.music_renderer_by_id(renderer_id)
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
let track_id = renderer.add_track_to_queue(uri, metadata, after_id, play)?;
renderer.sync_queue_state()?;
Ok(track_id)
}
/// Selects and plays a specific track from the renderer's backend queue.
///
/// For renderers with persistent queues (OpenHome), this uses the track ID.
/// For other renderers, this returns an error.
pub fn select_renderer_track(
&self,
renderer_id: &RendererId,
track_id: u32,
) -> anyhow::Result<()> {
let renderer = self.music_renderer_by_id(renderer_id)
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
renderer.select_queue_track(track_id)?;
self.runtime
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
renderer.sync_queue_state()
}
// Deprecated: Use clear_renderer_queue instead
#[deprecated(since = "0.1.0", note = "Use clear_renderer_queue instead")]
pub fn clear_openhome_playlist(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
self.clear_renderer_queue(renderer_id)
}
// Deprecated: Use add_track_to_renderer instead
#[deprecated(since = "0.1.0", note = "Use add_track_to_renderer instead")]
pub fn add_openhome_track(
&self,
renderer_id: &RendererId,
@@ -1350,21 +1345,18 @@ impl ControlPoint {
after_id: Option<u32>,
play: bool,
) -> anyhow::Result<()> {
let renderer = self.openhome_renderer(renderer_id)?;
renderer.openhome_playlist_add_track(uri, metadata, after_id, play)?;
self.sync_openhome_playlist_for(renderer_id)
self.add_track_to_renderer(renderer_id, uri, metadata, after_id, play)?;
Ok(())
}
// Deprecated: Use select_renderer_track instead
#[deprecated(since = "0.1.0", note = "Use select_renderer_track instead")]
pub fn play_openhome_track_id(
&self,
renderer_id: &RendererId,
track_id: u32,
) -> anyhow::Result<()> {
let renderer = self.openhome_renderer(renderer_id)?;
renderer.openhome_playlist_play_id(track_id)?;
self.runtime
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
self.sync_openhome_playlist_for(renderer_id)
self.select_renderer_track(renderer_id, track_id)
}
/// Plays the current queue item without advancing the index.
@@ -1383,10 +1375,18 @@ impl ControlPoint {
return Err(err);
}
if self.runtime.uses_openhome_playlist(renderer_id) {
return self.play_current_openhome(renderer_id);
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
anyhow!("Renderer {} not found", renderer_id.0)
})?;
// If renderer has a backend queue (OpenHome), use backend playback
if renderer.queue_snapshot()?.is_some() {
renderer.play_current_from_backend_queue()?;
self.runtime.set_playback_source(renderer_id, PlaybackSource::FromQueue);
return Ok(());
}
// Otherwise use local queue playback (AVTransport)
let Some((item, remaining)) = self.runtime.peek_current(renderer_id) else {
debug!(
renderer = renderer_id.0.as_str(),
@@ -1404,14 +1404,6 @@ impl ControlPoint {
"Playing current playback item from queue"
);
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
warn!(
renderer = renderer_id.0.as_str(),
"Renderer disappeared before queue playback could start"
);
anyhow!("Renderer {} not found", renderer_id.0)
})?;
let playback = (|| -> anyhow::Result<()> {
let didl_metadata = playback_item_to_didl(&item);
renderer.play_uri(&item.uri, &didl_metadata)?;
@@ -1464,8 +1456,14 @@ impl ControlPoint {
return Err(err);
}
if self.runtime.uses_openhome_playlist(renderer_id) {
self.play_next_openhome(renderer_id)?;
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
anyhow!("Renderer {} not found", renderer_id.0)
})?;
// If renderer has a backend queue (OpenHome), use backend playback
if renderer.queue_snapshot()?.is_some() {
renderer.play_next_from_backend_queue()?;
self.runtime.set_playback_source(renderer_id, PlaybackSource::FromQueue);
return Ok(());
}
@@ -1586,8 +1584,12 @@ impl ControlPoint {
return Err(err);
}
// For OpenHome, use select_track_index which handles the track_id lookup
if self.runtime.uses_openhome_playlist(renderer_id) {
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
anyhow!("Renderer {} not found", renderer_id.0)
})?;
// For backend queues (OpenHome), use select_track_index which handles the track_id lookup
if renderer.queue_snapshot()?.is_some() {
info!(
renderer = renderer_id.0.as_str(),
index,
@@ -1722,7 +1724,13 @@ impl ControlPoint {
return Ok(());
}
if self.runtime.uses_openhome_playlist(renderer_id) {
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
anyhow!("Renderer {} not found", renderer_id.0)
})?;
// For backend queues (OpenHome), play current (renderer tracks its own position)
// For local queues (AVTransport), dequeue next (we manage position locally)
if renderer.queue_snapshot()?.is_some() {
self.play_current_from_queue(renderer_id)
} else {
self.play_next_from_queue(renderer_id)
@@ -1828,10 +1836,8 @@ impl ControlPoint {
})?;
renderer.clear_for_playlist_attach()?;
// For OpenHome, also sync the local cache to reflect the empty state
if self.runtime.uses_openhome_playlist(renderer_id) {
self.sync_openhome_playlist_for(renderer_id)?;
}
// Sync backend state to local cache (backend-agnostic)
renderer.sync_queue_state()?;
// Clear the local queue (detach binding + clear runtime queue structure)
self.detach_playlist_binding(renderer_id, "attach_new_playlist");
@@ -2042,141 +2048,6 @@ impl ControlPoint {
Ok(())
}
fn play_current_openhome(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
let renderer = self.openhome_renderer(renderer_id)?;
// MICRO-PATCH 5: Pour OpenHome, récupérer les données directement depuis la playlist native
// au lieu du miroir local (qui peut être vide ou obsolète)
match self.get_cached_openhome_playlist_snapshot(
renderer_id,
OPENHOME_SNAPSHOT_CACHE_TTL,
) {
Ok(snapshot) => {
if snapshot.tracks.is_empty() {
debug!(
renderer = renderer_id.0.as_str(),
"OpenHome playlist is empty"
);
self.runtime
.set_playback_source(renderer_id, PlaybackSource::None);
return Ok(());
}
// Trouver le track_id courant depuis le snapshot OpenHome
let target_track_id = if let Some(current_id) = snapshot.current_id {
Some(current_id)
} else if let Some(current_idx) = snapshot.current_index {
snapshot.tracks.get(current_idx).map(|track| track.id)
} else {
snapshot.tracks.first().map(|track| track.id)
};
if let Some(track_id) = target_track_id {
match renderer.openhome_playlist_play_id(track_id) {
Ok(()) => {
self.runtime
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
self.sync_openhome_playlist_for(renderer_id)?;
info!(
renderer = renderer_id.0.as_str(),
track_id,
playlist_len = snapshot.tracks.len(),
"Started OpenHome playlist playback (current item)"
);
return Ok(());
}
Err(err) => {
warn!(
renderer = renderer_id.0.as_str(),
track_id,
error = %err,
"PlayId failed, falling back to Play()"
);
}
}
}
// Fallback: appeler Play() sans spécifier de track_id
renderer.play()?;
self.runtime
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
self.sync_openhome_playlist_for(renderer_id)?;
info!(
renderer = renderer_id.0.as_str(),
playlist_len = snapshot.tracks.len(),
"Started OpenHome native playlist playback"
);
return Ok(());
}
Err(err) => {
warn!(
renderer = renderer_id.0.as_str(),
error = %err,
"Failed to fetch OpenHome playlist snapshot, playlist might be empty"
);
debug!(
renderer = renderer_id.0.as_str(),
"OpenHome playlist is empty or unavailable"
);
self.runtime
.set_playback_source(renderer_id, PlaybackSource::None);
return Ok(());
}
}
}
fn play_next_openhome(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
let renderer = self.openhome_renderer(renderer_id)?;
// MICRO-PATCH 5: Récupérer les données directement depuis OpenHome au lieu du miroir local
let snapshot = self.get_cached_openhome_playlist_snapshot(
renderer_id,
OPENHOME_SNAPSHOT_CACHE_TTL,
)?;
if snapshot.tracks.is_empty() {
debug!(
renderer = renderer_id.0.as_str(),
"OpenHome playlist is empty, cannot play next"
);
self.runtime
.set_playback_source(renderer_id, PlaybackSource::None);
return Ok(());
}
// Déterminer le prochain track_id
let next_track_id = match snapshot.current_index {
Some(idx) => {
// Prendre la piste suivante si elle existe
snapshot
.tracks
.get(idx + 1)
.map(|track| track.id)
.or_else(|| snapshot.tracks.first().map(|track| track.id))
}
None => snapshot.tracks.first().map(|track| track.id),
};
let Some(track_id) = next_track_id else {
debug!(
renderer = renderer_id.0.as_str(),
"No OpenHome track available to advance to"
);
self.runtime
.set_playback_source(renderer_id, PlaybackSource::None);
return Ok(());
};
renderer.openhome_playlist_play_id(track_id)?;
self.runtime
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
self.sync_openhome_playlist_for(renderer_id)?;
info!(
renderer = renderer_id.0.as_str(),
track_id, "Advanced OpenHome playlist to next track"
);
Ok(())
}
}
#[cfg(feature = "pmoserver")]

View File

@@ -10,7 +10,7 @@ use crate::openhome_client::{
OPENHOME_PLAYLIST_HEAD_ID,
};
use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
use crate::queue_backend::{PlaybackItem, QueueBackend, QueueSnapshot};
use crate::queue_backend::{EnqueueMode, PlaybackItem, QueueBackend, QueueSnapshot};
/// Local mirror of an OpenHome playlist for a single renderer.
#[derive(Clone, Debug)]
@@ -837,4 +837,43 @@ impl QueueBackend for OpenHomeQueue {
self.track_ids[index] = new_id;
Ok(())
}
/// Override enqueue_items to add items directly to the OpenHome playlist.
fn enqueue_items(&mut self, items: Vec<PlaybackItem>, mode: EnqueueMode) -> Result<()> {
if items.is_empty() {
return Ok(());
}
match mode {
EnqueueMode::AppendToEnd => {
// Append to the end of the OpenHome playlist
let mut after_id = self.track_ids.last().copied();
for item in items {
after_id = Some(self.add_playback_item(item, after_id, false)?);
}
}
EnqueueMode::InsertAfterCurrent => {
// Insert after the current playing track
let after_id = if let Some(idx) = self.current_index {
self.track_ids.get(idx).copied()
} else {
None
};
let mut next_after_id = after_id;
for item in items {
next_after_id = Some(self.add_playback_item(item, next_after_id, false)?);
}
}
EnqueueMode::ReplaceAll => {
// Replace the entire playlist
self.replace_queue(items, None)?;
}
}
// Refresh local cache from OpenHome after modification
self.refresh_from_openhome()?;
Ok(())
}
}

View File

@@ -13,10 +13,10 @@ use crate::control_point::RendererRuntimeStateMut;
use crate::control_point::music_queue::MusicQueue;
use crate::control_point::openhome_queue::didl_id_from_metadata;
use crate::media_server::ServerId;
use crate::model::{RendererId, RendererInfo, RendererProtocol};
use crate::model::{RendererId, RendererInfo, RendererProtocol, TrackMetadata};
use crate::openhome_client::parse_track_metadata_from_didl;
use crate::openhome_playlist::OpenHomePlaylistSnapshot;
use crate::queue_backend::PlaybackItem;
use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
use crate::queue_backend::{PlaybackItem, QueueSnapshot};
use crate::{
ArylicTcpRenderer, ChromecastRenderer, DeviceRegistry, LinkPlayRenderer, OpenHomeRenderer,
PlaybackPosition, PlaybackState, TransportControl, UpnpRenderer, VolumeControl,
@@ -304,6 +304,240 @@ impl MusicRenderer {
}
}
/// Synchronize the local queue state with the backend's actual state.
///
/// - For OpenHome: fetches the playlist from the renderer and updates local cache
/// - For Internal queue/AVTransport: no-op (queue is already local)
pub fn sync_queue_state(&self) -> Result<()> {
match self {
MusicRenderer::OpenHome(_) => {
if let Some(provider) = OPENHOME_QUEUE_PROVIDER.get() {
// Fetch fresh playlist snapshot and update cache
provider.invalidate_openhome_cache(self.id())?;
}
Ok(())
}
MusicRenderer::Upnp(_)
| MusicRenderer::Chromecast(_)
| MusicRenderer::LinkPlay(_)
| MusicRenderer::ArylicTcp(_)
| MusicRenderer::HybridUpnpArylic { .. } => {
// No sync needed - queue is local only
Ok(())
}
}
}
/// Clear the queue on both the backend and in local state.
///
/// This ensures the backend renderer and local cache are consistent.
/// Should be called before queue mutations to ensure clean state.
pub fn clear_queue(&self) -> Result<()> {
match self {
MusicRenderer::OpenHome(_) => {
// For OpenHome: clear the playlist on the renderer itself
self.openhome_playlist_clear()
}
MusicRenderer::Upnp(_)
| MusicRenderer::Chromecast(_)
| MusicRenderer::LinkPlay(_)
| MusicRenderer::ArylicTcp(_)
| MusicRenderer::HybridUpnpArylic { .. } => {
// For other renderers: no persistent queue to clear
Ok(())
}
}
}
/// Add a track to the backend's queue, returning backend-specific track ID if applicable.
///
/// - For OpenHome: adds to OpenHome playlist and returns track ID
/// - For others: returns error (not supported for single-track renderers)
pub fn add_track_to_queue(
&self,
uri: &str,
metadata: &str,
after_id: Option<u32>,
play: bool,
) -> Result<Option<u32>> {
match self {
MusicRenderer::OpenHome(_) => {
let track_id = self.openhome_playlist_add_track(uri, metadata, after_id, play)?;
Ok(Some(track_id))
}
MusicRenderer::Upnp(_)
| MusicRenderer::Chromecast(_)
| MusicRenderer::LinkPlay(_)
| MusicRenderer::ArylicTcp(_)
| MusicRenderer::HybridUpnpArylic { .. } => Err(anyhow!(
"add_track_to_queue is not supported for {} backend",
self.unsupported_backend_name()
)),
}
}
/// Select and play a specific track from the backend's queue.
///
/// - For OpenHome: uses track ID to select from OpenHome playlist
/// - For others: returns error (use play_uri instead)
pub fn select_queue_track(&self, track_id: u32) -> Result<()> {
match self {
MusicRenderer::OpenHome(_) => self.openhome_playlist_play_id(track_id),
MusicRenderer::Upnp(_)
| MusicRenderer::Chromecast(_)
| MusicRenderer::LinkPlay(_)
| MusicRenderer::ArylicTcp(_)
| MusicRenderer::HybridUpnpArylic { .. } => Err(anyhow!(
"select_queue_track is not supported for {} backend",
self.unsupported_backend_name()
)),
}
}
/// Get the current queue state from the backend.
///
/// - For OpenHome: fetches current playlist snapshot
/// - For others: returns None (no persistent queue on backend)
pub fn queue_snapshot(&self) -> Result<Option<QueueSnapshot>> {
match self {
MusicRenderer::OpenHome(_) => {
let oh_snapshot = self.fetch_openhome_playlist_snapshot()?;
// Convert OpenHome tracks to PlaybackItems
let items: Vec<PlaybackItem> = oh_snapshot.tracks.iter().map(|track| {
Self::playback_item_from_openhome_track(self.id(), track)
}).collect();
let snapshot = QueueSnapshot {
items,
current_index: oh_snapshot.current_index,
};
Ok(Some(snapshot))
}
MusicRenderer::Upnp(_)
| MusicRenderer::Chromecast(_)
| MusicRenderer::LinkPlay(_)
| MusicRenderer::ArylicTcp(_)
| MusicRenderer::HybridUpnpArylic { .. } => {
// No backend queue for these renderers
Ok(None)
}
}
}
/// Play the current item from the backend queue.
///
/// - For OpenHome: Uses the native playlist to play the current track
/// - For others: Returns error (no backend queue)
pub fn play_current_from_backend_queue(&self) -> Result<()> {
match self {
MusicRenderer::OpenHome(_) => {
// Get the current OpenHome playlist snapshot
let snapshot = self.fetch_openhome_playlist_snapshot()?;
if snapshot.tracks.is_empty() {
return Err(anyhow!("OpenHome playlist is empty"));
}
// Find the track_id to play (prefer current_id, then current_index, then first)
let target_track_id = if let Some(current_id) = snapshot.current_id {
Some(current_id)
} else if let Some(current_idx) = snapshot.current_index {
snapshot.tracks.get(current_idx).map(|track| track.id)
} else {
snapshot.tracks.first().map(|track| track.id)
};
if let Some(track_id) = target_track_id {
self.openhome_playlist_play_id(track_id)?;
Ok(())
} else {
Err(anyhow!("No track to play in OpenHome playlist"))
}
}
MusicRenderer::Upnp(_)
| MusicRenderer::Chromecast(_)
| MusicRenderer::LinkPlay(_)
| MusicRenderer::ArylicTcp(_)
| MusicRenderer::HybridUpnpArylic { .. } => Err(anyhow!(
"play_current_from_backend_queue is not supported for {} backend (no persistent queue)",
self.unsupported_backend_name()
)),
}
}
/// Play the next item from the backend queue.
///
/// - For OpenHome: Advances to the next track in the playlist
/// - For others: Returns error (no backend queue)
pub fn play_next_from_backend_queue(&self) -> Result<()> {
match self {
MusicRenderer::OpenHome(_) => {
// Get the current OpenHome playlist snapshot
let snapshot = self.fetch_openhome_playlist_snapshot()?;
if snapshot.tracks.is_empty() {
return Err(anyhow!("OpenHome playlist is empty, cannot play next"));
}
// Determine the next track_id
let next_track_id = match snapshot.current_index {
Some(idx) => {
// Take the next track if it exists, otherwise loop to first
snapshot
.tracks
.get(idx + 1)
.map(|track| track.id)
.or_else(|| snapshot.tracks.first().map(|track| track.id))
}
None => snapshot.tracks.first().map(|track| track.id),
};
if let Some(track_id) = next_track_id {
self.openhome_playlist_play_id(track_id)?;
Ok(())
} else {
Err(anyhow!("No track available to advance to in OpenHome playlist"))
}
}
MusicRenderer::Upnp(_)
| MusicRenderer::Chromecast(_)
| MusicRenderer::LinkPlay(_)
| MusicRenderer::ArylicTcp(_)
| MusicRenderer::HybridUpnpArylic { .. } => Err(anyhow!(
"play_next_from_backend_queue is not supported for {} backend (no persistent queue)",
self.unsupported_backend_name()
)),
}
}
/// Convert an OpenHome playlist track to a PlaybackItem.
fn playback_item_from_openhome_track(
renderer_id: &RendererId,
track: &OpenHomePlaylistTrack,
) -> PlaybackItem {
let metadata = TrackMetadata {
title: track.title.clone(),
artist: track.artist.clone(),
album: track.album.clone(),
genre: None,
album_art_uri: track.album_art_uri.clone(),
date: None,
track_number: None,
creator: None,
};
PlaybackItem {
media_server_id: ServerId(format!("openhome:{}", renderer_id.0)),
didl_id: format!("openhome:{}", track.id),
uri: track.uri.clone(),
// OpenHome tracks don't provide protocolInfo, use generic default
protocol_info: "http-get:*:audio/*:*".to_string(),
metadata: Some(metadata),
}
}
pub fn openhome_playlist_add_track(
&self,
uri: &str,