- Update package versions in Cargo.toml, lockfile and version.txt to v0.3.47 - Optimize SQLite database initialization with WAL mode, larger cache and critical missing indexes (idx_metadata_key_value, idx_asset_last_used/hits) - Fix production bug in OpenHome renderer: avoid blocking main thread when loading metadata for large queues; offload preloading of next 10 items to background task with small delays - Minor formatting cleanup in db.rs
760 lines
26 KiB
Rust
760 lines
26 KiB
Rust
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
|
use std::time::SystemTime;
|
|
|
|
use crate::music_renderer::capabilities::{
|
|
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
|
|
TransportControl, VolumeControl,
|
|
};
|
|
use crate::music_renderer::time_utils::{format_hhmmss_u32, parse_time_flexible};
|
|
use crate::DeviceIdentity;
|
|
|
|
use crate::errors::ControlPointError;
|
|
use crate::model::{PlaybackState, RendererInfo};
|
|
use crate::music_renderer::musicrenderer::MusicRendererBackend;
|
|
use crate::music_renderer::openhome::{
|
|
build_info_client, build_playlist_client, build_product_client, build_radio_client,
|
|
build_time_client, build_volume_client,
|
|
};
|
|
use crate::music_renderer::RendererFromMediaRendererInfo;
|
|
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
|
|
use crate::upnp_clients::{
|
|
OhInfoClient, OhPlaylistClient, OhProductClient, OhRadioClient, OhTimeClient, OhVolumeClient,
|
|
OPENHOME_PLAYLIST_HEAD_ID,
|
|
};
|
|
use tracing::debug;
|
|
|
|
/// Cache for playback position to avoid redundant SOAP calls
|
|
#[derive(Debug)]
|
|
struct PositionCache {
|
|
/// Last cached position info
|
|
last_position: Option<PlaybackPositionInfo>,
|
|
/// Timestamp of last cache update
|
|
last_update: Option<SystemTime>,
|
|
/// Number of calls in the last second (for warning detection)
|
|
calls_in_last_second: Vec<SystemTime>,
|
|
}
|
|
|
|
impl PositionCache {
|
|
fn new() -> Self {
|
|
Self {
|
|
last_position: None,
|
|
last_update: None,
|
|
calls_in_last_second: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct OpenHomeRenderer {
|
|
playlist: Option<OhPlaylistClient>,
|
|
info_client: Option<OhInfoClient>,
|
|
time_client: Option<OhTimeClient>,
|
|
volume_client: Option<OhVolumeClient>,
|
|
product_client: Option<OhProductClient>,
|
|
#[allow(dead_code)]
|
|
radio_client: Option<OhRadioClient>,
|
|
queue: Arc<Mutex<MusicQueue>>,
|
|
/// Flag indicating if currently playing a continuous stream (radio without duration)
|
|
continuous_stream: Arc<Mutex<bool>>,
|
|
/// Cached current track URI to detect track changes
|
|
current_track_uri: Arc<Mutex<Option<String>>>,
|
|
/// Position cache to avoid redundant SOAP calls (OpenHome has second-precision only)
|
|
position_cache: Arc<Mutex<PositionCache>>,
|
|
}
|
|
|
|
impl OpenHomeRenderer {
|
|
pub fn new(
|
|
playlist: Option<OhPlaylistClient>,
|
|
info_client: Option<OhInfoClient>,
|
|
time_client: Option<OhTimeClient>,
|
|
volume_client: Option<OhVolumeClient>,
|
|
product_client: Option<OhProductClient>,
|
|
radio_client: Option<OhRadioClient>,
|
|
queue: Arc<Mutex<MusicQueue>>,
|
|
) -> Self {
|
|
Self {
|
|
playlist,
|
|
info_client,
|
|
time_client,
|
|
volume_client,
|
|
product_client,
|
|
radio_client,
|
|
queue,
|
|
continuous_stream: Arc::new(Mutex::new(false)),
|
|
current_track_uri: Arc::new(Mutex::new(None)),
|
|
position_cache: Arc::new(Mutex::new(PositionCache::new())),
|
|
}
|
|
}
|
|
|
|
/// Returns true if currently playing a continuous stream (radio without duration)
|
|
pub fn is_continuous_stream(&self) -> bool {
|
|
*self.continuous_stream.lock().unwrap()
|
|
}
|
|
|
|
pub fn has_playlist(&self) -> bool {
|
|
self.playlist.is_some()
|
|
}
|
|
|
|
pub fn has_info(&self) -> bool {
|
|
self.info_client.is_some()
|
|
}
|
|
|
|
pub fn has_time(&self) -> bool {
|
|
self.time_client.is_some()
|
|
}
|
|
|
|
pub fn has_volume(&self) -> bool {
|
|
self.volume_client.is_some()
|
|
}
|
|
|
|
pub fn has_any_openhome_service(&self) -> bool {
|
|
self.has_playlist() || self.has_info() || self.has_time() || self.has_volume()
|
|
}
|
|
|
|
fn playlist_client_for(&self, op: &str) -> Result<&OhPlaylistClient, ControlPointError> {
|
|
let playlist = self.playlist.as_ref().ok_or_else(|| {
|
|
ControlPointError::upnp_operation_not_supported(op, "OpenHome Playlist")
|
|
})?;
|
|
self.ensure_playlist_source_selected()?;
|
|
Ok(playlist)
|
|
}
|
|
|
|
fn info_client_for(&self, op: &str) -> Result<&OhInfoClient, ControlPointError> {
|
|
self.info_client
|
|
.as_ref()
|
|
.ok_or_else(|| ControlPointError::upnp_operation_not_supported(op, "OpenHome Info"))
|
|
}
|
|
|
|
fn time_client_for(&self, op: &str) -> Result<&OhTimeClient, ControlPointError> {
|
|
self.time_client
|
|
.as_ref()
|
|
.ok_or_else(|| ControlPointError::upnp_operation_not_supported(op, "OpenHome Time"))
|
|
}
|
|
|
|
fn volume_client_for(&self, op: &str) -> Result<&OhVolumeClient, ControlPointError> {
|
|
self.volume_client
|
|
.as_ref()
|
|
.ok_or_else(|| ControlPointError::upnp_operation_not_supported(op, "OpenHome Volume"))
|
|
}
|
|
|
|
fn ensure_playlist_source_selected(&self) -> Result<(), ControlPointError> {
|
|
if let Some(product) = &self.product_client {
|
|
product.ensure_playlist_source_selected()
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// pub(crate) fn snapshot_openhome_playlist(&self) -> Result<OpenHomePlaylistSnapshot> {
|
|
// let playlist = self.playlist_client_for("snapshot_openhome_playlist")?;
|
|
// let entries = playlist.read_all_tracks()?;
|
|
|
|
// // Get current track ID from the playlist service
|
|
// let current_id = playlist.id().ok();
|
|
|
|
// let current_index =
|
|
// current_id.and_then(|id| entries.iter().position(|entry| entry.id == id));
|
|
|
|
// debug!(
|
|
// current_id = ?current_id,
|
|
// current_index = ?current_index,
|
|
// track_count = entries.len(),
|
|
// "snapshot_openhome_playlist completed"
|
|
// );
|
|
|
|
// let tracks = entries.iter().map(convert_oh_track_entry).collect();
|
|
|
|
// Ok(OpenHomePlaylistSnapshot {
|
|
// current_id,
|
|
// current_index,
|
|
// tracks,
|
|
// })
|
|
// }
|
|
|
|
/// Retourne la longueur de la playlist OpenHome sans récupérer toutes les métadonnées.
|
|
/// Plus rapide que snapshot_openhome_playlist() pour juste connaître le nombre de pistes.
|
|
pub(crate) fn openhome_playlist_len(&self) -> Result<usize, ControlPointError> {
|
|
// Use queue.len() which uses cached track_ids() internally
|
|
let queue = self.queue.lock().unwrap();
|
|
queue.len()
|
|
}
|
|
|
|
/// Retourne les IDs des pistes de la playlist OpenHome.
|
|
/// Plus rapide que snapshot_openhome_playlist() car ne récupère pas les métadonnées.
|
|
pub(crate) fn openhome_playlist_ids(&self) -> Result<Vec<u32>, ControlPointError> {
|
|
let queue = self.queue.lock().unwrap();
|
|
if let Some(oh_queue) = queue.as_openhome() {
|
|
oh_queue.track_ids()
|
|
} else {
|
|
Err(ControlPointError::QueueError(
|
|
"Not an OpenHome queue".to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn clear_openhome_playlist(&self) -> Result<(), ControlPointError> {
|
|
let playlist = self.playlist_client_for("clear_openhome_playlist")?;
|
|
playlist.delete_all()
|
|
}
|
|
|
|
pub(crate) fn add_track_openhome(
|
|
&self,
|
|
uri: &str,
|
|
metadata: &str,
|
|
after_id: Option<u32>,
|
|
play: bool,
|
|
) -> Result<u32, ControlPointError> {
|
|
let playlist = self.playlist_client_for("add_track_openhome")?;
|
|
let insert_after = match after_id {
|
|
Some(id) => id,
|
|
None => {
|
|
let queue = self.queue.lock().unwrap();
|
|
if let Some(oh_queue) = queue.as_openhome() {
|
|
oh_queue
|
|
.track_ids()?
|
|
.last()
|
|
.copied()
|
|
.unwrap_or(OPENHOME_PLAYLIST_HEAD_ID)
|
|
} else {
|
|
return Err(ControlPointError::QueueError(
|
|
"Not an OpenHome queue".to_string(),
|
|
));
|
|
}
|
|
}
|
|
};
|
|
|
|
let new_id = playlist.insert(insert_after, uri, metadata)?;
|
|
if play {
|
|
playlist.seek_id(new_id)?;
|
|
}
|
|
Ok(new_id)
|
|
}
|
|
|
|
pub(crate) fn play_openhome_track_id(&self, id: u32) -> Result<(), ControlPointError> {
|
|
let playlist = self.playlist_client_for("play_openhome_track_id")?;
|
|
playlist.seek_id(id)
|
|
}
|
|
}
|
|
|
|
impl RendererFromMediaRendererInfo for OpenHomeRenderer {
|
|
fn from_renderer_info(info: &RendererInfo) -> Result<Self, ControlPointError> {
|
|
// Create the OpenHome queue
|
|
let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?));
|
|
|
|
let renderer = OpenHomeRenderer::new(
|
|
build_playlist_client(&info),
|
|
build_info_client(&info),
|
|
build_time_client(&info),
|
|
build_volume_client(&info),
|
|
build_product_client(&info),
|
|
build_radio_client(&info),
|
|
queue,
|
|
);
|
|
|
|
if renderer.has_any_openhome_service() {
|
|
Ok(renderer)
|
|
} else {
|
|
Err(ControlPointError::OpenHomeNotAValidDevice(format!(
|
|
"{:?}",
|
|
info.id()
|
|
)))
|
|
}
|
|
}
|
|
|
|
fn to_backend(self) -> MusicRendererBackend {
|
|
MusicRendererBackend::OpenHome(self)
|
|
}
|
|
}
|
|
|
|
impl RendererBackend for OpenHomeRenderer {
|
|
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
|
|
&self.queue
|
|
}
|
|
}
|
|
|
|
impl TransportControl for OpenHomeRenderer {
|
|
fn play_uri(&self, uri: &str, meta: &str) -> Result<(), ControlPointError> {
|
|
let playlist = self.playlist_client_for("play_uri")?;
|
|
|
|
if let Err(err) = playlist.delete_all() {
|
|
debug!(
|
|
// renderer = self.info.id.0.as_str(),
|
|
error = %err,
|
|
"Failed to clear OpenHome playlist before insert"
|
|
);
|
|
}
|
|
|
|
// Reuse the same insertion logic as the queue path so that we honor
|
|
// renderer expectations (IdArray sequencing, etc.).
|
|
self.add_track_openhome(uri, meta, None, true)?;
|
|
|
|
// Start playback (like UPnP renderer does with avt.play())
|
|
playlist.play()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn play(&self) -> Result<(), ControlPointError> {
|
|
let playlist = self.playlist_client_for("play")?;
|
|
playlist.play()
|
|
}
|
|
|
|
fn pause(&self) -> Result<(), ControlPointError> {
|
|
let playlist = self.playlist_client_for("pause")?;
|
|
playlist.pause()
|
|
}
|
|
|
|
fn stop(&self) -> Result<(), ControlPointError> {
|
|
let playlist = self.playlist_client_for("stop")?;
|
|
playlist.stop()
|
|
}
|
|
|
|
fn seek_rel_time(&self, hhmmss: &str) -> Result<(), ControlPointError> {
|
|
let seconds = parse_time_flexible(hhmmss)?;
|
|
let playlist = self.playlist_client_for("seek_rel_time")?;
|
|
playlist.seek_second_absolute(seconds)
|
|
}
|
|
}
|
|
|
|
impl VolumeControl for OpenHomeRenderer {
|
|
fn volume(&self) -> Result<u16, ControlPointError> {
|
|
let client = self.volume_client_for("volume")?;
|
|
client.volume()
|
|
}
|
|
|
|
fn set_volume(&self, v: u16) -> Result<(), ControlPointError> {
|
|
let client = self.volume_client_for("set_volume")?;
|
|
client.set_volume(v)
|
|
}
|
|
|
|
fn mute(&self) -> Result<bool, ControlPointError> {
|
|
let client = self.volume_client_for("mute")?;
|
|
client.mute()
|
|
}
|
|
|
|
fn set_mute(&self, m: bool) -> Result<(), ControlPointError> {
|
|
let client = self.volume_client_for("set_mute")?;
|
|
client.set_mute(m)
|
|
}
|
|
}
|
|
|
|
impl PlaybackStatus for OpenHomeRenderer {
|
|
fn playback_state(&self) -> Result<PlaybackState, ControlPointError> {
|
|
let client = self.playlist_client_for("playback_state")?;
|
|
let raw = client.transport_state().map_err(|err| {
|
|
tracing::warn!(
|
|
error = %err,
|
|
"OpenHome transport_state() failed — state change detection disabled"
|
|
);
|
|
err
|
|
})?;
|
|
let mapped = if raw.is_empty() {
|
|
tracing::trace!("OpenHome TransportState: empty (device initializing)");
|
|
PlaybackState::Transitioning
|
|
} else {
|
|
let mapped = map_openhome_state(&raw);
|
|
tracing::trace!(
|
|
raw_state = raw.as_str(),
|
|
mapped_state = ?mapped,
|
|
"OpenHome TransportState"
|
|
);
|
|
mapped
|
|
};
|
|
Ok(mapped)
|
|
}
|
|
}
|
|
|
|
impl PlaybackPosition for OpenHomeRenderer {
|
|
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
|
|
let now = SystemTime::now();
|
|
|
|
// Check cache first
|
|
{
|
|
let mut cache = self.position_cache.lock().unwrap();
|
|
|
|
// Track calls for warning detection
|
|
cache.calls_in_last_second.push(now);
|
|
// Keep only calls from last second
|
|
cache.calls_in_last_second.retain(|t| {
|
|
now.duration_since(*t)
|
|
.map(|d| d.as_millis() < 1000)
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
// Warn if called more than 3 times in last second
|
|
if cache.calls_in_last_second.len() > 3 {
|
|
tracing::warn!(
|
|
"OpenHome playback_position() called {} times in last second - possible inefficiency",
|
|
cache.calls_in_last_second.len()
|
|
);
|
|
}
|
|
|
|
// Return cached value if it's less than 900ms old (OpenHome has second precision)
|
|
if let (Some(last_pos), Some(last_update)) = (&cache.last_position, cache.last_update) {
|
|
if let Ok(elapsed) = now.duration_since(last_update) {
|
|
if elapsed.as_millis() < 900 {
|
|
tracing::trace!(
|
|
"OpenHome playback_position: returning cached value (age={}ms)",
|
|
elapsed.as_millis()
|
|
);
|
|
return Ok(last_pos.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache miss or stale - fetch from backend
|
|
let time_info = self.time_client_for("playback_position")?.position()?;
|
|
|
|
let mut track_id = None;
|
|
let mut track_uri = None;
|
|
let mut track_metadata_xml = None;
|
|
|
|
// Get track ID from queue (uses cached data)
|
|
let queue_guard_for_id = self.queue.lock().unwrap();
|
|
if let Some(oh_queue) = queue_guard_for_id.as_openhome() {
|
|
match oh_queue.current_track() {
|
|
Ok(id_opt) => track_id = id_opt,
|
|
Err(err) => debug!(
|
|
error = %err,
|
|
"Failed to read OpenHome track id"
|
|
),
|
|
}
|
|
}
|
|
drop(queue_guard_for_id);
|
|
|
|
// Use queue API to get current item with cached metadata
|
|
let mut queue_guard = self.queue.lock().unwrap();
|
|
if let Ok(Some((current_item, _))) = queue_guard.peek_current() {
|
|
// Use metadata from queue cache (updated via OpenHome events)
|
|
track_uri = Some(current_item.uri.clone());
|
|
|
|
// Build DIDL metadata XML from cached TrackMetadata
|
|
if let Some(ref metadata) = current_item.metadata {
|
|
track_metadata_xml = Some(
|
|
crate::music_renderer::musicrenderer::build_didl_lite_metadata(
|
|
metadata,
|
|
¤t_item.uri,
|
|
¤t_item.protocol_info,
|
|
),
|
|
);
|
|
}
|
|
|
|
// Check if the URI has changed to detect track changes
|
|
let mut cached_uri = self.current_track_uri.lock().unwrap();
|
|
let uri_changed = cached_uri.as_ref() != Some(¤t_item.uri);
|
|
|
|
if uri_changed {
|
|
tracing::debug!(
|
|
"OpenHome track URI changed: {:?} -> {:?}",
|
|
cached_uri,
|
|
current_item.uri
|
|
);
|
|
|
|
// Détecte si la nouvelle URL est un flux continu
|
|
let is_stream = crate::music_renderer::is_continuous_stream_url(¤t_item.uri);
|
|
*self.continuous_stream.lock().unwrap() = is_stream;
|
|
tracing::debug!("OpenHome URI changed, continuous_stream={}", is_stream);
|
|
|
|
*cached_uri = Some(current_item.uri.clone());
|
|
}
|
|
}
|
|
drop(queue_guard);
|
|
|
|
// Get duration from Time service - duration_secs=0 means stream (no duration)
|
|
let track_duration = if time_info.duration_secs == 0 {
|
|
None
|
|
} else {
|
|
Some(format_hhmmss_u32(time_info.duration_secs))
|
|
};
|
|
|
|
let rel_time = format_hhmmss_u32(time_info.elapsed_secs);
|
|
|
|
tracing::trace!(
|
|
"OpenHome playback_position: duration_secs={}, track_duration={:?}, elapsed_secs={}, rel_time={}",
|
|
time_info.duration_secs,
|
|
track_duration,
|
|
time_info.elapsed_secs,
|
|
rel_time
|
|
);
|
|
|
|
let position_info = PlaybackPositionInfo {
|
|
track: track_id,
|
|
rel_time: Some(rel_time),
|
|
abs_time: None,
|
|
track_duration,
|
|
track_metadata: track_metadata_xml,
|
|
track_uri,
|
|
};
|
|
|
|
// Update cache with fresh data
|
|
{
|
|
let mut cache = self.position_cache.lock().unwrap();
|
|
cache.last_position = Some(position_info.clone());
|
|
cache.last_update = Some(now);
|
|
}
|
|
|
|
Ok(position_info)
|
|
}
|
|
}
|
|
|
|
/// Parse duration from DIDL-Lite metadata XML (OpenHome version)
|
|
#[allow(dead_code)]
|
|
fn parse_didl_duration_openhome(didl: &str) -> Option<String> {
|
|
// Search for duration attribute in <res> element
|
|
let res_start = didl.find("<res ")?;
|
|
let after_res = &didl[res_start..];
|
|
let tag_close = after_res.find('>')?;
|
|
let tag_attrs = &after_res[..tag_close];
|
|
|
|
if let Some(duration_start) = tag_attrs.find("duration=\"") {
|
|
let duration_offset = duration_start + "duration=\"".len();
|
|
if let Some(duration_end) = tag_attrs[duration_offset..].find('"') {
|
|
let duration = &tag_attrs[duration_offset..duration_offset + duration_end];
|
|
return Some(duration.to_string());
|
|
}
|
|
}
|
|
|
|
tracing::debug!("OpenHome: No duration found in DIDL metadata");
|
|
None
|
|
}
|
|
|
|
pub(crate) fn map_openhome_state(raw: &str) -> PlaybackState {
|
|
match raw.trim().to_ascii_uppercase().as_str() {
|
|
"PLAYING" => PlaybackState::Playing,
|
|
"PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
|
|
"STOPPED" => PlaybackState::Stopped,
|
|
"BUFFERING" | "TRANSITIONING" => PlaybackState::Transitioning,
|
|
other => PlaybackState::Unknown(other.to_string()),
|
|
}
|
|
}
|
|
|
|
impl QueueTransportControl for OpenHomeRenderer {
|
|
fn play_from_queue(&self) -> Result<(), ControlPointError> {
|
|
{
|
|
let queue = self
|
|
.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?;
|
|
|
|
if queue.current_index()?.is_none() {
|
|
if queue.len()? > 0 {
|
|
drop(queue);
|
|
let mut queue = self.queue.lock().map_err(|_| {
|
|
ControlPointError::QueueError("Queue mutex poisoned".into())
|
|
})?;
|
|
queue.set_index(Some(0))?;
|
|
} else {
|
|
return Err(ControlPointError::QueueError("Queue is empty".into()));
|
|
}
|
|
}
|
|
}
|
|
|
|
let playlist = self.playlist_client_for("play_from_queue")?;
|
|
playlist.play()
|
|
}
|
|
|
|
fn play_next(&self) -> Result<(), ControlPointError> {
|
|
{
|
|
let mut queue = self
|
|
.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?;
|
|
let len = queue.len().unwrap_or(0);
|
|
let current = queue.current_index().ok().flatten();
|
|
let current_track_id = queue.current_track().ok().flatten();
|
|
let all_ids = queue.track_ids().ok().unwrap_or_default();
|
|
tracing::trace!(
|
|
queue_len = len,
|
|
current_index = ?current,
|
|
current_track_id = ?current_track_id,
|
|
all_track_ids = ?all_ids,
|
|
"OpenHome play_next: advancing queue"
|
|
);
|
|
if !queue.advance()? {
|
|
tracing::trace!(
|
|
queue_len = len,
|
|
current_index = ?current,
|
|
"OpenHome play_next: advance() returned false — no next track"
|
|
);
|
|
return Err(ControlPointError::QueueError("No next track".into()));
|
|
}
|
|
}
|
|
|
|
self.play_from_queue()
|
|
}
|
|
|
|
fn play_previous(&self) -> Result<(), ControlPointError> {
|
|
{
|
|
let mut queue = self
|
|
.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?;
|
|
if !queue.rewind()? {
|
|
return Err(ControlPointError::QueueError("No previous track".into()));
|
|
}
|
|
}
|
|
|
|
self.play_from_queue()
|
|
}
|
|
|
|
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
|
|
// For OpenHome, we need to convert index to track_id
|
|
let track_id = {
|
|
let queue = self
|
|
.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?;
|
|
queue.position_to_id(index)?
|
|
};
|
|
|
|
// Seek to the track by ID
|
|
let playlist = self.playlist_client_for("play_from_index")?;
|
|
playlist.seek_id(track_id)?;
|
|
|
|
// Update local queue index
|
|
{
|
|
let mut queue = self
|
|
.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?;
|
|
queue.set_index(Some(index))?;
|
|
}
|
|
|
|
// Start playback
|
|
playlist.play()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl QueueBackend for OpenHomeRenderer {
|
|
fn len(&self) -> Result<usize, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.len()
|
|
}
|
|
|
|
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.track_ids()
|
|
}
|
|
|
|
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.id_to_position(id)
|
|
}
|
|
|
|
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.position_to_id(id)
|
|
}
|
|
|
|
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.current_track()
|
|
}
|
|
|
|
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.current_index()
|
|
}
|
|
|
|
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.queue_snapshot()
|
|
}
|
|
|
|
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.set_index(index)
|
|
}
|
|
|
|
fn replace_queue(
|
|
&mut self,
|
|
items: Vec<PlaybackItem>,
|
|
current_index: Option<usize>,
|
|
) -> Result<(), ControlPointError> {
|
|
// ✅ CORRECTION BUG PRODUCTION: On ne charge PAS toutes les métadonnées
|
|
// dans le thread principal. OpenHome sur 1000 titres inondait la base SQLite
|
|
// et bloquait TOUS les autres threads (mutex >500ms).
|
|
//
|
|
// On fait juste l'insertion minimaliste maintenant. Le préchargement
|
|
// des métadonnées est délégué à un thread background.
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Mutex poisoned".into()))?
|
|
.replace_queue(items, current_index)?;
|
|
|
|
// Background worker: charge les métadonnées petit à petit sans bloquer personne
|
|
let queue = self.queue.clone();
|
|
tokio::task::spawn_blocking(move || {
|
|
debug!("🔄 OpenHome: préchargement métadonnées queue en background");
|
|
if let Ok(mut queue) = queue.lock() {
|
|
// On ne fait que les 10 prochains titres maintenant, le reste on s'en fout
|
|
if let Ok(Some(idx)) = queue.current_index() {
|
|
let end = std::cmp::min(idx + 10, queue.len().unwrap_or(0));
|
|
for i in idx..end {
|
|
let _ = queue.get_item(i);
|
|
// Petit délai pour ne pas noyer la base de données
|
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
|
}
|
|
}
|
|
}
|
|
debug!("✅ OpenHome: préchargement métadonnées terminé");
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn sync_queue(
|
|
&mut self,
|
|
items: Vec<PlaybackItem>,
|
|
cancel_token: &Arc<AtomicBool>,
|
|
on_ready: Option<Box<dyn FnOnce() + Send>>,
|
|
) -> Result<(), ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.sync_queue(items, cancel_token, on_ready)
|
|
}
|
|
|
|
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.get_item(index)
|
|
}
|
|
|
|
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.replace_item(index, item)
|
|
}
|
|
|
|
fn enqueue_items(
|
|
&mut self,
|
|
items: Vec<PlaybackItem>,
|
|
mode: EnqueueMode,
|
|
) -> Result<(), ControlPointError> {
|
|
self.queue
|
|
.lock()
|
|
.map_err(|_| ControlPointError::QueueError("Queue mutex poisoned".into()))?
|
|
.enqueue_items(items, mode)
|
|
}
|
|
}
|