Files
iradio/src/radio/app/mod.rs
T

184 lines
5.5 KiB
Rust

use crate::radio::station::StationInfo;
use color_eyre::{
Result,
eyre::{WrapErr, eyre},
};
use radiobrowser::{RadioBrowserAPI, StationOrder};
use ratatui::{DefaultTerminal, widgets::ListState};
use std::collections::HashSet;
use tokio::process::Child;
use tokio::sync::mpsc;
mod draw;
mod favorites;
mod input;
mod player;
#[cfg(test)]
mod tests;
#[derive(Debug, PartialEq)]
pub enum InputMode {
Normal,
Search,
}
#[derive(Debug)]
pub struct App {
exit: bool,
stations: Vec<StationInfo>,
station_list_state: ListState,
filtered_stations: Vec<StationInfo>,
search_query: String,
input_mode: InputMode,
player: Option<Child>,
current_station: Option<StationInfo>,
current_song: Option<String>,
played_songs: Vec<String>,
song_rx: Option<mpsc::UnboundedReceiver<String>>,
error_message: Option<String>,
spawn_player: Option<player::SpawnFn>,
favorites: HashSet<String>,
show_favorites_only: bool,
}
impl Default for App {
fn default() -> Self {
Self {
exit: false,
stations: Vec::new(),
station_list_state: {
let mut state = ListState::default();
state.select(Some(0));
state
},
filtered_stations: Vec::new(),
input_mode: InputMode::Normal,
search_query: String::new(),
player: None,
current_station: None,
current_song: None,
played_songs: Vec::new(),
song_rx: None,
error_message: None,
spawn_player: Some(player::new_mpv_spawn()),
favorites: favorites::load_favorites(),
show_favorites_only: false,
}
}
}
impl App {
pub fn without_player(mut self) -> Self {
self.spawn_player = None;
self
}
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()
.await
.wrap_err("handle events failed")?;
self.check_song_updates();
}
Ok(())
}
fn check_song_updates(&mut self) {
let mut new_songs = Vec::new();
if let Some(rx) = &mut self.song_rx {
while let Ok(song) = rx.try_recv() {
new_songs.push(song);
}
}
for song in new_songs {
self.update_current_song(song);
}
if let Some(child) = &mut self.player
&& let Ok(Some(_)) = child.try_wait()
{
self.player = None;
self.error_message = Some("Stream disconnected".to_string());
}
}
fn update_current_song(&mut self, song: String) {
if let Some(prev) = &self.current_song
&& prev != &song
{
self.played_songs.insert(0, prev.clone());
if self.played_songs.len() > 20 {
self.played_songs.pop();
}
}
self.current_song = Some(song);
}
fn update_filter(&mut self) {
if self.search_query.is_empty() {
self.filtered_stations = self.stations.clone();
} else {
let query = self.search_query.to_lowercase();
self.filtered_stations = self
.stations
.iter()
.filter(|s| {
s.name.to_lowercase().contains(&query)
|| s.tags.to_lowercase().contains(&query)
})
.cloned()
.collect();
}
if self.show_favorites_only {
self.filtered_stations.retain(|s| self.favorites.contains(&s.url));
}
if self.filtered_stations.is_empty() {
self.station_list_state.select(None);
} else {
self.station_list_state.select(Some(0));
}
}
pub async fn load_stations(&mut self) -> color_eyre::Result<()> {
let api = RadioBrowserAPI::new()
.await
.map_err(|e| eyre!("Failed to create RadioBrowserAPI: {e}"))?;
let (de, us, at) = tokio::try_join!(
async { api.get_stations().country("Germany").order(StationOrder::Clickcount).reverse(true).limit("5000").send().await.map_err(|e| eyre!("Failed to fetch DE stations: {e}")) },
async { api.get_stations().country("United States").order(StationOrder::Clickcount).reverse(true).limit("5000").send().await.map_err(|e| eyre!("Failed to fetch US stations: {e}")) },
async { api.get_stations().country("Austria").order(StationOrder::Clickcount).reverse(true).limit("5000").send().await.map_err(|e| eyre!("Failed to fetch AT stations: {e}")) },
)?;
let mut seen_urls = std::collections::HashSet::new();
let mut stations: Vec<StationInfo> = de
.into_iter()
.chain(us)
.chain(at)
.filter_map(|s: radiobrowser::ApiStation| {
if s.url.is_empty() || !seen_urls.insert(s.url.clone()) {
return None;
}
Some(StationInfo {
name: s.name,
url: s.url,
country_code: s.countrycode,
bitrate: s.bitrate,
codec: s.codec,
tags: s.tags,
})
})
.collect();
stations.sort_by_key(|a| a.name.to_lowercase());
self.stations = stations;
self.update_filter();
Ok(())
}
}