added song updates
This commit is contained in:
+77
-29
@@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user