use crate::device::{extract_ip_from_url, DeviceDescription};
use crate::error::Result;
use crate::ssdp::{SsdpClient, SsdpResponse};
use crate::DeviceEvent;
use std::collections::HashSet;
use std::time::Duration;
pub struct DiscoveryIterator {
ssdp_client: Option<SsdpClient>,
ssdp_buffer: Vec<SsdpResponse>,
buffer_index: usize,
seen_locations: HashSet<String>,
http_client: reqwest::blocking::Client,
finished: bool,
}
impl DiscoveryIterator {
pub fn new(timeout: Duration) -> Result<Self> {
let ssdp_client = SsdpClient::new(timeout)?;
let http_client = reqwest::blocking::Client::builder()
.timeout(timeout)
.build()
.map_err(|e| {
crate::error::DiscoveryError::NetworkError(format!(
"Failed to create HTTP client: {e}"
))
})?;
Ok(Self {
ssdp_client: Some(ssdp_client),
ssdp_buffer: Vec::new(),
buffer_index: 0,
seen_locations: HashSet::new(),
http_client,
finished: false,
})
}
pub(crate) fn empty() -> Self {
let http_client = reqwest::blocking::Client::new();
Self {
ssdp_client: None,
ssdp_buffer: Vec::new(),
buffer_index: 0,
seen_locations: HashSet::new(),
http_client,
finished: true,
}
}
fn is_likely_sonos(response: &SsdpResponse) -> bool {
if response.urn.contains("ZonePlayer") {
return true;
}
if response.usn.contains("RINCON") {
return true;
}
if let Some(ref server) = response.server {
if server.to_lowercase().contains("sonos") {
return true;
}
}
false
}
fn fetch_device_description(&self, location: &str) -> Result<DeviceDescription> {
let response = self.http_client.get(location).send().map_err(|e| {
crate::error::DiscoveryError::NetworkError(format!(
"Failed to fetch device description: {e}"
))
})?;
let xml = response.text().map_err(|e| {
crate::error::DiscoveryError::NetworkError(format!("Failed to read response body: {e}"))
})?;
DeviceDescription::from_xml(&xml)
}
fn fill_buffer(&mut self) {
if let Some(client) = self.ssdp_client.take() {
match client.search("urn:schemas-upnp-org:device:ZonePlayer:1") {
Ok(iter) => {
for response in iter.flatten() {
self.ssdp_buffer.push(response);
}
}
Err(_) => {
}
}
self.finished = true;
}
}
}
impl Iterator for DiscoveryIterator {
type Item = DeviceEvent;
fn next(&mut self) -> Option<Self::Item> {
if self.ssdp_client.is_some() {
self.fill_buffer();
}
loop {
if self.buffer_index >= self.ssdp_buffer.len() {
return None;
}
let ssdp_response = &self.ssdp_buffer[self.buffer_index];
self.buffer_index += 1;
if self.seen_locations.contains(&ssdp_response.location) {
continue;
}
self.seen_locations.insert(ssdp_response.location.clone());
if !Self::is_likely_sonos(ssdp_response) {
continue;
}
let device_desc = match self.fetch_device_description(&ssdp_response.location) {
Ok(desc) => desc,
Err(_) => continue, };
if !device_desc.is_sonos_device() {
continue;
}
let ip_address = match extract_ip_from_url(&ssdp_response.location) {
Some(ip) => ip,
None => continue, };
let device = device_desc.to_device(ip_address);
return Some(DeviceEvent::Found(device));
}
}
}
impl Drop for DiscoveryIterator {
fn drop(&mut self) {
if let Some(client) = self.ssdp_client.take() {
drop(client);
}
}
}