Browse and listen to thousands of radio stations across the globe right from your terminal ๐ŸŒŽ ๐Ÿ“ป ๐ŸŽตโœจ
radio rust tokio web-radio command-line-tool tui
at main 5.5 kB view raw
1use std::{process, thread, time::Duration}; 2 3use anyhow::Error; 4use hyper::header::HeaderValue; 5use tunein_cli::os_media_controls::OsMediaControls; 6 7use crate::{ 8 app::{App, CurrentDisplayMode, State, Volume}, 9 cfg::{SourceOptions, UiOptions}, 10 decoder::Mp3Decoder, 11 provider::{radiobrowser::Radiobrowser, tunein::Tunein, Provider}, 12 tui, 13}; 14 15pub async fn exec( 16 name_or_id: &str, 17 provider: &str, 18 volume: f32, 19 display_mode: CurrentDisplayMode, 20 enable_os_media_controls: bool, 21 poll_events_every: Duration, 22 poll_events_every_while_paused: Duration, 23) -> Result<(), Error> { 24 let _provider = provider; 25 let provider: Box<dyn Provider> = match provider { 26 "tunein" => Box::new(Tunein::new()), 27 "radiobrowser" => Box::new(Radiobrowser::new().await), 28 _ => { 29 return Err(anyhow::anyhow!(format!( 30 "Unsupported provider '{}'", 31 provider 32 ))) 33 } 34 }; 35 let station = provider.get_station(name_or_id.to_string()).await?; 36 if station.is_none() { 37 return Err(Error::msg("No station found")); 38 } 39 40 let station = station.unwrap(); 41 let stream_url = station.stream_url.clone(); 42 let id = station.id.clone(); 43 let now_playing = station.playing.clone().unwrap_or_default(); 44 45 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel::<State>(); 46 let (sink_cmd_tx, mut sink_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<SinkCommand>(); 47 let (frame_tx, frame_rx) = std::sync::mpsc::channel::<minimp3::Frame>(); 48 49 let ui = UiOptions { 50 scale: 1.0, 51 scatter: false, 52 no_reference: true, 53 no_ui: true, 54 no_braille: false, 55 }; 56 57 let opts = SourceOptions { 58 channels: 2, 59 buffer: 1152, 60 sample_rate: 44100, 61 tune: None, 62 }; 63 64 let os_media_controls = if enable_os_media_controls { 65 OsMediaControls::new() 66 .inspect_err(|err| { 67 eprintln!( 68 "error: failed to initialize os media controls due to `{}`", 69 err 70 ); 71 }) 72 .ok() 73 } else { 74 None 75 }; 76 77 let mut app = App::new( 78 &ui, 79 &opts, 80 frame_rx, 81 display_mode, 82 os_media_controls, 83 poll_events_every, 84 poll_events_every_while_paused, 85 ); 86 let station_name = station.name.clone(); 87 88 thread::spawn(move || { 89 let client = reqwest::blocking::Client::new(); 90 91 let response = client.get(stream_url).send().unwrap(); 92 93 let headers = response.headers(); 94 let volume = Volume::new(volume, false); 95 96 cmd_tx 97 .send(State { 98 name: match headers 99 .get("icy-name") 100 .unwrap_or(&HeaderValue::from_static("Unknown")) 101 .to_str() 102 .unwrap() 103 { 104 "Unknown" => station_name, 105 name => name.to_string(), 106 }, 107 now_playing, 108 genre: headers 109 .get("icy-genre") 110 .unwrap_or(&HeaderValue::from_static("Unknown")) 111 .to_str() 112 .unwrap() 113 .to_string(), 114 description: headers 115 .get("icy-description") 116 .unwrap_or(&HeaderValue::from_static("Unknown")) 117 .to_str() 118 .unwrap() 119 .to_string(), 120 br: headers 121 .get("icy-br") 122 .unwrap_or(&HeaderValue::from_static("")) 123 .to_str() 124 .unwrap() 125 .to_string(), 126 volume: volume.clone(), 127 }) 128 .unwrap(); 129 let location = response.headers().get("location"); 130 131 let response = match location { 132 Some(location) => { 133 let response = client.get(location.to_str().unwrap()).send().unwrap(); 134 let location = response.headers().get("location"); 135 match location { 136 Some(location) => client.get(location.to_str().unwrap()).send().unwrap(), 137 None => response, 138 } 139 } 140 None => response, 141 }; 142 143 let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); 144 let sink = rodio::Sink::try_new(&handle).unwrap(); 145 sink.set_volume(volume.volume_ratio()); 146 let decoder = Mp3Decoder::new(response, Some(frame_tx)).unwrap(); 147 sink.append(decoder); 148 149 loop { 150 while let Ok(sink_cmd) = sink_cmd_rx.try_recv() { 151 match sink_cmd { 152 SinkCommand::Play => { 153 sink.play(); 154 } 155 SinkCommand::Pause => { 156 sink.pause(); 157 } 158 SinkCommand::SetVolume(volume) => { 159 sink.set_volume(volume); 160 } 161 } 162 } 163 std::thread::sleep(Duration::from_millis(10)); 164 } 165 }); 166 167 let mut terminal = tui::init()?; 168 app.run(&mut terminal, cmd_rx, sink_cmd_tx, &id).await; 169 tui::restore()?; 170 171 process::exit(0); 172} 173 174/// Command for a sink. 175#[derive(Debug, Clone, PartialEq)] 176pub enum SinkCommand { 177 /// Play. 178 Play, 179 /// Pause. 180 Pause, 181 /// Set the volume. 182 SetVolume(f32), 183}