Files
pmomusic/pmocontrol/src/music_renderer/openhome_renderer.rs

340 lines
11 KiB
Rust

use crate::DeviceIdentity;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl,
VolumeControl,
};
use crate::music_renderer::time_utils::{parse_time_flexible, format_hhmmss_u32};
use crate::errors::ControlPointError;
use crate::model::{RendererInfo, PlaybackState};
use crate::music_renderer::RendererFromMediaRendererInfo;
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::upnp_clients::{
OPENHOME_PLAYLIST_HEAD_ID, OhInfoClient, OhPlaylistClient, OhProductClient, OhRadioClient,
OhTimeClient, OhVolumeClient,
};
use tracing::debug;
#[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>,
}
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>,
) -> Self {
Self {
playlist,
info_client,
time_client,
volume_client,
product_client,
radio_client,
}
}
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> {
let playlist = self.playlist_client_for("openhome_playlist_len")?;
let ids = playlist.id_array()?;
Ok(ids.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 playlist = self.playlist_client_for("openhome_playlist_ids")?;
playlist.id_array()
}
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 => playlist
.id_array()?
.last()
.copied()
.unwrap_or(OPENHOME_PLAYLIST_HEAD_ID),
};
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> {
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),
);
if renderer.has_any_openhome_service() {
Ok(renderer)
} else {
Err(ControlPointError::OpenHomeNotAValidDevice(format!(
"{:?}",
info.id()
)))
}
}
fn to_backend(self) -> MusicRendererBackend {
MusicRendererBackend::OpenHome(self)
}
}
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)?;
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 state = client.transport_state()?;
Ok(map_openhome_state(&state))
}
}
impl PlaybackPosition for OpenHomeRenderer {
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
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;
if let Some(playlist_client) = &self.playlist {
match playlist_client.id() {
Ok(id) => track_id = Some(id),
Err(err) => debug!(
// renderer = self.info.id.0.as_str(),
error = %err,
"Failed to read OpenHome track id"
),
}
}
if let Some(info_client) = &self.info_client {
match info_client.track() {
Ok(track) => {
track_uri = Some(track.uri);
track_metadata_xml = track.metadata_xml;
}
Err(err) => debug!(
// renderer = self.info.id.0.as_str(),
error = %err,
"Failed to read OpenHome track metadata"
),
}
}
Ok(PlaybackPositionInfo {
track: track_id,
rel_time: Some(format_hhmmss_u32(time_info.elapsed_secs)),
abs_time: None,
track_duration: Some(format_hhmmss_u32(time_info.duration_secs)),
track_metadata: track_metadata_xml,
track_uri,
})
}
}
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()),
}
}