diff options
| author | salmonik <raisersoft@proton.me> | 2026-06-07 21:19:46 +0300 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-06-07 21:19:46 +0300 |
| commit | 45a1728ea4a3812120728562084fd416b35b074c (patch) | |
| tree | ae2f74cd08e97102ff1dda4c3c5006bde15d7bea /src/daemon.rs | |
| parent | Merge pull request 'autocompletion' (#1) from autocompletion into main (diff) | |
| download | rsplayer-45a1728ea4a3812120728562084fd416b35b074c.tar.gz rsplayer-45a1728ea4a3812120728562084fd416b35b074c.zip | |
Add lyrics support
Diffstat (limited to 'src/daemon.rs')
| -rw-r--r-- | src/daemon.rs | 126 |
1 files changed, 113 insertions, 13 deletions
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}; | |||
| 15 | use std::path::{Path, PathBuf}; | 15 | use std::path::{Path, PathBuf}; |
| 16 | use std::sync::atomic::{AtomicBool, Ordering}; | 16 | use std::sync::atomic::{AtomicBool, Ordering}; |
| 17 | use std::sync::{Arc, Mutex, RwLock}; | 17 | use std::sync::{Arc, Mutex, RwLock}; |
| 18 | use std::thread; | ||
| 19 | use std::time::{Duration, Instant, SystemTime}; | 18 | use std::time::{Duration, Instant, SystemTime}; |
| 19 | use std::{fs, thread}; | ||
| 20 | 20 | ||
| 21 | pub struct ScrobbleState { | 21 | pub struct ScrobbleState { |
| 22 | pub track: Option<Track>, | 22 | pub track: Option<Track>, |
| @@ -419,9 +419,19 @@ fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { | |||
| 419 | let mut stream = UnixStream::connect(socket_path)?; | 419 | let mut stream = UnixStream::connect(socket_path)?; |
| 420 | stream.write_all(cmd.as_bytes())?; | 420 | stream.write_all(cmd.as_bytes())?; |
| 421 | 421 | ||
| 422 | let mut response = String::new(); | 422 | let mut buf = [0u8; 4096]; |
| 423 | stream.read_to_string(&mut response)?; | 423 | loop { |
| 424 | print!("{}", response); | 424 | match stream.read(&mut buf) { |
| 425 | Ok(0) => break, | ||
| 426 | Ok(n) => { | ||
| 427 | if let Ok(s) = std::str::from_utf8(&buf[..n]) { | ||
| 428 | print!("{}", s); | ||
| 429 | } | ||
| 430 | } | ||
| 431 | Err(_) => break, | ||
| 432 | } | ||
| 433 | } | ||
| 434 | let _ = std::io::stdout().flush(); | ||
| 425 | 435 | ||
| 426 | Ok(()) | 436 | Ok(()) |
| 427 | } | 437 | } |
| @@ -437,15 +447,104 @@ fn handle_client( | |||
| 437 | let mut buffer = [0; 1024]; | 447 | let mut buffer = [0; 1024]; |
| 438 | if let Ok(size) = stream.read(&mut buffer) { | 448 | if let Ok(size) = stream.read(&mut buffer) { |
| 439 | let cmd = String::from_utf8_lossy(&buffer[..size]); | 449 | let cmd = String::from_utf8_lossy(&buffer[..size]); |
| 440 | let response = process_command( | 450 | let trimmed = cmd.trim(); |
| 441 | cmd.trim(), | 451 | if trimmed == "lyrics" { |
| 442 | &audio, | 452 | handle_lyrics_stream(&mut stream, &audio); |
| 443 | &shutdown_flag, | 453 | } else { |
| 444 | &queue, | 454 | let response = process_command( |
| 445 | &scrobbler, | 455 | trimmed, |
| 446 | &scrobble_state, | 456 | &audio, |
| 447 | ); | 457 | &shutdown_flag, |
| 448 | let _ = stream.write_all(response.as_bytes()); | 458 | &queue, |
| 459 | &scrobbler, | ||
| 460 | &scrobble_state, | ||
| 461 | ); | ||
| 462 | let _ = stream.write_all(response.as_bytes()); | ||
| 463 | } | ||
| 464 | } | ||
| 465 | } | ||
| 466 | |||
| 467 | struct LrcLine { | ||
| 468 | time: f64, | ||
| 469 | text: String, | ||
| 470 | } | ||
| 471 | |||
| 472 | fn parse_lrc(content: &str) -> Vec<LrcLine> { | ||
| 473 | let mut lines: Vec<LrcLine> = Vec::new(); | ||
| 474 | for line in content.lines() { | ||
| 475 | let line = line.trim(); | ||
| 476 | if line.is_empty() { | ||
| 477 | continue; | ||
| 478 | } | ||
| 479 | if let Some(rest) = line.strip_prefix('[') | ||
| 480 | && let Some(idx) = rest.find(']') | ||
| 481 | { | ||
| 482 | let ts = &rest[..idx]; | ||
| 483 | let text = rest[idx + 1..].trim().to_string(); | ||
| 484 | if let Some(secs) = parse_lrc_timestamp(ts) { | ||
| 485 | lines.push(LrcLine { time: secs, text }); | ||
| 486 | } | ||
| 487 | } | ||
| 488 | } | ||
| 489 | lines.sort_by(|a, b| { | ||
| 490 | a.time | ||
| 491 | .partial_cmp(&b.time) | ||
| 492 | .unwrap_or(std::cmp::Ordering::Equal) | ||
| 493 | }); | ||
| 494 | lines | ||
| 495 | } | ||
| 496 | |||
| 497 | fn parse_lrc_timestamp(ts: &str) -> Option<f64> { | ||
| 498 | let ts = ts.trim(); | ||
| 499 | if let Some((mm, rest)) = ts.split_once(':') { | ||
| 500 | let mm: f64 = mm.parse().ok()?; | ||
| 501 | let ss: f64 = rest.parse().ok()?; | ||
| 502 | Some(mm * 60.0 + ss) | ||
| 503 | } else { | ||
| 504 | None | ||
| 505 | } | ||
| 506 | } | ||
| 507 | |||
| 508 | fn handle_lyrics_stream(stream: &mut UnixStream, audio: &Arc<AudioEngine>) { | ||
| 509 | let track_path = match audio.current_track() { | ||
| 510 | Some(p) => p, | ||
| 511 | None => return, | ||
| 512 | }; | ||
| 513 | let mut lrc_path = PathBuf::from(&track_path); | ||
| 514 | if !lrc_path.set_extension("lrc") || !lrc_path.exists() { | ||
| 515 | return; | ||
| 516 | } | ||
| 517 | let content = match fs::read_to_string(&lrc_path) { | ||
| 518 | Ok(c) => c, | ||
| 519 | Err(_) => return, | ||
| 520 | }; | ||
| 521 | let lyrics = parse_lrc(&content); | ||
| 522 | if lyrics.is_empty() { | ||
| 523 | return; | ||
| 524 | } | ||
| 525 | |||
| 526 | loop { | ||
| 527 | let pos = audio.position(); | ||
| 528 | let current_idx = lyrics.iter().rposition(|l| l.time <= pos).unwrap_or(0); | ||
| 529 | let start = current_idx.saturating_sub(1); | ||
| 530 | let end = (current_idx + 3).min(lyrics.len()); | ||
| 531 | |||
| 532 | let mut output = String::new(); | ||
| 533 | output.push_str("\x1b[2J\x1b[H"); | ||
| 534 | for (idx, l) in lyrics.iter().enumerate().take(end).skip(start) { | ||
| 535 | if idx == current_idx { | ||
| 536 | output.push_str(&format!(" > {}\n", l.text)); | ||
| 537 | } else { | ||
| 538 | output.push_str(&format!(" {}\n", l.text)); | ||
| 539 | } | ||
| 540 | } | ||
| 541 | if stream.write_all(output.as_bytes()).is_err() { | ||
| 542 | break; | ||
| 543 | } | ||
| 544 | if stream.flush().is_err() { | ||
| 545 | break; | ||
| 546 | } | ||
| 547 | thread::sleep(Duration::from_millis(200)); | ||
| 449 | } | 548 | } |
| 450 | } | 549 | } |
| 451 | 550 | ||
| @@ -592,6 +691,7 @@ fn process_command( | |||
| 592 | "Error accessing queue\n".to_string() | 691 | "Error accessing queue\n".to_string() |
| 593 | } | 692 | } |
| 594 | } | 693 | } |
| 694 | |||
| 595 | "shuffle" => { | 695 | "shuffle" => { |
| 596 | if let Ok(q) = queue.lock() { | 696 | if let Ok(q) = queue.lock() { |
| 597 | match q.shuffle_mode() { | 697 | match q.shuffle_mode() { |
