aboutsummaryrefslogtreecommitdiffstats
path: root/src/daemon.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/daemon.rs')
-rw-r--r--src/daemon.rs380
1 files changed, 76 insertions, 304 deletions
diff --git a/src/daemon.rs b/src/daemon.rs
index 9e02e3f..0921e11 100644
--- a/src/daemon.rs
+++ b/src/daemon.rs
@@ -1,5 +1,6 @@
1use crate::audio::AudioEngine; 1use crate::audio::AudioEngine;
2use crate::config::Config; 2use crate::config::Config;
3use crate::queue::{Queue, ShuffleMode, Track};
3use std::io::{Read, Write}; 4use std::io::{Read, Write};
4use std::os::unix::net::{UnixListener, UnixStream}; 5use std::os::unix::net::{UnixListener, UnixStream};
5use std::path::{Path, PathBuf}; 6use std::path::{Path, PathBuf};
@@ -8,217 +9,6 @@ use std::sync::{Arc, Mutex};
8use std::thread; 9use std::thread;
9use std::time::Duration; 10use std::time::Duration;
10 11
11#[derive(Debug, Clone)]
12enum ShuffleMode {
13 None,
14 All,
15 Album,
16}
17
18#[derive(Debug, Clone)]
19struct Track {
20 path: PathBuf,
21 title: Option<String>,
22 artist: Option<String>,
23 album: Option<String>,
24}
25
26impl Track {
27 fn from_path(path: PathBuf) -> Self {
28 let metadata = extract_metadata(&path);
29 let metadata = metadata.as_ref();
30 Track {
31 path,
32 title: metadata.and_then(|m| m.title.clone()),
33 artist: metadata.and_then(|m| m.artist.clone()),
34 album: metadata.and_then(|m| m.album.clone()),
35 }
36 }
37
38 fn display_string(&self, index: usize) -> String {
39 let album_artist = self
40 .artist
41 .clone()
42 .or_else(|| self.album.clone())
43 .unwrap_or_default();
44 let title = self.title.clone().unwrap_or_else(|| {
45 self.path
46 .file_name()
47 .and_then(|n| n.to_str())
48 .unwrap_or("Unknown")
49 .to_string()
50 });
51
52 if album_artist.is_empty() {
53 format!("{}. {}", index + 1, title)
54 } else {
55 format!("{}. {} - {}", index + 1, album_artist, title)
56 }
57 }
58}
59
60#[derive(Debug, Default, Clone)]
61struct AudioMetadata {
62 title: Option<String>,
63 artist: Option<String>,
64 album: Option<String>,
65}
66
67fn extract_metadata(path: &Path) -> Option<AudioMetadata> {
68 use symphonia::core::io::MediaSourceStream;
69 use symphonia::core::meta::StandardTagKey;
70 use symphonia::core::probe::Hint;
71
72 let file = std::fs::File::open(path).ok()?;
73 let mss = MediaSourceStream::new(Box::new(file), Default::default());
74
75 let hint = Hint::new();
76 let mut probed = symphonia::default::get_probe()
77 .format(&hint, mss, &Default::default(), &Default::default())
78 .ok()?;
79
80 let mut metadata = AudioMetadata::default();
81
82 // First check probed metadata (e.g., ID3v2 headers before container)
83 if let Some(meta) = probed.metadata.get() {
84 if let Some(rev) = meta.current() {
85 for tag in rev.tags() {
86 match tag.std_key {
87 Some(StandardTagKey::TrackTitle) => {
88 metadata.title = Some(tag.value.to_string());
89 }
90 Some(StandardTagKey::Artist) => {
91 metadata.artist = Some(tag.value.to_string());
92 }
93 Some(StandardTagKey::Album) => {
94 metadata.album = Some(tag.value.to_string());
95 }
96 _ => {}
97 }
98 }
99 }
100 }
101
102 // If nothing found, try container metadata
103 if metadata.title.is_none() && metadata.artist.is_none() && metadata.album.is_none() {
104 let mut format = probed.format;
105 if let Some(rev) = format.metadata().current() {
106 for tag in rev.tags() {
107 match tag.std_key {
108 Some(StandardTagKey::TrackTitle) => {
109 metadata.title = Some(tag.value.to_string());
110 }
111 Some(StandardTagKey::Artist) => {
112 metadata.artist = Some(tag.value.to_string());
113 }
114 Some(StandardTagKey::Album) => {
115 metadata.album = Some(tag.value.to_string());
116 }
117 _ => {}
118 }
119 }
120 }
121 }
122
123 if metadata.title.is_some() || metadata.artist.is_some() || metadata.album.is_some() {
124 Some(metadata)
125 } else {
126 None
127 }
128}
129
130struct Queue {
131 tracks: Vec<Track>,
132 position: usize,
133 shuffle: ShuffleMode,
134 shuffled_indices: Vec<usize>,
135}
136
137impl Queue {
138 fn new() -> Self {
139 Queue {
140 tracks: Vec::new(),
141 position: 0,
142 shuffle: ShuffleMode::None,
143 shuffled_indices: Vec::new(),
144 }
145 }
146
147 fn add_track(&mut self, path: PathBuf) {
148 self.tracks.push(Track::from_path(path));
149 self.update_shuffle_indices();
150 }
151
152 fn insert_after_current(&mut self, path: PathBuf) {
153 if self.tracks.is_empty() {
154 self.tracks.push(Track::from_path(path));
155 } else {
156 let insert_idx = self.position + 1;
157 self.tracks.insert(insert_idx, Track::from_path(path));
158 }
159 self.update_shuffle_indices();
160 }
161
162 fn track_at(&self, idx: usize) -> Option<&Track> {
163 self.tracks.get(idx)
164 }
165
166 fn set_shuffle(&mut self, mode: ShuffleMode) {
167 self.shuffle = mode.clone();
168 self.update_shuffle_indices();
169 }
170
171 fn update_shuffle_indices(&mut self) {
172 let len = self.tracks.len();
173 self.shuffled_indices = (0..len).collect();
174 if let ShuffleMode::All = self.shuffle {
175 use rand::seq::SliceRandom;
176 let mut rng = rand::rng();
177 let pos = self.position.min(len.saturating_sub(1));
178 if pos + 1 < len {
179 self.shuffled_indices[pos + 1..].shuffle(&mut rng);
180 }
181 }
182 }
183
184 fn current(&self) -> Option<&Track> {
185 if self.tracks.is_empty() {
186 None
187 } else if let ShuffleMode::All = self.shuffle {
188 self.shuffled_indices
189 .get(self.position)
190 .and_then(|&idx| self.tracks.get(idx))
191 } else if let ShuffleMode::Album = self.shuffle {
192 // TODO: When database is ready, only shuffle tracks in same album/folder
193 // For now, behave like All
194 self.shuffled_indices
195 .get(self.position)
196 .and_then(|&idx| self.tracks.get(idx))
197 } else {
198 self.tracks.get(self.position)
199 }
200 }
201
202 fn next(&mut self) -> Option<PathBuf> {
203 let max_pos = self.tracks.len().saturating_sub(1);
204 if self.position < max_pos {
205 self.position += 1;
206 self.current().map(|t| t.path.clone())
207 } else {
208 None
209 }
210 }
211
212 fn previous(&mut self) -> Option<PathBuf> {
213 if self.position > 0 {
214 self.position -= 1;
215 self.current().map(|t| t.path.clone())
216 } else {
217 None
218 }
219 }
220}
221
222pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { 12pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> {
223 let config = Config::load()?; 13 let config = Config::load()?;
224 let socket_path = config.get_socket_path(); 14 let socket_path = config.get_socket_path();
@@ -268,13 +58,13 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> {
268 if let Some(vol) = config.volume { 58 if let Some(vol) = config.volume {
269 audio_engine.set_volume(vol as f32 / 100.0); 59 audio_engine.set_volume(vol as f32 / 100.0);
270 } 60 }
271 if let Some(ref shuffle_str) = config.shuffle { 61 if let Some(ref shuffle_str) = config.shuffle
272 if let Ok(mut q) = queue.lock() { 62 && let Ok(mut q) = queue.lock()
273 match shuffle_str.as_str() { 63 {
274 "on" | "all" => q.set_shuffle(ShuffleMode::All), 64 match shuffle_str.as_str() {
275 "album" => q.set_shuffle(ShuffleMode::Album), 65 "on" | "all" => q.set_shuffle(ShuffleMode::All),
276 _ => {} 66 "album" => q.set_shuffle(ShuffleMode::Album),
277 } 67 _ => {}
278 } 68 }
279 } 69 }
280 70
@@ -285,13 +75,11 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> {
285 use std::time::{Duration, Instant}; 75 use std::time::{Duration, Instant};
286 let start = Instant::now(); 76 let start = Instant::now();
287 while start.elapsed() < Duration::from_millis(timeout_ms) { 77 while start.elapsed() < Duration::from_millis(timeout_ms) {
288 // If playing and the current track matches, assume started 78 if audio.is_playing()
289 if audio.is_playing() { 79 && let Some(cur) = audio.current_track()
290 if let Some(cur) = audio.current_track() { 80 && &cur == target
291 if &cur == target { 81 {
292 return true; 82 return true;
293 }
294 }
295 } 83 }
296 std::thread::sleep(Duration::from_millis(10)); 84 std::thread::sleep(Duration::from_millis(10));
297 } 85 }
@@ -304,36 +92,27 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> {
304 } 92 }
305 93
306 // Check if track ended and play next (robust with startup feedback) 94 // Check if track ended and play next (robust with startup feedback)
307 if audio_engine.poll_track_ended() { 95 if audio_engine.poll_track_ended()
308 if let Ok(mut q) = queue.lock() { 96 && let Ok(mut q) = queue.lock()
309 // Save the current position in case we need to revert 97 {
310 let original_position = q.position; 98 let original_position = q.position();
311 // Try a handful of next candidates until one starts 99 let mut started = false;
312 let mut started = false; 100 let max_trials = 4usize;
313 // We'll attempt up to 4 candidates ahead 101 for _ in 0..max_trials {
314 let max_trials = 4usize; 102 if let Some(next_track) = q.next() {
315 for _ in 0..max_trials { 103 audio_engine.play(next_track.clone());
316 if let Some(next_track) = q.next() { 104 if wait_for_start(&audio_engine, &next_track, 500) {
317 audio_engine.play(next_track.clone()); 105 started = true;
318 // Wait a short time to see if playback actually starts
319 if wait_for_start(&audio_engine, &next_track, 500) {
320 started = true;
321 break;
322 } else {
323 // Try next candidate - q.next() already advanced position
324 continue;
325 }
326 } else {
327 break; 106 break;
328 } 107 }
329 } 108 } else {
330 if !started { 109 break;
331 // Revert position since we couldn't start any track
332 q.position = original_position;
333 // No candidate started; stop playback safely
334 audio_engine.stop();
335 } 110 }
336 } 111 }
112 if !started {
113 q.set_position(original_position);
114 audio_engine.stop();
115 }
337 } 116 }
338 117
339 match listener.accept() { 118 match listener.accept() {
@@ -494,10 +273,8 @@ fn process_command(
494 } 273 }
495 "stop" => { 274 "stop" => {
496 audio.stop(); 275 audio.stop();
497 // Clear queue on stop
498 if let Ok(mut q) = queue.lock() { 276 if let Ok(mut q) = queue.lock() {
499 q.tracks.clear(); 277 q.clear();
500 q.position = 0;
501 } 278 }
502 "Stopped\n".to_string() 279 "Stopped\n".to_string()
503 } 280 }
@@ -533,7 +310,7 @@ fn process_command(
533 } 310 }
534 "shuffle" => { 311 "shuffle" => {
535 if let Ok(q) = queue.lock() { 312 if let Ok(q) = queue.lock() {
536 match q.shuffle { 313 match q.shuffle_mode() {
537 ShuffleMode::None => "Shuffle: off\n".to_string(), 314 ShuffleMode::None => "Shuffle: off\n".to_string(),
538 ShuffleMode::All => "Shuffle: on (all)\n".to_string(), 315 ShuffleMode::All => "Shuffle: on (all)\n".to_string(),
539 ShuffleMode::Album => "Shuffle: on (album)\n".to_string(), 316 ShuffleMode::Album => "Shuffle: on (album)\n".to_string(),
@@ -591,14 +368,15 @@ fn process_command(
591 let path = std::path::PathBuf::from(path_str.clone()); 368 let path = std::path::PathBuf::from(path_str.clone());
592 if path.is_file() { 369 if path.is_file() {
593 if let Ok(mut q) = queue.lock() { 370 if let Ok(mut q) = queue.lock() {
594 let was_empty = q.tracks.is_empty(); 371 let was_empty = q.is_empty();
595 match position { 372 match position {
596 "now" => { 373 "now" => {
597 q.insert_after_current(path.clone()); 374 q.insert_after_current(path.clone());
598 if !was_empty { 375 if !was_empty {
599 q.position += 1; 376 let new_pos = q.position() + 1;
377 q.set_position(new_pos);
600 } 378 }
601 if let Some(track) = q.track_at(q.position) { 379 if let Some(track) = q.track_at(q.position()) {
602 audio.play(track.path.clone()); 380 audio.play(track.path.clone());
603 } 381 }
604 format!("Playing now: {}\n", path_str) 382 format!("Playing now: {}\n", path_str)
@@ -608,9 +386,14 @@ fn process_command(
608 format!("Queued next: {}\n", path_str) 386 format!("Queued next: {}\n", path_str)
609 } 387 }
610 _ => { 388 _ => {
389 let was_playing = audio.is_playing();
611 q.add_track(path.clone()); 390 q.add_track(path.clone());
612 audio.play(path); 391 if !was_playing {
613 format!("Playing: {}\n", path_str) 392 audio.play(path);
393 format!("Playing: {}\n", path_str)
394 } else {
395 format!("Queued: {}\n", path_str)
396 }
614 } 397 }
615 } 398 }
616 } else { 399 } else {
@@ -620,16 +403,17 @@ fn process_command(
620 match find_audio_in_dir(&path) { 403 match find_audio_in_dir(&path) {
621 Some(files) if !files.is_empty() => { 404 Some(files) if !files.is_empty() => {
622 if let Ok(mut q) = queue.lock() { 405 if let Ok(mut q) = queue.lock() {
623 let was_empty = q.tracks.is_empty(); 406 let was_empty = q.is_empty();
624 match position { 407 match position {
625 "now" => { 408 "now" => {
626 for file in &files { 409 for file in files.iter().rev() {
627 q.insert_after_current(file.clone()); 410 q.insert_after_current(file.clone());
628 } 411 }
629 if !was_empty { 412 if !was_empty {
630 q.position += 1; 413 let new_pos = q.position() + 1;
414 q.set_position(new_pos);
631 } 415 }
632 if let Some(track) = q.track_at(q.position) { 416 if let Some(track) = q.track_at(q.position()) {
633 audio.play(track.path.clone()); 417 audio.play(track.path.clone());
634 } 418 }
635 format!( 419 format!(
@@ -639,7 +423,7 @@ fn process_command(
639 ) 423 )
640 } 424 }
641 "next" => { 425 "next" => {
642 for file in &files { 426 for file in files.iter().rev() {
643 q.insert_after_current(file.clone()); 427 q.insert_after_current(file.clone());
644 } 428 }
645 format!( 429 format!(
@@ -652,10 +436,10 @@ fn process_command(
652 for file in &files { 436 for file in &files {
653 q.add_track(file.clone()); 437 q.add_track(file.clone());
654 } 438 }
655 if !audio.is_playing() { 439 if !audio.is_playing()
656 if let Some(track) = files.first() { 440 && let Some(track) = files.first()
657 audio.play(track.clone()); 441 {
658 } 442 audio.play(track.clone());
659 } 443 }
660 format!("Queued {} tracks from: {}\n", files.len(), path_str) 444 format!("Queued {} tracks from: {}\n", files.len(), path_str)
661 } 445 }
@@ -687,43 +471,31 @@ fn process_command(
687 "Usage: volume <0-100>\n".to_string() 471 "Usage: volume <0-100>\n".to_string()
688 } 472 }
689 } 473 }
690 _ if cmd.starts_with("shuffle ") => {
691 let mode_str = cmd[8..].trim();
692 if let Ok(mut q) = queue.lock() {
693 if mode_str == "on" || mode_str == "all" {
694 q.set_shuffle(ShuffleMode::All);
695 "Shuffle: on\n".to_string()
696 } else if mode_str == "off" || mode_str == "none" {
697 q.set_shuffle(ShuffleMode::None);
698 "Shuffle: off\n".to_string()
699 } else {
700 "Usage: shuffle [on|off]\n".to_string()
701 }
702 } else {
703 "Error accessing queue\n".to_string()
704 }
705 }
706 _ if cmd.starts_with("queue") => { 474 _ if cmd.starts_with("queue") => {
707 if let Ok(q) = queue.lock() { 475 if let Ok(q) = queue.lock() {
708 if q.tracks.is_empty() { 476 if q.is_empty() {
709 "Queue is empty\n".to_string() 477 "Queue is empty\n".to_string()
710 } else { 478 } else {
711 let mut output = String::new(); 479 let mut output = String::new();
712 let current_position = q.position; 480 let current_position = q.position();
713 let playing_track_idx = match q.shuffle { 481 let playing_track_idx = match q.shuffle_mode() {
714 ShuffleMode::None => current_position, 482 ShuffleMode::None => current_position,
715 _ => *q.shuffled_indices.get(current_position).unwrap_or(&0), 483 _ => q
484 .shuffled_indices()
485 .get(current_position)
486 .copied()
487 .unwrap_or(0),
716 }; 488 };
717 // Use shuffled_indices for display order if shuffle is on 489 let indices: Vec<usize> = if *q.shuffle_mode() == ShuffleMode::None {
718 let indices: Vec<usize> = if let ShuffleMode::None = q.shuffle { 490 (0..q.len()).collect()
719 (0..q.tracks.len()).collect()
720 } else { 491 } else {
721 q.shuffled_indices.clone() 492 q.shuffled_indices().to_vec()
722 }; 493 };
494 let is_playing = audio.is_playing();
723 for (display_idx, &track_idx) in indices.iter().enumerate() { 495 for (display_idx, &track_idx) in indices.iter().enumerate() {
724 if let Some(track) = q.tracks.get(track_idx) { 496 if let Some(track) = q.track_at(track_idx) {
725 let track_str = track.display_string(display_idx); 497 let track_str = track.display_string(display_idx);
726 if track_idx == playing_track_idx { 498 if track_idx == playing_track_idx && is_playing {
727 output.push_str(&format!("> {} (playing)\n", track_str)); 499 output.push_str(&format!("> {} (playing)\n", track_str));
728 } else { 500 } else {
729 output.push_str(&format!(" {}\n", track_str)); 501 output.push_str(&format!(" {}\n", track_str));
@@ -766,18 +538,18 @@ fn find_audio_in_dir(dir: &Path) -> Option<Vec<std::path::PathBuf>> {
766 for entry in entries { 538 for entry in entries {
767 let path = entry.path(); 539 let path = entry.path();
768 if path.is_file() { 540 if path.is_file() {
769 if let Some(ext) = path.extension().and_then(OsStr::to_str) { 541 if let Some(ext) = path.extension().and_then(OsStr::to_str)
770 if matches!( 542 && matches!(
771 ext.to_lowercase().as_str(), 543 ext.to_lowercase().as_str(),
772 "mp3" | "flac" | "ogg" | "m4a" | "wav" | "opus" 544 "mp3" | "flac" | "ogg" | "m4a" | "wav" | "opus"
773 ) { 545 )
774 results.push(path); 546 {
775 } 547 results.push(path);
776 }
777 } else if path.is_dir() {
778 if let Some(mut found) = find_audio_in_dir(&path) {
779 results.append(&mut found);
780 } 548 }
549 } else if path.is_dir()
550 && let Some(mut found) = find_audio_in_dir(&path)
551 {
552 results.append(&mut found);
781 } 553 }
782 } 554 }
783 } 555 }