use crate::audio::AudioEngine; use crate::config::Config; use std::io::{Read, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; #[derive(Debug, Clone)] enum ShuffleMode { None, All, Album, } #[derive(Debug, Clone)] struct Track { path: PathBuf, title: Option, artist: Option, album: Option, } impl Track { fn from_path(path: PathBuf) -> Self { let metadata = extract_metadata(&path); let metadata = metadata.as_ref(); Track { path, title: metadata.and_then(|m| m.title.clone()), artist: metadata.and_then(|m| m.artist.clone()), album: metadata.and_then(|m| m.album.clone()), } } fn display_string(&self, index: usize) -> String { let album_artist = self .artist .clone() .or_else(|| self.album.clone()) .unwrap_or_default(); let title = self.title.clone().unwrap_or_else(|| { self.path .file_name() .and_then(|n| n.to_str()) .unwrap_or("Unknown") .to_string() }); if album_artist.is_empty() { format!("{}. {}", index + 1, title) } else { format!("{}. {} - {}", index + 1, album_artist, title) } } } #[derive(Debug, Default, Clone)] struct AudioMetadata { title: Option, artist: Option, album: Option, } fn extract_metadata(path: &Path) -> Option { use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::StandardTagKey; use symphonia::core::probe::Hint; let file = std::fs::File::open(path).ok()?; let mss = MediaSourceStream::new(Box::new(file), Default::default()); let hint = Hint::new(); let mut probed = symphonia::default::get_probe() .format(&hint, mss, &Default::default(), &Default::default()) .ok()?; let mut metadata = AudioMetadata::default(); // First check probed metadata (e.g., ID3v2 headers before container) if let Some(meta) = probed.metadata.get() { if let Some(rev) = meta.current() { for tag in rev.tags() { match tag.std_key { Some(StandardTagKey::TrackTitle) => { metadata.title = Some(tag.value.to_string()); } Some(StandardTagKey::Artist) => { metadata.artist = Some(tag.value.to_string()); } Some(StandardTagKey::Album) => { metadata.album = Some(tag.value.to_string()); } _ => {} } } } } // If nothing found, try container metadata if metadata.title.is_none() && metadata.artist.is_none() && metadata.album.is_none() { let mut format = probed.format; if let Some(rev) = format.metadata().current() { for tag in rev.tags() { match tag.std_key { Some(StandardTagKey::TrackTitle) => { metadata.title = Some(tag.value.to_string()); } Some(StandardTagKey::Artist) => { metadata.artist = Some(tag.value.to_string()); } Some(StandardTagKey::Album) => { metadata.album = Some(tag.value.to_string()); } _ => {} } } } } if metadata.title.is_some() || metadata.artist.is_some() || metadata.album.is_some() { Some(metadata) } else { None } } struct Queue { tracks: Vec, position: usize, shuffle: ShuffleMode, shuffled_indices: Vec, } impl Queue { fn new() -> Self { Queue { tracks: Vec::new(), position: 0, shuffle: ShuffleMode::None, shuffled_indices: Vec::new(), } } fn add_track(&mut self, path: PathBuf) { self.tracks.push(Track::from_path(path)); self.update_shuffle_indices(); } fn insert_after_current(&mut self, path: PathBuf) { let insert_idx = self.position + 1; self.tracks.insert(insert_idx, Track::from_path(path)); self.update_shuffle_indices(); } fn track_at(&self, idx: usize) -> Option<&Track> { self.tracks.get(idx) } fn set_shuffle(&mut self, mode: ShuffleMode) { self.shuffle = mode.clone(); self.update_shuffle_indices(); } fn update_shuffle_indices(&mut self) { self.shuffled_indices = (0..self.tracks.len()).collect(); if let ShuffleMode::All = self.shuffle { use rand::seq::SliceRandom; let mut rng = rand::rng(); self.shuffled_indices.shuffle(&mut rng); } } fn current(&self) -> Option<&Track> { if self.tracks.is_empty() { None } else if let ShuffleMode::All = self.shuffle { self.shuffled_indices .get(self.position) .and_then(|&idx| self.tracks.get(idx)) } else if let ShuffleMode::Album = self.shuffle { // TODO: When database is ready, only shuffle tracks in same album/folder // For now, behave like All self.shuffled_indices .get(self.position) .and_then(|&idx| self.tracks.get(idx)) } else { self.tracks.get(self.position) } } fn next(&mut self) -> Option { let max_pos = self.tracks.len().saturating_sub(1); if self.position < max_pos { self.position += 1; self.current().map(|t| t.path.clone()) } else { None } } fn previous(&mut self) -> Option { if self.position > 0 { self.position -= 1; self.current().map(|t| t.path.clone()) } else { None } } } pub fn start_daemon() -> Result<(), Box> { let config = Config::load()?; let socket_path = config.get_socket_path(); let pid_path = get_pid_path(&config); // Check if daemon is already running if let Ok(pid) = check_running_daemon(&pid_path) { return Err(format!("Daemon already running with PID {}", pid).into()); } // Clean up stale socket file if it exists if socket_path.exists() { std::fs::remove_file(&socket_path).ok(); } // Create runtime directory if let Some(parent) = socket_path.parent() { std::fs::create_dir_all(parent)?; } if let Some(parent) = pid_path.parent() { std::fs::create_dir_all(parent)?; } // Write PID file let pid = std::process::id(); std::fs::write(&pid_path, pid.to_string())?; println!("Daemon started with PID {}", pid); // Shared shutdown flag - can be triggered by signal or command let shutdown_flag = Arc::new(AtomicBool::new(false)); let shutdown_flag_signal = Arc::clone(&shutdown_flag); // Handle SIGTERM and SIGINT ctrlc::set_handler(move || { println!("Received shutdown signal"); shutdown_flag_signal.store(true, Ordering::SeqCst); })?; let listener = UnixListener::bind(&socket_path)?; println!("Listening on {:?}", socket_path); let audio_engine = Arc::new(AudioEngine::new()); let queue = Arc::new(Mutex::new(Queue::new())); // Load and apply saved settings let config = Config::load().unwrap_or_default(); if let Some(vol) = config.volume { audio_engine.set_volume(vol as f32 / 100.0); } if let Some(ref shuffle_str) = config.shuffle { if let Ok(mut q) = queue.lock() { match shuffle_str.as_str() { "on" | "all" => q.set_shuffle(ShuffleMode::All), "album" => q.set_shuffle(ShuffleMode::Album), _ => {} } } } // Accept connections until shutdown listener.set_nonblocking(true)?; // Helper: poll until a specific track actually starts playing or timeout fn wait_for_start(audio: &AudioEngine, target: &PathBuf, timeout_ms: u64) -> bool { use std::time::{Duration, Instant}; let start = Instant::now(); while start.elapsed() < Duration::from_millis(timeout_ms) { // If playing and the current track matches, assume started if audio.is_playing() { if let Some(cur) = audio.current_track() { if &cur == target { return true; } } } std::thread::sleep(Duration::from_millis(10)); } false } loop { // Check for shutdown signal if shutdown_flag.load(Ordering::SeqCst) { break; } // Check if track ended and play next (robust with startup feedback) if audio_engine.poll_track_ended() { if let Ok(mut q) = queue.lock() { // Save the current position in case we need to revert let original_position = q.position; // Try a handful of next candidates until one starts let mut started = false; // We'll attempt up to 4 candidates ahead let max_trials = 4usize; for _ in 0..max_trials { if let Some(next_track) = q.next() { audio_engine.play(next_track.clone()); // Wait a short time to see if playback actually starts if wait_for_start(&audio_engine, &next_track, 500) { started = true; break; } else { // Try next candidate - q.next() already advanced position continue; } } else { break; } } if !started { // Revert position since we couldn't start any track q.position = original_position; // No candidate started; stop playback safely audio_engine.stop(); } } } match listener.accept() { Ok((stream, _)) => { let audio_clone = Arc::clone(&audio_engine); let shutdown_flag_clone = Arc::clone(&shutdown_flag); let queue_clone = Arc::clone(&queue); thread::spawn(move || { handle_client(stream, audio_clone, shutdown_flag_clone, queue_clone); }); } Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { thread::sleep(Duration::from_millis(50)); continue; } Err(e) => eprintln!("Connection failed: {}", e), } thread::sleep(Duration::from_millis(50)); } // Cleanup cleanup_daemon(&socket_path, &pid_path); println!("Daemon stopped"); Ok(()) } pub fn stop_daemon() -> Result<(), Box> { let config = Config::load()?; let pid_path = get_pid_path(&config); match check_running_daemon(&pid_path) { Ok(pid) => { // Try to send stop command first for graceful shutdown if let Ok(()) = send_command_raw("stop_daemon".to_string()) { // Wait for daemon to stop for _ in 0..50 { thread::sleep(Duration::from_millis(100)); if !is_process_running(pid) { break; } } } // Force kill if still running if is_process_running(pid) { unsafe { libc::kill(pid as i32, libc::SIGTERM); } thread::sleep(Duration::from_millis(500)); // Force kill if still alive if is_process_running(pid) { unsafe { libc::kill(pid as i32, libc::SIGKILL); } } } // Clean up files let socket_path = config.get_socket_path(); std::fs::remove_file(&socket_path).ok(); std::fs::remove_file(&pid_path).ok(); println!("Daemon stopped"); Ok(()) } Err(e) => Err(format!("Daemon not running: {}", e).into()), } } pub fn restart_daemon() -> Result<(), Box> { // Stop existing daemon if let Ok(()) = stop_daemon() { // Wait a bit for cleanup thread::sleep(Duration::from_millis(500)); } // Start new daemon in background let current_exe = std::env::current_exe()?; std::process::Command::new(current_exe) .arg("start") .spawn()?; println!("Daemon restarted"); Ok(()) } pub fn send_command(cmd: String) -> Result<(), Box> { send_command_raw(cmd)?; Ok(()) } fn send_command_raw(cmd: String) -> Result<(), Box> { let config = Config::load()?; let socket_path = config.get_socket_path(); 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); Ok(()) } fn handle_client( mut stream: UnixStream, audio: Arc, shutdown_flag: Arc, queue: Arc>, ) { 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); let _ = stream.write_all(response.as_bytes()); } } fn process_command( cmd: &str, audio: &AudioEngine, shutdown_flag: &Arc, queue: &Arc>, ) -> String { match cmd { "status" => { if audio.is_playing() { if let Some(track_path) = audio.current_track() { let track = Track::from_path(track_path); let track_str = track.display_string(0); format!("Playing\nTrack: {}\n", track_str) } else { "Playing\n".to_string() } } else { "Stopped\n".to_string() } } "pause" => { audio.pause(); "Paused\n".to_string() } "resume" => { audio.resume(); "Resumed\n".to_string() } "stop" => { audio.stop(); // Clear queue on stop if let Ok(mut q) = queue.lock() { q.tracks.clear(); q.position = 0; } "Stopped\n".to_string() } "stop_daemon" => { // Signal the daemon to shut down shutdown_flag.store(true, Ordering::SeqCst); "Shutting down\n".to_string() } "next" => { if let Ok(mut q) = queue.lock() { if let Some(next_track) = q.next() { audio.play(next_track); "Playing next track\n".to_string() } else { audio.stop(); "Queue empty, stopped\n".to_string() } } else { "Error accessing queue\n".to_string() } } "previous" => { if let Ok(mut q) = queue.lock() { if let Some(prev_track) = q.previous() { audio.play(prev_track); "Playing previous track\n".to_string() } else { "No previous track\n".to_string() } } else { "Error accessing queue\n".to_string() } } "shuffle" => { if let Ok(q) = queue.lock() { match q.shuffle { ShuffleMode::None => "Shuffle: off\n".to_string(), ShuffleMode::All => "Shuffle: on (all)\n".to_string(), ShuffleMode::Album => "Shuffle: on (album)\n".to_string(), } } else { "Error accessing queue\n".to_string() } } _ if cmd.starts_with("shuffle ") => { let mode_str = cmd[8..].trim(); if let Ok(mut q) = queue.lock() { let result = if mode_str == "on" || mode_str == "all" { q.set_shuffle(ShuffleMode::All); "Shuffle: on (all)\n".to_string() } else if mode_str == "album" { q.set_shuffle(ShuffleMode::Album); "Shuffle: on (album)\n".to_string() } else if mode_str == "off" || mode_str == "none" { q.set_shuffle(ShuffleMode::None); "Shuffle: off\n".to_string() } else { "Usage: shuffle [on|off|album]\n".to_string() }; // Save to config if let Ok(mut config) = Config::load() { if mode_str == "off" || mode_str == "none" { config.shuffle = None; } else { config.shuffle = Some(mode_str.to_string()); } let _ = config.save(); } result } else { "Error accessing queue\n".to_string() } } _ if cmd.starts_with("play ") => { let rest = &cmd[5..].trim(); let (position, path_str): (&str, &str) = if let Some((pos, p)) = rest.split_once(' ') { if pos == "now" || pos == "next" || pos == "last" { (pos, p) } else { ("last", rest) } } else { ("last", rest) }; let path_str = path_str.to_string(); if path_str.is_empty() { return "Usage: play [now|next|last] \n".to_string(); } let path = std::path::PathBuf::from(path_str.clone()); if path.is_file() { if let Ok(mut q) = queue.lock() { match position { "now" => { q.insert_after_current(path.clone()); q.position += 1; if let Some(track) = q.track_at(q.position) { audio.play(track.path.clone()); } format!("Playing now: {}\n", path_str) } "next" => { q.insert_after_current(path.clone()); format!("Queued next: {}\n", path_str) } _ => { q.add_track(path.clone()); audio.play(path); format!("Playing: {}\n", path_str) } } } else { "Error accessing queue\n".to_string() } } else if path.is_dir() { match find_audio_in_dir(&path) { Some(files) if !files.is_empty() => { if let Ok(mut q) = queue.lock() { match position { "now" => { for file in &files { q.insert_after_current(file.clone()); } q.position += 1; if let Some(track) = q.track_at(q.position) { audio.play(track.path.clone()); } format!( "Playing now: {} tracks from {}\n", files.len(), path_str ) } "next" => { for file in &files { q.insert_after_current(file.clone()); } format!( "Queued next: {} tracks from {}\n", files.len(), path_str ) } _ => { for file in &files { q.add_track(file.clone()); } if let Some(track) = q.current() { audio.play(track.path.clone()); } format!("Queued {} tracks from: {}\n", files.len(), path_str) } } } else { "Error accessing queue\n".to_string() } } _ => format!("Error: No audio files found in '{}'\n", path_str), } } else { format!("File not found: {}\n", path_str) } } _ if cmd == "volume" => { let vol = audio.get_volume(); format!("Volume: {}\n", vol) } _ if cmd.starts_with("volume ") => { if let Ok(level) = cmd[7..].trim().parse::() { audio.set_volume(level as f32 / 100.0); // Save to config if let Ok(mut config) = Config::load() { config.volume = Some(level); let _ = config.save(); } format!("Volume set to {}\n", level) } else { "Usage: volume <0-100>\n".to_string() } } _ if cmd.starts_with("shuffle ") => { let mode_str = cmd[8..].trim(); if let Ok(mut q) = queue.lock() { if mode_str == "on" || mode_str == "all" { q.set_shuffle(ShuffleMode::All); "Shuffle: on\n".to_string() } else if mode_str == "off" || mode_str == "none" { q.set_shuffle(ShuffleMode::None); "Shuffle: off\n".to_string() } else { "Usage: shuffle [on|off]\n".to_string() } } else { "Error accessing queue\n".to_string() } } _ if cmd.starts_with("queue") => { if let Ok(q) = queue.lock() { if q.tracks.is_empty() { "Queue is empty\n".to_string() } else { let mut output = String::new(); let current_position = q.position; // Use shuffled_indices for display order if shuffle is on let indices: Vec = if let ShuffleMode::None = q.shuffle { (0..q.tracks.len()).collect() } else { q.shuffled_indices.clone() }; for (display_idx, &track_idx) in indices.iter().enumerate() { if let Some(track) = q.tracks.get(track_idx) { let track_str = track.display_string(display_idx); if track_idx == current_position { output.push_str(&format!("> {} (playing)\n", track_str)); } else { output.push_str(&format!(" {}\n", track_str)); } } } // Use pager if output is long (more than 24 lines) if output.lines().count() > 24 { use std::process::{Command, Stdio}; let pager = Command::new("less").arg("-R").stdin(Stdio::piped()).spawn(); if let Ok(mut child) = pager { use std::io::Write; let _ = child.stdin.as_mut().unwrap().write_all(output.as_bytes()); let _ = child.wait(); "".to_string() } else { output } } else { output } } } else { "Error accessing queue\n".to_string() } } _ => "Unknown command\n".to_string(), } } fn find_audio_in_dir(dir: &Path) -> Option> { use std::ffi::OsStr; let mut results = Vec::new(); if let Ok(entries) = std::fs::read_dir(dir) { let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); entries.sort_by_key(|e| e.file_name()); for entry in entries { let path = entry.path(); if path.is_file() { if let Some(ext) = path.extension().and_then(OsStr::to_str) { if matches!( ext.to_lowercase().as_str(), "mp3" | "flac" | "ogg" | "m4a" | "wav" | "opus" ) { results.push(path); } } } else if path.is_dir() { if let Some(mut found) = find_audio_in_dir(&path) { results.append(&mut found); } } } } if results.is_empty() { None } else { Some(results) } } /// Get the PID file path for the daemon fn get_pid_path(_config: &Config) -> std::path::PathBuf { let runtime_dir = std::env::var("XDG_RUNTIME_DIR") .map(std::path::PathBuf::from) .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")); runtime_dir.join("rsplayer").join("rsplayer.pid") } /// Check if a process with the given PID is running fn is_process_running(pid: u32) -> bool { unsafe { // send signal 0 to check if process exists libc::kill(pid as i32, 0) == 0 } } /// Check if daemon is running by reading PID file fn check_running_daemon(pid_path: &std::path::Path) -> Result> { if !pid_path.exists() { return Err("PID file not found".into()); } let contents = std::fs::read_to_string(pid_path)?; let pid = contents.trim().parse::()?; if is_process_running(pid) { Ok(pid) } else { // Stale PID file, clean it up std::fs::remove_file(pid_path).ok(); Err("Stale PID file, daemon not running".into()) } } /// Clean up daemon resources (socket and PID file) fn cleanup_daemon(socket_path: &std::path::Path, pid_path: &std::path::Path) { std::fs::remove_file(socket_path).ok(); std::fs::remove_file(pid_path).ok(); }