added song updates

This commit is contained in:
2025-08-16 08:36:31 +02:00
parent 8d216dbae6
commit 00faedb1fa
+77 -29
View File
@@ -12,8 +12,9 @@ use ratatui::{
style::Stylize, style::Stylize,
widgets::{Block, Paragraph}, widgets::{Block, Paragraph},
}; };
use std::io::{self}; use std::io;
use tokio::process::Child; use tokio::process::Child;
use tokio::sync::mpsc;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum InputMode { pub enum InputMode {
@@ -30,6 +31,9 @@ pub struct App {
search_query: String, search_query: String,
input_mode: InputMode, input_mode: InputMode,
player: Option<Child>, player: Option<Child>,
current_song: Option<String>,
played_songs: Vec<String>,
song_rx: Option<mpsc::UnboundedReceiver<String>>,
} }
impl Default for App { impl Default for App {
@@ -46,6 +50,9 @@ impl Default for App {
input_mode: InputMode::Normal, input_mode: InputMode::Normal,
search_query: String::new(), search_query: String::new(),
player: None, player: None,
current_song: None,
played_songs: Vec::new(),
song_rx: None,
} }
} }
} }
@@ -57,6 +64,7 @@ impl App {
self.handle_events() self.handle_events()
.await .await
.wrap_err("handle events failed")?; .wrap_err("handle events failed")?;
self.check_song_updates();
} }
Ok(()) Ok(())
} }
@@ -72,10 +80,10 @@ impl App {
Layout::vertical([Constraint::Percentage(10), Constraint::Percentage(90)]).areas(right); Layout::vertical([Constraint::Percentage(10), Constraint::Percentage(90)]).areas(right);
let [top_left, bottom_left] = 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) // Station list
let max_name_len = top_left.width.saturating_sub(4) as usize; let max_name_len = bottom_left.width.saturating_sub(4) as usize;
let station_items: Vec<ListItem> = self let station_items: Vec<ListItem> = self
.filtered_stations .filtered_stations
.iter() .iter()
@@ -96,30 +104,34 @@ impl App {
frame.render_stateful_widget( frame.render_stateful_widget(
stations_list, stations_list,
top_left, bottom_left,
&mut self.station_list_state.clone(), &mut self.station_list_state.clone(),
); );
let station = self // Info: show current song
.station_list_state let info_text = match &self.current_song {
.selected() Some(song) => format!("Now playing: {}", song),
.and_then(|i| self.filtered_stations.get(i)); None => "No song playing".to_string(),
let paragraph = Paragraph::new( };
station let paragraph = Paragraph::new(info_text).block(Block::bordered().title("Info"));
.map(|s| format!("Now selected: {}\n{}", s.name, s.url)) frame.render_widget(paragraph, top_left);
.unwrap_or_else(|| "No station selected".into()),
)
.block(Block::bordered().title("Info"));
frame.render_widget(paragraph, bottom_left);
// Search box
let mut search_string = self.search_query.clone(); let mut search_string = self.search_query.clone();
if self.input_mode == InputMode::Search { if self.input_mode == InputMode::Search {
search_string.push('█'); search_string.push('█');
} }
let search = Paragraph::new(search_string).block(Block::bordered().title("Search")); let search = Paragraph::new(search_string).block(Block::bordered().title("Search"));
frame.render_widget(search, top_right); frame.render_widget(search, top_right);
frame.render_widget(Block::bordered().title("Played Songs"), bottom_right);
// Played Songs list
let song_items: Vec<ListItem> = 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<()> { async fn handle_events(&mut self) -> io::Result<()> {
@@ -132,6 +144,32 @@ impl App {
Ok(()) 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) { pub async fn handle_key_event(&mut self, key_event: KeyEvent) {
match self.input_mode { match self.input_mode {
InputMode::Normal => match key_event.code { InputMode::Normal => match key_event.code {
@@ -192,7 +230,6 @@ impl App {
let stations = api let stations = api
.get_stations() .get_stations()
.country("Germany") .country("Germany")
.limit("50")
.send() .send()
.await .await
.map_err(|e| eyre!("Failed to fetch station: {e}"))?; .map_err(|e| eyre!("Failed to fetch station: {e}"))?;
@@ -242,20 +279,31 @@ impl App {
let _ = child.kill().await; 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(&url)
.arg("--no-video") .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()) .stderr(std::process::Stdio::null())
.spawn(); .spawn()
.expect("Failed to spawn mpv");
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);
}
}
});
match child {
Ok(child) => {
self.player = Some(child); self.player = Some(child);
} }
Err(e) => {
eprintln!("Failed to play station: {e}");
}
}
}
} }