push-yvskvtsqsmxs #97

Merged
eric merged 5 commits from push-yvskvtsqsmxs into main 2026-04-12 21:10:24 +02:00
7 changed files with 74 additions and 517 deletions
Showing only changes of commit 8ddee7d94f - Show all commits

View File

@@ -307,63 +307,6 @@ impl QueueTransportControl for ArylicTcpRenderer {
self.play_uri(&item.uri, "")
}
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
drop(queue);
let is_stream = crate::music_renderer::is_continuous_stream_url(&item.uri);
*self.continuous_stream.lock().unwrap() = is_stream;
self.play_item(&item)
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
impl HasQueue for ArylicTcpRenderer {

View File

@@ -53,14 +53,35 @@ pub trait QueueTransportControl: HasQueue + HasContinuousStream {
}
/// Play the next track from the queue.
fn play_next(&self) -> Result<(), ControlPointError>;
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue().lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
/// Play the previous track from the queue.
#[allow(dead_code)]
fn play_previous(&self) -> Result<(), ControlPointError>;
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue().lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
/// Play from a specific index in the queue.
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError>;
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue().lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
/// Logical playback position across backends.

View File

@@ -806,63 +806,6 @@ impl QueueTransportControl for ChromecastRenderer {
self.play_uri(&item.uri, "")
}
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
drop(queue);
let is_stream = crate::music_renderer::is_continuous_stream_url(&item.uri);
*self.continuous_stream.lock().unwrap() = is_stream;
self.play_item(&item)
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
impl HasQueue for ChromecastRenderer {

View File

@@ -184,66 +184,9 @@ impl QueueTransportControl for LinkPlayRenderer {
fn play_item(&self, item: &PlaybackItem) -> Result<(), ControlPointError> {
self.play_uri(&item.uri, "")
}
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
drop(queue);
let is_stream = crate::music_renderer::is_continuous_stream_url(&item.uri);
*self.continuous_stream.lock().unwrap() = is_stream;
self.play_item(&item)
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
impl HasQueue for LinkPlayRenderer {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
&self.queue

View File

@@ -2091,7 +2091,7 @@ impl RendererFromMediaRendererInfo for MusicRendererBackend {
/// Extracts the duration attribute from the <res> element in DIDL metadata.
/// This is used as a fallback when the renderer doesn't provide track_duration
/// in GetPositionInfo or similar calls.
fn parse_didl_duration(didl_xml: &str) -> Option<String> {
pub(crate) fn parse_didl_duration(didl_xml: &str) -> Option<String> {
// Parse DIDL-Lite XML properly using pmodidl
let didl = match DIDLLite::parse(didl_xml) {
Ok(d) => d,
@@ -2129,6 +2129,37 @@ fn parse_rfc3339_to_system_time(s: &str) -> Option<SystemTime> {
Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64))
}
/// Dispatch a method call to the inner backend, using the UPnP field for
/// HybridUpnpArylic.
macro_rules! dispatch_upnp {
($self:expr, $method:ident($($arg:expr),*)) => {
match $self {
MusicRendererBackend::Upnp(r) => r.$method($($arg),*),
MusicRendererBackend::OpenHome(r) => r.$method($($arg),*),
MusicRendererBackend::LinkPlay(r) => r.$method($($arg),*),
MusicRendererBackend::ArylicTcp(r) => r.$method($($arg),*),
MusicRendererBackend::Chromecast(cc) => cc.$method($($arg),*),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.$method($($arg),*),
}
};
}
/// Dispatch a method call to the inner backend, using the Arylic field for
/// HybridUpnpArylic.
macro_rules! dispatch_arylic {
($self:expr, $method:ident($($arg:expr),*)) => {
match $self {
MusicRendererBackend::Upnp(r) => r.$method($($arg),*),
MusicRendererBackend::OpenHome(r) => r.$method($($arg),*),
MusicRendererBackend::LinkPlay(r) => r.$method($($arg),*),
MusicRendererBackend::ArylicTcp(r) => r.$method($($arg),*),
MusicRendererBackend::Chromecast(cc) => cc.$method($($arg),*),
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.$method($($arg),*),
}
};
}
/// Transport control façade that dispatches to whichever backend can fulfill
/// the request, returning a standardized error if the backend lacks support.
impl TransportControl for MusicRendererBackend {
@@ -2145,38 +2176,9 @@ impl TransportControl for MusicRendererBackend {
}
}
fn play(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(upnp) => upnp.play(),
MusicRendererBackend::OpenHome(oh) => oh.play(),
MusicRendererBackend::LinkPlay(lp) => lp.play(),
MusicRendererBackend::ArylicTcp(ary) => ary.play(),
MusicRendererBackend::Chromecast(cc) => cc.play(),
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.play(),
}
}
fn pause(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(upnp) => upnp.pause(),
MusicRendererBackend::OpenHome(oh) => oh.pause(),
MusicRendererBackend::LinkPlay(lp) => lp.pause(),
MusicRendererBackend::ArylicTcp(ary) => ary.pause(),
MusicRendererBackend::Chromecast(cc) => cc.pause(),
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.pause(),
}
}
fn stop(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(upnp) => upnp.stop(),
MusicRendererBackend::OpenHome(oh) => oh.stop(),
MusicRendererBackend::LinkPlay(lp) => lp.stop(),
MusicRendererBackend::ArylicTcp(ary) => ary.stop(),
MusicRendererBackend::Chromecast(cc) => cc.stop(),
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.stop(),
}
}
fn play(&self) -> Result<(), ControlPointError> { dispatch_arylic!(self, play()) }
fn pause(&self) -> Result<(), ControlPointError> { dispatch_arylic!(self, pause()) }
fn stop(&self) -> Result<(), ControlPointError> { dispatch_arylic!(self, stop()) }
fn seek_rel_time(&self, hhmmss: &str) -> Result<(), ControlPointError> {
match self {
@@ -2197,49 +2199,10 @@ impl TransportControl for MusicRendererBackend {
/// Hybrid backends may read via Arylic TCP and write via UPnP, but callers
/// always depend on a single [`VolumeControl`] entry point.
impl VolumeControl for MusicRendererBackend {
fn volume(&self) -> Result<u16, ControlPointError> {
match self {
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.volume(),
MusicRendererBackend::ArylicTcp(ary) => ary.volume(),
MusicRendererBackend::OpenHome(oh) => oh.volume(),
MusicRendererBackend::Upnp(upnp) => upnp.volume(),
MusicRendererBackend::LinkPlay(lp) => lp.volume(),
MusicRendererBackend::Chromecast(cc) => cc.volume(),
}
}
fn set_volume(&self, vol: u16) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.set_volume(vol),
MusicRendererBackend::ArylicTcp(ary) => ary.set_volume(vol),
MusicRendererBackend::OpenHome(oh) => oh.set_volume(vol),
MusicRendererBackend::Upnp(upnp) => upnp.set_volume(vol),
MusicRendererBackend::LinkPlay(lp) => lp.set_volume(vol),
MusicRendererBackend::Chromecast(cc) => cc.set_volume(vol),
}
}
fn mute(&self) -> Result<bool, ControlPointError> {
match self {
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.mute(),
MusicRendererBackend::OpenHome(r) => r.mute(),
MusicRendererBackend::Upnp(r) => r.mute(),
MusicRendererBackend::LinkPlay(r) => r.mute(),
MusicRendererBackend::ArylicTcp(r) => r.mute(),
MusicRendererBackend::Chromecast(cc) => cc.mute(),
}
}
fn set_mute(&self, m: bool) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.set_mute(m),
MusicRendererBackend::OpenHome(r) => r.set_mute(m),
MusicRendererBackend::Upnp(r) => r.set_mute(m),
MusicRendererBackend::LinkPlay(r) => r.set_mute(m),
MusicRendererBackend::ArylicTcp(r) => r.set_mute(m),
MusicRendererBackend::Chromecast(cc) => cc.set_mute(m),
}
}
fn volume(&self) -> Result<u16, ControlPointError> { dispatch_arylic!(self, volume()) }
fn set_volume(&self, vol: u16) -> Result<(), ControlPointError> { dispatch_upnp!(self, set_volume(vol)) }
fn mute(&self) -> Result<bool, ControlPointError> { dispatch_arylic!(self, mute()) }
fn set_mute(&self, m: bool) -> Result<(), ControlPointError> { dispatch_arylic!(self, set_mute(m)) }
}
/// Playback-state queries sourced from the backend best suited for the job.
@@ -2263,96 +2226,28 @@ impl PlaybackStatus for MusicRendererBackend {
/// regardless of the backend providing the raw transport data.
impl PlaybackPosition for MusicRendererBackend {
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.playback_position(),
MusicRendererBackend::OpenHome(r) => r.playback_position(),
MusicRendererBackend::LinkPlay(r) => r.playback_position(),
MusicRendererBackend::ArylicTcp(r) => r.playback_position(),
MusicRendererBackend::Chromecast(cc) => cc.playback_position(),
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.playback_position(),
}
dispatch_arylic!(self, playback_position())
}
}
impl HasQueue for MusicRendererBackend {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
match self {
MusicRendererBackend::Upnp(r) => r.queue(),
MusicRendererBackend::OpenHome(r) => r.queue(),
MusicRendererBackend::LinkPlay(r) => r.queue(),
MusicRendererBackend::ArylicTcp(r) => r.queue(),
MusicRendererBackend::Chromecast(cc) => cc.queue(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.queue(),
}
}
fn queue(&self) -> &Arc<Mutex<MusicQueue>> { dispatch_upnp!(self, queue()) }
}
impl HasContinuousStream for MusicRendererBackend {
fn continuous_stream(&self) -> &Arc<Mutex<bool>> {
match self {
MusicRendererBackend::Upnp(r) => r.continuous_stream(),
MusicRendererBackend::OpenHome(r) => r.continuous_stream(),
MusicRendererBackend::LinkPlay(r) => r.continuous_stream(),
MusicRendererBackend::ArylicTcp(r) => r.continuous_stream(),
MusicRendererBackend::Chromecast(cc) => cc.continuous_stream(),
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.continuous_stream(),
}
dispatch_arylic!(self, continuous_stream())
}
}
impl QueueTransportControl for MusicRendererBackend {
fn play_item(&self, item: &PlaybackItem) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_item(item),
MusicRendererBackend::OpenHome(r) => r.play_item(item),
MusicRendererBackend::LinkPlay(r) => r.play_item(item),
MusicRendererBackend::ArylicTcp(r) => r.play_item(item),
MusicRendererBackend::Chromecast(cc) => cc.play_item(item),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_item(item),
}
dispatch_upnp!(self, play_item(item))
}
fn play_from_queue(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_from_queue(),
MusicRendererBackend::OpenHome(r) => r.play_from_queue(),
MusicRendererBackend::LinkPlay(r) => r.play_from_queue(),
MusicRendererBackend::ArylicTcp(r) => r.play_from_queue(),
MusicRendererBackend::Chromecast(cc) => cc.play_from_queue(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_from_queue(),
}
dispatch_upnp!(self, play_from_queue())
}
fn play_next(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_next(),
MusicRendererBackend::OpenHome(r) => r.play_next(),
MusicRendererBackend::LinkPlay(r) => r.play_next(),
MusicRendererBackend::ArylicTcp(r) => r.play_next(),
MusicRendererBackend::Chromecast(cc) => cc.play_next(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_next(),
}
}
fn play_previous(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_previous(),
MusicRendererBackend::OpenHome(r) => r.play_previous(),
MusicRendererBackend::LinkPlay(r) => r.play_previous(),
MusicRendererBackend::ArylicTcp(r) => r.play_previous(),
MusicRendererBackend::Chromecast(cc) => cc.play_previous(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_previous(),
}
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_from_index(index),
MusicRendererBackend::OpenHome(r) => r.play_from_index(index),
MusicRendererBackend::LinkPlay(r) => r.play_from_index(index),
MusicRendererBackend::ArylicTcp(r) => r.play_from_index(index),
MusicRendererBackend::Chromecast(cc) => cc.play_from_index(index),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_from_index(index),
}
dispatch_upnp!(self, play_from_index(index))
}
}

View File

@@ -492,28 +492,6 @@ impl PlaybackPosition for OpenHomeRenderer {
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,
@@ -554,50 +532,6 @@ impl QueueTransportControl for OpenHomeRenderer {
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 = {

View File

@@ -6,7 +6,9 @@ use crate::music_renderer::capabilities::{
HasContinuousStream, PlaybackPosition, PlaybackPositionInfo, PlaybackStatus,
QueueTransportControl, TransportControl, VolumeControl,
};
use crate::music_renderer::musicrenderer::{build_didl_lite_metadata, MusicRendererBackend};
use crate::music_renderer::musicrenderer::{
build_didl_lite_metadata, parse_didl_duration, MusicRendererBackend,
};
use crate::music_renderer::HasQueue;
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
@@ -203,100 +205,6 @@ impl QueueTransportControl for UpnpRenderer {
Ok(())
}
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
drop(queue);
let is_stream = crate::music_renderer::is_continuous_stream_url(&item.uri);
*self.continuous_stream.lock().unwrap() = is_stream;
let queue_state = {
let queue = self.queue.lock().unwrap();
let idx = queue.current_index().unwrap_or(None);
let len = queue.len().unwrap_or(0);
let uri = item.uri.clone();
let title = item.metadata.as_ref().and_then(|m| m.title.clone());
(idx, len, uri, title)
};
tracing::debug!(
"UpnpRenderer play_from_queue: index={:?}/{}, uri={}, title={:?}, continuous_stream={}",
queue_state.0,
queue_state.1,
queue_state.2,
queue_state.3,
is_stream
);
self.play_item(&item)
}
fn play_next(&self) -> Result<(), ControlPointError> {
let current_idx = {
let queue = self.queue.lock().unwrap();
let idx = queue.current_index().unwrap_or(None);
let len = queue.len().unwrap_or(0);
tracing::debug!(
current_index = ?idx,
queue_len = len,
"play_next: attempting to advance"
);
idx
};
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
let new_idx = queue.current_index().unwrap_or(None);
tracing::debug!(
previous_index = ?current_idx,
new_index = ?new_idx,
"play_next: advanced"
);
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
// CORRECTIF: Quand on change l'index manuellement (shuffle, sélection d'un titre)
// on logue pour être sûr que c'est bien appelé
tracing::debug!(index = index, "✅ SHUFFLE / SEEK: play_from_index appelé");
self.play_from_queue()
}
}
impl HasQueue for UpnpRenderer {
@@ -310,36 +218,6 @@ impl HasContinuousStream for UpnpRenderer {
&self.continuous_stream
}
}
/// Parse le DIDL-Lite pour extraire la durée du premier élément <res>
fn parse_didl_duration(didl: &str) -> Option<String> {
// Recherche de l'élément <res> (avec ou sans espace après)
let res_start = didl
.find("<res ")
.or_else(|| didl.find("<res>"))
.or_else(|| didl.find("<res\n"))
.or_else(|| didl.find("<res\t"))?;
let after_res = &didl[res_start..];
// Recherche de l'attribut duration dans cet élément <res>
// Il doit être avant la fermeture du tag (avant '>')
if let Some(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::warn!("No duration attribute found in DIDL <res> element");
None
}
/// Implémentation UPnP AV de `TransportControl` pour [`UpnpRenderer`].
///
/// Cette impl se base sur AVTransport (InstanceID = 0).