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::{thread, time::Duration};
2
3use anyhow::Error;
4use hyper::header::HeaderValue;
5
6use crate::{
7 app::{App, CurrentDisplayMode, State, Volume},
8 cfg::{SourceOptions, UiOptions},
9 decoder::Mp3Decoder,
10 provider::{radiobrowser::Radiobrowser, tunein::Tunein, Provider},
11 tui,
12};
13
14pub async fn exec(
15 name_or_id: &str,
16 provider: &str,
17 volume: f32,
18 display_mode: CurrentDisplayMode,
19) -> Result<(), Error> {
20 let _provider = provider;
21 let provider: Box<dyn Provider> = match provider {
22 "tunein" => Box::new(Tunein::new()),
23 "radiobrowser" => Box::new(Radiobrowser::new().await),
24 _ => {
25 return Err(anyhow::anyhow!(format!(
26 "Unsupported provider '{}'",
27 provider
28 )))
29 }
30 };
31 let station = provider.get_station(name_or_id.to_string()).await?;
32 if station.is_none() {
33 return Err(Error::msg("No station found"));
34 }
35
36 let station = station.unwrap();
37 let stream_url = station.stream_url.clone();
38 let id = station.id.clone();
39 let now_playing = station.playing.clone().unwrap_or_default();
40
41 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel::<State>();
42 let (sink_cmd_tx, mut sink_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<SinkCommand>();
43 let (frame_tx, frame_rx) = std::sync::mpsc::channel::<minimp3::Frame>();
44
45 let ui = UiOptions {
46 scale: 1.0,
47 scatter: false,
48 no_reference: true,
49 no_ui: true,
50 no_braille: false,
51 };
52
53 let opts = SourceOptions {
54 channels: 2,
55 buffer: 1152,
56 sample_rate: 44100,
57 tune: None,
58 };
59
60 let mut app = App::new(&ui, &opts, frame_rx, display_mode);
61 let station_name = station.name.clone();
62
63 thread::spawn(move || {
64 let client = reqwest::blocking::Client::new();
65
66 let response = client.get(stream_url).send().unwrap();
67
68 let headers = response.headers();
69 let volume = Volume::new(volume, false);
70
71 cmd_tx
72 .send(State {
73 name: match headers
74 .get("icy-name")
75 .unwrap_or(&HeaderValue::from_static("Unknown"))
76 .to_str()
77 .unwrap()
78 {
79 "Unknown" => station_name,
80 name => name.to_string(),
81 },
82 now_playing,
83 genre: headers
84 .get("icy-genre")
85 .unwrap_or(&HeaderValue::from_static("Unknown"))
86 .to_str()
87 .unwrap()
88 .to_string(),
89 description: headers
90 .get("icy-description")
91 .unwrap_or(&HeaderValue::from_static("Unknown"))
92 .to_str()
93 .unwrap()
94 .to_string(),
95 br: headers
96 .get("icy-br")
97 .unwrap_or(&HeaderValue::from_static(""))
98 .to_str()
99 .unwrap()
100 .to_string(),
101 volume: volume.clone(),
102 })
103 .unwrap();
104 let location = response.headers().get("location");
105
106 let response = match location {
107 Some(location) => {
108 let response = client.get(location.to_str().unwrap()).send().unwrap();
109 let location = response.headers().get("location");
110 match location {
111 Some(location) => client.get(location.to_str().unwrap()).send().unwrap(),
112 None => response,
113 }
114 }
115 None => response,
116 };
117
118 let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
119 let sink = rodio::Sink::try_new(&handle).unwrap();
120 sink.set_volume(volume.volume_ratio());
121 let decoder = Mp3Decoder::new(response, frame_tx).unwrap();
122 sink.append(decoder);
123
124 loop {
125 while let Ok(sink_cmd) = sink_cmd_rx.try_recv() {
126 match sink_cmd {
127 SinkCommand::Play => {
128 sink.play();
129 }
130 SinkCommand::Pause => {
131 sink.pause();
132 }
133 SinkCommand::SetVolume(volume) => {
134 sink.set_volume(volume);
135 }
136 }
137 }
138 std::thread::sleep(Duration::from_millis(10));
139 }
140 });
141
142 let mut terminal = tui::init()?;
143 app.run(&mut terminal, cmd_rx, sink_cmd_tx, &id).await;
144 tui::restore()?;
145 Ok(())
146}
147
148/// Command for a sink.
149#[derive(Debug, Clone, PartialEq)]
150pub enum SinkCommand {
151 /// Play.
152 Play,
153 /// Pause.
154 Pause,
155 /// Set the volume.
156 SetVolume(f32),
157}