From 8a803edd539b1d3fe0653664e0a471cf5c526a1a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 21 Apr 2022 20:56:01 +0200 Subject: [PATCH] searchbuilders --- Cargo.toml | 3 +- src/api.rs | 101 +++++++++++++++ src/bin/test.rs | 21 ++- src/countrysearchbuilder.rs | 82 ++++++++++++ src/external.rs | 20 +++ src/lib.rs | 251 ++---------------------------------- src/stationsearchbuilder.rs | 209 ++++++++++++++++++++++++++++++ src/structs.rs | 92 +++++++++++++ 8 files changed, 535 insertions(+), 244 deletions(-) create mode 100644 src/api.rs create mode 100644 src/countrysearchbuilder.rs create mode 100644 src/external.rs create mode 100644 src/stationsearchbuilder.rs create mode 100644 src/structs.rs diff --git a/Cargo.toml b/Cargo.toml index efcdbb8..1005b40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-std = { version = "*", features = ["attributes", "tokio1"] } +async-std = { version = "1.11.0", features = ["attributes", "tokio1"] } async-std-resolver = "0.21.2" chrono = { version = "0.4.19", features = ["serde"] } +futures = { version = "0.3.21" } rand = { version = "0.8.5" } reqwest = { version = "0.11.10", features = ["json"] } serde = { version = "1.0.136", features = ["derive"] } \ No newline at end of file diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..fc49e7c --- /dev/null +++ b/src/api.rs @@ -0,0 +1,101 @@ +use crate::CountrySearchBuilder; +use crate::StationSearchBuilder; +use crate::ApiConfig; +use crate::ApiCountry; +use crate::external::post_api; + +use serde::de::DeserializeOwned; +use std::collections::HashMap; +use std::error::Error; + +use rand::seq::SliceRandom; +use rand::thread_rng; + +use async_std_resolver::proto::rr::RecordType; +use async_std_resolver::proto::xfer::DnsRequestOptions; +use async_std_resolver::{config, resolver}; + +#[derive(Clone, Debug)] +pub struct RadioBrowserAPI { + servers: Vec, + current: usize, +} + +impl RadioBrowserAPI { + pub async fn new() -> Result> { + Ok(RadioBrowserAPI { + servers: RadioBrowserAPI::get_servers().await?, + current: 0, + }) + } + + pub fn get_current_server(&mut self) -> String { + if self.servers.len() > 0 { + self.current = self.current % self.servers.len(); + format!("https://{}", self.servers[self.current]) + } else { + String::from("https://de1.api.radio-browser.info") + } + } + + pub async fn post_api>( + &mut self, + endpoint: A, + ) -> Result> { + let mapjson = HashMap::new(); + post_api(self.get_current_server(), endpoint.as_ref(), mapjson).await + } + + pub async fn get_server_config(&mut self) -> Result> { + Ok(self.post_api("/json/config").await?) + } + + pub fn search(&self) -> StationSearchBuilder { + StationSearchBuilder::new(self.clone()) + } + + pub fn get_countries(&mut self) -> CountrySearchBuilder { + CountrySearchBuilder::new(self.clone()) + } + + pub async fn send, Q: DeserializeOwned>( + &mut self, + endpoint: P, + mapjson: HashMap, + ) -> Result> { + post_api(self.get_current_server(), endpoint, mapjson).await + } + + pub async fn get_countries_filtered>( + &mut self, + filter: P, + ) -> Result, Box> { + Ok(self + .post_api(format!("/json/countries/{}", filter.as_ref())) + .await?) + } + + pub async fn get_servers() -> Result, Box> { + let resolver = resolver( + config::ResolverConfig::default(), + config::ResolverOpts::default(), + ) + .await?; + let response = resolver + .lookup( + "_api._tcp.radio-browser.info", + RecordType::SRV, + DnsRequestOptions::default(), + ) + .await?; + let mut list: Vec = response + .iter() + .filter_map(|entry| entry.as_srv()) + .map(|entry| entry.target().to_string().trim_matches('.').to_string()) + .collect(); + + list.shuffle(&mut thread_rng()); + println!("Servers: {:?}", list); + Ok(list) + } +} \ No newline at end of file diff --git a/src/bin/test.rs b/src/bin/test.rs index 4a991c0..2b6e733 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -1,14 +1,23 @@ +use futures::join; use radiobrowser_lib_rust::RadioBrowserAPI; +use radiobrowser_lib_rust::StationOrder; use std::error::Error; #[async_std::main] async fn main() -> Result<(), Box> { let mut api = RadioBrowserAPI::new().await?; - let config = api.get_server_config().await?; - println!("{:#?}", config); - let countries = api.get_countries().await?; - println!("{:#?}", countries); - let stations = api.search().name("jazz").send().await?; - println!("{:#?}", stations); + let countries = api.get_countries().filter("a").send(); + let stations = api + .search() + .name("jazz") + .reverse(true) + .order(StationOrder::Clickcount) + .send(); + let config = api.get_server_config(); + let (stations, config, countries) = join!(stations, config, countries); + + println!("Config: {:#?}", config?); + println!("Countries found: {}", countries?.len()); + println!("Stations found: {}", stations?.len()); Ok(()) } diff --git a/src/countrysearchbuilder.rs b/src/countrysearchbuilder.rs new file mode 100644 index 0000000..de99fcc --- /dev/null +++ b/src/countrysearchbuilder.rs @@ -0,0 +1,82 @@ +use crate::ApiCountry; +use crate::RadioBrowserAPI; +use std::fmt::Display; + +use std::collections::HashMap; +use std::error::Error; + +pub enum CountryOrder { + Name, + StationCount, +} + +impl Display for CountryOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + CountryOrder::Name => write!(f, "name"), + CountryOrder::StationCount => write!(f, "stationcount"), + } + } +} + +#[derive(Clone, Debug)] +pub struct CountrySearchBuilder { + map: HashMap, + api: RadioBrowserAPI, + filter: Option, +} + +impl CountrySearchBuilder { + pub fn new(api: RadioBrowserAPI) -> Self { + CountrySearchBuilder { + api, + map: HashMap::new(), + filter: None, + } + } + + pub fn filter>(mut self, filter: P) -> Self { + self.filter = Some(filter.as_ref().to_string()); + self + } + + pub fn order(mut self, order: CountryOrder) -> Self { + self.map.insert(String::from("order"), order.to_string()); + self + } + + pub fn reverse(mut self, reverse: bool) -> Self { + self.map + .insert(String::from("reverse"), reverse.to_string()); + self + } + + pub fn offset>(mut self, offset: P) -> Self { + self.map + .insert(String::from("offset"), offset.as_ref().to_string()); + self + } + + pub fn limit>(mut self, limit: P) -> Self { + self.map + .insert(String::from("limit"), limit.as_ref().to_string()); + self + } + + pub fn hidebroken(mut self, hidebroken: bool) -> Self { + self.map + .insert(String::from("hidebroken"), hidebroken.to_string()); + self + } + + pub async fn send(mut self) -> Result, Box> { + if let Some(filter) = self.filter { + Ok(self + .api + .send(format!("/json/countries/{}", filter), self.map) + .await?) + } else { + Ok(self.api.send("/json/countries", self.map).await?) + } + } +} diff --git a/src/external.rs b/src/external.rs new file mode 100644 index 0000000..d680a6b --- /dev/null +++ b/src/external.rs @@ -0,0 +1,20 @@ +use reqwest; +use serde::de::DeserializeOwned; +use std::collections::HashMap; +use std::error::Error; + +pub async fn post_api, B: AsRef>( + server: A, + endpoint: B, + mapjson: HashMap, +) -> Result> { + let client = reqwest::Client::new(); + let res = client + .post(format!("{}{}", server.as_ref(), endpoint.as_ref())) + .json(&mapjson) + .send() + .await? + .json::

() + .await?; + Ok(res) +} diff --git a/src/lib.rs b/src/lib.rs index 1121cef..c2fa04e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,238 +1,15 @@ -use reqwest; -use serde::de::DeserializeOwned; -use serde::Deserialize; -use std::collections::HashMap; -use std::error::Error; +mod api; +mod external; +mod stationsearchbuilder; +mod countrysearchbuilder; +mod structs; -use rand::seq::SliceRandom; -use rand::thread_rng; - -use async_std_resolver::proto::rr::RecordType; -use async_std_resolver::proto::xfer::DnsRequestOptions; -use async_std_resolver::{config, resolver}; - -use chrono::DateTime; -use chrono::Utc; - -#[derive(PartialEq, Deserialize, Debug)] -pub struct ApiStation { - pub changeuuid: String, - pub stationuuid: String, - pub serveruuid: Option, - pub name: String, - pub url: String, - pub url_resolved: String, - pub homepage: String, - pub favicon: String, - pub tags: String, - pub country: String, - pub countrycode: String, - pub iso_3166_2: Option, - pub state: String, - pub language: String, - pub languagecodes: Option, - pub votes: i32, - pub lastchangetime: String, - pub lastchangetime_iso8601: Option>, - pub codec: String, - pub bitrate: u32, - pub hls: i8, - pub lastcheckok: i8, - pub lastchecktime: String, - pub lastchecktime_iso8601: Option>, - pub lastcheckoktime: String, - pub lastcheckoktime_iso8601: Option>, - pub lastlocalchecktime: String, - pub lastlocalchecktime_iso8601: Option>, - pub clicktimestamp: String, - pub clicktimestamp_iso8601: Option>, - pub clickcount: u32, - pub clicktrend: i32, - pub ssl_error: Option, - pub geo_lat: Option, - pub geo_long: Option, - pub has_extended_info: Option, -} - -#[derive(PartialEq, Eq, Deserialize, Debug)] -pub struct ApiCountry { - pub name: String, - pub iso_3166_1: String, - pub stationcount: u32, -} - -#[derive(PartialEq, Eq, Deserialize, Debug)] -pub struct ApiLanguage { - pub name: String, - pub iso_639: Option, - pub stationcount: u32, -} - -#[derive(PartialEq, Deserialize, Debug)] -pub struct ApiStreamingServer { - pub uuid: String, - pub url: String, - pub statusurl: Option, - pub status: Option, - pub error: Option, -} - -#[derive(Debug, Deserialize)] -pub struct ApiConfig { - pub check_enabled: bool, - pub prometheus_exporter_enabled: bool, - pub pull_servers: Vec, - pub tcp_timeout_seconds: u32, - pub broken_stations_never_working_timeout_seconds: u32, - pub broken_stations_timeout_seconds: u32, - pub checks_timeout_seconds: u32, - pub click_valid_timeout_seconds: u32, - pub clicks_timeout_seconds: u32, - pub mirror_pull_interval_seconds: u32, - pub update_caches_interval_seconds: u32, - pub server_name: String, - pub server_location: String, - pub server_country_code: String, - pub check_retries: u32, - pub check_batchsize: u32, - pub check_pause_seconds: u32, - pub api_threads: u32, - pub cache_type: String, - pub cache_ttl: u32, - pub language_replace_filepath: String, - pub language_to_code_filepath: String, -} - -#[derive(Clone, Debug)] -pub struct SearchBuilder { - map: HashMap, - api: RadioBrowserAPI, -} - -impl SearchBuilder { - pub fn new(api: RadioBrowserAPI) -> Self { - SearchBuilder { - api, - map: HashMap::new(), - } - } - - pub fn name>(mut self, name: P) -> Self { - self.map - .insert(String::from("name"), name.as_ref().to_string()); - self - } - - pub fn countrycode>(mut self, countrycode: P) -> Self { - self.map.insert( - String::from("countrycode"), - countrycode.as_ref().to_string(), - ); - self - } - - pub async fn send(mut self) -> Result, Box> { - Ok(self.api.send("/json/stations/search", self.map).await?) - } -} - -#[derive(Clone, Debug)] -pub struct RadioBrowserAPI { - servers: Vec, - current: usize, -} - -impl RadioBrowserAPI { - pub async fn new() -> Result> { - Ok(RadioBrowserAPI { - servers: RadioBrowserAPI::get_servers().await?, - current: 0, - }) - } - - pub fn get_current_server(&mut self) -> String { - if self.servers.len() > 0 { - self.current = self.current % self.servers.len(); - format!("https://{}", self.servers[self.current]) - } else { - String::from("https://de1.api.radio-browser.info") - } - } - - pub async fn post_api>( - &mut self, - endpoint: A, - ) -> Result> { - let mapjson = HashMap::new(); - post_api(self.get_current_server(), endpoint.as_ref(), mapjson).await - } - - pub fn search(&self) -> SearchBuilder { - SearchBuilder::new(self.clone()) - } - - pub async fn get_server_config(&mut self) -> Result> { - Ok(self.post_api("/json/config").await?) - } - - pub async fn get_countries(&mut self) -> Result, Box> { - Ok(self.post_api("/json/countries").await?) - } - - pub async fn send, Q: DeserializeOwned>( - &mut self, - endpoint: P, - mapjson: HashMap, - ) -> Result> { - post_api(self.get_current_server(), endpoint, mapjson).await - } - - pub async fn get_countries_filtered>( - &mut self, - filter: P, - ) -> Result, Box> { - Ok(self - .post_api(format!("/json/countries/{}", filter.as_ref())) - .await?) - } - - pub async fn get_servers() -> Result, Box> { - let resolver = resolver( - config::ResolverConfig::default(), - config::ResolverOpts::default(), - ) - .await?; - let response = resolver - .lookup( - "_api._tcp.radio-browser.info", - RecordType::SRV, - DnsRequestOptions::default(), - ) - .await?; - let mut list: Vec = response - .iter() - .filter_map(|entry| entry.as_srv()) - .map(|entry| entry.target().to_string().trim_matches('.').to_string()) - .collect(); - - list.shuffle(&mut thread_rng()); - println!("Servers: {:?}", list); - Ok(list) - } -} - -pub async fn post_api, B: AsRef>( - server: A, - endpoint: B, - mapjson: HashMap, -) -> Result> { - let client = reqwest::Client::new(); - let res = client - .post(format!("{}{}", server.as_ref(), endpoint.as_ref())) - .json(&mapjson) - .send() - .await? - .json::

() - .await?; - Ok(res) -} +pub use api::RadioBrowserAPI; +pub use structs::ApiConfig; +pub use structs::ApiCountry; +pub use structs::ApiLanguage; +pub use structs::ApiStation; +pub use structs::ApiStreamingServer; +pub use stationsearchbuilder::StationSearchBuilder; +pub use stationsearchbuilder::StationOrder; +pub use countrysearchbuilder::CountrySearchBuilder; \ No newline at end of file diff --git a/src/stationsearchbuilder.rs b/src/stationsearchbuilder.rs new file mode 100644 index 0000000..3d14c2d --- /dev/null +++ b/src/stationsearchbuilder.rs @@ -0,0 +1,209 @@ +use crate::api::RadioBrowserAPI; +use crate::structs::ApiStation; +use std::fmt::Display; + +use std::collections::HashMap; +use std::error::Error; + +pub enum StationOrder { + Name, + Url, + Homepage, + Favicon, + Tags, + Country, + State, + Language, + Votes, + Codec, + Bitrate, + Lastcheckok, + Lastchecktime, + Clicktimestamp, + Clickcount, + Clicktrend, + Changetimestamp, + Random, +} + +impl Display for StationOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + StationOrder::Name => write!(f, "name"), + StationOrder::Url => write!(f, "url"), + StationOrder::Homepage => write!(f, "homepage"), + StationOrder::Favicon => write!(f, "favicon"), + StationOrder::Tags => write!(f, "tags"), + StationOrder::Country => write!(f, "country"), + StationOrder::State => write!(f, "state"), + StationOrder::Language => write!(f, "language"), + StationOrder::Votes => write!(f, "votes"), + StationOrder::Codec => write!(f, "codec"), + StationOrder::Bitrate => write!(f, "bitrate"), + StationOrder::Lastcheckok => write!(f, "lastcheckok"), + StationOrder::Lastchecktime => write!(f, "lastchecktime"), + StationOrder::Clicktimestamp => write!(f, "clicktimestamp"), + StationOrder::Clickcount => write!(f, "clickcount"), + StationOrder::Clicktrend => write!(f, "clicktrend"), + StationOrder::Changetimestamp => write!(f, "changetimestamp"), + StationOrder::Random => write!(f, "random"), + } + } +} + +#[derive(Clone, Debug)] +pub struct StationSearchBuilder { + map: HashMap, + api: RadioBrowserAPI, +} + +impl StationSearchBuilder { + pub fn new(api: RadioBrowserAPI) -> Self { + StationSearchBuilder { + api, + map: HashMap::new(), + } + } + + pub fn name>(mut self, name: P) -> Self { + self.map + .insert(String::from("name"), name.as_ref().to_string()); + self + } + + pub fn name_exact(mut self, name_exact: bool) -> Self { + self.map + .insert(String::from("nameExact"), name_exact.to_string()); + self + } + + pub fn country>(mut self, country: P) -> Self { + self.map + .insert(String::from("country"), country.as_ref().to_string()); + self + } + + pub fn country_exact(mut self, country_exact: bool) -> Self { + self.map + .insert(String::from("countryExact"), country_exact.to_string()); + self + } + + pub fn countrycode>(mut self, countrycode: P) -> Self { + self.map.insert( + String::from("countrycode"), + countrycode.as_ref().to_string(), + ); + self + } + + pub fn state>(mut self, state: P) -> Self { + self.map + .insert(String::from("state"), state.as_ref().to_string()); + self + } + + pub fn state_exact(mut self, state_exact: bool) -> Self { + self.map + .insert(String::from("stateExact"), state_exact.to_string()); + self + } + + pub fn language>(mut self, language: P) -> Self { + self.map + .insert(String::from("language"), language.as_ref().to_string()); + self + } + + pub fn language_exact(mut self, language_exact: bool) -> Self { + self.map + .insert(String::from("languageExact"), language_exact.to_string()); + self + } + + pub fn tag>(mut self, tag: P) -> Self { + self.map + .insert(String::from("tag"), tag.as_ref().to_string()); + self + } + + pub fn tag_exact(mut self, tag_exact: bool) -> Self { + self.map + .insert(String::from("tagExact"), tag_exact.to_string()); + self + } + + /* + tagList STRING, STRING, ... OPTIONAL. , a comma-separated list of tag. It can also be an array of string in JSON HTTP POST parameters. All tags in list have to match. + */ + + pub fn codec>(mut self, codec: P) -> Self { + self.map + .insert(String::from("codec"), codec.as_ref().to_string()); + self + } + + pub fn bitrate_min>(mut self, bitrate_min: P) -> Self { + self.map + .insert(String::from("bitrateMin"), bitrate_min.as_ref().to_string()); + self + } + pub fn bitrate_max>(mut self, bitrate_max: P) -> Self { + self.map + .insert(String::from("bitrateMax"), bitrate_max.as_ref().to_string()); + self + } + + pub fn has_geo_info(mut self, has_geo_info: bool) -> Self { + self.map + .insert(String::from("has_geo_info"), has_geo_info.to_string()); + self + } + + pub fn has_extended_info(mut self, has_extended_info: bool) -> Self { + self.map.insert( + String::from("has_extended_info"), + has_extended_info.to_string(), + ); + self + } + + pub fn is_https(mut self, is_https: bool) -> Self { + self.map + .insert(String::from("is_https"), is_https.to_string()); + self + } + + pub fn order(mut self, order: StationOrder) -> Self { + self.map.insert(String::from("order"), order.to_string()); + self + } + + pub fn reverse(mut self, reverse: bool) -> Self { + self.map + .insert(String::from("reverse"), reverse.to_string()); + self + } + + pub fn offset>(mut self, offset: P) -> Self { + self.map + .insert(String::from("offset"), offset.as_ref().to_string()); + self + } + + pub fn limit>(mut self, limit: P) -> Self { + self.map + .insert(String::from("limit"), limit.as_ref().to_string()); + self + } + + pub fn hidebroken(mut self, hidebroken: bool) -> Self { + self.map + .insert(String::from("hidebroken"), hidebroken.to_string()); + self + } + + pub async fn send(mut self) -> Result, Box> { + Ok(self.api.send("/json/stations/search", self.map).await?) + } +} diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..f5b5b91 --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,92 @@ +use serde::Deserialize; +use chrono::DateTime; +use chrono::Utc; + +#[derive(PartialEq, Deserialize, Debug)] +pub struct ApiStation { + pub changeuuid: String, + pub stationuuid: String, + pub serveruuid: Option, + pub name: String, + pub url: String, + pub url_resolved: String, + pub homepage: String, + pub favicon: String, + pub tags: String, + pub country: String, + pub countrycode: String, + pub iso_3166_2: Option, + pub state: String, + pub language: String, + pub languagecodes: Option, + pub votes: i32, + pub lastchangetime: String, + pub lastchangetime_iso8601: Option>, + pub codec: String, + pub bitrate: u32, + pub hls: i8, + pub lastcheckok: i8, + pub lastchecktime: String, + pub lastchecktime_iso8601: Option>, + pub lastcheckoktime: String, + pub lastcheckoktime_iso8601: Option>, + pub lastlocalchecktime: String, + pub lastlocalchecktime_iso8601: Option>, + pub clicktimestamp: String, + pub clicktimestamp_iso8601: Option>, + pub clickcount: u32, + pub clicktrend: i32, + pub ssl_error: Option, + pub geo_lat: Option, + pub geo_long: Option, + pub has_extended_info: Option, +} + +#[derive(PartialEq, Eq, Deserialize, Debug)] +pub struct ApiCountry { + pub name: String, + pub iso_3166_1: String, + pub stationcount: u32, +} + +#[derive(PartialEq, Eq, Deserialize, Debug)] +pub struct ApiLanguage { + pub name: String, + pub iso_639: Option, + pub stationcount: u32, +} + +#[derive(PartialEq, Deserialize, Debug)] +pub struct ApiStreamingServer { + pub uuid: String, + pub url: String, + pub statusurl: Option, + pub status: Option, + pub error: Option, +} + +#[derive(Debug, Deserialize)] +pub struct ApiConfig { + pub check_enabled: bool, + pub prometheus_exporter_enabled: bool, + pub pull_servers: Vec, + pub tcp_timeout_seconds: u32, + pub broken_stations_never_working_timeout_seconds: u32, + pub broken_stations_timeout_seconds: u32, + pub checks_timeout_seconds: u32, + pub click_valid_timeout_seconds: u32, + pub clicks_timeout_seconds: u32, + pub mirror_pull_interval_seconds: u32, + pub update_caches_interval_seconds: u32, + pub server_name: String, + pub server_location: String, + pub server_country_code: String, + pub check_retries: u32, + pub check_batchsize: u32, + pub check_pause_seconds: u32, + pub api_threads: u32, + pub cache_type: String, + pub cache_ttl: u32, + pub language_replace_filepath: String, + pub language_to_code_filepath: String, +} \ No newline at end of file