diff --git a/src/radio/app.rs b/src/radio/app.rs index 06f7c0c..7ec753f 100644 --- a/src/radio/app.rs +++ b/src/radio/app.rs @@ -12,8 +12,9 @@ use ratatui::{ style::Stylize, widgets::{Block, Paragraph}, }; -use std::io::{self}; +use std::io; use tokio::process::Child; +use tokio::sync::mpsc; #[derive(Debug, PartialEq)] pub enum InputMode { @@ -30,6 +31,9 @@ pub struct App { search_query: String, input_mode: InputMode, player: Option, + current_song: Option, + played_songs: Vec, + song_rx: Option>, } impl Default for App { @@ -46,6 +50,9 @@ impl Default for App { input_mode: InputMode::Normal, search_query: String::new(), player: None, + current_song: None, + played_songs: Vec::new(), + song_rx: None, } } } @@ -57,6 +64,7 @@ impl App { self.handle_events() .await .wrap_err("handle events failed")?; + self.check_song_updates(); } Ok(()) } @@ -72,10 +80,10 @@ impl App { Layout::vertical([Constraint::Percentage(10), Constraint::Percentage(90)]).areas(right); let [top_left, bottom_left] = - Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(left); + Layout::vertical([Constraint::Percentage(10), Constraint::Percentage(90)]).areas(left); - // Render the left block (e.g., station selection) - let max_name_len = top_left.width.saturating_sub(4) as usize; + // Station list + let max_name_len = bottom_left.width.saturating_sub(4) as usize; let station_items: Vec = self .filtered_stations .iter() @@ -96,30 +104,34 @@ impl App { frame.render_stateful_widget( stations_list, - top_left, + bottom_left, &mut self.station_list_state.clone(), ); - let station = self - .station_list_state - .selected() - .and_then(|i| self.filtered_stations.get(i)); - let paragraph = Paragraph::new( - station - .map(|s| format!("Now selected: {}\n{}", s.name, s.url)) - .unwrap_or_else(|| "No station selected".into()), - ) - .block(Block::bordered().title("Info")); - frame.render_widget(paragraph, bottom_left); + // Info: show current song + let info_text = match &self.current_song { + Some(song) => format!("Now playing: {}", song), + None => "No song playing".to_string(), + }; + let paragraph = Paragraph::new(info_text).block(Block::bordered().title("Info")); + frame.render_widget(paragraph, top_left); + // Search box let mut search_string = self.search_query.clone(); if self.input_mode == InputMode::Search { search_string.push('█'); } let search = Paragraph::new(search_string).block(Block::bordered().title("Search")); - frame.render_widget(search, top_right); - frame.render_widget(Block::bordered().title("Played Songs"), bottom_right); + + // Played Songs list + let song_items: Vec = self + .played_songs + .iter() + .map(|s| ListItem::new(s.clone())) + .collect(); + let songs_list = List::new(song_items).block(Block::bordered().title("Played Songs")); + frame.render_widget(songs_list, bottom_right); } async fn handle_events(&mut self) -> io::Result<()> { @@ -132,6 +144,32 @@ impl App { 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); + } + } + + fn update_current_song(&mut self, song: String) { + if let Some(prev) = &self.current_song { + if prev != &song { + self.played_songs.insert(0, prev.clone()); + if self.played_songs.len() > 20 { + self.played_songs.pop(); + } + } + } + self.current_song = Some(song); + } + pub async fn handle_key_event(&mut self, key_event: KeyEvent) { match self.input_mode { InputMode::Normal => match key_event.code { @@ -192,7 +230,6 @@ impl App { let stations = api .get_stations() .country("Germany") - .limit("50") .send() .await .map_err(|e| eyre!("Failed to fetch station: {e}"))?; @@ -242,20 +279,31 @@ impl App { let _ = child.kill().await; } - let child = tokio::process::Command::new("mpv") + let (tx, rx) = mpsc::unbounded_channel(); + self.song_rx = Some(rx); + + let mut child = tokio::process::Command::new("mpv") .arg(&url) .arg("--no-video") - .stdout(std::process::Stdio::null()) + .arg("--quiet") + .arg("--term-playing-msg=${media-title}") + .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) - .spawn(); + .spawn() + .expect("Failed to spawn mpv"); - match child { - Ok(child) => { - self.player = Some(child); + let mut stdout = child.stdout.take().expect("No stdout"); + tokio::spawn(async move { + use tokio::io::{AsyncBufReadExt, BufReader}; + let mut reader = BufReader::new(&mut stdout).lines(); + while let Ok(Some(line)) = reader.next_line().await { + let title = line.trim().to_string(); + if !title.is_empty() { + let _ = tx.send(title); + } } - Err(e) => { - eprintln!("Failed to play station: {e}"); - } - } + }); + + self.player = Some(child); } }