énorme refactoring de PMOcontrol step 1

This commit is contained in:
2025-12-28 17:45:06 +01:00
parent 000d378789
commit 64450185c5
54 changed files with 3840 additions and 2769 deletions

View File

@@ -3,7 +3,7 @@ use std::thread;
use std::time::Duration;
use pmocontrol::{
ControlPoint, DeviceRegistryRead, MusicRenderer, PlaybackPosition, PlaybackPositionInfo,
ControlPoint, DeviceRegistryRead, MusicRendererBackend, PlaybackPosition, PlaybackPositionInfo,
PlaybackState, PlaybackStatus, TransportControl, VolumeControl,
};
@@ -38,10 +38,10 @@ fn main() -> io::Result<()> {
);
}
let arylic_renderers: Vec<MusicRenderer> = renderers
let arylic_renderers: Vec<MusicRendererBackend> = renderers
.iter()
.filter(|info| info.capabilities.has_arylic_tcp)
.filter_map(|info| MusicRenderer::from_registry_info(info.clone(), &registry))
.filter_map(|info| MusicRendererBackend::from_renderer_info(info.clone(), &registry))
.collect();
if arylic_renderers.is_empty() {

View File

@@ -3,7 +3,7 @@ use std::thread;
use std::time::Duration;
use pmocontrol::RendererProtocol;
use pmocontrol::{ControlPoint, DeviceRegistryRead, MediaServerInfo, RendererInfo};
use pmocontrol::{ControlPoint, DeviceRegistryRead, UpnpMediaServer, RendererInfo};
fn main() -> std::io::Result<()> {
// Un tout petit logging optionnel
@@ -21,7 +21,7 @@ fn main() -> std::io::Result<()> {
let reg = reg.read().expect("registry poisoned");
let renderers: Vec<RendererInfo> = reg.list_renderers();
let servers: Vec<MediaServerInfo> = reg.list_servers();
let servers: Vec<UpnpMediaServer> = reg.list_servers();
println!("=====================");
println!("Renderers detected : {}", renderers.len());
@@ -29,7 +29,7 @@ fn main() -> std::io::Result<()> {
let proto = match r.protocol {
RendererProtocol::UpnpAvOnly => "UPnP AV",
RendererProtocol::OpenHomeOnly => "OpenHome",
RendererProtocol::Hybrid => "Hybrid",
RendererProtocol::OpenHomeHybrid => "Hybrid",
};
println!(

View File

@@ -19,8 +19,8 @@ use crossterm::terminal::{
};
use pmocontrol::model::TrackMetadata;
use pmocontrol::{
ControlPoint, DeviceRegistryRead, MediaBrowser, MediaEntry, MediaServerEvent, MediaServerInfo,
MusicServer, PlaybackItem, PlaybackPosition, PlaybackPositionInfo, PlaybackStatus,
ControlPoint, DeviceRegistryRead, MediaBrowser, MediaEntry, MediaServerEvent, UpnpMediaServer,
UpnpMediaServer, PlaybackItem, PlaybackPosition, PlaybackPositionInfo, PlaybackStatus,
RendererEvent, RendererInfo, TransportControl, VolumeControl,
};
use ratatui::Terminal;
@@ -102,7 +102,7 @@ fn main() -> Result<()> {
let servers = {
let reg = registry.read().expect("registry poisoned");
let list: Vec<MediaServerInfo> = reg
let list: Vec<UpnpMediaServer> = reg
.list_servers()
.into_iter()
.filter(|s| s.has_content_directory && s.content_directory_control_url.is_some())
@@ -129,10 +129,10 @@ struct App {
renderers: Vec<RendererInfo>,
renderer_index: usize,
renderer_info: Option<RendererInfo>,
servers: Vec<MediaServerInfo>,
servers: Vec<UpnpMediaServer>,
server_index: usize,
server_info: Option<MediaServerInfo>,
music_server: Option<MusicServer>,
server_info: Option<UpnpMediaServer>,
music_server: Option<UpnpMediaServer>,
browser: Option<BrowserState>,
mode: Mode,
ui_state: UiState,
@@ -168,7 +168,7 @@ impl App {
fn new(
control_point: Arc<ControlPoint>,
renderers: Vec<RendererInfo>,
servers: Vec<MediaServerInfo>,
servers: Vec<UpnpMediaServer>,
) -> Self {
Self {
control_point,
@@ -192,7 +192,7 @@ impl App {
}
}
fn renderer_id(&self) -> Option<pmocontrol::model::RendererId> {
fn renderer_id(&self) -> Option<pmocontrol::model::ServiceId> {
self.renderer_info.as_ref().map(|info| info.id.clone())
}
@@ -552,7 +552,7 @@ impl App {
}
KeyCode::Enter => {
let info = self.servers[self.server_index].clone();
match MusicServer::from_info(&info, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) {
match UpnpMediaServer::from_info(&info, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) {
Ok(server) => {
let entries = server.browse_root()?;
let browser = BrowserState::new(entries);
@@ -947,7 +947,7 @@ impl App {
}
}
fn refresh_queue_snapshot(&mut self, renderer_id: &pmocontrol::model::RendererId) {
fn refresh_queue_snapshot(&mut self, renderer_id: &pmocontrol::model::ServiceId) {
if let Ok((queue, current_index)) = self.control_point.get_full_queue_snapshot(renderer_id)
{
self.queue_snapshot = queue;
@@ -967,7 +967,7 @@ impl App {
fn peek_next_queue_item(
&self,
renderer_id: &pmocontrol::model::RendererId,
renderer_id: &pmocontrol::model::ServiceId,
) -> Option<PlaybackItem> {
self.control_point
.get_queue_snapshot(renderer_id)
@@ -975,7 +975,7 @@ impl App {
.and_then(|queue| queue.into_iter().next())
}
fn get_renderer(&self) -> Result<pmocontrol::MusicRenderer> {
fn get_renderer(&self) -> Result<pmocontrol::MusicRendererBackend> {
let renderer_id = self
.renderer_id()
.ok_or_else(|| anyhow!("Renderer not selected"))?;
@@ -1230,7 +1230,7 @@ impl NavigationState {
/// Collect playable items from MediaEntry list (including nested containers).
fn collect_playable_items(
server: &MusicServer,
server: &UpnpMediaServer,
entries: &[MediaEntry],
) -> Result<Vec<PlaybackItem>> {
let mut items = Vec::new();
@@ -1255,7 +1255,7 @@ fn collect_playable_items(
}
/// Convert MediaEntry to PlaybackItem.
fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<PlaybackItem> {
fn playback_item_from_entry(server: &UpnpMediaServer, entry: &MediaEntry) -> Option<PlaybackItem> {
let resource = entry.resources.iter().find(|res| res.is_audio())?;
let metadata = TrackMetadata {
title: Some(entry.title.clone()),

View File

@@ -3,7 +3,7 @@ use std::thread;
use std::time::Duration;
use pmocontrol::{
ControlPoint, DeviceRegistryRead, MusicRenderer, PlaybackPosition, PlaybackPositionInfo,
ControlPoint, DeviceRegistryRead, MusicRendererBackend, PlaybackPosition, PlaybackPositionInfo,
PlaybackState, PlaybackStatus, VolumeControl,
};
@@ -34,10 +34,10 @@ fn main() -> io::Result<()> {
);
}
let linkplay_renderers: Vec<MusicRenderer> = renderers
let linkplay_renderers: Vec<MusicRendererBackend> = renderers
.iter()
.filter(|info| info.capabilities.has_linkplay_http)
.filter_map(|info| MusicRenderer::from_registry_info(info.clone(), &registry))
.filter_map(|info| MusicRendererBackend::from_renderer_info(info.clone(), &registry))
.collect();
if linkplay_renderers.is_empty() {

View File

@@ -10,8 +10,8 @@ use std::time::Duration;
use anyhow::{Context, Result};
use pmocontrol::model::TrackMetadata;
use pmocontrol::{
ControlPoint, DeviceRegistryRead, MediaBrowser, MediaEntry, MediaServerEvent, MediaServerInfo,
MusicRenderer, MusicServer, PlaybackItem, PlaybackPosition, PlaybackPositionInfo, RendererInfo,
ControlPoint, DeviceRegistryRead, MediaBrowser, MediaEntry, MediaServerEvent, UpnpMediaServer,
MusicRendererBackend, UpnpMediaServer, PlaybackItem, PlaybackPosition, PlaybackPositionInfo, RendererInfo,
};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
@@ -69,7 +69,7 @@ fn main() -> Result<()> {
server_info.friendly_name, server_info.location, server_info.id.0
);
let renderer_instance = MusicRenderer::from_registry_info(renderer.clone(), &registry)
let renderer_instance = MusicRendererBackend::from_renderer_info(renderer.clone(), &registry)
.expect("Selected renderer is not usable by MusicRenderer façade");
let supports_set_next = renderer_instance
.as_upnp()
@@ -82,7 +82,7 @@ fn main() -> Result<()> {
let timeout = Duration::from_secs(config.timeout_secs);
let server =
MusicServer::from_info(&server_info, timeout).context("Failed to init MusicServer")?;
UpnpMediaServer::from_info(&server_info, timeout).context("Failed to init MusicServer")?;
println!("Searching for a Live Playlist container in ContentDirectory...");
let live_playlist_container = find_live_playlist_container(&server)
@@ -355,8 +355,8 @@ fn pick_renderer(renderers: Vec<RendererInfo>) -> Option<RendererInfo> {
Some(selected)
}
fn pick_pmomusic_server(servers: Vec<MediaServerInfo>) -> Option<MediaServerInfo> {
let mut candidates: Vec<MediaServerInfo> = servers
fn pick_pmomusic_server(servers: Vec<UpnpMediaServer>) -> Option<UpnpMediaServer> {
let mut candidates: Vec<UpnpMediaServer> = servers
.into_iter()
.filter(|info| info.has_content_directory)
.filter(|info| info.content_directory_control_url.is_some())
@@ -381,7 +381,7 @@ fn pick_pmomusic_server(servers: Vec<MediaServerInfo>) -> Option<MediaServerInfo
Some(candidates.remove(0))
}
fn is_pmomusic_server(info: &MediaServerInfo) -> bool {
fn is_pmomusic_server(info: &UpnpMediaServer) -> bool {
let name = info.friendly_name.to_ascii_lowercase();
let model = info.model_name.to_ascii_lowercase();
let manufacturer = info.manufacturer.to_ascii_lowercase();
@@ -395,7 +395,7 @@ fn is_pmomusic_renderer(info: &RendererInfo) -> bool {
}
/// Search for a container whose title contains "Live Playlist" using BFS.
fn find_live_playlist_container(server: &MusicServer) -> Result<Option<MediaEntry>> {
fn find_live_playlist_container(server: &UpnpMediaServer) -> Result<Option<MediaEntry>> {
let root_entries = server
.browse_root()
.context("Failed to browse ContentDirectory root")?;
@@ -477,7 +477,7 @@ fn find_live_playlist_container(server: &MusicServer) -> Result<Option<MediaEntr
/// Collect playable items from a specific container.
/// Retries with fewer items if the initial browse times out.
fn collect_playable_items_from_container(
server: &MusicServer,
server: &UpnpMediaServer,
container_id: &str,
max_tracks: usize,
) -> Result<Vec<PlaybackItem>> {
@@ -532,7 +532,7 @@ fn collect_playable_items_from_container(
Ok(items)
}
fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<PlaybackItem> {
fn playback_item_from_entry(server: &UpnpMediaServer, entry: &MediaEntry) -> Option<PlaybackItem> {
// Skip live streams (we're looking for regular tracks in a live playlist)
if entry.title.to_ascii_lowercase().contains("live stream") {
return None;

View File

@@ -4,7 +4,7 @@ use std::time::{Duration, Instant};
use anyhow::Result;
use crossbeam_channel::RecvTimeoutError;
use pmocontrol::{ControlPoint, MediaServerEvent, MediaServerInfo, ServerId};
use pmocontrol::{ControlPoint, MediaServerEvent, UpnpMediaServer, ServerId};
const DISCOVERY_WAIT_SECS: u64 = 5;
const MONITOR_DURATION_SECS: u64 = 90;
@@ -33,7 +33,7 @@ fn main() -> Result<()> {
);
}
let mut cache: HashMap<ServerId, MediaServerInfo> = servers
let mut cache: HashMap<ServerId, UpnpMediaServer> = servers
.into_iter()
.map(|info| (info.id.clone(), info))
.collect();
@@ -64,7 +64,7 @@ fn main() -> Result<()> {
fn print_event(
cp: &ControlPoint,
cache: &mut HashMap<ServerId, MediaServerInfo>,
cache: &mut HashMap<ServerId, UpnpMediaServer>,
event: &MediaServerEvent,
) {
match event {
@@ -101,7 +101,7 @@ fn print_event(
fn describe_server(
cp: &ControlPoint,
cache: &mut HashMap<ServerId, MediaServerInfo>,
cache: &mut HashMap<ServerId, UpnpMediaServer>,
id: &ServerId,
) -> String {
if let Some(info) = cache.get(id) {

View File

@@ -5,7 +5,7 @@ use std::time::{Duration, Instant};
use anyhow::{Context, Result, anyhow};
use pmocontrol::{
DeviceDescriptionProvider, DiscoveredEndpoint, HttpXmlDescriptionProvider, MusicRenderer,
DeviceDescriptionProvider, DiscoveredEndpoint, HttpXmlDescriptionProvider, MusicRendererBackend,
RendererInfo,
control_point::ControlPoint,
openhome_client::{
@@ -86,7 +86,7 @@ fn auto_discover_renderer() -> Result<RendererInfo> {
let mut openhome_infos = Vec::new();
for renderer in renderers {
if let MusicRenderer::OpenHome(oh) = renderer {
if let MusicRendererBackend::OpenHome(oh) = renderer {
if seen.insert(oh.id().0.clone()) {
openhome_infos.push(oh.info.clone());
}

View File

@@ -10,8 +10,8 @@ use std::time::Duration;
use anyhow::{Context, Result};
use pmocontrol::model::TrackMetadata;
use pmocontrol::{
ControlPoint, DeviceRegistryRead, MediaBrowser, MediaEntry, MediaServerEvent, MediaServerInfo,
MusicRenderer, MusicServer, PlaybackItem, PlaybackPosition, PlaybackPositionInfo, RendererInfo,
ControlPoint, DeviceRegistryRead, MediaBrowser, MediaEntry, MediaServerEvent, UpnpMediaServer,
MusicRendererBackend, UpnpMediaServer, PlaybackItem, PlaybackPosition, PlaybackPositionInfo, RendererInfo,
};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
@@ -72,7 +72,7 @@ fn main() -> Result<()> {
server_info.friendly_name, server_info.location, server_info.id.0
);
let renderer_instance = MusicRenderer::from_registry_info(renderer.clone(), &registry)
let renderer_instance = MusicRendererBackend::from_renderer_info(renderer.clone(), &registry)
.expect("Selected renderer is not usable by MusicRenderer façade");
let supports_set_next = renderer_instance
.as_upnp()
@@ -85,7 +85,7 @@ fn main() -> Result<()> {
let timeout = Duration::from_secs(config.timeout_secs);
let server =
MusicServer::from_info(&server_info, timeout).context("Failed to init MusicServer")?;
UpnpMediaServer::from_info(&server_info, timeout).context("Failed to init MusicServer")?;
let root_entries = server
.browse_root()
@@ -322,8 +322,8 @@ fn pick_renderer(renderers: Vec<RendererInfo>) -> Option<RendererInfo> {
Some(selected)
}
fn pick_media_server(servers: Vec<MediaServerInfo>) -> Option<MediaServerInfo> {
let mut candidates: Vec<MediaServerInfo> = servers
fn pick_media_server(servers: Vec<UpnpMediaServer>) -> Option<UpnpMediaServer> {
let mut candidates: Vec<UpnpMediaServer> = servers
.into_iter()
.filter(|info| info.has_content_directory)
.filter(|info| info.content_directory_control_url.is_some())
@@ -346,7 +346,7 @@ fn pick_media_server(servers: Vec<MediaServerInfo>) -> Option<MediaServerInfo> {
}
}
fn is_pmomusic_server(info: &MediaServerInfo) -> bool {
fn is_pmomusic_server(info: &UpnpMediaServer) -> bool {
let name = info.friendly_name.to_ascii_lowercase();
let header = info.server_header.to_ascii_lowercase();
name.contains("pmomusic") || header.contains("pmomusic")
@@ -359,7 +359,7 @@ fn is_pmomusic_renderer(info: &RendererInfo) -> bool {
}
fn collect_playable_items_with_binding(
server: &MusicServer,
server: &UpnpMediaServer,
entries: &[MediaEntry],
max_tracks: usize,
) -> Result<(Vec<PlaybackItem>, Option<String>)> {
@@ -423,7 +423,7 @@ fn collect_playable_items_with_binding(
}
fn gather_items_from_entry(
server: &MusicServer,
server: &UpnpMediaServer,
entry: &MediaEntry,
max_tracks: usize,
depth: usize,
@@ -464,7 +464,7 @@ fn gather_items_from_entry(
Ok(())
}
fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<PlaybackItem> {
fn playback_item_from_entry(server: &UpnpMediaServer, entry: &MediaEntry) -> Option<PlaybackItem> {
if entry.title.to_ascii_lowercase().contains("live stream") {
return None;
}

View File

@@ -17,7 +17,7 @@ use pmocontrol::PlaybackPosition;
use pmocontrol::model::RendererInfo;
use pmocontrol::openhome_client::{OhInfoClient, OhPlaylistClient, OhTimeClient};
use pmocontrol::{
ControlPoint, MusicRenderer, PlaybackState, PlaybackStatus, RendererCapabilities,
ControlPoint, MusicRendererBackend, PlaybackState, PlaybackStatus, RendererCapabilities,
RendererProtocol, TransportControl, VolumeControl,
};
use std::env;
@@ -37,7 +37,7 @@ fn main() -> Result<()> {
thread::sleep(Duration::from_secs(5));
// 2. Snapshot of logical music renderers
let mut renderers: Vec<MusicRenderer> = cp.list_music_renderers();
let mut renderers: Vec<MusicRendererBackend> = cp.list_music_renderers();
// Filter out the in-dev PMOMusic renderer (if present)
renderers.retain(|r| {
@@ -276,20 +276,20 @@ fn print_openhome_details(prefix: &str, info: &RendererInfo) {
}
}
fn print_backend(prefix: &str, renderer: &MusicRenderer) {
fn print_backend(prefix: &str, renderer: &MusicRendererBackend) {
let backend = match renderer {
MusicRenderer::Upnp(_) => "UpnpRenderer (UPnP AV / DLNA)",
MusicRenderer::LinkPlay(_) => "LinkPlayRenderer (LinkPlay HTTP)",
MusicRenderer::ArylicTcp(_) => "ArylicTcpRenderer (ARylic TCP Protocol)",
MusicRenderer::HybridUpnpArylic { .. } => {
MusicRendererBackend::Upnp(_) => "UpnpRenderer (UPnP AV / DLNA)",
MusicRendererBackend::LinkPlay(_) => "LinkPlayRenderer (LinkPlay HTTP)",
MusicRendererBackend::ArylicTcp(_) => "ArylicTcpRenderer (ARylic TCP Protocol)",
MusicRendererBackend::HybridUpnpArylic { .. } => {
"Hybrid UpnpArylicRenderer (UPnP AV / DLNA + ARylic TCP Protocol)"
}
MusicRenderer::OpenHome(_) => "OpenHomeRenderer (native OpenHome stack)",
MusicRendererBackend::OpenHome(_) => "OpenHomeRenderer (native OpenHome stack)",
};
println!("{prefix}Backend : {backend}");
}
fn dump_renderer_state(renderer: &MusicRenderer, label: &str) -> Result<()> {
fn dump_renderer_state(renderer: &MusicRendererBackend, label: &str) -> Result<()> {
println!("\n[{label}]");
if let Ok(state) = renderer.playback_state() {
@@ -344,7 +344,7 @@ fn dump_renderer_state(renderer: &MusicRenderer, label: &str) -> Result<()> {
Ok(())
}
fn progress_monitor(renderer: &MusicRenderer, label: &str, iterations: usize, interval_secs: u64) {
fn progress_monitor(renderer: &MusicRendererBackend, label: &str, iterations: usize, interval_secs: u64) {
println!(
"\n[{label}] polling playback state/position {} times (every {} s)...",
iterations, interval_secs
@@ -370,7 +370,7 @@ fn progress_monitor(renderer: &MusicRenderer, label: &str, iterations: usize, in
}
}
fn volume_demo(renderer: &MusicRenderer) -> Result<()> {
fn volume_demo(renderer: &MusicRendererBackend) -> Result<()> {
// Try to get current volume
let original = renderer.volume()?;
println!(" Current music volume : {}", original);