1use crate::podcast::PublicResolver;
3use serde_json::{Value, json};
4use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
5use ureq::unversioned::transport::DefaultConnector;
6mod policy;
7use policy::*;
8const LIMIT: u64 = 1024 * 1024;
9
10#[derive(Clone)]
16pub struct Directory {
17 agent: ureq::Agent,
18 mirrors: Vec<String>,
19 discovered: Option<Instant>,
20 next: usize,
21}
22impl Default for Directory {
23 fn default() -> Self {
24 let config = ureq::Agent::config_builder()
25 .max_redirects(0)
26 .proxy(None)
27 .build();
28 Self {
29 agent: ureq::Agent::with_parts(config, DefaultConnector::default(), PublicResolver),
30 mirrors: Vec::new(),
31 discovered: None,
32 next: 0,
33 }
34 }
35}
36impl Directory {
37 fn read(
38 &self,
39 started: Instant,
40 host: &str,
41 path: &str,
42 query: &[(&str, &str)],
43 ) -> Result<Value, String> {
44 let mut request = self
45 .agent
46 .get(format!("{host}{path}"))
47 .config()
48 .timeout_global(Some(request_timeout(started.elapsed())?))
49 .build()
50 .header(
51 "User-Agent",
52 concat!(
53 "Phemios/",
54 env!("CARGO_PKG_VERSION"),
55 " (Radio Browser desktop client)"
56 ),
57 )
58 .header("Accept", "application/json");
59 for (key, value) in query {
60 request = request.query(key, value);
61 }
62 let mut response = request.call().map_err(request_failure)?;
63 let bytes = response
64 .body_mut()
65 .with_config()
66 .limit(LIMIT)
67 .read_to_vec()
68 .map_err(|_| "Réponse Radio Browser trop grande ou interrompue.")?;
69 serde_json::from_slice(&bytes).map_err(|_| "Réponse Radio Browser invalide.".into())
70 }
71 fn candidates(&mut self, started: Instant) -> Result<Vec<String>, String> {
72 if self
73 .discovered
74 .is_none_or(|time| time.elapsed() >= Duration::from_secs(600))
75 {
76 match discover_mirrors(|host| self.read(started, host, "/json/servers", &[])) {
78 Ok(hosts) => {
79 self.mirrors = hosts;
80 self.discovered = Some(Instant::now());
81 }
82 Err(error) if self.mirrors.is_empty() => return Err(error),
83 Err(_) => {} }
85 self.next = SystemTime::now()
86 .duration_since(UNIX_EPOCH)
87 .map_or(0, |d| d.subsec_nanos() as usize)
88 % self.mirrors.len();
89 }
90 let mut hosts = Vec::with_capacity(self.mirrors.len().min(3));
91 for _ in 0..self.mirrors.len().min(3) {
92 hosts.push(self.mirrors[self.next % self.mirrors.len()].clone());
93 self.next = self.next.wrapping_add(1);
94 }
95 if hosts.is_empty() {
96 return Err("Aucun serveur Radio Browser disponible.".into());
97 }
98 Ok(hosts)
99 }
100 pub fn search(
102 &mut self,
103 name: &str,
104 country: &str,
105 tag: &str,
106 offset: u32,
107 secure_only: bool,
108 ) -> Result<Value, String> {
109 if [name, country, tag]
110 .iter()
111 .any(|s| s.len() > 256 || s.chars().any(char::is_control))
112 || offset > 1_000_000
113 {
114 return Err("Recherche radio invalide ou trop longue.".into());
115 }
116 let started = Instant::now();
117 let hosts = self.candidates(started)?;
118 let offset = offset.to_string();
119 let (country_field, country_value) = country_filter(country);
120 let mut query = vec![
121 ("name", name.trim()),
122 (country_field, country_value.as_str()),
123 ("tag", tag.trim()),
124 ("offset", &offset),
125 ("limit", "51"),
126 ("hidebroken", "true"),
127 ("order", "clickcount"),
128 ("reverse", "true"),
129 ];
130 if secure_only {
131 query.push(("is_https", "true"));
132 }
133 let stations = retry_mirrors(
134 &hosts,
135 offset == "0"
136 && name.trim().is_empty()
137 && tag.trim().is_empty()
138 && !country.trim().is_empty(),
139 |host| {
140 self.read(started, host, "/json/stations/search", &query)
141 .and_then(station_page)
142 },
143 )?;
144 Ok(stations)
145 }
146 pub fn statistics(&mut self) -> Result<Value, String> {
148 let started = Instant::now();
149 let hosts = self.candidates(started)?;
150 retry_mirrors(&hosts, false, |host| {
151 self.read(started, host, "/json/stats", &[])
152 .and_then(station_statistics)
153 })
154 }
155 pub fn resolve(&mut self, uuid: &str) -> Result<Value, String> {
157 if !valid_uuid(uuid) {
158 return Err("Identifiant radio invalide.".into());
159 }
160 let started = Instant::now();
161 let hosts = self.candidates(started)?;
162 let answer = retry_mirrors(&hosts, false, |host| {
163 self.read(started, host, &format!("/json/url/{uuid}"), &[])
164 .and_then(|value| resolved_station(value, uuid))
165 })?;
166 Ok(
167 json!({"stationuuid": uuid, "url": preferred_stream(answer["url"].as_str().ok_or("Adresse du flux invalide.")?)}),
168 )
169 }
170}
171#[cfg(test)]
172#[path = "radio_tests.rs"]
173mod tests;