use std::net::SocketAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use crate::error::NetworkError;
use tracing::debug;
#[cfg(not(target_os = "macos"))]
fn discover_dacp_port(dacp_id: &str, _remote_ip: std::net::IpAddr) -> Option<u16> {
let daemon = mdns_sd::ServiceDaemon::new().ok()?;
let receiver = daemon.browse("_dacp._tcp.local.").ok()?;
let target = dacp_id.to_uppercase();
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while std::time::Instant::now() < deadline {
match receiver.recv_timeout(deadline.duration_since(std::time::Instant::now())) {
Ok(mdns_sd::ServiceEvent::ServiceResolved(info)) => {
if info.get_fullname().to_uppercase().contains(&target) {
let port = info.get_port();
let _ = daemon.shutdown();
return Some(port);
}
}
Err(_) => break,
_ => continue,
}
}
let _ = daemon.shutdown();
None
}
#[cfg(target_os = "macos")]
fn discover_dacp_port(dacp_id: &str, _remote_ip: std::net::IpAddr) -> Option<u16> {
let _ = dacp_id;
None
}
#[derive(Debug)]
pub struct DacpClient {
dacp_id: String,
active_remote: String,
addr: Option<SocketAddr>,
}
impl DacpClient {
pub fn new(dacp_id: &str, active_remote: &str) -> Self {
Self {
dacp_id: dacp_id.to_string(),
active_remote: active_remote.to_string(),
addr: None,
}
}
pub fn discover_from_remote(&mut self, remote_ip: std::net::IpAddr) {
self.addr = match discover_dacp_port(&self.dacp_id, remote_ip) {
Some(port) => {
debug!(port, dacp_id = %self.dacp_id, "DACP service discovered via mDNS");
Some(SocketAddr::new(remote_ip, port))
}
None => {
debug!(dacp_id = %self.dacp_id, "DACP mDNS discovery failed, falling back to port 3689");
Some(SocketAddr::new(remote_ip, 3689))
}
};
}
pub fn set_addr(&mut self, addr: SocketAddr) {
self.addr = Some(addr);
}
pub async fn play_pause(&self) -> Result<(), NetworkError> {
debug!("DACP: play_pause");
self.command("/ctrl-int/1/playpause").await
}
pub async fn next(&self) -> Result<(), NetworkError> {
debug!("DACP: next");
self.command("/ctrl-int/1/nextitem").await
}
pub async fn prev(&self) -> Result<(), NetworkError> {
debug!("DACP: prev");
self.command("/ctrl-int/1/previtem").await
}
pub async fn stop(&self) -> Result<(), NetworkError> {
self.command("/ctrl-int/1/stop").await
}
pub async fn set_volume(&self, volume: u8) -> Result<(), NetworkError> {
let vol = volume.min(100);
self.command(&format!("/ctrl-int/1/setproperty?dmcp.volume={vol}"))
.await
}
pub async fn set_shuffle(&self, on: bool) -> Result<(), NetworkError> {
let v = if on { 1 } else { 0 };
self.command(&format!("/ctrl-int/1/setproperty?dacp.shufflestate={v}"))
.await
}
pub async fn set_repeat(&self, state: u8) -> Result<(), NetworkError> {
self.command(&format!("/ctrl-int/1/setproperty?dacp.repeatstate={state}"))
.await
}
pub async fn command(&self, path: &str) -> Result<(), NetworkError> {
let addr = self
.addr
.ok_or_else(|| NetworkError::Mdns("DACP not discovered yet — call discover() first".into()))?;
let mut stream = TcpStream::connect(addr).await?;
let request = format!(
"GET {path} HTTP/1.1\r\nActive-Remote: {}\r\nHost: {addr}\r\n\r\n",
self.active_remote
);
stream.write_all(request.as_bytes()).await?;
let mut buf = [0u8; 1024];
let _ = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf)).await;
Ok(())
}
}