Refactoring, improve code structure
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user