Browse and listen to thousands of radio stations across the globe right from your terminal 🌎 📻 🎵✨
radio
rust
tokio
web-radio
command-line-tool
tui
1use crate::types::Station;
2
3use super::Provider;
4use anyhow::Error;
5use async_trait::async_trait;
6use tunein::TuneInClient;
7
8pub struct Tunein {
9 client: TuneInClient,
10}
11
12impl Tunein {
13 pub fn new() -> Self {
14 Self {
15 client: TuneInClient::new(),
16 }
17 }
18}
19
20#[async_trait]
21impl Provider for Tunein {
22 async fn search(&self, name: String) -> Result<Vec<Station>, Error> {
23 let results = self
24 .client
25 .search(&name)
26 .await
27 .map_err(|e| Error::msg(e.to_string()))?;
28 let stations = results.into_iter().map(|x| Station::from(x)).collect();
29 Ok(stations)
30 }
31
32 async fn get_station(&self, id: String) -> Result<Option<Station>, Error> {
33 let stations = self
34 .client
35 .get_station(&id)
36 .await
37 .map_err(|e| Error::msg(e.to_string()))?;
38 match stations.len() {
39 0 => {
40 let results = self.search(id.clone()).await?;
41 let station = results.first().cloned();
42 match station {
43 Some(st) => {
44 let stations = self
45 .client
46 .get_station(&st.id)
47 .await
48 .map_err(|e| Error::msg(e.to_string()))?;
49 let mut station = Station::from(stations[0].clone());
50 station.id = st.id.clone();
51 station.name = st.name.clone();
52 station.playing = st.playing.clone();
53 return Ok(Some(station));
54 }
55 None => Ok(None),
56 }
57 }
58 _ => {
59 let mut station = Station::from(stations[0].clone());
60 // Preserve the original station ID since StationLinkDetails doesn't contain it
61 station.id = id;
62 Ok(Some(station))
63 }
64 }
65 }
66
67 async fn browse(
68 &self,
69 category: String,
70 _offset: u32,
71 _limit: u32,
72 ) -> Result<Vec<Station>, Error> {
73 let guide_id = category.clone();
74 let category = match category.to_lowercase().as_str() {
75 "by location" => Some(tunein::types::Category::ByLocation),
76 "by language" => Some(tunein::types::Category::ByLanguage),
77 "sports" => Some(tunein::types::Category::Sports),
78 "talk" => Some(tunein::types::Category::Talk),
79 "music" => Some(tunein::types::Category::Music),
80 "local radio" => Some(tunein::types::Category::LocalRadio),
81 "podcasts" => Some(tunein::types::Category::Podcasts),
82 _ => None,
83 };
84
85 if category.is_none() {
86 let category_stations = self
87 .client
88 .browse_by_id(&guide_id)
89 .await
90 .map_err(|e| Error::msg(e.to_string()))?;
91
92 let mut stations = vec![];
93
94 for st in category_stations {
95 if let Some(children) = st.clone().children {
96 stations = [stations, vec![Box::new(st.clone())], children].concat();
97 }
98 }
99
100 let stations = stations.into_iter().map(|x| Station::from(x)).collect();
101 return Ok(stations);
102 }
103
104 let category_stations = self
105 .client
106 .browse(category)
107 .await
108 .map_err(|e| Error::msg(e.to_string()))?;
109
110 let stations = category_stations
111 .clone()
112 .into_iter()
113 .map(|x| Station::from(x))
114 .collect::<Vec<Station>>();
115
116 let mut _stations = vec![];
117 for st in category_stations {
118 if let Some(children) = st.children {
119 _stations = [_stations, children].concat();
120 }
121 }
122 let _stations = _stations
123 .into_iter()
124 .map(|x| Station::from(x))
125 .collect::<Vec<Station>>();
126
127 Ok([stations, _stations].concat())
128 }
129
130 async fn categories(&self, _offset: u32, _limit: u32) -> Result<Vec<String>, Error> {
131 let categories = self
132 .client
133 .browse(None)
134 .await
135 .map_err(|e| Error::msg(e.to_string()))?;
136 let categories = categories.into_iter().map(|x| x.text).collect();
137 Ok(categories)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[tokio::test]
146 pub async fn test_search() {
147 let provider = Tunein::new();
148 let name = "alternativeradio";
149 let stations = provider.search(name.to_string()).await.unwrap();
150 println!("Search: {:#?}", stations);
151 assert!(stations.len() > 0)
152 }
153
154 #[tokio::test]
155 pub async fn test_get_station() {
156 let provider = Tunein::new();
157 let name = "s288303".to_string();
158 let station = provider.get_station(name).await.unwrap();
159 println!("Station: {:#?}", station);
160 assert!(station.is_some())
161 }
162
163 #[tokio::test]
164 pub async fn test_browse() {
165 let provider = Tunein::new();
166 let stations = provider.browse("music".to_string(), 0, 100).await.unwrap();
167 println!("Browse: {:#?}", stations);
168 assert!(stations.len() > 0)
169 }
170
171 #[tokio::test]
172 pub async fn test_browse_by_id() {
173 let provider = Tunein::new();
174 let stations = provider.browse("c57942".to_string(), 0, 100).await.unwrap();
175 println!("Browse by category id: {:#?}", stations);
176 assert!(stations.len() > 0)
177 }
178
179 #[tokio::test]
180 pub async fn test_categories() {
181 let provider = Tunein::new();
182 let categories = provider.categories(0, 100).await.unwrap();
183 println!("Categories: {:#?}", categories);
184 assert!(categories.len() > 0)
185 }
186}