From 45a1728ea4a3812120728562084fd416b35b074c Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 7 Jun 2026 21:19:46 +0300 Subject: Add lyrics support --- src/daemon.rs | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 13 deletions(-) (limited to 'src/daemon.rs') diff --git a/src/daemon.rs b/src/daemon.rs index 5ad6654..0590f71 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -15,8 +15,8 @@ use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -use std::thread; use std::time::{Duration, Instant, SystemTime}; +use std::{fs, thread}; pub struct ScrobbleState { pub track: Option, @@ -419,9 +419,19 @@ fn send_command_raw(cmd: String) -> Result<(), Box> { let mut stream = UnixStream::connect(socket_path)?; stream.write_all(cmd.as_bytes())?; - let mut response = String::new(); - stream.read_to_string(&mut response)?; - print!("{}", response); + let mut buf = [0u8; 4096]; + loop { + match stream.read(&mut buf) { + Ok(0) => break, + Ok(n) => { + if let Ok(s) = std::str::from_utf8(&buf[..n]) { + print!("{}", s); + } + } + Err(_) => break, + } + } + let _ = std::io::stdout().flush(); Ok(()) } @@ -437,15 +447,104 @@ fn handle_client( let mut buffer = [0; 1024]; if let Ok(size) = stream.read(&mut buffer) { let cmd = String::from_utf8_lossy(&buffer[..size]); - let response = process_command( - cmd.trim(), - &audio, - &shutdown_flag, - &queue, - &scrobbler, - &scrobble_state, - ); - let _ = stream.write_all(response.as_bytes()); + let trimmed = cmd.trim(); + if trimmed == "lyrics" { + handle_lyrics_stream(&mut stream, &audio); + } else { + let response = process_command( + trimmed, + &audio, + &shutdown_flag, + &queue, + &scrobbler, + &scrobble_state, + ); + let _ = stream.write_all(response.as_bytes()); + } + } +} + +struct LrcLine { + time: f64, + text: String, +} + +fn parse_lrc(content: &str) -> Vec { + let mut lines: Vec = Vec::new(); + for line in content.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + if let Some(rest) = line.strip_prefix('[') + && let Some(idx) = rest.find(']') + { + let ts = &rest[..idx]; + let text = rest[idx + 1..].trim().to_string(); + if let Some(secs) = parse_lrc_timestamp(ts) { + lines.push(LrcLine { time: secs, text }); + } + } + } + lines.sort_by(|a, b| { + a.time + .partial_cmp(&b.time) + .unwrap_or(std::cmp::Ordering::Equal) + }); + lines +} + +fn parse_lrc_timestamp(ts: &str) -> Option { + let ts = ts.trim(); + if let Some((mm, rest)) = ts.split_once(':') { + let mm: f64 = mm.parse().ok()?; + let ss: f64 = rest.parse().ok()?; + Some(mm * 60.0 + ss) + } else { + None + } +} + +fn handle_lyrics_stream(stream: &mut UnixStream, audio: &Arc) { + let track_path = match audio.current_track() { + Some(p) => p, + None => return, + }; + let mut lrc_path = PathBuf::from(&track_path); + if !lrc_path.set_extension("lrc") || !lrc_path.exists() { + return; + } + let content = match fs::read_to_string(&lrc_path) { + Ok(c) => c, + Err(_) => return, + }; + let lyrics = parse_lrc(&content); + if lyrics.is_empty() { + return; + } + + loop { + let pos = audio.position(); + let current_idx = lyrics.iter().rposition(|l| l.time <= pos).unwrap_or(0); + let start = current_idx.saturating_sub(1); + let end = (current_idx + 3).min(lyrics.len()); + + let mut output = String::new(); + output.push_str("\x1b[2J\x1b[H"); + for (idx, l) in lyrics.iter().enumerate().take(end).skip(start) { + if idx == current_idx { + output.push_str(&format!(" > {}\n", l.text)); + } else { + output.push_str(&format!(" {}\n", l.text)); + } + } + if stream.write_all(output.as_bytes()).is_err() { + break; + } + if stream.flush().is_err() { + break; + } + thread::sleep(Duration::from_millis(200)); } } @@ -592,6 +691,7 @@ fn process_command( "Error accessing queue\n".to_string() } } + "shuffle" => { if let Ok(q) = queue.lock() { match q.shuffle_mode() { -- cgit v1.2.3