diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-05 21:12:32 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-05 21:12:32 +0200 |
| commit | bd29978b2a0d0d6def486fcdaa63bc8934d68045 (patch) | |
| tree | 5df0efb17d350ae7f095eb9c0940b1bbf568de96 /src/daemon.rs | |
| download | rsplayer-bd29978b2a0d0d6def486fcdaa63bc8934d68045.tar.gz rsplayer-bd29978b2a0d0d6def486fcdaa63bc8934d68045.zip | |
fix(daemon): robust auto-advance; ensure next track starts even if playback fails
Diffstat (limited to 'src/daemon.rs')
| -rw-r--r-- | src/daemon.rs | 574 |
1 files changed, 574 insertions, 0 deletions
diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..8ac3a89 --- /dev/null +++ b/src/daemon.rs | |||
| @@ -0,0 +1,574 @@ | |||
| 1 | use crate::audio::AudioEngine; | ||
| 2 | use crate::config::Config; | ||
| 3 | use std::io::{Read, Write}; | ||
| 4 | use std::os::unix::net::{UnixListener, UnixStream}; | ||
| 5 | use std::path::{Path, PathBuf}; | ||
| 6 | use std::sync::atomic::{AtomicBool, Ordering}; | ||
| 7 | use std::sync::{Arc, Mutex}; | ||
| 8 | use std::thread; | ||
| 9 | use std::time::Duration; | ||
| 10 | |||
| 11 | #[derive(Debug, Clone)] | ||
| 12 | enum ShuffleMode { | ||
| 13 | None, | ||
| 14 | All, | ||
| 15 | Album, | ||
| 16 | } | ||
| 17 | |||
| 18 | struct Queue { | ||
| 19 | tracks: Vec<PathBuf>, | ||
| 20 | position: usize, | ||
| 21 | shuffle: ShuffleMode, | ||
| 22 | shuffled_indices: Vec<usize>, | ||
| 23 | } | ||
| 24 | |||
| 25 | impl Queue { | ||
| 26 | fn new() -> Self { | ||
| 27 | Queue { | ||
| 28 | tracks: Vec::new(), | ||
| 29 | position: 0, | ||
| 30 | shuffle: ShuffleMode::None, | ||
| 31 | shuffled_indices: Vec::new(), | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | fn add_track(&mut self, path: PathBuf) { | ||
| 36 | self.tracks.push(path); | ||
| 37 | self.update_shuffle_indices(); | ||
| 38 | } | ||
| 39 | |||
| 40 | fn set_shuffle(&mut self, mode: ShuffleMode) { | ||
| 41 | self.shuffle = mode.clone(); | ||
| 42 | self.update_shuffle_indices(); | ||
| 43 | } | ||
| 44 | |||
| 45 | fn update_shuffle_indices(&mut self) { | ||
| 46 | self.shuffled_indices = (0..self.tracks.len()).collect(); | ||
| 47 | if let ShuffleMode::All = self.shuffle { | ||
| 48 | use rand::seq::SliceRandom; | ||
| 49 | let mut rng = rand::rng(); | ||
| 50 | self.shuffled_indices.shuffle(&mut rng); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | fn current(&self) -> Option<&PathBuf> { | ||
| 55 | if self.tracks.is_empty() { | ||
| 56 | None | ||
| 57 | } else if let ShuffleMode::All = self.shuffle { | ||
| 58 | self.shuffled_indices | ||
| 59 | .get(self.position) | ||
| 60 | .and_then(|&idx| self.tracks.get(idx)) | ||
| 61 | } else if let ShuffleMode::Album = self.shuffle { | ||
| 62 | // TODO: When database is ready, only shuffle tracks in same album/folder | ||
| 63 | // For now, behave like All | ||
| 64 | self.shuffled_indices | ||
| 65 | .get(self.position) | ||
| 66 | .and_then(|&idx| self.tracks.get(idx)) | ||
| 67 | } else { | ||
| 68 | self.tracks.get(self.position) | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | fn next(&mut self) -> Option<PathBuf> { | ||
| 73 | let max_pos = self.tracks.len().saturating_sub(1); | ||
| 74 | if self.position < max_pos { | ||
| 75 | self.position += 1; | ||
| 76 | self.current().cloned() | ||
| 77 | } else { | ||
| 78 | None | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | fn previous(&mut self) -> Option<PathBuf> { | ||
| 83 | if self.position > 0 { | ||
| 84 | self.position -= 1; | ||
| 85 | self.current().cloned() | ||
| 86 | } else { | ||
| 87 | None | ||
| 88 | } | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | ||
| 93 | let config = Config::load()?; | ||
| 94 | let socket_path = config.get_socket_path(); | ||
| 95 | let pid_path = get_pid_path(&config); | ||
| 96 | |||
| 97 | // Check if daemon is already running | ||
| 98 | if let Ok(pid) = check_running_daemon(&pid_path) { | ||
| 99 | return Err(format!("Daemon already running with PID {}", pid).into()); | ||
| 100 | } | ||
| 101 | |||
| 102 | // Clean up stale socket file if it exists | ||
| 103 | if socket_path.exists() { | ||
| 104 | std::fs::remove_file(&socket_path).ok(); | ||
| 105 | } | ||
| 106 | |||
| 107 | // Create runtime directory | ||
| 108 | if let Some(parent) = socket_path.parent() { | ||
| 109 | std::fs::create_dir_all(parent)?; | ||
| 110 | } | ||
| 111 | if let Some(parent) = pid_path.parent() { | ||
| 112 | std::fs::create_dir_all(parent)?; | ||
| 113 | } | ||
| 114 | |||
| 115 | // Write PID file | ||
| 116 | let pid = std::process::id(); | ||
| 117 | std::fs::write(&pid_path, pid.to_string())?; | ||
| 118 | println!("Daemon started with PID {}", pid); | ||
| 119 | |||
| 120 | // Shared shutdown flag - can be triggered by signal or command | ||
| 121 | let shutdown_flag = Arc::new(AtomicBool::new(false)); | ||
| 122 | let shutdown_flag_signal = Arc::clone(&shutdown_flag); | ||
| 123 | |||
| 124 | // Handle SIGTERM and SIGINT | ||
| 125 | ctrlc::set_handler(move || { | ||
| 126 | println!("Received shutdown signal"); | ||
| 127 | shutdown_flag_signal.store(true, Ordering::SeqCst); | ||
| 128 | })?; | ||
| 129 | |||
| 130 | let listener = UnixListener::bind(&socket_path)?; | ||
| 131 | println!("Listening on {:?}", socket_path); | ||
| 132 | |||
| 133 | let audio_engine = Arc::new(AudioEngine::new()); | ||
| 134 | let queue = Arc::new(Mutex::new(Queue::new())); | ||
| 135 | |||
| 136 | // Load and apply saved settings | ||
| 137 | let config = Config::load().unwrap_or_default(); | ||
| 138 | if let Some(vol) = config.volume { | ||
| 139 | audio_engine.set_volume(vol as f32 / 100.0); | ||
| 140 | } | ||
| 141 | if let Some(ref shuffle_str) = config.shuffle { | ||
| 142 | if let Ok(mut q) = queue.lock() { | ||
| 143 | match shuffle_str.as_str() { | ||
| 144 | "on" | "all" => q.set_shuffle(ShuffleMode::All), | ||
| 145 | "album" => q.set_shuffle(ShuffleMode::Album), | ||
| 146 | _ => {} | ||
| 147 | } | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | // Accept connections until shutdown | ||
| 152 | listener.set_nonblocking(true)?; | ||
| 153 | loop { | ||
| 154 | // Check for shutdown signal | ||
| 155 | if shutdown_flag.load(Ordering::SeqCst) { | ||
| 156 | break; | ||
| 157 | } | ||
| 158 | |||
| 159 | // Check if track ended and play next | ||
| 160 | if audio_engine.poll_track_ended() { | ||
| 161 | if let Ok(mut q) = queue.lock() { | ||
| 162 | if let Some(next_track) = q.next() { | ||
| 163 | audio_engine.play(next_track); | ||
| 164 | } else { | ||
| 165 | audio_engine.stop(); | ||
| 166 | } | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | match listener.accept() { | ||
| 171 | Ok((stream, _)) => { | ||
| 172 | let audio_clone = Arc::clone(&audio_engine); | ||
| 173 | let shutdown_flag_clone = Arc::clone(&shutdown_flag); | ||
| 174 | let queue_clone = Arc::clone(&queue); | ||
| 175 | thread::spawn(move || { | ||
| 176 | handle_client(stream, audio_clone, shutdown_flag_clone, queue_clone); | ||
| 177 | }); | ||
| 178 | } | ||
| 179 | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { | ||
| 180 | thread::sleep(Duration::from_millis(50)); | ||
| 181 | continue; | ||
| 182 | } | ||
| 183 | Err(e) => eprintln!("Connection failed: {}", e), | ||
| 184 | } | ||
| 185 | |||
| 186 | thread::sleep(Duration::from_millis(50)); | ||
| 187 | } | ||
| 188 | |||
| 189 | // Cleanup | ||
| 190 | cleanup_daemon(&socket_path, &pid_path); | ||
| 191 | println!("Daemon stopped"); | ||
| 192 | Ok(()) | ||
| 193 | } | ||
| 194 | |||
| 195 | pub fn stop_daemon() -> Result<(), Box<dyn std::error::Error>> { | ||
| 196 | let config = Config::load()?; | ||
| 197 | let pid_path = get_pid_path(&config); | ||
| 198 | |||
| 199 | match check_running_daemon(&pid_path) { | ||
| 200 | Ok(pid) => { | ||
| 201 | // Try to send stop command first for graceful shutdown | ||
| 202 | if let Ok(()) = send_command_raw("stop_daemon".to_string()) { | ||
| 203 | // Wait for daemon to stop | ||
| 204 | for _ in 0..50 { | ||
| 205 | thread::sleep(Duration::from_millis(100)); | ||
| 206 | if !is_process_running(pid) { | ||
| 207 | break; | ||
| 208 | } | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | // Force kill if still running | ||
| 213 | if is_process_running(pid) { | ||
| 214 | unsafe { | ||
| 215 | libc::kill(pid as i32, libc::SIGTERM); | ||
| 216 | } | ||
| 217 | thread::sleep(Duration::from_millis(500)); | ||
| 218 | |||
| 219 | // Force kill if still alive | ||
| 220 | if is_process_running(pid) { | ||
| 221 | unsafe { | ||
| 222 | libc::kill(pid as i32, libc::SIGKILL); | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | // Clean up files | ||
| 228 | let socket_path = config.get_socket_path(); | ||
| 229 | std::fs::remove_file(&socket_path).ok(); | ||
| 230 | std::fs::remove_file(&pid_path).ok(); | ||
| 231 | |||
| 232 | println!("Daemon stopped"); | ||
| 233 | Ok(()) | ||
| 234 | } | ||
| 235 | Err(e) => Err(format!("Daemon not running: {}", e).into()), | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | pub fn restart_daemon() -> Result<(), Box<dyn std::error::Error>> { | ||
| 240 | // Stop existing daemon | ||
| 241 | if let Ok(()) = stop_daemon() { | ||
| 242 | // Wait a bit for cleanup | ||
| 243 | thread::sleep(Duration::from_millis(500)); | ||
| 244 | } | ||
| 245 | |||
| 246 | // Start new daemon in background | ||
| 247 | let current_exe = std::env::current_exe()?; | ||
| 248 | std::process::Command::new(current_exe) | ||
| 249 | .arg("start") | ||
| 250 | .spawn()?; | ||
| 251 | |||
| 252 | println!("Daemon restarted"); | ||
| 253 | Ok(()) | ||
| 254 | } | ||
| 255 | |||
| 256 | pub fn send_command(cmd: String) -> Result<(), Box<dyn std::error::Error>> { | ||
| 257 | send_command_raw(cmd)?; | ||
| 258 | Ok(()) | ||
| 259 | } | ||
| 260 | |||
| 261 | fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { | ||
| 262 | let config = Config::load()?; | ||
| 263 | let socket_path = config.get_socket_path(); | ||
| 264 | |||
| 265 | let mut stream = UnixStream::connect(socket_path)?; | ||
| 266 | stream.write_all(cmd.as_bytes())?; | ||
| 267 | |||
| 268 | let mut response = String::new(); | ||
| 269 | stream.read_to_string(&mut response)?; | ||
| 270 | print!("{}", response); | ||
| 271 | |||
| 272 | Ok(()) | ||
| 273 | } | ||
| 274 | |||
| 275 | fn handle_client( | ||
| 276 | mut stream: UnixStream, | ||
| 277 | audio: Arc<AudioEngine>, | ||
| 278 | shutdown_flag: Arc<AtomicBool>, | ||
| 279 | queue: Arc<Mutex<Queue>>, | ||
| 280 | ) { | ||
| 281 | let mut buffer = [0; 1024]; | ||
| 282 | if let Ok(size) = stream.read(&mut buffer) { | ||
| 283 | let cmd = String::from_utf8_lossy(&buffer[..size]); | ||
| 284 | let response = process_command(cmd.trim(), &audio, &shutdown_flag, &queue); | ||
| 285 | let _ = stream.write_all(response.as_bytes()); | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | fn process_command( | ||
| 290 | cmd: &str, | ||
| 291 | audio: &AudioEngine, | ||
| 292 | shutdown_flag: &Arc<AtomicBool>, | ||
| 293 | queue: &Arc<Mutex<Queue>>, | ||
| 294 | ) -> String { | ||
| 295 | match cmd { | ||
| 296 | "status" => { | ||
| 297 | if audio.is_playing() { | ||
| 298 | if let Some(track) = audio.current_track() { | ||
| 299 | format!("Playing\nTrack: {}\n", track.display()) | ||
| 300 | } else { | ||
| 301 | "Playing\n".to_string() | ||
| 302 | } | ||
| 303 | } else { | ||
| 304 | "Stopped\n".to_string() | ||
| 305 | } | ||
| 306 | } | ||
| 307 | "pause" => { | ||
| 308 | audio.pause(); | ||
| 309 | "Paused\n".to_string() | ||
| 310 | } | ||
| 311 | "resume" => { | ||
| 312 | audio.resume(); | ||
| 313 | "Resumed\n".to_string() | ||
| 314 | } | ||
| 315 | "stop" => { | ||
| 316 | audio.stop(); | ||
| 317 | // Clear queue on stop | ||
| 318 | if let Ok(mut q) = queue.lock() { | ||
| 319 | q.tracks.clear(); | ||
| 320 | q.position = 0; | ||
| 321 | } | ||
| 322 | "Stopped\n".to_string() | ||
| 323 | } | ||
| 324 | "stop_daemon" => { | ||
| 325 | // Signal the daemon to shut down | ||
| 326 | shutdown_flag.store(true, Ordering::SeqCst); | ||
| 327 | "Shutting down\n".to_string() | ||
| 328 | } | ||
| 329 | "next" => { | ||
| 330 | if let Ok(mut q) = queue.lock() { | ||
| 331 | if let Some(next_track) = q.next() { | ||
| 332 | audio.play(next_track); | ||
| 333 | "Playing next track\n".to_string() | ||
| 334 | } else { | ||
| 335 | audio.stop(); | ||
| 336 | "Queue empty, stopped\n".to_string() | ||
| 337 | } | ||
| 338 | } else { | ||
| 339 | "Error accessing queue\n".to_string() | ||
| 340 | } | ||
| 341 | } | ||
| 342 | "previous" => { | ||
| 343 | if let Ok(mut q) = queue.lock() { | ||
| 344 | if let Some(prev_track) = q.previous() { | ||
| 345 | audio.play(prev_track); | ||
| 346 | "Playing previous track\n".to_string() | ||
| 347 | } else { | ||
| 348 | "No previous track\n".to_string() | ||
| 349 | } | ||
| 350 | } else { | ||
| 351 | "Error accessing queue\n".to_string() | ||
| 352 | } | ||
| 353 | } | ||
| 354 | "shuffle" => { | ||
| 355 | if let Ok(q) = queue.lock() { | ||
| 356 | match q.shuffle { | ||
| 357 | ShuffleMode::None => "Shuffle: off\n".to_string(), | ||
| 358 | ShuffleMode::All => "Shuffle: on (all)\n".to_string(), | ||
| 359 | ShuffleMode::Album => "Shuffle: on (album)\n".to_string(), | ||
| 360 | } | ||
| 361 | } else { | ||
| 362 | "Error accessing queue\n".to_string() | ||
| 363 | } | ||
| 364 | } | ||
| 365 | _ if cmd.starts_with("shuffle ") => { | ||
| 366 | let mode_str = cmd[8..].trim(); | ||
| 367 | if let Ok(mut q) = queue.lock() { | ||
| 368 | let result = if mode_str == "on" || mode_str == "all" { | ||
| 369 | q.set_shuffle(ShuffleMode::All); | ||
| 370 | "Shuffle: on (all)\n".to_string() | ||
| 371 | } else if mode_str == "album" { | ||
| 372 | q.set_shuffle(ShuffleMode::Album); | ||
| 373 | "Shuffle: on (album)\n".to_string() | ||
| 374 | } else if mode_str == "off" || mode_str == "none" { | ||
| 375 | q.set_shuffle(ShuffleMode::None); | ||
| 376 | "Shuffle: off\n".to_string() | ||
| 377 | } else { | ||
| 378 | "Usage: shuffle [on|off|album]\n".to_string() | ||
| 379 | }; | ||
| 380 | // Save to config | ||
| 381 | if let Ok(mut config) = Config::load() { | ||
| 382 | if mode_str == "off" || mode_str == "none" { | ||
| 383 | config.shuffle = None; | ||
| 384 | } else { | ||
| 385 | config.shuffle = Some(mode_str.to_string()); | ||
| 386 | } | ||
| 387 | let _ = config.save(); | ||
| 388 | } | ||
| 389 | result | ||
| 390 | } else { | ||
| 391 | "Error accessing queue\n".to_string() | ||
| 392 | } | ||
| 393 | } | ||
| 394 | _ if cmd.starts_with("play ") => { | ||
| 395 | let path_str = &cmd[5..].trim(); | ||
| 396 | let path = std::path::PathBuf::from(path_str); | ||
| 397 | if path.is_file() { | ||
| 398 | // Add to queue and play | ||
| 399 | if let Ok(mut q) = queue.lock() { | ||
| 400 | q.add_track(path.clone()); | ||
| 401 | } | ||
| 402 | audio.play(path); | ||
| 403 | format!("Playing: {}\n", path_str) | ||
| 404 | } else if path.is_dir() { | ||
| 405 | // Find all audio files in directory | ||
| 406 | match find_audio_in_dir(&path) { | ||
| 407 | Some(files) if !files.is_empty() => { | ||
| 408 | if let Ok(mut q) = queue.lock() { | ||
| 409 | for file in &files { | ||
| 410 | q.add_track(file.clone()); | ||
| 411 | } | ||
| 412 | // If shuffle is on, shuffled_indices is already set | ||
| 413 | // Play the first track from the shuffled list | ||
| 414 | if let Some(track) = q.current().cloned() { | ||
| 415 | audio.play(track); | ||
| 416 | } | ||
| 417 | } | ||
| 418 | format!("Queued {} tracks from: {}\n", files.len(), path_str) | ||
| 419 | } | ||
| 420 | _ => format!("Error: No audio files found in '{}'\n", path_str), | ||
| 421 | } | ||
| 422 | } else { | ||
| 423 | format!("File not found: {}\n", path_str) | ||
| 424 | } | ||
| 425 | } | ||
| 426 | _ if cmd.starts_with("volume ") => { | ||
| 427 | if let Ok(level) = cmd[7..].trim().parse::<u8>() { | ||
| 428 | audio.set_volume(level as f32 / 100.0); | ||
| 429 | // Save to config | ||
| 430 | if let Ok(mut config) = Config::load() { | ||
| 431 | config.volume = Some(level); | ||
| 432 | let _ = config.save(); | ||
| 433 | } | ||
| 434 | format!("Volume set to {}\n", level) | ||
| 435 | } else { | ||
| 436 | "Usage: volume <0-100>\n".to_string() | ||
| 437 | } | ||
| 438 | } | ||
| 439 | _ if cmd.starts_with("shuffle ") => { | ||
| 440 | let mode_str = cmd[8..].trim(); | ||
| 441 | if let Ok(mut q) = queue.lock() { | ||
| 442 | if mode_str == "on" || mode_str == "all" { | ||
| 443 | q.set_shuffle(ShuffleMode::All); | ||
| 444 | "Shuffle: on\n".to_string() | ||
| 445 | } else if mode_str == "off" || mode_str == "none" { | ||
| 446 | q.set_shuffle(ShuffleMode::None); | ||
| 447 | "Shuffle: off\n".to_string() | ||
| 448 | } else { | ||
| 449 | "Usage: shuffle [on|off]\n".to_string() | ||
| 450 | } | ||
| 451 | } else { | ||
| 452 | "Error accessing queue\n".to_string() | ||
| 453 | } | ||
| 454 | } | ||
| 455 | _ if cmd.starts_with("queue") => { | ||
| 456 | if let Ok(q) = queue.lock() { | ||
| 457 | if q.tracks.is_empty() { | ||
| 458 | "Queue is empty\n".to_string() | ||
| 459 | } else { | ||
| 460 | let mut output = String::new(); | ||
| 461 | let current_position = q.position; | ||
| 462 | // Use shuffled_indices for display order if shuffle is on | ||
| 463 | let indices: Vec<usize> = if let ShuffleMode::None = q.shuffle { | ||
| 464 | (0..q.tracks.len()).collect() | ||
| 465 | } else { | ||
| 466 | q.shuffled_indices.clone() | ||
| 467 | }; | ||
| 468 | for (display_idx, &track_idx) in indices.iter().enumerate() { | ||
| 469 | if let Some(track) = q.tracks.get(track_idx) { | ||
| 470 | if display_idx == current_position { | ||
| 471 | output.push_str(&format!("> {} (playing)\n", track.display())); | ||
| 472 | } else { | ||
| 473 | output.push_str(&format!(" {}\n", track.display())); | ||
| 474 | } | ||
| 475 | } | ||
| 476 | } | ||
| 477 | // Use pager if output is long (more than 24 lines) | ||
| 478 | if output.lines().count() > 24 { | ||
| 479 | use std::process::{Command, Stdio}; | ||
| 480 | let pager = Command::new("less").arg("-R").stdin(Stdio::piped()).spawn(); | ||
| 481 | if let Ok(mut child) = pager { | ||
| 482 | use std::io::Write; | ||
| 483 | let _ = child.stdin.as_mut().unwrap().write_all(output.as_bytes()); | ||
| 484 | let _ = child.wait(); | ||
| 485 | "".to_string() | ||
| 486 | } else { | ||
| 487 | output | ||
| 488 | } | ||
| 489 | } else { | ||
| 490 | output | ||
| 491 | } | ||
| 492 | } | ||
| 493 | } else { | ||
| 494 | "Error accessing queue\n".to_string() | ||
| 495 | } | ||
| 496 | } | ||
| 497 | _ => "Unknown command\n".to_string(), | ||
| 498 | } | ||
| 499 | } | ||
| 500 | |||
| 501 | fn find_audio_in_dir(dir: &Path) -> Option<Vec<std::path::PathBuf>> { | ||
| 502 | use std::ffi::OsStr; | ||
| 503 | |||
| 504 | let mut results = Vec::new(); | ||
| 505 | |||
| 506 | if let Ok(entries) = std::fs::read_dir(dir) { | ||
| 507 | let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); | ||
| 508 | entries.sort_by_key(|e| e.file_name()); | ||
| 509 | |||
| 510 | for entry in entries { | ||
| 511 | let path = entry.path(); | ||
| 512 | if path.is_file() { | ||
| 513 | if let Some(ext) = path.extension().and_then(OsStr::to_str) { | ||
| 514 | if matches!( | ||
| 515 | ext.to_lowercase().as_str(), | ||
| 516 | "mp3" | "flac" | "ogg" | "m4a" | "wav" | "opus" | ||
| 517 | ) { | ||
| 518 | results.push(path); | ||
| 519 | } | ||
| 520 | } | ||
| 521 | } else if path.is_dir() { | ||
| 522 | if let Some(mut found) = find_audio_in_dir(&path) { | ||
| 523 | results.append(&mut found); | ||
| 524 | } | ||
| 525 | } | ||
| 526 | } | ||
| 527 | } | ||
| 528 | |||
| 529 | if results.is_empty() { | ||
| 530 | None | ||
| 531 | } else { | ||
| 532 | Some(results) | ||
| 533 | } | ||
| 534 | } | ||
| 535 | |||
| 536 | /// Get the PID file path for the daemon | ||
| 537 | fn get_pid_path(_config: &Config) -> std::path::PathBuf { | ||
| 538 | let runtime_dir = std::env::var("XDG_RUNTIME_DIR") | ||
| 539 | .map(std::path::PathBuf::from) | ||
| 540 | .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")); | ||
| 541 | runtime_dir.join("rsplayer").join("rsplayer.pid") | ||
| 542 | } | ||
| 543 | |||
| 544 | /// Check if a process with the given PID is running | ||
| 545 | fn is_process_running(pid: u32) -> bool { | ||
| 546 | unsafe { | ||
| 547 | // send signal 0 to check if process exists | ||
| 548 | libc::kill(pid as i32, 0) == 0 | ||
| 549 | } | ||
| 550 | } | ||
| 551 | |||
| 552 | /// Check if daemon is running by reading PID file | ||
| 553 | fn check_running_daemon(pid_path: &std::path::Path) -> Result<u32, Box<dyn std::error::Error>> { | ||
| 554 | if !pid_path.exists() { | ||
| 555 | return Err("PID file not found".into()); | ||
| 556 | } | ||
| 557 | |||
| 558 | let contents = std::fs::read_to_string(pid_path)?; | ||
| 559 | let pid = contents.trim().parse::<u32>()?; | ||
| 560 | |||
| 561 | if is_process_running(pid) { | ||
| 562 | Ok(pid) | ||
| 563 | } else { | ||
| 564 | // Stale PID file, clean it up | ||
| 565 | std::fs::remove_file(pid_path).ok(); | ||
| 566 | Err("Stale PID file, daemon not running".into()) | ||
| 567 | } | ||
| 568 | } | ||
| 569 | |||
| 570 | /// Clean up daemon resources (socket and PID file) | ||
| 571 | fn cleanup_daemon(socket_path: &std::path::Path, pid_path: &std::path::Path) { | ||
| 572 | std::fs::remove_file(socket_path).ok(); | ||
| 573 | std::fs::remove_file(pid_path).ok(); | ||
| 574 | } | ||
