1use std::path::{ Path, PathBuf };
8use std::sync::{ Arc, Mutex };
9use std::time::{ Duration, Instant };
10
11use tokio::sync::{ broadcast, mpsc };
12
13use oxidio_core::player::PlaybackState;
14use oxidio_core::library::LibraryScanner;
15use oxidio_core::{ Player, Playlist, RepeatMode };
16
17use oxidio_protocol::{
18 AppCommand, BrowserEntry, BrowserSnapshot, PlaybackStateValue,
19 RepeatModeValue, SettingsSnapshot, StateSnapshot, StateUpdate,
20 TrackEntry, TrackInfo,
21};
22
23
24#[derive( Debug, Clone, serde::Serialize, serde::Deserialize )]
26#[serde( default )]
27pub struct ProcessorSettings {
28 pub discord_enabled: bool,
29 pub smtc_enabled: bool,
30 pub web_enabled: bool,
31 pub web_port: u16,
32 pub web_bind: String,
33}
34
35
36impl Default for ProcessorSettings {
37 fn default() -> Self {
38 Self {
39 discord_enabled: true,
40 smtc_enabled: true,
41 web_enabled: false,
42 web_port: 8384,
43 web_bind: "127.0.0.1".to_string(),
44 }
45 }
46}
47
48
49impl ProcessorSettings {
50 fn settings_path() -> Option<PathBuf> {
52 dirs::config_dir().map( |p| p.join( "oxidio" ).join( "settings.json" ) )
53 }
54
55
56 pub fn load() -> Self {
58 let path = match Self::settings_path() {
59 Some( p ) => p,
60 None => return Self::default(),
61 };
62
63 if !path.exists() {
64 return Self::default();
65 }
66
67 match std::fs::read_to_string( &path ) {
68 Ok( contents ) => {
69 serde_json::from_str( &contents ).unwrap_or_default()
70 }
71 Err( e ) => {
72 tracing::warn!( "Failed to read settings: {}", e );
73 Self::default()
74 }
75 }
76 }
77
78
79 pub fn save( &self ) {
81 let path = match Self::settings_path() {
82 Some( p ) => p,
83 None => return,
84 };
85
86 if let Some( parent ) = path.parent() {
87 if !parent.exists() {
88 if let Err( e ) = std::fs::create_dir_all( parent ) {
89 tracing::warn!( "Failed to create settings directory: {}", e );
90 return;
91 }
92 }
93 }
94
95 match serde_json::to_string_pretty( self ) {
96 Ok( json ) => {
97 if let Err( e ) = std::fs::write( &path, json ) {
98 tracing::warn!( "Failed to save settings: {}", e );
99 }
100 }
101 Err( e ) => {
102 tracing::warn!( "Failed to serialize settings: {}", e );
103 }
104 }
105 }
106
107
108 pub fn to_snapshot( &self ) -> SettingsSnapshot {
110 SettingsSnapshot {
111 discord_enabled: self.discord_enabled,
112 smtc_enabled: self.smtc_enabled,
113 web_enabled: self.web_enabled,
114 web_port: self.web_port,
115 web_bind: self.web_bind.clone(),
116 }
117 }
118}
119
120
121struct BrowserState {
123 current_dir: PathBuf,
124 entries: Vec<BrowserEntryInternal>,
125 selected: usize,
126}
127
128
129#[derive( Debug, Clone )]
130struct BrowserEntryInternal {
131 path: PathBuf,
132 name: String,
133 is_dir: bool,
134 is_audio: bool,
135}
136
137
138const AUDIO_EXTENSIONS: &[&str] = &[
140 "mp3", "flac", "ogg", "wav", "m4a", "aac", "opus", "wma", "aiff", "alac",
141];
142
143
144impl BrowserState {
145 fn new( path: PathBuf ) -> Self {
146 let mut state = Self {
147 current_dir: path,
148 entries: Vec::new(),
149 selected: 0,
150 };
151 state.refresh();
152 state
153 }
154
155
156 fn refresh( &mut self ) {
157 self.entries.clear();
158 self.selected = 0;
159
160 if let Some( parent ) = self.current_dir.parent() {
161 self.entries.push( BrowserEntryInternal {
162 path: parent.to_path_buf(),
163 name: "..".to_string(),
164 is_dir: true,
165 is_audio: false,
166 });
167 }
168
169 let mut dirs = Vec::new();
170 let mut files = Vec::new();
171
172 if let Ok( read_dir ) = std::fs::read_dir( &self.current_dir ) {
173 for entry in read_dir.flatten() {
174 let path = entry.path();
175 let name = entry.file_name().to_string_lossy().to_string();
176
177 if name.starts_with( '.' ) {
178 continue;
179 }
180
181 let is_dir = path.is_dir();
182 let is_audio = !is_dir && Self::is_audio_file( &path );
183
184 let browser_entry = BrowserEntryInternal { path, name, is_dir, is_audio };
185
186 if is_dir {
187 dirs.push( browser_entry );
188 } else if is_audio {
189 files.push( browser_entry );
190 }
191 }
192 }
193
194 dirs.sort_by( |a, b| a.name.to_lowercase().cmp( &b.name.to_lowercase() ) );
195 files.sort_by( |a, b| a.name.to_lowercase().cmp( &b.name.to_lowercase() ) );
196
197 self.entries.extend( dirs );
198 self.entries.extend( files );
199 }
200
201
202 fn navigate_to( &mut self, path: &std::path::Path ) {
203 let canonical = if path.is_absolute() {
204 path.to_path_buf()
205 } else {
206 self.current_dir.join( path )
207 };
208
209 if canonical.is_dir() {
210 self.current_dir = canonical;
211 self.refresh();
212 }
213 }
214
215
216 fn to_snapshot( &self ) -> BrowserSnapshot {
217 BrowserSnapshot {
218 current_dir: self.current_dir.to_string_lossy().to_string(),
219 entries: self.entries.iter().map( |e| BrowserEntry {
220 name: e.name.clone(),
221 path: e.path.to_string_lossy().to_string(),
222 is_dir: e.is_dir,
223 is_audio: e.is_audio,
224 }).collect(),
225 selected_index: self.selected,
226 }
227 }
228
229
230 fn is_audio_file( path: &std::path::Path ) -> bool {
231 path.extension()
232 .and_then( |e| e.to_str() )
233 .map( |e| AUDIO_EXTENSIONS.contains( &e.to_lowercase().as_str() ) )
234 .unwrap_or( false )
235 }
236}
237
238
239pub struct CommandProcessor {
244 player: Arc<Player>,
245 settings: ProcessorSettings,
246 browser: BrowserState,
247
248 command_rx: mpsc::Receiver<AppCommand>,
250 broadcast_tx: broadcast::Sender<StateUpdate>,
251
252 last_playback_state: PlaybackState,
254 last_track_path: Option<PathBuf>,
255 last_volume: f32,
256 last_shuffle: bool,
257 last_repeat: RepeatMode,
258 last_position_broadcast: Instant,
259 last_vis_broadcast: Instant,
260
261 view_mode: String,
263
264 cover_art_path: Arc<Mutex<Option<PathBuf>>>,
266}
267
268
269impl CommandProcessor {
270 pub fn new(
279 player: Arc<Player>,
280 settings: ProcessorSettings,
281 start_path: PathBuf,
282 browse: bool,
283 command_rx: mpsc::Receiver<AppCommand>,
284 broadcast_tx: broadcast::Sender<StateUpdate>,
285 ) -> Self {
286 let browser = BrowserState::new( start_path );
287
288 let volume = player.volume();
289 let shuffle = player.playlist().read().unwrap().shuffle();
290 let repeat = player.playlist().read().unwrap().repeat();
291
292 let view_mode = if browse {
293 "browser".to_string()
294 } else {
295 "playlist".to_string()
296 };
297
298 Self {
299 player,
300 settings,
301 browser,
302 command_rx,
303 broadcast_tx,
304 last_playback_state: PlaybackState::Stopped,
305 last_track_path: None,
306 last_volume: volume,
307 last_shuffle: shuffle,
308 last_repeat: repeat,
309 last_position_broadcast: Instant::now(),
310 last_vis_broadcast: Instant::now(),
311 view_mode,
312 cover_art_path: Arc::new( Mutex::new( None ) ),
313 }
314 }
315
316
317 pub fn cover_art_path( &self ) -> Arc<Mutex<Option<PathBuf>>> {
321 Arc::clone( &self.cover_art_path )
322 }
323
324
325 pub async fn run( &mut self ) {
329 let mut tick_interval = tokio::time::interval( Duration::from_millis( 100 ) );
330
331 loop {
332 tokio::select! {
333 Some( cmd ) = self.command_rx.recv() => {
334 if matches!( cmd, AppCommand::Quit ) {
335 self.save_session();
336 break;
337 }
338 self.handle_command( cmd );
339 }
340
341 _ = tick_interval.tick() => {
342 self.tick();
343 }
344 }
345 }
346 }
347
348
349 fn tick( &mut self ) {
351 if self.player.track_ended() {
353 match self.player.play_next() {
354 Ok( true ) => {
355 tracing::info!( "Auto-advanced to next track" );
356 }
357 Ok( false ) => {}
358 Err( e ) => {
359 tracing::warn!( "Auto-advance error: {}", e );
360 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
361 message: format!( "Auto-advance error: {}", e ),
362 });
363 }
364 }
365 }
366
367 self.broadcast_changes();
368 }
369
370
371 fn broadcast_changes( &mut self ) {
373 let current_state = self.player.state();
374 let current_track = self.player.current_track();
375 let current_volume = self.player.volume();
376
377 let playlist_arc = self.player.playlist();
378 let playlist = playlist_arc.read().unwrap();
379 let current_shuffle = playlist.shuffle();
380 let current_repeat = playlist.repeat();
381 drop( playlist );
382
383 if current_state != self.last_playback_state {
385 let _ = self.broadcast_tx.send( StateUpdate::PlaybackStateChanged {
386 state: playback_state_to_value( current_state ),
387 });
388 self.last_playback_state = current_state;
389 }
390
391 if current_track != self.last_track_path {
393 let track_info = current_track.as_ref().map( |path| {
394 self.build_track_info( path )
395 });
396 let duration = self.player.duration().map( |d| d.as_secs_f64() );
397
398 let _ = self.broadcast_tx.send( StateUpdate::TrackChanged {
399 track: track_info,
400 duration_secs: duration,
401 });
402 self.last_track_path = current_track;
403 }
404
405 if ( current_volume - self.last_volume ).abs() > 0.001 {
407 let _ = self.broadcast_tx.send( StateUpdate::VolumeChanged {
408 level: current_volume,
409 });
410 self.last_volume = current_volume;
411 }
412
413 if current_shuffle != self.last_shuffle || current_repeat != self.last_repeat {
415 let _ = self.broadcast_tx.send( StateUpdate::ModeChanged {
416 shuffle: current_shuffle,
417 repeat_mode: repeat_mode_to_value( current_repeat ),
418 });
419 self.last_shuffle = current_shuffle;
420 self.last_repeat = current_repeat;
421 }
422
423 let now = Instant::now();
425 if current_state == PlaybackState::Playing
426 && now.duration_since( self.last_position_broadcast ) >= Duration::from_millis( 500 )
427 {
428 let position = self.player.position();
429 let _ = self.broadcast_tx.send( StateUpdate::Position {
430 secs: position.as_secs_f64(),
431 });
432 self.last_position_broadcast = now;
433 }
434
435 if current_state == PlaybackState::Playing
437 && now.duration_since( self.last_vis_broadcast ) >= Duration::from_millis( 100 )
438 {
439 if let Some( vis ) = self.player.vis_data() {
440 let _ = self.broadcast_tx.send( StateUpdate::VisualizerData {
441 bars: vis.to_vec(),
442 });
443 self.last_vis_broadcast = now;
444 }
445 }
446 }
447
448
449 fn handle_command( &mut self, cmd: AppCommand ) {
451 match cmd {
452 AppCommand::Play => {
454 self.play_selected();
455 }
456 AppCommand::Pause => {
457 let _ = self.player.pause();
458 }
459 AppCommand::Resume => {
460 let _ = self.player.resume();
461 }
462 AppCommand::TogglePlayback => {
463 match self.player.state() {
464 PlaybackState::Playing => { let _ = self.player.pause(); }
465 PlaybackState::Paused => { let _ = self.player.resume(); }
466 PlaybackState::Stopped => { self.play_selected(); }
467 }
468 }
469 AppCommand::Stop => {
470 let _ = self.player.stop();
471 }
472 AppCommand::Next => {
473 self.play_next();
474 }
475 AppCommand::Previous => {
476 self.play_previous();
477 }
478 AppCommand::Seek { position_secs } => {
479 let _ = self.player.seek( Duration::from_secs_f64( position_secs ) );
480 }
481 AppCommand::SetVolume { level } => {
482 self.player.set_volume( level.clamp( 0.0, 1.5 ) );
483 }
484 AppCommand::VolumeUp => {
485 let vol = ( self.player.volume() + 0.05 ).min( 1.5 );
486 self.player.set_volume( vol );
487 }
488 AppCommand::VolumeDown => {
489 let vol = ( self.player.volume() - 0.05 ).max( 0.0 );
490 self.player.set_volume( vol );
491 }
492
493 AppCommand::PlayTrack { index } => {
495 let track = {
496 let playlist_arc = self.player.playlist();
497 let mut playlist = playlist_arc.write().unwrap();
498 playlist.jump_to( index ).cloned()
499 };
500 if let Some( path ) = track {
501 let _ = self.player.play( path );
502 }
503 self.broadcast_playlist();
504 }
505 AppCommand::AddPath { path } => {
506 let path = PathBuf::from( &path );
507 if path.is_dir() {
508 let mut scanner = LibraryScanner::new();
509 scanner.add_root( path );
510 if let Ok( tracks ) = scanner.scan() {
511 let playlist_arc = self.player.playlist();
512 let mut playlist = playlist_arc.write().unwrap();
513 playlist.add_many( tracks.into_iter().map( |t| t.path ) );
514 }
515 } else {
516 let playlist_arc = self.player.playlist();
517 let mut playlist = playlist_arc.write().unwrap();
518 playlist.add( path );
519 }
520 self.broadcast_playlist();
521 }
522 AppCommand::RemoveTrack { index } => {
523 let playlist_arc = self.player.playlist();
524 let mut playlist = playlist_arc.write().unwrap();
525 playlist.remove( index );
526 drop( playlist );
527 self.broadcast_playlist();
528 }
529 AppCommand::ClearPlaylist => {
530 let _ = self.player.stop();
531 let playlist_arc = self.player.playlist();
532 let mut playlist = playlist_arc.write().unwrap();
533 playlist.clear();
534 drop( playlist );
535 self.broadcast_playlist();
536 }
537 AppCommand::ToggleShuffle => {
538 let playlist_arc = self.player.playlist();
539 let mut playlist = playlist_arc.write().unwrap();
540 let new_val = !playlist.shuffle();
541 playlist.set_shuffle( new_val );
542 }
543 AppCommand::SetRepeat { mode } => {
544 let repeat = match mode {
545 RepeatModeValue::Off => RepeatMode::Off,
546 RepeatModeValue::One => RepeatMode::One,
547 RepeatModeValue::All => RepeatMode::All,
548 };
549 let playlist_arc = self.player.playlist();
550 let mut playlist = playlist_arc.write().unwrap();
551 playlist.set_repeat( repeat );
552 }
553 AppCommand::CycleRepeat => {
554 let playlist_arc = self.player.playlist();
555 let mut playlist = playlist_arc.write().unwrap();
556 let next = match playlist.repeat() {
557 RepeatMode::Off => RepeatMode::One,
558 RepeatMode::One => RepeatMode::All,
559 RepeatMode::All => RepeatMode::Off,
560 };
561 playlist.set_repeat( next );
562 }
563 AppCommand::MoveTrack { from, to } => {
564 let playlist_arc = self.player.playlist();
565 let mut playlist = playlist_arc.write().unwrap();
566 playlist.move_track( from, to );
567 drop( playlist );
568 self.broadcast_playlist();
569 }
570 AppCommand::Dedup => {
571 let playlist_arc = self.player.playlist();
572 let mut playlist = playlist_arc.write().unwrap();
573 let removed = playlist.dedup();
574 drop( playlist );
575 if removed > 0 {
576 self.broadcast_playlist();
577 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
578 message: format!( "Removed {} duplicate(s)", removed ),
579 });
580 }
581 }
582 AppCommand::SavePlaylist { name } => {
583 if let Some( dir ) = Playlist::ensure_playlist_dir() {
584 let path = dir.join( format!( "{}.m3u", name ) );
585 let playlist_arc = self.player.playlist();
586 let playlist = playlist_arc.read().unwrap();
587 match playlist.save( &path ) {
588 Ok(()) => {
589 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
590 message: format!( "Saved playlist: {}", name ),
591 });
592 }
593 Err( e ) => {
594 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
595 message: format!( "Save error: {}", e ),
596 });
597 }
598 }
599 }
600 }
601 AppCommand::LoadPlaylist { name } => {
602 if let Some( dir ) = Playlist::playlist_dir() {
603 let path = dir.join( format!( "{}.m3u", name ) );
604 match Playlist::load( &path ) {
605 Ok( loaded ) => {
606 let _ = self.player.stop();
607 let playlist_arc = self.player.playlist();
608 let mut playlist = playlist_arc.write().unwrap();
609 *playlist = loaded;
610 drop( playlist );
611 self.broadcast_playlist();
612 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
613 message: format!( "Loaded playlist: {}", name ),
614 });
615 }
616 Err( e ) => {
617 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
618 message: format!( "Load error: {}", e ),
619 });
620 }
621 }
622 }
623 }
624
625 AppCommand::BrowseTo { path } => {
627 self.browser.navigate_to( &PathBuf::from( &path ) );
628 let _ = self.broadcast_tx.send( StateUpdate::BrowserChanged {
629 browser: self.browser.to_snapshot(),
630 });
631 }
632 AppCommand::BrowseUp => {
633 if let Some( parent ) = self.browser.current_dir.parent() {
634 let parent = parent.to_path_buf();
635 self.browser.navigate_to( &parent );
636 let _ = self.broadcast_tx.send( StateUpdate::BrowserChanged {
637 browser: self.browser.to_snapshot(),
638 });
639 }
640 }
641 AppCommand::BrowseHome => {
642 if let Some( home ) = dirs::home_dir() {
643 self.browser.navigate_to( &home );
644 let _ = self.broadcast_tx.send( StateUpdate::BrowserChanged {
645 browser: self.browser.to_snapshot(),
646 });
647 }
648 }
649 AppCommand::BrowseOpen { index } => {
650 if let Some( entry ) = self.browser.entries.get( index ).cloned() {
651 if entry.is_dir {
652 self.browser.navigate_to( &entry.path );
653 let _ = self.broadcast_tx.send( StateUpdate::BrowserChanged {
654 browser: self.browser.to_snapshot(),
655 });
656 }
657 }
658 }
659 AppCommand::BrowseAddToPlaylist { index } => {
660 if let Some( entry ) = self.browser.entries.get( index ).cloned() {
661 let path_str = entry.path.to_string_lossy().to_string();
662 self.handle_command( AppCommand::AddPath { path: path_str } );
663 }
664 }
665
666 AppCommand::ListPlaylists => {
668 if let Some( dir ) = Playlist::playlist_dir() {
669 let mut names = Vec::new();
670 if let Ok( entries ) = std::fs::read_dir( &dir ) {
671 for entry in entries.flatten() {
672 let path = entry.path();
673 if path.extension().and_then( |e| e.to_str() ) == Some( "m3u" ) {
674 if let Some( name ) = path.file_stem().and_then( |s| s.to_str() ) {
675 if name != "_last" {
676 names.push( name.to_string() );
677 }
678 }
679 }
680 }
681 }
682 names.sort();
683 let msg = if names.is_empty() {
684 "No saved playlists".to_string()
685 } else {
686 format!( "Playlists: {}", names.join( ", " ) )
687 };
688 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage { message: msg } );
689 }
690 }
691 AppCommand::DeletePlaylist { name } => {
692 if let Some( dir ) = Playlist::playlist_dir() {
693 let path = dir.join( format!( "{}.m3u", name ) );
694 if path.exists() {
695 match std::fs::remove_file( &path ) {
696 Ok(()) => {
697 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
698 message: format!( "Deleted playlist: {}", name ),
699 });
700 }
701 Err( e ) => {
702 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
703 message: format!( "Delete error: {}", e ),
704 });
705 }
706 }
707 } else {
708 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
709 message: format!( "Playlist not found: {}", name ),
710 });
711 }
712 }
713 }
714
715 AppCommand::ToggleSetting { key } => {
717 match key.as_str() {
718 "discord_enabled" => {
719 self.settings.discord_enabled = !self.settings.discord_enabled;
720 }
721 "smtc_enabled" => {
722 self.settings.smtc_enabled = !self.settings.smtc_enabled;
723 }
724 "web_enabled" => {
725 self.settings.web_enabled = !self.settings.web_enabled;
726 }
727 _ => {
728 tracing::warn!( "Unknown setting key: {}", key );
729 return;
730 }
731 }
732 self.settings.save();
733 let _ = self.broadcast_tx.send( StateUpdate::SettingsChanged {
734 settings: self.settings.to_snapshot(),
735 });
736 }
737
738 AppCommand::SetView { view } => {
740 self.view_mode = view;
741 }
742
743 AppCommand::RequestFullState => {
745 let snapshot = self.build_full_snapshot();
746 let _ = self.broadcast_tx.send( StateUpdate::FullState { state: snapshot } );
747 }
748
749 AppCommand::Quit => {}
751 }
752 }
753
754
755 fn play_selected( &self ) {
757 let track = {
758 let playlist_arc = self.player.playlist();
759 let playlist = playlist_arc.read().unwrap();
760 playlist.current().cloned()
761 .or_else( || {
762 if !playlist.is_empty() {
763 Some( playlist.tracks()[ 0 ].clone() )
764 } else {
765 None
766 }
767 })
768 };
769
770 if let Some( path ) = track {
771 {
772 let playlist_arc = self.player.playlist();
773 let mut playlist = playlist_arc.write().unwrap();
774 if playlist.current_index().is_none() && !playlist.is_empty() {
775 playlist.jump_to( 0 );
776 }
777 }
778 let _ = self.player.play( path );
779 }
780 }
781
782
783 fn play_next( &self ) {
785 match self.player.play_next() {
786 Ok( true ) => {
787 self.broadcast_playlist();
788 }
789 Ok( false ) => {
790 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
791 message: "End of playlist".to_string(),
792 });
793 }
794 Err( e ) => {
795 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
796 message: format!( "Next track error: {}", e ),
797 });
798 }
799 }
800 }
801
802
803 fn play_previous( &self ) {
805 match self.player.play_previous() {
806 Ok( true ) => {
807 self.broadcast_playlist();
808 }
809 Ok( false ) => {}
810 Err( e ) => {
811 let _ = self.broadcast_tx.send( StateUpdate::StatusMessage {
812 message: format!( "Previous track error: {}", e ),
813 });
814 }
815 }
816 }
817
818
819 fn broadcast_playlist( &self ) {
821 let playlist_arc = self.player.playlist();
822 let playlist = playlist_arc.read().unwrap();
823 let entries = build_track_entries( &playlist );
824 let index = playlist.current_index();
825 drop( playlist );
826
827 let _ = self.broadcast_tx.send( StateUpdate::PlaylistChanged {
828 playlist: entries,
829 index,
830 });
831 }
832
833
834 fn build_full_snapshot( &self ) -> StateSnapshot {
836 let playlist_arc = self.player.playlist();
837 let playlist = playlist_arc.read().unwrap();
838 let entries = build_track_entries( &playlist );
839 let playlist_index = playlist.current_index();
840 let shuffle = playlist.shuffle();
841 let repeat = playlist.repeat();
842 drop( playlist );
843
844 let current_track = self.player.current_track().map( |path| {
845 self.build_track_info( &path )
846 });
847
848 StateSnapshot {
849 playback_state: playback_state_to_value( self.player.state() ),
850 current_track,
851 position_secs: self.player.position().as_secs_f64(),
852 duration_secs: self.player.duration().map( |d| d.as_secs_f64() ),
853 volume: self.player.volume(),
854 playlist: entries,
855 playlist_index,
856 shuffle,
857 repeat_mode: repeat_mode_to_value( repeat ),
858 view_mode: self.view_mode.clone(),
859 visualizer_data: self.player.vis_data().map( |v| v.to_vec() ),
860 browser: Some( self.browser.to_snapshot() ),
861 settings: self.settings.to_snapshot(),
862 }
863 }
864
865
866 fn build_track_info( &self, path: &PathBuf ) -> TrackInfo {
868 let metadata = self.player.metadata();
869 let duration = self.player.duration();
870
871 let cover_art = find_cover_art( path );
873 let has_cover_art = cover_art.is_some();
874 if let Ok( mut guard ) = self.cover_art_path.lock() {
875 *guard = cover_art;
876 }
877
878 TrackInfo {
879 path: path.to_string_lossy().to_string(),
880 title: metadata.as_ref().and_then( |m| m.title.clone() ),
881 artist: metadata.as_ref().and_then( |m| m.artist.clone() ),
882 album: metadata.as_ref().and_then( |m| m.album.clone() ),
883 album_artist: metadata.as_ref().and_then( |m| m.album_artist.clone() ),
884 track_number: metadata.as_ref().and_then( |m| m.track_number ),
885 genre: metadata.as_ref().and_then( |m| m.genre.clone() ),
886 year: metadata.as_ref().and_then( |m| m.year ),
887 codec: metadata.as_ref().and_then( |m| m.codec.clone() ),
888 bitrate: metadata.as_ref().and_then( |m| m.bitrate ),
889 sample_rate: metadata.as_ref().and_then( |m| m.sample_rate ),
890 channels: metadata.as_ref().and_then( |m| m.channels ),
891 duration_secs: duration.map( |d| d.as_secs_f64() ),
892 has_cover_art,
893 }
894 }
895
896
897 fn save_session( &self ) {
899 let playlist_arc = self.player.playlist();
900 let playlist = playlist_arc.read().unwrap();
901
902 if playlist.is_empty() {
903 return;
904 }
905
906 if let Some( dir ) = Playlist::ensure_playlist_dir() {
907 let path = dir.join( "_last.m3u" );
908 let _ = playlist.save( &path );
909
910 let session = oxidio_core::SessionState {
911 playlist_name: "_last".to_string(),
912 track_index: playlist.current_index(),
913 shuffle: playlist.shuffle(),
914 repeat: playlist.repeat(),
915 volume: self.player.volume(),
916 };
917 let _ = Playlist::save_session( &session );
918 }
919 }
920
921
922 pub fn settings( &self ) -> &ProcessorSettings {
924 &self.settings
925 }
926}
927
928
929fn find_cover_art( track_path: &Path ) -> Option<PathBuf> {
939 let parent = track_path.parent()?;
940
941 let art_names = [
942 "cover", "folder", "album", "front", "art", "albumart", "album_art",
943 ];
944 let extensions = [ "jpg", "jpeg", "png", "bmp", "gif" ];
945
946 let mut found_path: Option<PathBuf> = None;
947
948 match std::fs::read_dir( parent ) {
949 Ok( entries ) => {
950 for entry in entries.flatten() {
951 let path = entry.path();
952 let filename = path.file_stem()
953 .and_then( |s| s.to_str() )
954 .map( |s| s.to_lowercase() );
955 let ext = path.extension()
956 .and_then( |e| e.to_str() )
957 .map( |e| e.to_lowercase() );
958
959 if let ( Some( name ), Some( ext ) ) = ( filename, ext ) {
960 if extensions.contains( &ext.as_str() ) {
961 if art_names.contains( &name.as_str() ) {
962 found_path = Some( path );
963 break;
964 }
965 if found_path.is_none() {
966 found_path = Some( path );
967 }
968 }
969 }
970 }
971 }
972 Err( e ) => {
973 tracing::warn!( "Failed to read directory {:?}: {}", parent, e );
974 }
975 }
976
977 found_path
978}
979
980
981fn build_track_entries( playlist: &Playlist ) -> Vec<TrackEntry> {
983 playlist.tracks().iter().enumerate().map( |( i, path )| {
984 let display_name = path.file_stem()
985 .map( |s| s.to_string_lossy().to_string() )
986 .unwrap_or_else( || path.to_string_lossy().to_string() );
987 TrackEntry {
988 index: i,
989 path: path.to_string_lossy().to_string(),
990 display_name,
991 }
992 }).collect()
993}
994
995
996fn playback_state_to_value( state: PlaybackState ) -> PlaybackStateValue {
998 match state {
999 PlaybackState::Stopped => PlaybackStateValue::Stopped,
1000 PlaybackState::Playing => PlaybackStateValue::Playing,
1001 PlaybackState::Paused => PlaybackStateValue::Paused,
1002 }
1003}
1004
1005
1006fn repeat_mode_to_value( mode: RepeatMode ) -> RepeatModeValue {
1008 match mode {
1009 RepeatMode::Off => RepeatModeValue::Off,
1010 RepeatMode::One => RepeatModeValue::One,
1011 RepeatMode::All => RepeatModeValue::All,
1012 }
1013}