Remise au propre des abstractions de PMOcontrol.
This commit is contained in:
@@ -52,7 +52,7 @@ use crate::openhome_client::parse_track_metadata_from_didl;
|
|||||||
use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
|
use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
|
||||||
use crate::openhome_renderer::{format_seconds, map_openhome_state};
|
use crate::openhome_renderer::{format_seconds, map_openhome_state};
|
||||||
use crate::provider::HttpXmlDescriptionProvider;
|
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::queue_interne::InternalQueue;
|
||||||
use crate::registry::{DeviceRegistry, DeviceRegistryRead, DeviceUpdate};
|
use crate::registry::{DeviceRegistry, DeviceRegistryRead, DeviceUpdate};
|
||||||
use crate::upnp_renderer::UpnpRenderer;
|
use crate::upnp_renderer::UpnpRenderer;
|
||||||
@@ -941,17 +941,15 @@ impl ControlPoint {
|
|||||||
// User-driven mutation: detach any playlist binding
|
// User-driven mutation: detach any playlist binding
|
||||||
self.detach_playlist_binding(renderer_id, "clear_queue");
|
self.detach_playlist_binding(renderer_id, "clear_queue");
|
||||||
|
|
||||||
if self.runtime.uses_openhome_playlist(renderer_id) {
|
// Clear the queue on the backend (backend-agnostic)
|
||||||
let renderer = self.openhome_renderer(renderer_id)?;
|
let renderer = self.music_renderer_by_id(renderer_id)
|
||||||
renderer.openhome_playlist_clear()?;
|
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
|
||||||
self.sync_openhome_playlist_for(renderer_id)?;
|
renderer.clear_queue()?;
|
||||||
debug!(
|
|
||||||
renderer = renderer_id.0.as_str(),
|
|
||||||
"Cleared OpenHome playlist"
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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 = self.runtime.with_music_queue_mut(renderer_id, |queue| {
|
||||||
let removed = queue.upcoming_len()?;
|
let removed = queue.upcoming_len()?;
|
||||||
queue.clear_queue()?;
|
queue.clear_queue()?;
|
||||||
@@ -998,11 +996,7 @@ impl ControlPoint {
|
|||||||
// User-driven mutation: detach any playlist binding
|
// User-driven mutation: detach any playlist binding
|
||||||
self.detach_playlist_binding(renderer_id, "enqueue_items");
|
self.detach_playlist_binding(renderer_id, "enqueue_items");
|
||||||
|
|
||||||
if self.runtime.uses_openhome_playlist(renderer_id) {
|
// Enqueue items using QueueBackend abstraction (works for both backends)
|
||||||
self.enqueue_items_openhome(renderer_id, items)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let item_count = items.len();
|
let item_count = items.len();
|
||||||
let new_len = self.runtime.with_music_queue_mut(renderer_id, |queue| {
|
let new_len = self.runtime.with_music_queue_mut(renderer_id, |queue| {
|
||||||
queue.enqueue_items(items, EnqueueMode::AppendToEnd)?;
|
queue.enqueue_items(items, EnqueueMode::AppendToEnd)?;
|
||||||
@@ -1088,6 +1082,29 @@ impl ControlPoint {
|
|||||||
self.sync_openhome_playlist_for(renderer_id)
|
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(
|
pub fn get_openhome_playlist_snapshot(
|
||||||
&self,
|
&self,
|
||||||
renderer_id: &RendererId,
|
renderer_id: &RendererId,
|
||||||
@@ -1105,6 +1122,8 @@ impl ControlPoint {
|
|||||||
self.runtime.openhome_snapshot_cached(&renderer, ttl)
|
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> {
|
pub fn get_openhome_playlist_len(&self, renderer_id: &RendererId) -> anyhow::Result<usize> {
|
||||||
let renderer = self.openhome_renderer(renderer_id)?;
|
let renderer = self.openhome_renderer(renderer_id)?;
|
||||||
renderer.openhome_playlist_len()
|
renderer.openhome_playlist_len()
|
||||||
@@ -1121,123 +1140,31 @@ impl ControlPoint {
|
|||||||
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
|
.ok_or_else(|| anyhow!("Renderer {} not found", renderer_id.0))?;
|
||||||
let info = renderer.info();
|
let info = renderer.info();
|
||||||
|
|
||||||
// MICRO-PATCH 5: Chemin OpenHome complètement découplé du miroir local
|
// Get runtime snapshot (needed for all backends: volume, state, position)
|
||||||
if self.runtime.uses_openhome_playlist(renderer_id) {
|
let (runtime_snapshot, runtime_queue_items, runtime_current_index) =
|
||||||
// Pour OpenHome: récupérer UNIQUEMENT runtime_snapshot pour volume/state/position
|
self.runtime.renderer_snapshot_bundle(renderer_id);
|
||||||
// Ne PAS utiliser queue_items/current_index du runtime (miroir local)
|
|
||||||
let (runtime_snapshot, _, _) = self.runtime.renderer_snapshot_bundle(renderer_id);
|
|
||||||
|
|
||||||
// OpenHome est la source de vérité pour la queue - pas de fallback au runtime
|
// Try to get queue from backend (returns Some for OpenHome, None for others)
|
||||||
let snapshot = self.get_cached_openhome_playlist_snapshot(
|
let backend_queue = renderer.queue_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)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// 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!(
|
debug!(
|
||||||
renderer = renderer_id.0.as_str(),
|
renderer = renderer_id.0.as_str(),
|
||||||
current_id = ?snapshot.current_id,
|
current_index = ?snapshot.current_index,
|
||||||
current_index = ?queue_current_index,
|
track_count = snapshot.items.len(),
|
||||||
track_count = snapshot.tracks.len(),
|
"renderer_full_snapshot: using backend queue (OpenHome)"
|
||||||
"renderer_full_snapshot: OpenHome snapshot retrieved"
|
|
||||||
);
|
);
|
||||||
|
(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 playback_source = self.runtime.playback_source(renderer_id);
|
||||||
let queue_len = queue_items.len();
|
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(position) = runtime_snapshot.position.as_ref() {
|
||||||
if let Some(uri) = position.track_uri.as_ref() {
|
if let Some(uri) = position.track_uri.as_ref() {
|
||||||
if let Some(idx) = queue_items.iter().position(|item| item.uri == *uri) {
|
if let Some(idx) = queue_items.iter().position(|item| item.uri == *uri) {
|
||||||
@@ -1250,18 +1177,19 @@ impl ControlPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if queue_current_index.is_none()
|
// Final fallback: if playing from queue and no index, assume first track
|
||||||
&& matches!(playback_source, PlaybackSource::FromQueue)
|
if queue_current_index.is_none()
|
||||||
&& runtime_snapshot
|
&& matches!(playback_source, PlaybackSource::FromQueue)
|
||||||
.state
|
&& runtime_snapshot
|
||||||
.as_ref()
|
.state
|
||||||
.map(|state| matches!(state, PlaybackState::Playing | PlaybackState::Paused))
|
.as_ref()
|
||||||
.unwrap_or(false)
|
.map(|state| matches!(state, PlaybackState::Playing | PlaybackState::Paused))
|
||||||
&& !queue_items.is_empty()
|
.unwrap_or(false)
|
||||||
{
|
&& !queue_items.is_empty()
|
||||||
queue_current_index = Some(0);
|
{
|
||||||
|
queue_current_index = Some(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let queue_view_items: Vec<QueueItem> = queue_items
|
let queue_view_items: Vec<QueueItem> = queue_items
|
||||||
@@ -1299,16 +1227,32 @@ impl ControlPoint {
|
|||||||
.and_then(|idx| queue_items.get(idx))
|
.and_then(|idx| queue_items.get(idx))
|
||||||
.map(current_track_from_playback_item);
|
.map(current_track_from_playback_item);
|
||||||
|
|
||||||
let current_track = runtime_snapshot
|
// For backend queues (OpenHome), prefer queue metadata since runtime isn't updated
|
||||||
.last_metadata
|
// For runtime queues (AVTransport), prefer runtime metadata which is fresher
|
||||||
.as_ref()
|
let current_track = if from_backend {
|
||||||
.map(|meta| CurrentTrackMetadata {
|
queue_current_metadata.or_else(|| {
|
||||||
title: meta.title.clone(),
|
runtime_snapshot
|
||||||
artist: meta.artist.clone(),
|
.last_metadata
|
||||||
album: meta.album.clone(),
|
.as_ref()
|
||||||
album_art_uri: meta.album_art_uri.clone(),
|
.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 {
|
let state_view = RendererStateView {
|
||||||
id: renderer_id.0.clone(),
|
id: renderer_id.0.clone(),
|
||||||
@@ -1336,12 +1280,63 @@ impl ControlPoint {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_openhome_playlist(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
|
/// Clears the renderer's backend queue.
|
||||||
let renderer = self.openhome_renderer(renderer_id)?;
|
///
|
||||||
renderer.openhome_playlist_clear()?;
|
/// For renderers with persistent queues (OpenHome), this clears the queue on the renderer.
|
||||||
self.sync_openhome_playlist_for(renderer_id)
|
/// 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(
|
pub fn add_openhome_track(
|
||||||
&self,
|
&self,
|
||||||
renderer_id: &RendererId,
|
renderer_id: &RendererId,
|
||||||
@@ -1350,21 +1345,18 @@ impl ControlPoint {
|
|||||||
after_id: Option<u32>,
|
after_id: Option<u32>,
|
||||||
play: bool,
|
play: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let renderer = self.openhome_renderer(renderer_id)?;
|
self.add_track_to_renderer(renderer_id, uri, metadata, after_id, play)?;
|
||||||
renderer.openhome_playlist_add_track(uri, metadata, after_id, play)?;
|
Ok(())
|
||||||
self.sync_openhome_playlist_for(renderer_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use select_renderer_track instead
|
||||||
|
#[deprecated(since = "0.1.0", note = "Use select_renderer_track instead")]
|
||||||
pub fn play_openhome_track_id(
|
pub fn play_openhome_track_id(
|
||||||
&self,
|
&self,
|
||||||
renderer_id: &RendererId,
|
renderer_id: &RendererId,
|
||||||
track_id: u32,
|
track_id: u32,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let renderer = self.openhome_renderer(renderer_id)?;
|
self.select_renderer_track(renderer_id, track_id)
|
||||||
renderer.openhome_playlist_play_id(track_id)?;
|
|
||||||
self.runtime
|
|
||||||
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
|
|
||||||
self.sync_openhome_playlist_for(renderer_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Plays the current queue item without advancing the index.
|
/// Plays the current queue item without advancing the index.
|
||||||
@@ -1383,10 +1375,18 @@ impl ControlPoint {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.runtime.uses_openhome_playlist(renderer_id) {
|
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
|
||||||
return self.play_current_openhome(renderer_id);
|
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 {
|
let Some((item, remaining)) = self.runtime.peek_current(renderer_id) else {
|
||||||
debug!(
|
debug!(
|
||||||
renderer = renderer_id.0.as_str(),
|
renderer = renderer_id.0.as_str(),
|
||||||
@@ -1404,14 +1404,6 @@ impl ControlPoint {
|
|||||||
"Playing current playback item from queue"
|
"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 playback = (|| -> anyhow::Result<()> {
|
||||||
let didl_metadata = playback_item_to_didl(&item);
|
let didl_metadata = playback_item_to_didl(&item);
|
||||||
renderer.play_uri(&item.uri, &didl_metadata)?;
|
renderer.play_uri(&item.uri, &didl_metadata)?;
|
||||||
@@ -1464,8 +1456,14 @@ impl ControlPoint {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.runtime.uses_openhome_playlist(renderer_id) {
|
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
|
||||||
self.play_next_openhome(renderer_id)?;
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1586,8 +1584,12 @@ impl ControlPoint {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For OpenHome, use select_track_index which handles the track_id lookup
|
let renderer = self.music_renderer_by_id(renderer_id).ok_or_else(|| {
|
||||||
if self.runtime.uses_openhome_playlist(renderer_id) {
|
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!(
|
info!(
|
||||||
renderer = renderer_id.0.as_str(),
|
renderer = renderer_id.0.as_str(),
|
||||||
index,
|
index,
|
||||||
@@ -1722,7 +1724,13 @@ impl ControlPoint {
|
|||||||
return Ok(());
|
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)
|
self.play_current_from_queue(renderer_id)
|
||||||
} else {
|
} else {
|
||||||
self.play_next_from_queue(renderer_id)
|
self.play_next_from_queue(renderer_id)
|
||||||
@@ -1828,10 +1836,8 @@ impl ControlPoint {
|
|||||||
})?;
|
})?;
|
||||||
renderer.clear_for_playlist_attach()?;
|
renderer.clear_for_playlist_attach()?;
|
||||||
|
|
||||||
// For OpenHome, also sync the local cache to reflect the empty state
|
// Sync backend state to local cache (backend-agnostic)
|
||||||
if self.runtime.uses_openhome_playlist(renderer_id) {
|
renderer.sync_queue_state()?;
|
||||||
self.sync_openhome_playlist_for(renderer_id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the local queue (detach binding + clear runtime queue structure)
|
// Clear the local queue (detach binding + clear runtime queue structure)
|
||||||
self.detach_playlist_binding(renderer_id, "attach_new_playlist");
|
self.detach_playlist_binding(renderer_id, "attach_new_playlist");
|
||||||
@@ -2042,141 +2048,6 @@ impl ControlPoint {
|
|||||||
Ok(())
|
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")]
|
#[cfg(feature = "pmoserver")]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::openhome_client::{
|
|||||||
OPENHOME_PLAYLIST_HEAD_ID,
|
OPENHOME_PLAYLIST_HEAD_ID,
|
||||||
};
|
};
|
||||||
use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
|
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.
|
/// Local mirror of an OpenHome playlist for a single renderer.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -837,4 +837,43 @@ impl QueueBackend for OpenHomeQueue {
|
|||||||
self.track_ids[index] = new_id;
|
self.track_ids[index] = new_id;
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ use crate::control_point::RendererRuntimeStateMut;
|
|||||||
use crate::control_point::music_queue::MusicQueue;
|
use crate::control_point::music_queue::MusicQueue;
|
||||||
use crate::control_point::openhome_queue::didl_id_from_metadata;
|
use crate::control_point::openhome_queue::didl_id_from_metadata;
|
||||||
use crate::media_server::ServerId;
|
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_client::parse_track_metadata_from_didl;
|
||||||
use crate::openhome_playlist::OpenHomePlaylistSnapshot;
|
use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
|
||||||
use crate::queue_backend::PlaybackItem;
|
use crate::queue_backend::{PlaybackItem, QueueSnapshot};
|
||||||
use crate::{
|
use crate::{
|
||||||
ArylicTcpRenderer, ChromecastRenderer, DeviceRegistry, LinkPlayRenderer, OpenHomeRenderer,
|
ArylicTcpRenderer, ChromecastRenderer, DeviceRegistry, LinkPlayRenderer, OpenHomeRenderer,
|
||||||
PlaybackPosition, PlaybackState, TransportControl, UpnpRenderer, VolumeControl,
|
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(
|
pub fn openhome_playlist_add_track(
|
||||||
&self,
|
&self,
|
||||||
uri: &str,
|
uri: &str,
|
||||||
|
|||||||
Reference in New Issue
Block a user