1pub mod commands;
2pub mod state;
3pub mod undo;
4
5use std::path::Path;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::thread;
9
10use thiserror::Error;
11
12use crate::audio::{
13 analyzer::VizAnalyzer,
14 backend::{self, AudioBackend, AudioEngineHandle, BackendError},
15 buffer, streaming,
16 viz::{VizBuffer, VizSnapshot},
17};
18use buffer::PlaybackTimeline;
19use commands::{CommandChannel, PlayerCommand};
20use state::{LoadState, PlaybackSource, PlaybackState, QueueItemId, SharedPlayerState, TrackInfo};
21use undo::{UndoEntry, UndoStack};
22
23const RING_BUFFER_SIZE: usize = 192_000 * 2;
25
26#[derive(Debug, Error)]
27pub enum PlayerError {
28 #[error("backend error: {0}")]
29 Backend(#[from] BackendError),
30 #[error("decode error: {0}")]
31 Decode(#[from] buffer::DecodeError),
32}
33
34pub struct Player {
36 shared_state: Arc<SharedPlayerState>,
37 commands: CommandChannel,
38 active_playback: Option<ActivePlayback>,
39 timeline: Arc<PlaybackTimeline>,
40 viz_buffer: Arc<VizBuffer>,
41 viz_snapshot: Arc<VizSnapshot>,
42 _viz_analyzer: VizAnalyzer,
44 undo_stack: UndoStack,
45 batch_buffer: Option<Vec<UndoEntry>>,
48 output_device_name: Option<String>,
50 backend: Box<dyn AudioBackend>,
52}
53
54struct ActivePlayback {
56 engine: Box<dyn AudioEngineHandle>,
57 decode_handle: buffer::DecodeHandle,
58}
59
60impl Default for Player {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl Player {
67 pub fn new() -> Self {
68 let viz_buffer = VizBuffer::new();
69 let viz_snapshot = VizSnapshot::new();
70 let cfg = crate::config::Config::load_or_default();
71 let viz_analyzer = VizAnalyzer::spawn_with_snapshot(
72 Arc::clone(&viz_buffer),
73 &cfg.visualizer,
74 Arc::clone(&viz_snapshot),
75 );
76
77 Self {
78 shared_state: SharedPlayerState::new(),
79 commands: CommandChannel::new(),
80 active_playback: None,
81 timeline: PlaybackTimeline::new(),
82 viz_buffer,
83 viz_snapshot,
84 _viz_analyzer: viz_analyzer,
85 undo_stack: UndoStack::new(),
86 batch_buffer: None,
87 output_device_name: cfg.playback.output_device.clone(),
88 backend: crate::audio::platform_backend(),
89 }
90 }
91
92 pub fn shared_state(&self) -> Arc<SharedPlayerState> {
94 self.shared_state.clone()
95 }
96
97 pub fn timeline(&self) -> Arc<PlaybackTimeline> {
99 self.timeline.clone()
100 }
101
102 pub fn viz_buffer(&self) -> Arc<VizBuffer> {
104 self.viz_buffer.clone()
105 }
106
107 pub fn viz_snapshot(&self) -> Arc<VizSnapshot> {
110 self.viz_snapshot.clone()
111 }
112
113 pub fn undo_stack(&self) -> &UndoStack {
115 &self.undo_stack
116 }
117
118 fn resolve_device(&self) -> Result<backend::DeviceInfo, PlayerError> {
121 if let Some(ref name) = self.output_device_name {
122 match self.backend.list_devices() {
123 Ok(devices) => {
124 if let Some(dev) = devices.into_iter().find(|d| d.name == *name) {
125 return Ok(dev);
126 }
127 log::warn!(
128 "configured output device '{}' not found, falling back to default",
129 name,
130 );
131 }
132 Err(e) => {
133 log::warn!("failed to list devices while resolving '{}': {}", name, e);
134 }
135 }
136 }
137 Ok(self.backend.default_device()?)
138 }
139
140 pub fn set_output_device(&mut self, name: String) {
143 log::info!("switching output device to: {}", name);
144 self.output_device_name = Some(name.clone());
145
146 if let Ok(mut cfg) = crate::config::Config::load() {
148 cfg.playback.output_device = Some(name);
149 if let Err(e) = cfg.save() {
150 log::error!("failed to save output device config: {}", e);
151 }
152 }
153
154 self.restart_on_current_track();
155 }
156
157 pub fn clear_output_device(&mut self) {
159 log::info!("reverting to system default output device");
160 self.output_device_name = None;
161
162 if let Ok(mut cfg) = crate::config::Config::load() {
163 cfg.playback.output_device = None;
164 if let Err(e) = cfg.save() {
165 log::error!("failed to save output device config: {}", e);
166 }
167 }
168
169 self.restart_on_current_track();
170 }
171
172 fn restart_on_current_track(&mut self) {
175 if let Some(info) = self.shared_state.track_info() {
176 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
177 let position_ms = self.shared_state.position_ms();
178 if let Err(e) = self.start_playback(info.id, &info.path, position_ms) {
179 log::error!("failed to restart playback on device switch: {}", e);
180 return;
181 }
182 if was_paused {
183 self.pause();
184 }
185 }
186 }
187
188 pub fn output_device_name(&self) -> Option<&str> {
190 self.output_device_name.as_deref()
191 }
192
193 pub fn command_sender(&self) -> crossbeam_channel::Sender<PlayerCommand> {
195 self.commands.tx.clone()
196 }
197
198 pub fn play(&mut self, id: QueueItemId) {
201 self.shared_state.set_cursor(Some(id));
202
203 match self.shared_state.item_playback_source(id) {
204 Some(PlaybackSource::Ready(path)) => {
205 if let Err(e) = self.start_playback(id, &path, 0) {
206 log::error!("play failed: {}", e);
207 }
208 }
209 Some(PlaybackSource::Streaming {
210 path,
211 bytes_written,
212 total,
213 }) => {
214 if let Err(e) = self.start_streaming_playback(id, &path, bytes_written, total) {
215 log::error!("streaming play failed, waiting for full download: {}", e);
216 self.stop_engine();
218 self.shared_state.set_playback_state(PlaybackState::Stopped);
219 }
220 }
221 None => {
222 self.stop_engine();
224 self.shared_state.set_playback_state(PlaybackState::Stopped);
225 log::info!("play: item {:?} not ready, waiting for TrackReady", id);
226 }
227 }
228 }
229
230 fn start_playback(
232 &mut self,
233 id: QueueItemId,
234 path: &Path,
235 seek_ms: u64,
236 ) -> Result<(), PlayerError> {
237 self.stop_engine();
238
239 let info = buffer::probe_file(path)?;
240
241 self.shared_state.set_track_info(Some(TrackInfo {
245 id,
246 path: path.to_path_buf(),
247 codec: info.codec.clone(),
248 sample_rate: info.sample_rate,
249 bit_depth: info.bit_depth,
250 channels: info.channels,
251 duration_ms: info.duration_ms,
252 }));
253 self.shared_state.set_position_ms(seek_ms);
254 log::info!(
255 "playing: {} ({:?}) — {} {}Hz/{}bit/{}ch, {}ms{}",
256 path.display(),
257 id,
258 info.codec,
259 info.sample_rate,
260 info.bit_depth,
261 info.channels,
262 info.duration_ms,
263 if seek_ms > 0 {
264 format!(" @{}ms", seek_ms)
265 } else {
266 String::new()
267 }
268 );
269
270 let device = self.resolve_device()?;
271 let device_rate = self.backend.get_device_sample_rate(&device)?;
272 let source_rate = info.sample_rate as f64;
273
274 if (device_rate - source_rate).abs() > 0.1 {
275 log::info!(
276 "switching device sample rate: {}Hz → {}Hz",
277 device_rate,
278 source_rate
279 );
280 if let Err(e) = self.backend.set_device_sample_rate(&device, source_rate) {
281 log::warn!(
282 "failed to set sample rate (continuing at device rate): {}",
283 e
284 );
285 }
286 }
287
288 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
289
290 let _generation = self.shared_state.bump_generation();
291
292 self.timeline.reset();
294
295 let advance_state = self.shared_state.clone();
299 let decode_cursor = std::sync::Mutex::new(Some(id));
300 let next_track = move || {
301 let current = decode_cursor.lock().ok()?.take()?;
302 let next = advance_state.peek_next_ready_after(current);
303 if let Some((next_id, _)) = &next
304 && let Ok(mut guard) = decode_cursor.lock()
305 {
306 *guard = Some(*next_id);
307 }
308 next
309 };
310
311 let cfg = crate::config::Config::load_or_default();
313 let rg_mode = cfg.playback.replaygain;
314 let pre_amp_db = cfg.playback.pre_amp_db;
315
316 let (_stream_info, decode_handle) = buffer::start_decode_file(
317 id,
318 path,
319 producer,
320 seek_ms,
321 next_track,
322 self.timeline.clone(),
323 Some(self.viz_buffer.clone()),
324 rg_mode,
325 pre_amp_db,
326 )?;
327
328 let actual_rate = self
330 .backend
331 .get_device_sample_rate(&device)
332 .unwrap_or(source_rate);
333 let engine = self.backend.create_engine(
334 &device,
335 actual_rate,
336 info.channels as u32,
337 consumer,
338 self.timeline.samples_played_counter(),
339 )?;
340 engine.start()?;
341
342 self.shared_state.set_playback_state(PlaybackState::Playing);
343
344 self.active_playback = Some(ActivePlayback {
345 engine,
346 decode_handle,
347 });
348
349 Ok(())
350 }
351
352 fn start_streaming_playback(
359 &mut self,
360 id: QueueItemId,
361 path: &Path,
362 bytes_written: Arc<AtomicU64>,
363 total: u64,
364 ) -> Result<(), PlayerError> {
365 self.stop_engine();
366
367 let stream_buf = streaming::StreamBuffer::new(if total > 0 { Some(total) } else { None });
369
370 let pump_path = path.to_path_buf();
374 let pump_buf = stream_buf.clone();
375 let pump_written = bytes_written.clone();
376 thread::Builder::new()
377 .name("koan-stream-pump".into())
378 .spawn(move || {
379 use std::fs::File;
380 use std::io::Read;
381 let mut file = match File::open(&pump_path) {
382 Ok(f) => f,
383 Err(e) => {
384 log::error!("stream pump: failed to open {}: {}", pump_path.display(), e);
385 pump_buf.finish();
386 return;
387 }
388 };
389 let mut buf = vec![0u8; 65536];
390 let mut offset: u64 = 0;
391 loop {
392 let available = pump_written.load(Ordering::Acquire);
393 if offset >= available {
394 if total > 0 && available >= total {
395 break; }
397 thread::sleep(std::time::Duration::from_millis(10));
398 continue;
399 }
400 let to_read = ((available - offset) as usize).min(buf.len());
401 match file.read(&mut buf[..to_read]) {
402 Ok(0) => {
403 if total > 0 && offset >= total {
406 break;
407 }
408 let latest = pump_written.load(Ordering::Acquire);
409 if total > 0 && latest >= total && offset >= latest {
410 break;
411 }
412 thread::sleep(std::time::Duration::from_millis(1));
414 continue;
415 }
416 Ok(n) => {
417 pump_buf.push(&buf[..n]);
418 offset += n as u64;
419 }
420 Err(e) => {
421 log::warn!("stream pump read error: {}", e);
422 break;
423 }
424 }
425 }
426 pump_buf.finish();
427 })
428 .map_err(|e| PlayerError::Decode(buffer::DecodeError::Io(e)))?;
429
430 let probe_reader = stream_buf.reader();
432 let probe_hint = {
433 let mut h = symphonia::core::probe::Hint::new();
434 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
435 h.with_extension(ext);
436 }
437 h
438 };
439 let probe_mss =
440 symphonia::core::io::MediaSourceStream::new(Box::new(probe_reader), Default::default());
441 let info = buffer::probe_source(probe_mss, &probe_hint)?;
442
443 self.shared_state.set_track_info(Some(TrackInfo {
444 id,
445 path: path.to_path_buf(),
446 codec: info.codec.clone(),
447 sample_rate: info.sample_rate,
448 bit_depth: info.bit_depth,
449 channels: info.channels,
450 duration_ms: info.duration_ms,
451 }));
452 self.shared_state.set_position_ms(0);
453 log::info!(
454 "streaming: {} ({:?}) — {} {}Hz/{}bit/{}ch, {}ms",
455 path.display(),
456 id,
457 info.codec,
458 info.sample_rate,
459 info.bit_depth,
460 info.channels,
461 info.duration_ms,
462 );
463
464 let device = self.resolve_device()?;
465 let device_rate = self.backend.get_device_sample_rate(&device)?;
466 let source_rate = info.sample_rate as f64;
467
468 if (device_rate - source_rate).abs() > 0.1 {
469 log::info!(
470 "switching device sample rate: {}Hz → {}Hz",
471 device_rate,
472 source_rate
473 );
474 if let Err(e) = self.backend.set_device_sample_rate(&device, source_rate) {
475 log::warn!(
476 "failed to set sample rate (continuing at device rate): {}",
477 e
478 );
479 }
480 }
481
482 let (producer, consumer) = rtrb::RingBuffer::new(RING_BUFFER_SIZE);
483
484 let _generation = self.shared_state.bump_generation();
485 self.timeline.reset();
486
487 let advance_state = self.shared_state.clone();
489 let decode_cursor = std::sync::Mutex::new(Some(id));
490 let next_track = move || {
491 let current = decode_cursor.lock().ok()?.take()?;
492 let next = advance_state.peek_next_ready_after(current);
493 if let Some((next_id, _)) = &next
494 && let Ok(mut guard) = decode_cursor.lock()
495 {
496 *guard = Some(*next_id);
497 }
498 next
499 };
500
501 let decode_reader = stream_buf.reader();
506 let path_buf = path.to_path_buf();
507 let ext = path
508 .extension()
509 .and_then(|e| e.to_str())
510 .unwrap_or("")
511 .to_string();
512 let mut decode_hint = symphonia::core::probe::Hint::new();
513 if !ext.is_empty() {
514 decode_hint.with_extension(&ext);
515 }
516 let first = buffer::SourceEntry {
517 id,
518 path: path_buf,
519 hint: decode_hint,
520 make_mss: Box::new(move || {
521 Ok(symphonia::core::io::MediaSourceStream::new(
522 Box::new(decode_reader),
523 Default::default(),
524 ))
525 }),
526 };
527
528 let cfg = crate::config::Config::load_or_default();
530 let rg_mode = cfg.playback.replaygain;
531 let pre_amp_db = cfg.playback.pre_amp_db;
532
533 let (_stream_info, decode_handle) = buffer::start_decode(
534 first,
535 producer,
536 0,
537 move || {
538 let (next_id, next_path) = next_track()?;
539 Some(buffer::SourceEntry::from_file(next_id, next_path))
540 },
541 self.timeline.clone(),
542 Some(self.viz_buffer.clone()),
543 rg_mode,
544 pre_amp_db,
545 )?;
546
547 let actual_rate = self
548 .backend
549 .get_device_sample_rate(&device)
550 .unwrap_or(source_rate);
551 let engine = self.backend.create_engine(
552 &device,
553 actual_rate,
554 info.channels as u32,
555 consumer,
556 self.timeline.samples_played_counter(),
557 )?;
558 engine.start()?;
559
560 self.shared_state.set_playback_state(PlaybackState::Playing);
561
562 self.active_playback = Some(ActivePlayback {
563 engine,
564 decode_handle,
565 });
566
567 Ok(())
568 }
569
570 pub fn seek(&mut self, position_ms: u64) {
573 let info = match self.shared_state.track_info() {
574 Some(info) => info,
575 None => return,
576 };
577 let id = info.id;
578 let path = info.path.clone();
579 let duration = info.duration_ms;
580
581 let mut clamped = if duration > 0 {
583 position_ms.min(duration.saturating_sub(500))
584 } else {
585 position_ms
586 };
587
588 if let Some(dl_frac) = self.shared_state.current_download_fraction() {
591 let max_ms = (dl_frac * duration as f64) as u64;
592 if max_ms > 5_000 {
593 clamped = clamped.min(max_ms - 5_000);
594 }
595 }
596
597 let was_paused = self.shared_state.playback_state() == PlaybackState::Paused;
598
599 if let Err(e) = self.start_playback(id, &path, clamped) {
600 log::error!("seek failed: {}", e);
601 return;
602 }
603
604 if was_paused {
605 self.pause();
606 }
607 }
608
609 pub fn next_track(&mut self) {
611 match self.shared_state.advance_cursor() {
613 Some((id, path)) => {
614 if let Err(e) = self.start_playback(id, &path, 0) {
615 log::error!("next track failed: {}", e);
616 }
617 }
618 None => {
619 log::info!("no more tracks in playlist");
620 self.stop_playback_and_clear_state();
621 }
622 }
623 }
624
625 pub fn prev_track(&mut self) {
627 match self.shared_state.retreat_cursor() {
628 Some((id, path)) => {
629 if matches!(path.try_exists(), Ok(true)) {
630 if let Err(e) = self.start_playback(id, &path, 0) {
631 log::error!("prev track failed: {}", e);
632 }
633 } else {
634 log::warn!("prev track path doesn't exist: {}", path.display());
635 }
636 }
637 None => {
638 if let Some(info) = self.shared_state.track_info()
640 && let Err(e) = self.start_playback(info.id, &info.path, 0)
641 {
642 log::error!("restart failed: {}", e);
643 }
644 }
645 }
646 }
647
648 pub fn pause(&mut self) {
650 if let Some(ref playback) = self.active_playback {
651 let _ = playback.engine.stop();
652 self.shared_state.set_playback_state(PlaybackState::Paused);
653 }
654 }
655
656 pub fn resume(&mut self) {
658 if let Some(ref playback) = self.active_playback {
659 let _ = playback.engine.start();
660 self.shared_state.set_playback_state(PlaybackState::Playing);
661 }
662 }
663
664 pub fn stop(&mut self) {
666 self.shared_state.clear_playlist();
667 self.stop_playback_and_clear_state();
668 }
669
670 fn stop_engine(&mut self) {
677 if let Some(playback) = self.active_playback.take() {
678 let _ = playback.engine.stop();
680 playback.decode_handle.signal_stop();
682 thread::Builder::new()
684 .name("koan-cleanup".into())
685 .spawn(move || drop(playback))
686 .ok();
687 }
688 }
689
690 fn stop_playback_and_clear_state(&mut self) {
692 self.stop_engine();
693 self.timeline.reset();
694 self.shared_state.set_playback_state(PlaybackState::Stopped);
695 self.shared_state.set_position_ms(0);
696 self.shared_state.set_track_info(None);
697 }
698
699 pub fn remove_from_playlist(&mut self, id: QueueItemId) {
701 let was_cursor = self.shared_state.is_cursor(id);
702 self.shared_state.remove_item(id);
703 if was_cursor {
704 self.next_track();
705 }
706 }
707
708 pub fn track_ready(&mut self, id: QueueItemId) {
711 self.shared_state.update_load_state(id, LoadState::Ready);
713
714 if !self.shared_state.is_cursor(id) {
715 return;
716 }
717
718 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
719 let current_track_id = self.shared_state.track_info().map(|t| t.id);
720
721 if is_playing && current_track_id == Some(id) {
722 log::info!(
725 "track_ready: download complete while streaming {:?}, refreshing metadata",
726 id
727 );
728 self.refresh_track_metadata(id);
729 return;
730 }
731
732 if !is_playing && let Some(path) = self.shared_state.item_path_if_ready(id) {
734 log::info!("track_ready: starting playback for {:?}", id);
735 if let Err(e) = self.start_playback(id, &path, 0) {
736 log::error!("track_ready playback failed: {}", e);
737 }
738 }
739 }
740
741 pub fn track_stream_ready(&mut self, id: QueueItemId) {
744 if !self.shared_state.is_cursor(id) {
745 return;
746 }
747
748 let is_playing = self.shared_state.playback_state() == PlaybackState::Playing;
749 if is_playing {
750 return; }
752
753 match self.shared_state.item_playback_source(id) {
754 Some(PlaybackSource::Streaming {
755 path,
756 bytes_written,
757 total,
758 }) => {
759 log::info!(
760 "track_stream_ready: starting streaming playback for {:?}",
761 id
762 );
763 if let Err(e) = self.start_streaming_playback(id, &path, bytes_written, total) {
764 log::error!("track_stream_ready streaming failed: {}", e);
765 }
766 }
767 Some(PlaybackSource::Ready(path)) => {
768 log::info!(
770 "track_stream_ready: track already ready, starting normal playback for {:?}",
771 id
772 );
773 if let Err(e) = self.start_playback(id, &path, 0) {
774 log::error!("track_stream_ready playback failed: {}", e);
775 }
776 }
777 None => {} }
779 }
780
781 fn refresh_track_metadata(&mut self, id: QueueItemId) {
785 use crate::index::metadata;
786
787 let path = match self.shared_state.item_path_if_ready(id) {
788 Some(p) => p,
789 None => return,
790 };
791
792 match metadata::read_metadata(&path) {
793 Ok(meta) => {
794 self.shared_state.update_item_metadata(
796 id,
797 meta.title,
798 meta.artist,
799 meta.album_artist.unwrap_or_default(),
800 meta.album,
801 meta.duration_ms.map(|d| d as u64),
802 );
803
804 if let Ok(stream_info) = buffer::probe_file(&path)
809 && let Some(current) = self.shared_state.track_info()
810 && current.id == id
811 && stream_info.duration_ms > current.duration_ms
812 {
813 log::info!(
814 "track_ready: duration corrected {}ms → {}ms",
815 current.duration_ms,
816 stream_info.duration_ms
817 );
818 self.shared_state.set_track_info(Some(TrackInfo {
819 duration_ms: stream_info.duration_ms,
820 ..current
821 }));
822 }
823
824 self.shared_state.signal_metadata_refresh();
826 log::info!("track_ready: metadata refreshed for {:?}", id);
827 }
828 Err(e) => {
829 log::warn!("track_ready: metadata refresh failed for {:?}: {}", id, e);
830 }
831 }
832 }
833
834 pub fn update_playback_state(&self) {
837 if self.active_playback.is_none() {
838 return;
839 }
840
841 if let Some((id, path, info, position_ms)) = self.timeline.current_playback() {
842 self.shared_state.set_position_ms(position_ms);
843
844 let current_id = self.shared_state.track_info().map(|t| t.id);
847 if current_id != Some(id) {
848 log::info!("timeline: now playing {:?}", id);
849 self.shared_state.set_track_info(Some(TrackInfo {
850 id,
851 path,
852 codec: info.codec,
853 sample_rate: info.sample_rate,
854 bit_depth: info.bit_depth,
855 channels: info.channels,
856 duration_ms: info.duration_ms,
857 }));
858 self.shared_state.set_cursor(Some(id));
859 }
860 }
861 }
862
863 fn push_undo(&mut self, entry: UndoEntry) {
865 if let Some(ref mut batch) = self.batch_buffer {
866 batch.push(entry);
867 } else {
868 self.undo_stack.push(entry);
869 }
870 }
871
872 pub fn process_command(&mut self, cmd: PlayerCommand) {
874 match cmd {
875 PlayerCommand::Play(id) => self.play(id),
876 PlayerCommand::Pause => self.pause(),
877 PlayerCommand::Resume => self.resume(),
878 PlayerCommand::Stop => self.stop(),
879 PlayerCommand::Seek(pos) => self.seek(pos),
880 PlayerCommand::NextTrack => self.next_track(),
881 PlayerCommand::PrevTrack => self.prev_track(),
882 PlayerCommand::AddToPlaylist(items) => {
883 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
884 self.shared_state.add_items(items);
885 self.push_undo(UndoEntry::Added { ids });
886 }
887 PlayerCommand::UpdatePaths(updates) => {
888 self.shared_state.update_paths(&updates);
889 if let Some(info) = self.shared_state.track_info()
890 && let Some((_, new_path)) = updates.iter().find(|(id, _)| *id == info.id)
891 {
892 self.shared_state.set_track_info(Some(TrackInfo {
893 path: new_path.clone(),
894 ..info
895 }));
896 }
897 }
898 PlayerCommand::InsertInPlaylist { items, after } => {
899 let ids: Vec<QueueItemId> = items.iter().map(|i| i.id).collect();
900 self.shared_state.insert_items_after(items, after);
901 self.push_undo(UndoEntry::Inserted { ids });
902 }
903 PlayerCommand::ClearPlaylist => {
904 self.stop_playback_and_clear_state();
908 let (items, cursor) = self.shared_state.snapshot_playlist();
909 self.shared_state.clear_playlist();
910 self.push_undo(UndoEntry::Replaced { items, cursor });
911 }
912 PlayerCommand::RemoveFromPlaylist(id) => {
913 let item = self.shared_state.get_item(id);
914 let after = self.shared_state.item_before(id);
915 self.remove_from_playlist(id);
916 if let Some(item) = item {
917 self.push_undo(UndoEntry::Removed {
918 items: vec![(Box::new(item), after)],
919 });
920 }
921 }
922 PlayerCommand::RemoveFromPlaylistBatch(ids) => {
923 let items_with_pos: Vec<_> = ids
925 .iter()
926 .filter_map(|&id| {
927 let item = self.shared_state.get_item(id)?;
928 let after = self.shared_state.item_before(id);
929 Some((Box::new(item), after))
930 })
931 .collect();
932 for &id in &ids {
933 self.remove_from_playlist(id);
934 }
935 if !items_with_pos.is_empty() {
936 self.push_undo(UndoEntry::Removed {
937 items: items_with_pos,
938 });
939 }
940 }
941 PlayerCommand::MoveInPlaylist { id, target, after } => {
942 let was_after = self.shared_state.item_before(id);
943 self.shared_state.move_item(id, target, after);
944 self.push_undo(UndoEntry::Moved { id, was_after });
945 }
946 PlayerCommand::MoveItemsInPlaylist { ids, target, after } => {
947 let entries = self.shared_state.items_before(&ids);
948 self.shared_state.move_items(&ids, target, after);
949 self.push_undo(UndoEntry::MovedBatch { entries });
950 }
951 PlayerCommand::TrackReady(id) => self.track_ready(id),
952 PlayerCommand::TrackStreamReady(id) => self.track_stream_ready(id),
953 PlayerCommand::Undo => self.execute_undo(),
954 PlayerCommand::Redo => self.execute_redo(),
955 PlayerCommand::BeginUndoBatch => {
956 self.batch_buffer = Some(Vec::new());
957 }
958 PlayerCommand::EndUndoBatch => {
959 if let Some(entries) = self.batch_buffer.take() {
960 if entries.len() == 1 {
961 self.undo_stack.push(entries.into_iter().next().unwrap());
963 } else if !entries.is_empty() {
964 self.undo_stack.push(UndoEntry::Batch(entries));
965 }
966 }
967 }
968 PlayerCommand::SetOutputDevice(name) => self.set_output_device(name),
969 PlayerCommand::ClearOutputDevice => self.clear_output_device(),
970 }
971 }
972
973 fn apply_entry(&mut self, entry: UndoEntry) -> Option<UndoEntry> {
975 match entry {
976 UndoEntry::Added { ids } => {
977 let items_with_pos: Vec<_> = ids
979 .iter()
980 .filter_map(|&id| {
981 let item = self.shared_state.get_item(id)?;
982 let after = self.shared_state.item_before(id);
983 Some((Box::new(item), after))
984 })
985 .collect();
986 self.shared_state.remove_items(&ids);
987 Some(UndoEntry::Removed {
988 items: items_with_pos,
989 })
990 }
991 UndoEntry::Removed { items } => {
992 let mut ids = Vec::with_capacity(items.len());
994 for (item, after) in items {
995 ids.push(item.id);
996 self.shared_state.insert_item_at(*item, after);
997 }
998 Some(UndoEntry::Added { ids })
999 }
1000 UndoEntry::Inserted { ids } => {
1001 let items_with_pos: Vec<_> = ids
1003 .iter()
1004 .filter_map(|&id| {
1005 let item = self.shared_state.get_item(id)?;
1006 let after = self.shared_state.item_before(id);
1007 Some((Box::new(item), after))
1008 })
1009 .collect();
1010 self.shared_state.remove_items(&ids);
1011 Some(UndoEntry::Removed {
1012 items: items_with_pos,
1013 })
1014 }
1015 UndoEntry::Moved { id, was_after } => {
1016 let current_after = self.shared_state.item_before(id);
1017 self.shared_state.move_item_to(id, was_after);
1018 Some(UndoEntry::Moved {
1019 id,
1020 was_after: current_after,
1021 })
1022 }
1023 UndoEntry::MovedBatch { entries } => {
1024 let ids: Vec<QueueItemId> = entries.iter().map(|(id, _)| *id).collect();
1025 let current_positions = self.shared_state.items_before(&ids);
1026 self.shared_state.move_items_to(&entries);
1027 Some(UndoEntry::MovedBatch {
1028 entries: current_positions,
1029 })
1030 }
1031 UndoEntry::Replaced { items, cursor } => {
1032 let (current_items, current_cursor) = self.shared_state.snapshot_playlist();
1033 self.shared_state.restore_playlist(items, cursor);
1034 Some(UndoEntry::Replaced {
1035 items: current_items,
1036 cursor: current_cursor,
1037 })
1038 }
1039 UndoEntry::Batch(entries) => {
1040 let mut inverses = Vec::with_capacity(entries.len());
1042 for entry in entries.into_iter().rev() {
1043 if let Some(inverse) = self.apply_entry(entry) {
1044 inverses.push(inverse);
1045 }
1046 }
1047 inverses.reverse();
1048 Some(UndoEntry::Batch(inverses))
1049 }
1050 }
1051 }
1052
1053 fn execute_undo(&mut self) {
1055 let Some(entry) = self.undo_stack.pop_undo() else {
1056 return;
1057 };
1058 if let Some(inverse) = self.apply_entry(entry) {
1059 self.undo_stack.push_redo(inverse);
1060 }
1061 }
1062
1063 fn execute_redo(&mut self) {
1065 let Some(entry) = self.undo_stack.pop_redo() else {
1066 return;
1067 };
1068 if let Some(inverse) = self.apply_entry(entry) {
1069 self.undo_stack.push_undo_keep_redo(inverse);
1070 }
1071 }
1072
1073 pub fn run(&mut self) {
1075 use std::time::Duration;
1076
1077 let rx = self.commands.rx.clone();
1078 loop {
1079 match rx.recv_timeout(Duration::from_millis(50)) {
1081 Ok(cmd) => self.process_command(cmd),
1082 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
1083 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
1084 }
1085 self.update_playback_state();
1086 }
1087 self.stop();
1088 }
1089
1090 pub fn spawn() -> (
1093 Arc<SharedPlayerState>,
1094 Arc<PlaybackTimeline>,
1095 Arc<VizSnapshot>,
1096 crossbeam_channel::Sender<PlayerCommand>,
1097 ) {
1098 let mut player = Self::new();
1099 let state = player.shared_state();
1100 let timeline = player.timeline();
1101 let viz_snapshot = player.viz_snapshot();
1102 let tx = player.command_sender();
1103
1104 thread::Builder::new()
1105 .name("koan-player".into())
1106 .spawn(move || player.run())
1107 .expect("failed to spawn player thread");
1108
1109 (state, timeline, viz_snapshot, tx)
1110 }
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116 use state::PlaylistItem;
1117 use std::path::PathBuf;
1118
1119 fn make_item(title: &str) -> PlaylistItem {
1120 PlaylistItem {
1121 id: QueueItemId::new(),
1122 path: PathBuf::from(format!("/music/{title}.flac")),
1123 title: title.to_string(),
1124 artist: String::new(),
1125 album_artist: String::new(),
1126 album: String::new(),
1127 year: None,
1128 codec: None,
1129 track_number: None,
1130 disc: None,
1131 duration_ms: None,
1132 load_state: LoadState::Ready,
1133 }
1134 }
1135
1136 fn playlist_ids(player: &Player) -> Vec<QueueItemId> {
1137 let (items, _) = player.shared_state.snapshot_playlist();
1138 items.iter().map(|i| i.id).collect()
1139 }
1140
1141 fn playlist_titles(player: &Player) -> Vec<String> {
1142 let (items, _) = player.shared_state.snapshot_playlist();
1143 items.iter().map(|i| i.title.clone()).collect()
1144 }
1145
1146 #[test]
1149 fn undo_add_removes_items() {
1150 let mut player = Player::new();
1151 let items = vec![make_item("A"), make_item("B")];
1152 let ids: Vec<_> = items.iter().map(|i| i.id).collect();
1153
1154 player.process_command(PlayerCommand::AddToPlaylist(items));
1155 assert_eq!(playlist_ids(&player), ids);
1156 assert!(player.undo_stack().can_undo());
1157
1158 player.process_command(PlayerCommand::Undo);
1159 assert!(playlist_ids(&player).is_empty());
1160 assert!(player.undo_stack().can_redo());
1161 }
1162
1163 #[test]
1164 fn redo_add_restores_items() {
1165 let mut player = Player::new();
1166 let items = vec![make_item("A"), make_item("B")];
1167
1168 player.process_command(PlayerCommand::AddToPlaylist(items));
1169 player.process_command(PlayerCommand::Undo);
1170 assert!(playlist_ids(&player).is_empty());
1171
1172 player.process_command(PlayerCommand::Redo);
1173 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1174 }
1175
1176 #[test]
1179 fn undo_remove_restores_item_at_position() {
1180 let mut player = Player::new();
1181 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1182 let b_id = items[1].id;
1183
1184 player.process_command(PlayerCommand::AddToPlaylist(items));
1185 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1186 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1187
1188 player.process_command(PlayerCommand::Undo);
1189 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1190 }
1191
1192 #[test]
1193 fn undo_remove_first_item() {
1194 let mut player = Player::new();
1195 let items = vec![make_item("A"), make_item("B")];
1196 let a_id = items[0].id;
1197
1198 player.process_command(PlayerCommand::AddToPlaylist(items));
1199 player.process_command(PlayerCommand::RemoveFromPlaylist(a_id));
1200 assert_eq!(playlist_titles(&player), vec!["B"]);
1201
1202 player.process_command(PlayerCommand::Undo);
1203 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1204 }
1205
1206 #[test]
1207 fn undo_batch_remove_restores_all() {
1208 let mut player = Player::new();
1209 let items = vec![
1210 make_item("A"),
1211 make_item("B"),
1212 make_item("C"),
1213 make_item("D"),
1214 ];
1215 let b_id = items[1].id;
1216 let c_id = items[2].id;
1217
1218 player.process_command(PlayerCommand::AddToPlaylist(items));
1219 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![b_id, c_id]));
1220 assert_eq!(playlist_titles(&player), vec!["A", "D"]);
1221
1222 player.process_command(PlayerCommand::Undo);
1224 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1225 }
1226
1227 #[test]
1228 fn redo_batch_remove() {
1229 let mut player = Player::new();
1230 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1231 let a_id = items[0].id;
1232 let b_id = items[1].id;
1233
1234 player.process_command(PlayerCommand::AddToPlaylist(items));
1235 player.process_command(PlayerCommand::RemoveFromPlaylistBatch(vec![a_id, b_id]));
1236 player.process_command(PlayerCommand::Undo);
1237 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1238
1239 player.process_command(PlayerCommand::Redo);
1240 assert_eq!(playlist_titles(&player), vec!["C"]);
1241 }
1242
1243 #[test]
1244 fn redo_remove() {
1245 let mut player = Player::new();
1246 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1247 let b_id = items[1].id;
1248
1249 player.process_command(PlayerCommand::AddToPlaylist(items));
1250 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1251 player.process_command(PlayerCommand::Undo);
1252 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1253
1254 player.process_command(PlayerCommand::Redo);
1255 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1256 }
1257
1258 #[test]
1261 fn undo_insert_removes_inserted_items() {
1262 let mut player = Player::new();
1263 let items = vec![make_item("A"), make_item("C")];
1264 let a_id = items[0].id;
1265
1266 player.process_command(PlayerCommand::AddToPlaylist(items));
1267
1268 let inserted = vec![make_item("B")];
1269 player.process_command(PlayerCommand::InsertInPlaylist {
1270 items: inserted,
1271 after: a_id,
1272 });
1273 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1274
1275 player.process_command(PlayerCommand::Undo);
1276 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1277 }
1278
1279 #[test]
1282 fn undo_move_restores_position() {
1283 let mut player = Player::new();
1284 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1285 let a_id = items[0].id;
1286 let c_id = items[2].id;
1287
1288 player.process_command(PlayerCommand::AddToPlaylist(items));
1289
1290 player.process_command(PlayerCommand::MoveInPlaylist {
1292 id: a_id,
1293 target: c_id,
1294 after: true,
1295 });
1296 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1297
1298 player.process_command(PlayerCommand::Undo);
1299 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1300 }
1301
1302 #[test]
1303 fn redo_move() {
1304 let mut player = Player::new();
1305 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1306 let a_id = items[0].id;
1307 let c_id = items[2].id;
1308
1309 player.process_command(PlayerCommand::AddToPlaylist(items));
1310 player.process_command(PlayerCommand::MoveInPlaylist {
1311 id: a_id,
1312 target: c_id,
1313 after: true,
1314 });
1315 player.process_command(PlayerCommand::Undo);
1316 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1317
1318 player.process_command(PlayerCommand::Redo);
1319 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1320 }
1321
1322 #[test]
1325 fn undo_batch_move() {
1326 let mut player = Player::new();
1327 let items = vec![
1328 make_item("A"),
1329 make_item("B"),
1330 make_item("C"),
1331 make_item("D"),
1332 ];
1333 let a_id = items[0].id;
1334 let b_id = items[1].id;
1335 let d_id = items[3].id;
1336
1337 player.process_command(PlayerCommand::AddToPlaylist(items));
1338
1339 player.process_command(PlayerCommand::MoveItemsInPlaylist {
1341 ids: vec![a_id, b_id],
1342 target: d_id,
1343 after: true,
1344 });
1345 assert_eq!(playlist_titles(&player), vec!["C", "D", "A", "B"]);
1346
1347 player.process_command(PlayerCommand::Undo);
1348 assert_eq!(playlist_titles(&player), vec!["A", "B", "C", "D"]);
1349 }
1350
1351 #[test]
1354 fn undo_clear_restores_playlist() {
1355 let mut player = Player::new();
1356 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1357
1358 player.process_command(PlayerCommand::AddToPlaylist(items));
1359 player.process_command(PlayerCommand::ClearPlaylist);
1360 assert!(playlist_ids(&player).is_empty());
1361
1362 player.process_command(PlayerCommand::Undo);
1363 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1364 }
1365
1366 #[test]
1367 fn redo_clear() {
1368 let mut player = Player::new();
1369 let items = vec![make_item("A"), make_item("B")];
1370
1371 player.process_command(PlayerCommand::AddToPlaylist(items));
1372 player.process_command(PlayerCommand::ClearPlaylist);
1373 player.process_command(PlayerCommand::Undo);
1374 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1375
1376 player.process_command(PlayerCommand::Redo);
1377 assert!(playlist_ids(&player).is_empty());
1378 }
1379
1380 #[test]
1383 fn multiple_undos_in_sequence() {
1384 let mut player = Player::new();
1385
1386 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("A")]));
1387 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1388 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("C")]));
1389 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1390
1391 player.process_command(PlayerCommand::Undo);
1392 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1393
1394 player.process_command(PlayerCommand::Undo);
1395 assert_eq!(playlist_titles(&player), vec!["A"]);
1396
1397 player.process_command(PlayerCommand::Undo);
1398 assert!(playlist_ids(&player).is_empty());
1399 }
1400
1401 #[test]
1402 fn undo_redo_undo_cycle() {
1403 let mut player = Player::new();
1404 let items = vec![make_item("A"), make_item("B")];
1405
1406 player.process_command(PlayerCommand::AddToPlaylist(items));
1407 player.process_command(PlayerCommand::Undo);
1408 assert!(playlist_ids(&player).is_empty());
1409
1410 player.process_command(PlayerCommand::Redo);
1411 assert_eq!(playlist_titles(&player), vec!["A", "B"]);
1412
1413 player.process_command(PlayerCommand::Undo);
1414 assert!(playlist_ids(&player).is_empty());
1415 }
1416
1417 #[test]
1418 fn new_action_clears_redo_stack() {
1419 let mut player = Player::new();
1420 let items = vec![make_item("A")];
1421
1422 player.process_command(PlayerCommand::AddToPlaylist(items));
1423 player.process_command(PlayerCommand::Undo);
1424 assert!(player.undo_stack().can_redo());
1425
1426 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("B")]));
1428 assert!(!player.undo_stack().can_redo());
1429 }
1430
1431 #[test]
1432 fn undo_on_empty_stack_is_noop() {
1433 let mut player = Player::new();
1434 player.process_command(PlayerCommand::Undo);
1435 assert!(playlist_ids(&player).is_empty());
1436 }
1437
1438 #[test]
1439 fn redo_on_empty_stack_is_noop() {
1440 let mut player = Player::new();
1441 player.process_command(PlayerCommand::Redo);
1442 assert!(playlist_ids(&player).is_empty());
1443 }
1444
1445 #[test]
1448 fn playback_commands_not_undoable() {
1449 let mut player = Player::new();
1450 player.process_command(PlayerCommand::Pause);
1451 player.process_command(PlayerCommand::Resume);
1452 player.process_command(PlayerCommand::NextTrack);
1453 player.process_command(PlayerCommand::PrevTrack);
1454 assert!(!player.undo_stack().can_undo());
1455 }
1456
1457 #[test]
1458 fn update_paths_not_undoable() {
1459 let mut player = Player::new();
1460 let items = vec![make_item("A")];
1461 let id = items[0].id;
1462 player.process_command(PlayerCommand::AddToPlaylist(items));
1463
1464 let undo_count = player.undo_stack().undo_len();
1465 player.process_command(PlayerCommand::UpdatePaths(vec![(
1466 id,
1467 PathBuf::from("/new/path.flac"),
1468 )]));
1469 assert_eq!(player.undo_stack().undo_len(), undo_count);
1470 }
1471
1472 #[test]
1475 fn add_remove_undo_undo_produces_original() {
1476 let mut player = Player::new();
1477 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1478 let b_id = items[1].id;
1479 let original_titles = vec!["A", "B", "C"];
1480
1481 player.process_command(PlayerCommand::AddToPlaylist(items));
1482 player.process_command(PlayerCommand::RemoveFromPlaylist(b_id));
1483 assert_eq!(playlist_titles(&player), vec!["A", "C"]);
1484
1485 player.process_command(PlayerCommand::Undo);
1487 assert_eq!(playlist_titles(&player), original_titles);
1488
1489 player.process_command(PlayerCommand::Undo);
1491 assert!(playlist_ids(&player).is_empty());
1492 }
1493
1494 #[test]
1495 fn interleaved_adds_and_moves_undo() {
1496 let mut player = Player::new();
1497 let items = vec![make_item("A"), make_item("B"), make_item("C")];
1498 let a_id = items[0].id;
1499 let c_id = items[2].id;
1500
1501 player.process_command(PlayerCommand::AddToPlaylist(items));
1502
1503 player.process_command(PlayerCommand::MoveInPlaylist {
1505 id: a_id,
1506 target: c_id,
1507 after: true,
1508 });
1509 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1510
1511 player.process_command(PlayerCommand::AddToPlaylist(vec![make_item("D")]));
1513 assert_eq!(playlist_titles(&player), vec!["B", "C", "A", "D"]);
1514
1515 player.process_command(PlayerCommand::Undo);
1517 assert_eq!(playlist_titles(&player), vec!["B", "C", "A"]);
1518
1519 player.process_command(PlayerCommand::Undo);
1521 assert_eq!(playlist_titles(&player), vec!["A", "B", "C"]);
1522 }
1523}