♻️ refactor queue control methods into trait default impls
Remove duplicate `play_from_queue`, ` play_next `, `play_previous , and `` pay _from_index implementations across all renderers (ArylicTcp, Chromecast, LinkPlay , OpenHome UPnP) and instead provide default implementations in the `QueueTransportControl `trait. Also introduce dispatch macros to simplify backend delegation for Transport/Volume control, and expose parse_didl_duration as public(crate).
This commit is contained in:
@@ -307,63 +307,6 @@ impl QueueTransportControl for ArylicTcpRenderer {
|
|||||||
self.play_uri(&item.uri, "")
|
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 {
|
impl HasQueue for ArylicTcpRenderer {
|
||||||
|
|||||||
@@ -53,14 +53,35 @@ pub trait QueueTransportControl: HasQueue + HasContinuousStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Play the next track from the queue.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// Logical playback position across backends.
|
||||||
|
|||||||
@@ -806,63 +806,6 @@ impl QueueTransportControl for ChromecastRenderer {
|
|||||||
self.play_uri(&item.uri, "")
|
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 {
|
impl HasQueue for ChromecastRenderer {
|
||||||
|
|||||||
@@ -184,66 +184,9 @@ impl QueueTransportControl for LinkPlayRenderer {
|
|||||||
fn play_item(&self, item: &PlaybackItem) -> Result<(), ControlPointError> {
|
fn play_item(&self, item: &PlaybackItem) -> Result<(), ControlPointError> {
|
||||||
self.play_uri(&item.uri, "")
|
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 {
|
impl HasQueue for LinkPlayRenderer {
|
||||||
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
|
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
|
||||||
&self.queue
|
&self.queue
|
||||||
|
|||||||
@@ -2091,7 +2091,7 @@ impl RendererFromMediaRendererInfo for MusicRendererBackend {
|
|||||||
/// Extracts the duration attribute from the <res> element in DIDL metadata.
|
/// 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
|
/// This is used as a fallback when the renderer doesn't provide track_duration
|
||||||
/// in GetPositionInfo or similar calls.
|
/// 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
|
// Parse DIDL-Lite XML properly using pmodidl
|
||||||
let didl = match DIDLLite::parse(didl_xml) {
|
let didl = match DIDLLite::parse(didl_xml) {
|
||||||
Ok(d) => d,
|
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))
|
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
|
/// Transport control façade that dispatches to whichever backend can fulfill
|
||||||
/// the request, returning a standardized error if the backend lacks support.
|
/// the request, returning a standardized error if the backend lacks support.
|
||||||
impl TransportControl for MusicRendererBackend {
|
impl TransportControl for MusicRendererBackend {
|
||||||
@@ -2145,38 +2176,9 @@ impl TransportControl for MusicRendererBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play(&self) -> Result<(), ControlPointError> {
|
fn play(&self) -> Result<(), ControlPointError> { dispatch_arylic!(self, play()) }
|
||||||
match self {
|
fn pause(&self) -> Result<(), ControlPointError> { dispatch_arylic!(self, pause()) }
|
||||||
MusicRendererBackend::Upnp(upnp) => upnp.play(),
|
fn stop(&self) -> Result<(), ControlPointError> { dispatch_arylic!(self, stop()) }
|
||||||
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 seek_rel_time(&self, hhmmss: &str) -> Result<(), ControlPointError> {
|
fn seek_rel_time(&self, hhmmss: &str) -> Result<(), ControlPointError> {
|
||||||
match self {
|
match self {
|
||||||
@@ -2197,49 +2199,10 @@ impl TransportControl for MusicRendererBackend {
|
|||||||
/// Hybrid backends may read via Arylic TCP and write via UPnP, but callers
|
/// Hybrid backends may read via Arylic TCP and write via UPnP, but callers
|
||||||
/// always depend on a single [`VolumeControl`] entry point.
|
/// always depend on a single [`VolumeControl`] entry point.
|
||||||
impl VolumeControl for MusicRendererBackend {
|
impl VolumeControl for MusicRendererBackend {
|
||||||
fn volume(&self) -> Result<u16, ControlPointError> {
|
fn volume(&self) -> Result<u16, ControlPointError> { dispatch_arylic!(self, volume()) }
|
||||||
match self {
|
fn set_volume(&self, vol: u16) -> Result<(), ControlPointError> { dispatch_upnp!(self, set_volume(vol)) }
|
||||||
MusicRendererBackend::HybridUpnpArylic { arylic, .. } => arylic.volume(),
|
fn mute(&self) -> Result<bool, ControlPointError> { dispatch_arylic!(self, mute()) }
|
||||||
MusicRendererBackend::ArylicTcp(ary) => ary.volume(),
|
fn set_mute(&self, m: bool) -> Result<(), ControlPointError> { dispatch_arylic!(self, set_mute(m)) }
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Playback-state queries sourced from the backend best suited for the job.
|
/// 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.
|
/// regardless of the backend providing the raw transport data.
|
||||||
impl PlaybackPosition for MusicRendererBackend {
|
impl PlaybackPosition for MusicRendererBackend {
|
||||||
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
|
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
|
||||||
match self {
|
dispatch_arylic!(self, playback_position())
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasQueue for MusicRendererBackend {
|
impl HasQueue for MusicRendererBackend {
|
||||||
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
|
fn queue(&self) -> &Arc<Mutex<MusicQueue>> { dispatch_upnp!(self, queue()) }
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasContinuousStream for MusicRendererBackend {
|
impl HasContinuousStream for MusicRendererBackend {
|
||||||
fn continuous_stream(&self) -> &Arc<Mutex<bool>> {
|
fn continuous_stream(&self) -> &Arc<Mutex<bool>> {
|
||||||
match self {
|
dispatch_arylic!(self, continuous_stream())
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueueTransportControl for MusicRendererBackend {
|
impl QueueTransportControl for MusicRendererBackend {
|
||||||
fn play_item(&self, item: &PlaybackItem) -> Result<(), ControlPointError> {
|
fn play_item(&self, item: &PlaybackItem) -> Result<(), ControlPointError> {
|
||||||
match self {
|
dispatch_upnp!(self, play_item(item))
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play_from_queue(&self) -> Result<(), ControlPointError> {
|
fn play_from_queue(&self) -> Result<(), ControlPointError> {
|
||||||
match self {
|
dispatch_upnp!(self, play_from_queue())
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
|
||||||
match self {
|
dispatch_upnp!(self, play_from_index(index))
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -492,28 +492,6 @@ impl PlaybackPosition for OpenHomeRenderer {
|
|||||||
Ok(position_info)
|
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 {
|
pub(crate) fn map_openhome_state(raw: &str) -> PlaybackState {
|
||||||
match raw.trim().to_ascii_uppercase().as_str() {
|
match raw.trim().to_ascii_uppercase().as_str() {
|
||||||
"PLAYING" => PlaybackState::Playing,
|
"PLAYING" => PlaybackState::Playing,
|
||||||
@@ -554,50 +532,6 @@ impl QueueTransportControl for OpenHomeRenderer {
|
|||||||
playlist.play()
|
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> {
|
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
|
||||||
// For OpenHome, we need to convert index to track_id
|
// For OpenHome, we need to convert index to track_id
|
||||||
let track_id = {
|
let track_id = {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use crate::music_renderer::capabilities::{
|
|||||||
HasContinuousStream, PlaybackPosition, PlaybackPositionInfo, PlaybackStatus,
|
HasContinuousStream, PlaybackPosition, PlaybackPositionInfo, PlaybackStatus,
|
||||||
QueueTransportControl, TransportControl, VolumeControl,
|
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::HasQueue;
|
||||||
use crate::music_renderer::RendererFromMediaRendererInfo;
|
use crate::music_renderer::RendererFromMediaRendererInfo;
|
||||||
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
|
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
|
||||||
@@ -203,100 +205,6 @@ impl QueueTransportControl for UpnpRenderer {
|
|||||||
Ok(())
|
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 {
|
impl HasQueue for UpnpRenderer {
|
||||||
@@ -310,36 +218,6 @@ impl HasContinuousStream for UpnpRenderer {
|
|||||||
&self.continuous_stream
|
&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`].
|
/// Implémentation UPnP AV de `TransportControl` pour [`UpnpRenderer`].
|
||||||
///
|
///
|
||||||
/// Cette impl se base sur AVTransport (InstanceID = 0).
|
/// Cette impl se base sur AVTransport (InstanceID = 0).
|
||||||
|
|||||||
Reference in New Issue
Block a user