diff --git a/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue b/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue
index 691a77aa..41b94ee0 100644
--- a/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue
+++ b/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue
@@ -2,7 +2,11 @@
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { usePlaybackStore } from '@/stores/playback'
-import type { RendererSummary, RendererState } from '@/services/pmocontrol/types'
+import type {
+ RendererCapabilitiesSummary,
+ RendererSummary,
+ RendererState,
+} from '@/services/pmocontrol/types'
import StatusBadge from './StatusBadge.vue'
import { Music, Volume2, VolumeX } from 'lucide-vue-next'
@@ -18,30 +22,44 @@ const metadata = computed(() => playbackStore.getTrackMetadata(props.renderer.id
const protocolLabel = computed(() => {
switch (props.renderer.protocol) {
- case 'UpnpAvOnly':
+ case 'upnp':
return 'UPnP AV'
- case 'OpenHomeOnly':
+ case 'openhome':
return 'OpenHome'
- case 'Hybrid':
- return 'Hybrid'
+ case 'hybrid':
+ return 'Hybrid (UPnP + OpenHome)'
default:
- return 'Unknown'
+ return 'Inconnu'
}
})
const protocolClass = computed(() => {
switch (props.renderer.protocol) {
- case 'UpnpAvOnly':
+ case 'upnp':
return 'protocol-upnp'
- case 'OpenHomeOnly':
+ case 'openhome':
return 'protocol-openhome'
- case 'Hybrid':
+ case 'hybrid':
return 'protocol-hybrid'
default:
return 'protocol-unknown'
}
})
+const capabilityBadges = computed(() => {
+ const caps = props.renderer.capabilities
+ if (!caps) return []
+ const mapping: Array<{ key: keyof RendererCapabilitiesSummary; label: string }> = [
+ { key: 'has_avtransport', label: 'AVTransport' },
+ { key: 'has_oh_playlist', label: 'OpenHome' },
+ { key: 'has_linkplay_http', label: 'Hybrid' },
+ { key: 'has_oh_volume', label: 'Vol' },
+ { key: 'has_oh_time', label: 'Time' },
+ { key: 'has_oh_info', label: 'Info' },
+ ]
+ return mapping.filter(({ key }) => caps[key]).map(({ key, label }) => ({ key, label }))
+})
+
const hasCover = computed(() => !!metadata.value?.album_art_uri)
function goToRenderer() {
@@ -66,6 +84,12 @@ function goToRenderer() {
+
+
+ {{ badge.label }}
+
+
+
![]()
{
+ const resp = await fetch(
+ `${API_BASE}/renderers/${encodeURIComponent(rendererId)}/oh/playlist`,
+ )
+ if (!resp.ok) {
+ throw new Error(`Failed to fetch OpenHome playlist: ${resp.status} ${resp.statusText}`)
+ }
+ return resp.json()
+}
+
+export async function clearOpenHomePlaylist(rendererId: string): Promise
{
+ const resp = await fetch(
+ `${API_BASE}/renderers/${encodeURIComponent(rendererId)}/oh/playlist/clear`,
+ { method: 'POST' },
+ )
+ if (!resp.ok) {
+ throw new Error(`Failed to clear OpenHome playlist: ${resp.status} ${resp.statusText}`)
+ }
+}
+
+export async function addOpenHomeTrack(
+ rendererId: string,
+ payload: OpenHomePlaylistAddRequest,
+): Promise {
+ const resp = await fetch(
+ `${API_BASE}/renderers/${encodeURIComponent(rendererId)}/oh/playlist/add`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ },
+ )
+ if (!resp.ok) {
+ throw new Error(`Failed to add track to OpenHome playlist: ${resp.status} ${resp.statusText}`)
+ }
+}
+
+export async function playOpenHomeTrack(rendererId: string, trackId: number): Promise {
+ const resp = await fetch(
+ `${API_BASE}/renderers/${encodeURIComponent(
+ rendererId,
+ )}/oh/playlist/play/${encodeURIComponent(trackId.toString())}`,
+ { method: 'POST' },
+ )
+ if (!resp.ok) {
+ throw new Error(`Failed to play OpenHome track ${trackId}: ${resp.status} ${resp.statusText}`)
+ }
+}
diff --git a/pmoapp/webapp/src/services/pmocontrol/types.ts b/pmoapp/webapp/src/services/pmocontrol/types.ts
index 0becbf5d..57b3e097 100644
--- a/pmoapp/webapp/src/services/pmocontrol/types.ts
+++ b/pmoapp/webapp/src/services/pmocontrol/types.ts
@@ -5,11 +5,28 @@
// RENDERERS
// ============================================================================
+export type RendererProtocolSummary = 'upnp' | 'openhome' | 'hybrid'
+
+export interface RendererCapabilitiesSummary {
+ has_avtransport: boolean
+ has_avtransport_set_next: boolean
+ has_rendering_control: boolean
+ has_connection_manager: boolean
+ has_linkplay_http: boolean
+ has_arylic_tcp: boolean
+ has_oh_playlist: boolean
+ has_oh_volume: boolean
+ has_oh_info: boolean
+ has_oh_time: boolean
+ has_oh_radio: boolean
+}
+
export interface RendererSummary {
id: string
friendly_name: string
model_name: string
- protocol: 'UpnpAvOnly' | 'OpenHomeOnly' | 'Hybrid'
+ protocol: RendererProtocolSummary
+ capabilities: RendererCapabilitiesSummary
online: boolean
}
@@ -51,6 +68,32 @@ export interface QueueSnapshot {
current_index: number | null // Index de la piste en cours (null si rien en lecture)
}
+// ============================================================================
+// OPENHOME PLAYLIST
+// ============================================================================
+
+export interface OpenHomePlaylistTrack {
+ id: number
+ uri: string
+ title: string | null
+ artist: string | null
+ album: string | null
+ album_art_uri: string | null
+}
+
+export interface OpenHomePlaylistSnapshot {
+ renderer_id: string
+ current_id: number | null
+ tracks: OpenHomePlaylistTrack[]
+}
+
+export interface OpenHomePlaylistAddRequest {
+ uri: string
+ metadata: string
+ after_id?: number | null
+ play?: boolean
+}
+
// ============================================================================
// MEDIA SERVERS
// ============================================================================
diff --git a/pmoapp/webapp/src/views/RendererView.vue b/pmoapp/webapp/src/views/RendererView.vue
index 6f7ba4c7..dc13fa80 100644
--- a/pmoapp/webapp/src/views/RendererView.vue
+++ b/pmoapp/webapp/src/views/RendererView.vue
@@ -1,5 +1,5 @@
@@ -106,6 +201,65 @@ const protocolLabel = computed(() => {
+
+
+ OpenHome Playlist
+
+ Chargement de la playlist…
+ {{ ohError }}
+
+
+ Playlist vide.
+
+
+
+
+
+
+ | # |
+ Titre |
+ Artiste |
+ Album |
+ Actions |
+
+
+
+
+ | {{ track.id }} |
+ {{ track.title || '—' }} |
+ {{ track.artist || '—' }} |
+ {{ track.album || '—' }} |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -244,6 +398,77 @@ const protocolLabel = computed(() => {
margin: 0 0 var(--spacing-md);
}
+.openhome-playlist h2 {
+ margin: 0 0 var(--spacing-md);
+ font-size: var(--text-lg);
+}
+
+.oh-playlist-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: var(--spacing-md);
+}
+
+.oh-playlist-table th,
+.oh-playlist-table td {
+ padding: var(--spacing-xs);
+ border-bottom: 1px solid var(--color-border);
+ font-size: var(--text-sm);
+}
+
+.oh-playlist-table tbody tr:hover {
+ background-color: var(--color-bg-tertiary);
+}
+
+.oh-playlist-table tr.current {
+ background-color: rgba(16, 185, 129, 0.15);
+}
+
+.actions-cell {
+ text-align: center;
+}
+
+.btn-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+}
+
+.oh-controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-md);
+}
+
+.oh-add-form {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+}
+
+.oh-add-form input,
+.oh-add-form textarea {
+ width: 100%;
+ padding: var(--spacing-sm);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background-color: var(--color-bg-tertiary);
+ color: var(--color-text);
+}
+
+.oh-add-form button {
+ align-self: flex-start;
+}
+
+.error {
+ color: var(--status-error, #dc2626);
+ font-weight: 600;
+}
+
/* Responsive - Desktop */
@media (min-width: 1024px) {
.renderer-content {
diff --git a/pmocontrol/examples/event_demo.rs b/pmocontrol/examples/event_demo.rs
index bf3e7ac5..471577a7 100644
--- a/pmocontrol/examples/event_demo.rs
+++ b/pmocontrol/examples/event_demo.rs
@@ -19,11 +19,11 @@ use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use pmocontrol::model::TrackMetadata;
+use pmocontrol::openhome_renderer::{format_seconds, map_openhome_state};
use pmocontrol::{
ControlPoint, DeviceRegistryRead, PlaybackPositionInfo, PlaybackState, RendererEvent,
RendererId, RendererInfo,
};
-use pmocontrol::openhome_renderer::{format_seconds, map_openhome_state};
fn main() -> io::Result<()> {
// Logging simple (tracing_subscriber est déjà utilisé dans les autres exemples)
@@ -200,10 +200,7 @@ fn print_openhome_summary(prefix: &str, info: &RendererInfo, cp: &ControlPoint)
match client.transport_state() {
Ok(state) => {
let logical = map_openhome_state(&state);
- println!(
- "{prefix} Transport state : {} ({:?})",
- state, logical
- );
+ println!("{prefix} Transport state : {} ({:?})", state, logical);
}
Err(err) => println!("{prefix} Transport state : "),
}
diff --git a/pmocontrol/examples/pmo_remote_control.rs b/pmocontrol/examples/pmo_remote_control.rs
index d0afb9b5..45a12577 100644
--- a/pmocontrol/examples/pmo_remote_control.rs
+++ b/pmocontrol/examples/pmo_remote_control.rs
@@ -8,28 +8,28 @@ use std::fs::{File, OpenOptions};
use std::io::{self, Stdout, Write};
use std::process;
use std::sync::mpsc::TryRecvError;
-use std::sync::{mpsc, Arc, Mutex};
+use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::{Duration, Instant};
-use anyhow::{anyhow, bail, Context, Result};
+use anyhow::{Context, Result, anyhow, bail};
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent};
use crossterm::execute;
use crossterm::terminal::{
- disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
+ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
-use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
+use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
+use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph};
-use ratatui::Terminal;
use serde::de::{DeserializeOwned, Deserializer};
use serde::{Deserialize, Serialize};
use tracing::info;
-use tracing_subscriber::fmt::writer::BoxMakeWriter;
use tracing_subscriber::EnvFilter;
+use tracing_subscriber::fmt::writer::BoxMakeWriter;
use ureq::http;
use ureq::{Agent, Body};
diff --git a/pmocontrol/examples/renderer_demo.rs b/pmocontrol/examples/renderer_demo.rs
index d1741b34..06da20b4 100644
--- a/pmocontrol/examples/renderer_demo.rs
+++ b/pmocontrol/examples/renderer_demo.rs
@@ -14,12 +14,12 @@
use anyhow::Result;
use pmocontrol::PlaybackPosition;
+use pmocontrol::model::RendererInfo;
+use pmocontrol::openhome_client::{OhInfoClient, OhPlaylistClient, OhTimeClient};
use pmocontrol::{
ControlPoint, MusicRenderer, PlaybackState, PlaybackStatus, RendererCapabilities,
RendererProtocol, TransportControl, VolumeControl,
};
-use pmocontrol::model::RendererInfo;
-use pmocontrol::openhome_renderer::{format_seconds, map_openhome_state};
use std::env;
use std::thread;
use std::time::Duration;
@@ -85,7 +85,7 @@ fn main() -> Result<()> {
);
print_backend(" ", r);
print_capabilities(" ", &info.capabilities, &info.protocol);
- print_openhome_details(" ", info, &cp);
+ print_openhome_details(" ", info);
}
// 5. Select renderer
@@ -101,7 +101,7 @@ fn main() -> Result<()> {
println!(" Protocol : {:?}", info.protocol);
print_backend(" ", renderer);
print_capabilities(" ", &info.capabilities, &info.protocol);
- print_openhome_details(" ", info, &cp);
+ print_openhome_details(" ", info);
if let Some(upnp) = renderer.as_upnp() {
println!(
@@ -228,7 +228,7 @@ fn print_capabilities(prefix: &str, caps: &RendererCapabilities, proto: &Rendere
println!("{prefix} OH Radio : {}", caps.has_oh_radio);
}
-fn print_openhome_details(prefix: &str, info: &RendererInfo, cp: &ControlPoint) {
+fn print_openhome_details(prefix: &str, info: &RendererInfo) {
if !info.capabilities.has_oh_playlist
&& !info.capabilities.has_oh_info
&& !info.capabilities.has_oh_time
@@ -236,12 +236,9 @@ fn print_openhome_details(prefix: &str, info: &RendererInfo, cp: &ControlPoint)
return;
}
- let registry = cp.registry();
- let reg = registry.read().unwrap();
- let playlist_client = reg.oh_playlist_client_for_renderer(&info.id);
- let info_client = reg.oh_info_client_for_renderer(&info.id);
- let time_client = reg.oh_time_client_for_renderer(&info.id);
- drop(reg);
+ let playlist_client = build_playlist_client(info);
+ let info_client = build_info_client(info);
+ let time_client = build_time_client(info);
if playlist_client.is_none() && info_client.is_none() && time_client.is_none() {
return;
@@ -260,10 +257,7 @@ fn print_openhome_details(prefix: &str, info: &RendererInfo, cp: &ControlPoint)
match client.transport_state() {
Ok(state) => {
let logical = map_openhome_state(&state);
- println!(
- "{prefix} Transport state : {} ({:?})",
- state, logical
- );
+ println!("{prefix} Transport state : {} ({:?})", state, logical);
}
Err(err) => println!("{prefix} Transport state : "),
}
@@ -290,6 +284,7 @@ fn print_backend(prefix: &str, renderer: &MusicRenderer) {
MusicRenderer::HybridUpnpArylic { .. } => {
"Hybrid UpnpArylicRenderer (UPnP AV / DLNA + ARylic TCP Protocol)"
}
+ MusicRenderer::OpenHome(_) => "OpenHomeRenderer (native OpenHome stack)",
};
println!("{prefix}Backend : {backend}");
}
@@ -407,3 +402,41 @@ fn volume_demo(renderer: &MusicRenderer) -> Result<()> {
Ok(())
}
+
+fn build_playlist_client(info: &RendererInfo) -> Option {
+ let control_url = info.oh_playlist_control_url.as_ref()?;
+ let service_type = info.oh_playlist_service_type.as_ref()?;
+ Some(OhPlaylistClient::new(
+ control_url.clone(),
+ service_type.clone(),
+ ))
+}
+
+fn build_info_client(info: &RendererInfo) -> Option {
+ let control_url = info.oh_info_control_url.as_ref()?;
+ let service_type = info.oh_info_service_type.as_ref()?;
+ Some(OhInfoClient::new(control_url.clone(), service_type.clone()))
+}
+
+fn build_time_client(info: &RendererInfo) -> Option {
+ let control_url = info.oh_time_control_url.as_ref()?;
+ let service_type = info.oh_time_service_type.as_ref()?;
+ Some(OhTimeClient::new(control_url.clone(), service_type.clone()))
+}
+
+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()),
+ }
+}
+
+fn format_seconds(seconds: u32) -> String {
+ let hours = seconds / 3600;
+ let minutes = (seconds % 3600) / 60;
+ let secs = seconds % 60;
+ format!("{hours:02}:{minutes:02}:{secs:02}")
+}
diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs
index 6476795b..66b56392 100644
--- a/pmocontrol/src/control_point.rs
+++ b/pmocontrol/src/control_point.rs
@@ -6,9 +6,10 @@ use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant};
-use anyhow::{anyhow, Context};
+use anyhow::{Context, anyhow};
use crossbeam_channel::{Receiver, Sender, unbounded};
use pmoupnp::ssdp::SsdpClient;
+use thiserror::Error;
use tracing::{debug, error, info, warn};
use ureq::{Agent, http};
use xmltree::{Element, XMLNode};
@@ -26,7 +27,8 @@ use crate::media_server::{
use crate::media_server_events::spawn_media_server_event_runtime;
use crate::model::TrackMetadata;
use crate::model::{MediaServerEvent, RendererEvent, RendererId, RendererInfo};
-use crate::openhome_client::{OhPlaylistClient, OhTrackEntry, parse_track_metadata_from_didl};
+use crate::openhome_client::parse_track_metadata_from_didl;
+use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
use crate::openhome_renderer::{format_seconds, map_openhome_state};
use crate::playback_queue::{PlaybackItem, PlaybackQueue};
use crate::provider::HttpXmlDescriptionProvider;
@@ -54,6 +56,14 @@ pub struct PlaylistBinding {
pub(crate) auto_play_on_refresh: bool,
}
+#[derive(Debug, Error)]
+pub enum OpenHomeAccessError {
+ #[error("Renderer {0} not found")]
+ RendererNotFound(String),
+ #[error("Renderer {0} has no OpenHome playlist service")]
+ PlaylistNotSupported(String),
+}
+
/// Control point minimal :
/// - lance un SsdpClient dans un thread,
/// - passe les SsdpEvent au DiscoveryManager,
@@ -164,9 +174,7 @@ impl ControlPoint {
};
let previous_backend = runtime_cp.runtime.playlist_backend(&info.id);
if previous_backend != backend {
- runtime_cp
- .runtime
- .set_playlist_backend(&info.id, backend);
+ runtime_cp.runtime.set_playlist_backend(&info.id, backend);
if matches!(backend, PlaylistBackend::OpenHome) {
if let Err(err) = sync_openhome_playlist(
&runtime_cp.registry,
@@ -217,10 +225,12 @@ impl ControlPoint {
artist = metadata.artist.as_deref(),
"Emitting metadata changed event"
);
- runtime_cp.emit_renderer_event(RendererEvent::MetadataChanged {
- id: renderer_id.clone(),
- metadata: metadata.clone(),
- });
+ runtime_cp.emit_renderer_event(
+ RendererEvent::MetadataChanged {
+ id: renderer_id.clone(),
+ metadata: metadata.clone(),
+ },
+ );
new_snapshot.last_metadata = Some(metadata);
}
}
@@ -568,8 +578,8 @@ impl ControlPoint {
self.detach_binding_on_user_mutation(renderer_id, "clear_queue");
if self.runtime.uses_openhome_playlist(renderer_id) {
- let client = self.openhome_playlist_client(renderer_id)?;
- client.delete_all()?;
+ let renderer = self.openhome_renderer(renderer_id)?;
+ renderer.openhome_playlist_clear()?;
self.sync_openhome_playlist_for(renderer_id)?;
debug!(
renderer = renderer_id.0.as_str(),
@@ -695,6 +705,45 @@ impl ControlPoint {
self.sync_openhome_playlist_for(renderer_id)
}
+ pub fn get_openhome_playlist_snapshot(
+ &self,
+ renderer_id: &RendererId,
+ ) -> anyhow::Result {
+ let renderer = self.openhome_renderer(renderer_id)?;
+ renderer.openhome_playlist_snapshot()
+ }
+
+ pub fn clear_openhome_playlist(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
+ let renderer = self.openhome_renderer(renderer_id)?;
+ renderer.openhome_playlist_clear()?;
+ self.sync_openhome_playlist_for(renderer_id)
+ }
+
+ pub fn add_openhome_track(
+ &self,
+ renderer_id: &RendererId,
+ uri: &str,
+ metadata: &str,
+ after_id: Option,
+ play: bool,
+ ) -> anyhow::Result<()> {
+ let renderer = self.openhome_renderer(renderer_id)?;
+ renderer.openhome_playlist_add_track(uri, metadata, after_id, play)?;
+ self.sync_openhome_playlist_for(renderer_id)
+ }
+
+ pub fn play_openhome_track_id(
+ &self,
+ renderer_id: &RendererId,
+ track_id: u32,
+ ) -> anyhow::Result<()> {
+ let renderer = self.openhome_renderer(renderer_id)?;
+ renderer.openhome_playlist_play_id(track_id)?;
+ self.runtime
+ .set_playback_source(renderer_id, PlaybackSource::FromQueue);
+ self.sync_openhome_playlist_for(renderer_id)
+ }
+
/// Play the current item from the queue without advancing the index.
///
/// This is useful after a Stop operation to resume playback from the current
@@ -917,10 +966,7 @@ impl ControlPoint {
anyhow!("Renderer {} not found", renderer_id.0)
})?;
- debug!(
- renderer = renderer_id.0.as_str(),
- "User-requested stop"
- );
+ debug!(renderer = renderer_id.0.as_str(), "User-requested stop");
renderer.stop()
}
@@ -1141,17 +1187,18 @@ impl ControlPoint {
)
}
- fn sync_openhome_playlist_for(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
- sync_openhome_playlist(&self.registry, &self.runtime, &self.event_bus, renderer_id)
+ fn openhome_renderer(&self, renderer_id: &RendererId) -> anyhow::Result {
+ let renderer = self
+ .music_renderer_by_id(renderer_id)
+ .ok_or_else(|| OpenHomeAccessError::RendererNotFound(renderer_id.0.clone()))?;
+ if !renderer.info().capabilities.has_oh_playlist {
+ return Err(OpenHomeAccessError::PlaylistNotSupported(renderer_id.0.clone()).into());
+ }
+ Ok(renderer)
}
- fn openhome_playlist_client(
- &self,
- renderer_id: &RendererId,
- ) -> anyhow::Result {
- let reg = self.registry.read().unwrap();
- reg.oh_playlist_client_for_renderer(renderer_id)
- .ok_or_else(|| anyhow!("Renderer {} has no OpenHome playlist", renderer_id.0))
+ fn sync_openhome_playlist_for(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
+ sync_openhome_playlist(&self.registry, &self.runtime, &self.event_bus, renderer_id)
}
fn enqueue_items_openhome(
@@ -1163,16 +1210,16 @@ impl ControlPoint {
return Ok(());
}
- let client = self.openhome_playlist_client(renderer_id)?;
+ let renderer = self.openhome_renderer(renderer_id)?;
let mut after_id = self
.runtime
.queue_full_snapshot(renderer_id)
- .and_then(|(queue, _)| queue.last().and_then(openhome_track_id_from_item))
- .unwrap_or(0);
+ .and_then(|(queue, _)| queue.last().and_then(openhome_track_id_from_item));
for item in items.iter() {
let metadata = item.to_didl_metadata();
- after_id = client.insert(after_id, &item.uri, &metadata)?;
+ after_id =
+ Some(renderer.openhome_playlist_add_track(&item.uri, &metadata, after_id, false)?);
}
self.sync_openhome_playlist_for(renderer_id)?;
@@ -1192,28 +1239,49 @@ impl ControlPoint {
let track_id = openhome_track_id_from_item(&item)
.ok_or_else(|| anyhow!("Current OpenHome item has no track id"))?;
- let client = self.openhome_playlist_client(renderer_id)?;
- client.play_id(track_id)?;
+ let renderer = self.openhome_renderer(renderer_id)?;
+ renderer.openhome_playlist_play_id(track_id)?;
self.runtime
.set_playback_source(renderer_id, PlaybackSource::FromQueue);
self.sync_openhome_playlist_for(renderer_id)?;
info!(
renderer = renderer_id.0.as_str(),
- track_id,
- "Started OpenHome playlist playback (current item)"
+ track_id, "Started OpenHome playlist playback (current item)"
);
Ok(())
}
fn play_next_openhome(&self, renderer_id: &RendererId) -> anyhow::Result<()> {
- let client = self.openhome_playlist_client(renderer_id)?;
- client.next()?;
+ let renderer = self.openhome_renderer(renderer_id)?;
+ let (queue, current_index) = self
+ .runtime
+ .queue_full_snapshot(renderer_id)
+ .ok_or_else(|| Self::runtime_entry_missing(renderer_id))?;
+
+ let next_item = match current_index {
+ Some(idx) => queue.get(idx + 1),
+ None => queue.first(),
+ };
+
+ let Some(item) = next_item 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(());
+ };
+
+ let track_id = openhome_track_id_from_item(item)
+ .ok_or_else(|| anyhow!("Next OpenHome item has no 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)?;
info!(
renderer = renderer_id.0.as_str(),
- "Advanced OpenHome playlist to next track"
+ track_id, "Advanced OpenHome playlist to next track"
);
Ok(())
}
@@ -1389,10 +1457,7 @@ impl RuntimeState {
}
fn uses_openhome_playlist(&self, id: &RendererId) -> bool {
- matches!(
- self.playlist_backend(id),
- PlaylistBackend::OpenHome
- )
+ matches!(self.playlist_backend(id), PlaylistBackend::OpenHome)
}
fn mark_user_stop_requested(&self, id: &RendererId) {
@@ -1688,21 +1753,13 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<
const OPENHOME_TRACK_PREFIX: &str = "openhome:";
-fn playback_item_from_openhome_entry(entry: &OhTrackEntry) -> PlaybackItem {
- let mut item = PlaybackItem::new(entry.uri.clone());
- item.object_id = Some(format!("{}{}", OPENHOME_TRACK_PREFIX, entry.id));
-
- if let Some(metadata) = parse_track_metadata_from_didl(&entry.metadata_xml) {
- item.title = metadata.title;
- item.artist = metadata.artist;
- item.album = metadata.album;
- item.genre = metadata.genre;
- item.album_art_uri = metadata.album_art_uri;
- item.date = metadata.date;
- item.track_number = metadata.track_number;
- item.creator = metadata.creator;
- }
-
+fn playback_item_from_openhome_track(track: &OpenHomePlaylistTrack) -> PlaybackItem {
+ let mut item = PlaybackItem::new(track.uri.clone());
+ item.object_id = Some(format!("{}{}", OPENHOME_TRACK_PREFIX, track.id));
+ item.title = track.title.clone();
+ item.artist = track.artist.clone();
+ item.album = track.album.clone();
+ item.album_art_uri = track.album_art_uri.clone();
item
}
@@ -1718,23 +1775,28 @@ fn sync_openhome_playlist(
event_bus: &RendererEventBus,
renderer_id: &RendererId,
) -> anyhow::Result<()> {
- let (playlist_client, info_client) = {
- let reg = registry.read().unwrap();
- let playlist = reg
- .oh_playlist_client_for_renderer(renderer_id)
- .ok_or_else(|| anyhow!("Renderer has no OpenHome playlist service"))?;
- let info = reg.oh_info_client_for_renderer(renderer_id);
- (playlist, info)
+ let renderer = {
+ let info = {
+ let reg = registry.read().unwrap();
+ reg.get_renderer(renderer_id)
+ .ok_or_else(|| OpenHomeAccessError::RendererNotFound(renderer_id.0.clone()))?
+ };
+ MusicRenderer::from_registry_info(info, registry)
+ .and_then(|r| match r {
+ MusicRenderer::OpenHome(_) => Some(r),
+ _ => None,
+ })
+ .ok_or_else(|| OpenHomeAccessError::PlaylistNotSupported(renderer_id.0.clone()))?
};
- let track_entries = playlist_client.read_all_tracks()?;
- let playback_items: Vec = track_entries
+ let snapshot = renderer.openhome_playlist_snapshot()?;
+ let playback_items: Vec = snapshot
+ .tracks
.iter()
- .map(playback_item_from_openhome_entry)
+ .map(playback_item_from_openhome_track)
.collect();
- let current_id = info_client
- .and_then(|client| client.id().ok());
+ let current_id = snapshot.current_id;
let current_index = current_id.and_then(|id| {
playback_items
@@ -1904,19 +1966,16 @@ impl OpenHomeEventRuntime {
info: RendererInfo,
event_url: String,
) {
- let entry = self
- .subscriptions
- .entry(key.clone())
- .or_insert_with(|| OpenHomeSubscriptionState::new(info.clone(), key.service, event_url.clone()));
+ let entry = self.subscriptions.entry(key.clone()).or_insert_with(|| {
+ OpenHomeSubscriptionState::new(info.clone(), key.service, event_url.clone())
+ });
entry.update(info, event_url);
self.path_index
.insert(entry.callback_path.clone(), key.clone());
if entry.sid.is_none() && entry.should_retry() {
- if let Err(err) =
- Self::subscribe_entry(self.listener_port, self.http_timeout, entry)
- {
+ if let Err(err) = Self::subscribe_entry(self.listener_port, self.http_timeout, entry) {
warn!(
renderer = entry.renderer.friendly_name.as_str(),
service = entry.service.as_str(),
@@ -1977,18 +2036,19 @@ impl OpenHomeEventRuntime {
}
}
- let properties = parse_openhome_propertyset(&entry.renderer.id, &entry.service, ¬ify.body);
+ let properties =
+ parse_openhome_propertyset(&entry.renderer.id, &entry.service, ¬ify.body);
if properties.is_empty() {
return;
}
match entry.service {
OpenHomeServiceKind::Playlist => {
- if properties.iter().any(|(name, _)| is_id_array_property(name)) {
- if self
- .runtime
- .uses_openhome_playlist(&entry.renderer.id)
- {
+ if properties
+ .iter()
+ .any(|(name, _)| is_id_array_property(name))
+ {
+ if self.runtime.uses_openhome_playlist(&entry.renderer.id) {
if let Err(err) = sync_openhome_playlist(
&self.registry,
&self.runtime,
@@ -2013,11 +2073,7 @@ impl OpenHomeEventRuntime {
}
}
- fn handle_info_properties(
- &self,
- renderer_id: &RendererId,
- properties: Vec<(String, String)>,
- ) {
+ fn handle_info_properties(&self, renderer_id: &RendererId, properties: Vec<(String, String)>) {
let mut metadata_xml: Option = None;
let mut transport_state: Option = None;
let mut track_id: Option = None;
@@ -2090,11 +2146,7 @@ impl OpenHomeEventRuntime {
}
}
- fn handle_time_properties(
- &self,
- renderer_id: &RendererId,
- properties: Vec<(String, String)>,
- ) {
+ fn handle_time_properties(&self, renderer_id: &RendererId, properties: Vec<(String, String)>) {
let mut duration: Option = None;
let mut seconds: Option = None;
@@ -2123,10 +2175,9 @@ impl OpenHomeEventRuntime {
new_position.rel_time = Some(format_seconds(s));
}
- self.runtime
- .update_snapshot_with(renderer_id, |snapshot| {
- snapshot.position = Some(new_position.clone());
- });
+ self.runtime.update_snapshot_with(renderer_id, |snapshot| {
+ snapshot.position = Some(new_position.clone());
+ });
let _ = self.event_tx.send(RendererEvent::PositionChanged {
id: renderer_id.clone(),
@@ -2175,10 +2226,7 @@ impl OpenHomeEventRuntime {
let response = build_agent(http_timeout).run(request)?;
if !response.status().is_success() {
- anyhow::bail!(
- "SUBSCRIBE returned HTTP {}",
- response.status()
- );
+ anyhow::bail!("SUBSCRIBE returned HTTP {}", response.status());
}
let sid = response
@@ -2226,10 +2274,7 @@ impl OpenHomeEventRuntime {
.map_err(anyhow::Error::new)?;
let response = build_agent(http_timeout).run(request)?;
if !response.status().is_success() {
- anyhow::bail!(
- "SUBSCRIBE renewal failed with {}",
- response.status()
- );
+ anyhow::bail!("SUBSCRIBE renewal failed with {}", response.status());
}
let timeout = parse_timeout(
response
@@ -2332,9 +2377,7 @@ impl OpenHomeSubscriptionState {
}
fn update(&mut self, renderer: RendererInfo, event_url: String) {
- if self.renderer.location != renderer.location
- || self.event_sub_url != event_url
- {
+ if self.renderer.location != renderer.location || self.event_sub_url != event_url {
self.event_sub_url = event_url;
self.sid = None;
self.expires_at = None;
@@ -2364,15 +2407,15 @@ struct OpenHomeIncomingNotify {
body: Vec,
}
-fn run_openhome_http_listener(
- listener: TcpListener,
- notify_tx: Sender,
-) {
+fn run_openhome_http_listener(listener: TcpListener, notify_tx: Sender) {
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
if let Err(err) = stream.set_read_timeout(Some(Duration::from_secs(5))) {
- warn!("Failed to set read timeout on OpenHome notify connection: {}", err);
+ warn!(
+ "Failed to set read timeout on OpenHome notify connection: {}",
+ err
+ );
}
match read_openhome_http_request(&mut stream) {
@@ -2495,11 +2538,7 @@ fn build_openhome_callback_path(id: &RendererId, service: OpenHomeServiceKind) -
id.hash(&mut hasher);
service.hash(&mut hasher);
let suffix = hasher.finish();
- format!(
- "/openhome-events/{}/{:x}",
- service.as_str(),
- suffix
- )
+ format!("/openhome-events/{}/{:x}", service.as_str(), suffix)
}
fn parse_openhome_propertyset(
@@ -2536,9 +2575,7 @@ fn parse_openhome_propertyset(
}
fn is_id_array_property(name: &str) -> bool {
- name.trim()
- .to_ascii_lowercase()
- .ends_with("idarray")
+ name.trim().to_ascii_lowercase().ends_with("idarray")
}
fn empty_playback_position() -> PlaybackPositionInfo {
diff --git a/pmocontrol/src/lib.rs b/pmocontrol/src/lib.rs
index a738b196..3715d8cb 100644
--- a/pmocontrol/src/lib.rs
+++ b/pmocontrol/src/lib.rs
@@ -12,6 +12,7 @@ pub mod media_server;
pub mod model;
pub mod music_renderer;
pub mod openhome_client;
+pub mod openhome_playlist;
pub mod openhome_renderer;
pub mod playback_queue;
pub mod provider;
@@ -45,6 +46,7 @@ pub use media_server::{
UpnpMediaServer,
};
pub use music_renderer::MusicRenderer;
+pub use openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
pub use openhome_renderer::OpenHomeRenderer;
pub use playback_queue::{PlaybackItem, PlaybackQueue};
pub use rendering_control_client::RenderingControlClient;
diff --git a/pmocontrol/src/music_renderer.rs b/pmocontrol/src/music_renderer.rs
index cd63cdfe..32b74219 100644
--- a/pmocontrol/src/music_renderer.rs
+++ b/pmocontrol/src/music_renderer.rs
@@ -10,6 +10,7 @@ use std::sync::{Arc, RwLock};
use crate::capabilities::{PlaybackPositionInfo, PlaybackStatus};
use crate::model::{RendererId, RendererInfo, RendererProtocol};
+use crate::openhome_playlist::OpenHomePlaylistSnapshot;
use crate::{
ArylicTcpRenderer, DeviceRegistry, LinkPlayRenderer, OpenHomeRenderer, PlaybackPosition,
PlaybackState, TransportControl, UpnpRenderer, VolumeControl,
@@ -101,10 +102,12 @@ impl MusicRenderer {
info: RendererInfo,
registry: &Arc>,
) -> Option {
- if matches!(info.protocol, RendererProtocol::OpenHomeOnly | RendererProtocol::Hybrid) {
+ if matches!(
+ info.protocol,
+ RendererProtocol::OpenHomeOnly | RendererProtocol::Hybrid
+ ) {
if let Some(renderer) = {
- let reg = registry.read().unwrap();
- let renderer = OpenHomeRenderer::new(info.clone(), &*reg);
+ let renderer = OpenHomeRenderer::new(info.clone());
renderer.has_any_openhome_service().then_some(renderer)
} {
return Some(MusicRenderer::OpenHome(renderer));
@@ -157,6 +160,64 @@ impl MusicRenderer {
RendererProtocol::OpenHomeOnly => None,
}
}
+
+ pub fn openhome_playlist_snapshot(&self) -> Result {
+ match self {
+ MusicRenderer::OpenHome(renderer) => renderer.snapshot_openhome_playlist(),
+ _ => Err(op_not_supported(
+ "openhome_playlist_snapshot",
+ self.unsupported_backend_name(),
+ )),
+ }
+ }
+
+ pub fn openhome_playlist_clear(&self) -> Result<()> {
+ match self {
+ MusicRenderer::OpenHome(renderer) => renderer.clear_openhome_playlist(),
+ _ => Err(op_not_supported(
+ "openhome_playlist_clear",
+ self.unsupported_backend_name(),
+ )),
+ }
+ }
+
+ pub fn openhome_playlist_add_track(
+ &self,
+ uri: &str,
+ metadata: &str,
+ after_id: Option,
+ play: bool,
+ ) -> Result {
+ match self {
+ MusicRenderer::OpenHome(renderer) => {
+ renderer.add_track_openhome(uri, metadata, after_id, play)
+ }
+ _ => Err(op_not_supported(
+ "openhome_playlist_add_track",
+ self.unsupported_backend_name(),
+ )),
+ }
+ }
+
+ pub fn openhome_playlist_play_id(&self, id: u32) -> Result<()> {
+ match self {
+ MusicRenderer::OpenHome(renderer) => renderer.play_openhome_track_id(id),
+ _ => Err(op_not_supported(
+ "openhome_playlist_play_id",
+ self.unsupported_backend_name(),
+ )),
+ }
+ }
+
+ fn unsupported_backend_name(&self) -> &'static str {
+ match self {
+ MusicRenderer::Upnp(_) => "UPnP",
+ MusicRenderer::OpenHome(_) => "OpenHome",
+ MusicRenderer::LinkPlay(_) => "LinkPlay",
+ MusicRenderer::ArylicTcp(_) => "ArylicTcp",
+ MusicRenderer::HybridUpnpArylic { .. } => "HybridUpnpArylic",
+ }
+ }
}
/// Transport control façade that dispatches to whichever backend can fulfill
diff --git a/pmocontrol/src/openapi.rs b/pmocontrol/src/openapi.rs
index e2dfc3e7..c7eea71e 100644
--- a/pmocontrol/src/openapi.rs
+++ b/pmocontrol/src/openapi.rs
@@ -133,35 +133,8 @@ pub struct QueueSnapshot {
// OPENHOME PLAYLIST
// ============================================================================
-/// Snapshot de la playlist native OpenHome
#[cfg(feature = "pmoserver")]
-#[derive(Debug, Clone, Serialize, ToSchema)]
-pub struct OpenHomePlaylistSnapshot {
- /// ID du renderer concerné
- pub renderer_id: String,
- /// ID courant dans la playlist (si connu)
- pub current_id: Option,
- /// Tracks présents dans la playlist native
- pub tracks: Vec,
-}
-
-/// Track issue de la playlist native OpenHome
-#[cfg(feature = "pmoserver")]
-#[derive(Debug, Clone, Serialize, ToSchema)]
-pub struct OpenHomePlaylistTrack {
- /// ID interne OpenHome
- pub id: u32,
- /// URI du flux
- pub uri: String,
- /// Titre
- pub title: Option,
- /// Artiste
- pub artist: Option,
- /// Album
- pub album: Option,
- /// Pochette (si disponible)
- pub album_art_uri: Option,
-}
+pub use crate::openhome_playlist::{OpenHomePlaylistSnapshot, OpenHomePlaylistTrack};
/// Requête pour ajouter un track à la playlist OpenHome
#[cfg(feature = "pmoserver")]
diff --git a/pmocontrol/src/openhome_client.rs b/pmocontrol/src/openhome_client.rs
index bbb0fa45..c42bb973 100644
--- a/pmocontrol/src/openhome_client.rs
+++ b/pmocontrol/src/openhome_client.rs
@@ -1,6 +1,6 @@
-use anyhow::{Result, anyhow};
use crate::model::TrackMetadata;
use crate::soap_client::{SoapCallResult, invoke_upnp_action};
+use anyhow::{Result, anyhow};
use pmoupnp::soap::SoapEnvelope;
use xmltree::{Element, XMLNode};
@@ -64,12 +64,8 @@ impl OhPlaylistClient {
.join(",");
let args = [("aIdList", id_list_csv.as_str())];
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "ReadList",
- &args,
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "ReadList", &args)?;
let envelope = ensure_success("ReadList", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "ReadListResponse")
@@ -87,12 +83,8 @@ impl OhPlaylistClient {
("aMetadata", metadata),
];
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "Insert",
- &args,
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "Insert", &args)?;
let envelope = ensure_success("Insert", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "InsertResponse")
@@ -109,37 +101,29 @@ impl OhPlaylistClient {
let id_str = id.to_string();
let args = [("aId", id_str.as_str())];
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "PlayId",
- &args,
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "PlayId", &args)?;
handle_action_response("PlayId", &call_result)
}
pub fn play(&self) -> Result<()> {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Play", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Play", &[])?;
handle_action_response("Play", &call_result)
}
pub fn pause(&self) -> Result<()> {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Pause", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Pause", &[])?;
handle_action_response("Pause", &call_result)
}
pub fn stop(&self) -> Result<()> {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Stop", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Stop", &[])?;
handle_action_response("Stop", &call_result)
}
pub fn next(&self) -> Result<()> {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Next", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Next", &[])?;
handle_action_response("Next", &call_result)
}
@@ -166,12 +150,8 @@ impl OhPlaylistClient {
let id_str = id.to_string();
let args = [("aId", id_str.as_str())];
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "DeleteId",
- &args,
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "DeleteId", &args)?;
handle_action_response("DeleteId", &call_result)
}
@@ -183,12 +163,8 @@ impl OhPlaylistClient {
}
pub fn tracks_max(&self) -> Result {
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "TracksMax",
- &[],
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "TracksMax", &[])?;
let envelope = ensure_success("TracksMax", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "TracksMaxResponse")
@@ -202,12 +178,8 @@ impl OhPlaylistClient {
}
pub fn id_array(&self) -> Result> {
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "IdArray",
- &[],
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "IdArray", &[])?;
let envelope = ensure_success("IdArray", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "IdArrayResponse")
.ok_or_else(|| anyhow!("Missing IdArrayResponse element in SOAP body"))?;
@@ -259,8 +231,7 @@ impl OhInfoClient {
}
pub fn track(&self) -> Result {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Track", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Track", &[])?;
let envelope = ensure_success("Track", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "TrackResponse")
@@ -275,8 +246,7 @@ impl OhInfoClient {
}
pub fn next(&self) -> Result {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Next", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Next", &[])?;
let envelope = ensure_success("Next", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "NextResponse")
@@ -291,8 +261,7 @@ impl OhInfoClient {
}
pub fn id(&self) -> Result {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Id", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Id", &[])?;
let envelope = ensure_success("Id", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "IdResponse")
@@ -305,17 +274,12 @@ impl OhInfoClient {
}
pub fn transport_state(&self) -> Result {
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "TransportState",
- &[],
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "TransportState", &[])?;
let envelope = ensure_success("TransportState", &call_result)?;
- let response =
- find_child_with_suffix(&envelope.body.content, "TransportStateResponse")
- .ok_or_else(|| anyhow!("Missing TransportStateResponse element in SOAP body"))?;
+ let response = find_child_with_suffix(&envelope.body.content, "TransportStateResponse")
+ .ok_or_else(|| anyhow!("Missing TransportStateResponse element in SOAP body"))?;
let state = extract_child_text(response, "aState")?;
Ok(state)
}
@@ -341,8 +305,7 @@ impl OhTimeClient {
}
pub fn position(&self) -> Result {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Time", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Time", &[])?;
let envelope = ensure_success("Time", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "TimeResponse")
@@ -381,8 +344,7 @@ impl OhVolumeClient {
}
pub fn volume(&self) -> Result {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Volume", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Volume", &[])?;
let envelope = ensure_success("Volume", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "VolumeResponse")
.ok_or_else(|| anyhow!("Missing VolumeResponse element in SOAP body"))?;
@@ -396,18 +358,13 @@ impl OhVolumeClient {
pub fn set_volume(&self, vol: u16) -> Result<()> {
let vol_str = vol.to_string();
let args = [("aVolume", vol_str.as_str())];
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "SetVolume",
- &args,
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "SetVolume", &args)?;
handle_action_response("SetVolume", &call_result)
}
pub fn mute(&self) -> Result {
- let call_result =
- invoke_upnp_action(&self.control_url, &self.service_type, "Mute", &[])?;
+ let call_result = invoke_upnp_action(&self.control_url, &self.service_type, "Mute", &[])?;
let envelope = ensure_success("Mute", &call_result)?;
let response = find_child_with_suffix(&envelope.body.content, "MuteResponse")
.ok_or_else(|| anyhow!("Missing MuteResponse element in SOAP body"))?;
@@ -418,12 +375,8 @@ impl OhVolumeClient {
pub fn set_mute(&self, mute: bool) -> Result<()> {
let mute_str = if mute { "1" } else { "0" };
let args = [("aMute", mute_str)];
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "SetMute",
- &args,
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "SetMute", &args)?;
handle_action_response("SetMute", &call_result)
}
}
@@ -445,12 +398,8 @@ impl OhRadioClient {
pub fn play_channel(&self, id: u32) -> Result<()> {
let id_str = id.to_string();
let args = [("aId", id_str.as_str())];
- let call_result = invoke_upnp_action(
- &self.control_url,
- &self.service_type,
- "PlayChannel",
- &args,
- )?;
+ let call_result =
+ invoke_upnp_action(&self.control_url, &self.service_type, "PlayChannel", &args)?;
handle_action_response("PlayChannel", &call_result)
}
@@ -529,10 +478,7 @@ fn parse_track_entry(elem: &Element) -> Result {
})
}
-fn ensure_success<'a>(
- action: &str,
- call_result: &'a SoapCallResult,
-) -> Result<&'a SoapEnvelope> {
+fn ensure_success<'a>(action: &str, call_result: &'a SoapCallResult) -> Result<&'a SoapEnvelope> {
if !call_result.status.is_success() {
if let Some(env) = &call_result.envelope {
if let Some(err) = parse_upnp_error(env) {
@@ -631,7 +577,10 @@ fn extract_child_text(parent: &Element, suffix: &str) -> Result {
fn extract_child_text_optional(parent: &Element, suffix: &str) -> Result