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 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}