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, } 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(path); self.update_shuffle_indices(); } 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<&PathBuf> { 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().cloned() } else { None } } fn previous(&mut self) -> Option { if self.position > 0 { self.position -= 1; self.current().cloned() } 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)?; loop { // Check for shutdown signal if shutdown_flag.load(Ordering::SeqCst) { break; } // Check if track ended and play next if audio_engine.poll_track_ended() { if let Ok(mut q) = queue.lock() { if let Some(next_track) = q.next() { audio_engine.play(next_track); } else { 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) = audio.current_track() { format!("Playing\nTrack: {}\n", track.display()) } 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 path_str = &cmd[5..].trim(); let path = std::path::PathBuf::from(path_str); if path.is_file() { // Add to queue and play if let Ok(mut q) = queue.lock() { q.add_track(path.clone()); } audio.play(path); format!("Playing: {}\n", path_str) } else if path.is_dir() { // Find all audio files in directory match find_audio_in_dir(&path) { Some(files) if !files.is_empty() => { if let Ok(mut q) = queue.lock() { for file in &files { q.add_track(file.clone()); } // If shuffle is on, shuffled_indices is already set // Play the first track from the shuffled list if let Some(track) = q.current().cloned() { audio.play(track); } } format!("Queued {} tracks from: {}\n", files.len(), path_str) } _ => format!("Error: No audio files found in '{}'\n", path_str), } } else { format!("File not found: {}\n", path_str) } } _ 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) { if display_idx == current_position { output.push_str(&format!("> {} (playing)\n", track.display())); } else { output.push_str(&format!(" {}\n", track.display())); } } } // 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(); }