use std::net::SocketAddr;
use std::time::Duration;
use std::io::{Read, Write};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use crate::error::NetworkError;
use tracing::debug;
const PLAY_PAUSE_PATH: &str = "/ctrl-int/1/playpause";
const NEXT_PATH: &str = "/ctrl-int/1/nextitem";
const PREVIOUS_PATH: &str = "/ctrl-int/1/previtem";
const STOP_PATH: &str = "/ctrl-int/1/stop";
fn volume_path(volume: u8) -> String {
format!("/ctrl-int/1/setproperty?dmcp.volume={}", volume.min(100))
}
fn shuffle_path(on: bool) -> String {
let state = if on { 1 } else { 0 };
format!("/ctrl-int/1/setproperty?dacp.shufflestate={state}")
}
fn repeat_path(state: u8) -> String {
format!("/ctrl-int/1/setproperty?dacp.repeatstate={state}")
}
#[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(PLAY_PAUSE_PATH).await
}
pub async fn next(&self) -> Result<(), NetworkError> {
debug!("DACP: next");
self.command(NEXT_PATH).await
}
pub async fn prev(&self) -> Result<(), NetworkError> {
debug!("DACP: prev");
self.command(PREVIOUS_PATH).await
}
pub async fn stop(&self) -> Result<(), NetworkError> {
self.command(STOP_PATH).await
}
pub async fn set_volume(&self, volume: u8) -> Result<(), NetworkError> {
self.command(&volume_path(volume)).await
}
pub async fn set_shuffle(&self, on: bool) -> Result<(), NetworkError> {
self.command(&shuffle_path(on)).await
}
pub async fn set_repeat(&self, state: u8) -> Result<(), NetworkError> {
self.command(&repeat_path(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(())
}
pub(crate) fn command_blocking(&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 = std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(2))?;
stream.set_write_timeout(Some(Duration::from_secs(2)))?;
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
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())?;
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf);
Ok(())
}
pub(crate) fn play_pause_blocking(&self) -> Result<(), NetworkError> {
self.command_blocking(PLAY_PAUSE_PATH)
}
pub(crate) fn next_blocking(&self) -> Result<(), NetworkError> {
self.command_blocking(NEXT_PATH)
}
pub(crate) fn prev_blocking(&self) -> Result<(), NetworkError> {
self.command_blocking(PREVIOUS_PATH)
}
pub(crate) fn stop_blocking(&self) -> Result<(), NetworkError> {
self.command_blocking(STOP_PATH)
}
pub(crate) fn set_volume_blocking(&self, volume: u8) -> Result<(), NetworkError> {
self.command_blocking(&volume_path(volume))
}
pub(crate) fn set_shuffle_blocking(&self, on: bool) -> Result<(), NetworkError> {
self.command_blocking(&shuffle_path(on))
}
pub(crate) fn set_repeat_blocking(&self, state: u8) -> Result<(), NetworkError> {
self.command_blocking(&repeat_path(state))
}
}