mecomp_daemon/
controller.rs

1//----------------------------------------------------------------------------------------- std lib
2use std::{fs::File, ops::Range, path::PathBuf, sync::Arc, time::Duration};
3//--------------------------------------------------------------------------------- other libraries
4use ::tarpc::context::Context;
5use log::{debug, error, info, warn};
6use surrealdb::{Surreal, engine::local::Db};
7use tap::TapFallible;
8use tokio::sync::{Mutex, RwLock};
9use tracing::{Instrument, instrument};
10//-------------------------------------------------------------------------------- MECOMP libraries
11use mecomp_core::{
12    audio::{
13        AudioKernelSender,
14        commands::{AudioCommand, QueueCommand, VolumeCommand},
15    },
16    config::Settings,
17    errors::{BackupError, SerializableLibraryError},
18    rpc::{
19        AlbumId, ArtistId, CollectionId, DynamicPlaylistId, MusicPlayer, PlaylistId, SearchResult,
20        SongId,
21    },
22    state::{
23        RepeatMode, SeekType, StateAudio,
24        library::{LibraryBrief, LibraryFull, LibraryHealth},
25    },
26    udp::{Event, Message, Sender},
27};
28use mecomp_storage::{
29    db::schemas::{
30        self,
31        album::{Album, AlbumBrief},
32        artist::{Artist, ArtistBrief},
33        collection::{Collection, CollectionBrief},
34        dynamic::{DynamicPlaylist, DynamicPlaylistChangeSet, query::Query},
35        playlist::{Playlist, PlaylistBrief, PlaylistChangeSet},
36        song::{Song, SongBrief},
37    },
38    errors::Error,
39};
40use one_or_many::OneOrMany;
41
42use crate::{
43    services::{
44        self,
45        backup::{
46            export_dynamic_playlists, export_playlist, import_dynamic_playlists, import_playlist,
47            validate_file_path,
48        },
49    },
50    termination::{self, Terminator},
51};
52
53#[derive(Clone, Debug)]
54pub struct MusicPlayerServer {
55    db: Arc<Surreal<Db>>,
56    settings: Arc<Settings>,
57    audio_kernel: Arc<AudioKernelSender>,
58    library_rescan_lock: Arc<Mutex<()>>,
59    library_analyze_lock: Arc<Mutex<()>>,
60    collection_recluster_lock: Arc<Mutex<()>>,
61    publisher: Arc<RwLock<Sender<Message>>>,
62    terminator: Arc<Mutex<Terminator>>,
63    interrupt: Arc<termination::InterruptReceiver>,
64}
65
66impl MusicPlayerServer {
67    #[must_use]
68    #[inline]
69    pub fn new(
70        db: Arc<Surreal<Db>>,
71        settings: Arc<Settings>,
72        audio_kernel: Arc<AudioKernelSender>,
73        event_publisher: Arc<RwLock<Sender<Message>>>,
74        terminator: Terminator,
75        interrupt: termination::InterruptReceiver,
76    ) -> Self {
77        Self {
78            db,
79            publisher: event_publisher,
80            settings,
81            audio_kernel,
82            library_rescan_lock: Arc::new(Mutex::new(())),
83            library_analyze_lock: Arc::new(Mutex::new(())),
84            collection_recluster_lock: Arc::new(Mutex::new(())),
85            terminator: Arc::new(Mutex::new(terminator)),
86            interrupt: Arc::new(interrupt),
87        }
88    }
89
90    /// Publish a message to all listeners.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if the message could not be sent or encoded.
95    #[instrument]
96    pub async fn publish(
97        &self,
98        message: impl Into<Message> + Send + Sync + std::fmt::Debug,
99    ) -> Result<(), mecomp_core::errors::UdpError> {
100        self.publisher.read().await.send(message).await
101    }
102}
103
104#[allow(clippy::missing_inline_in_public_items)]
105impl MusicPlayer for MusicPlayerServer {
106    #[instrument]
107    async fn register_listener(self, context: Context, listener_addr: std::net::SocketAddr) {
108        info!("Registering listener: {listener_addr}");
109        self.publisher.write().await.add_subscriber(listener_addr);
110    }
111
112    async fn ping(self, _: Context) -> String {
113        "pong".to_string()
114    }
115
116    /// Rescans the music library, only error is if a rescan is already in progress.
117    #[instrument]
118    async fn library_rescan(self, context: Context) -> Result<(), SerializableLibraryError> {
119        info!("Rescanning library");
120
121        if self.library_rescan_lock.try_lock().is_err() {
122            warn!("Library rescan already in progress");
123            return Err(SerializableLibraryError::RescanInProgress);
124        }
125
126        let span = tracing::Span::current();
127
128        tokio::task::spawn(
129            async move {
130                let _guard = self.library_rescan_lock.lock().await;
131                match services::library::rescan(
132                    &self.db,
133                    &self.settings.daemon.library_paths,
134                    &self.settings.daemon.artist_separator,
135                    &self.settings.daemon.protected_artist_names,
136                    self.settings.daemon.genre_separator.as_deref(),
137                    self.settings.daemon.conflict_resolution,
138                )
139                .await
140                {
141                    Ok(()) => info!("Library rescan complete"),
142                    Err(e) => error!("Error in library_rescan: {e}"),
143                }
144
145                let result = self.publish(Event::LibraryRescanFinished).await;
146                if let Err(e) = result {
147                    error!("Error notifying clients that library_rescan_finished: {e}");
148                }
149            }
150            .instrument(span),
151        );
152
153        Ok(())
154    }
155    /// Check if a rescan is in progress.
156    #[instrument]
157    async fn library_rescan_in_progress(self, context: Context) -> bool {
158        self.library_rescan_lock.try_lock().is_err()
159    }
160    /// Analyze the music library, only error is if an analysis is already in progress.
161    #[instrument]
162    async fn library_analyze(
163        self,
164        context: Context,
165        overwrite: bool,
166    ) -> Result<(), SerializableLibraryError> {
167        info!("Analyzing library");
168
169        if self.library_analyze_lock.try_lock().is_err() {
170            warn!("Library analysis already in progress");
171            return Err(SerializableLibraryError::AnalysisInProgress);
172        }
173        let span = tracing::Span::current();
174
175        tokio::task::spawn(
176            async move {
177                let _guard = self.library_analyze_lock.lock().await;
178                match services::library::analyze(&self.db, self.interrupt.resubscribe(), overwrite)
179                    .await
180                {
181                    Ok(()) => info!("Library analysis complete"),
182                    Err(e) => error!("Error in library_analyze: {e}"),
183                }
184
185                let result = self.publish(Event::LibraryAnalysisFinished).await;
186                if let Err(e) = result {
187                    error!("Error notifying clients that library_analysis_finished: {e}");
188                }
189            }
190            .instrument(span),
191        );
192
193        Ok(())
194    }
195    /// Check if an analysis is in progress.
196    #[instrument]
197    async fn library_analyze_in_progress(self, context: Context) -> bool {
198        self.library_analyze_lock.try_lock().is_err()
199    }
200    /// Recluster the music library, only error is if a recluster is already in progress.
201    #[instrument]
202    async fn library_recluster(self, context: Context) -> Result<(), SerializableLibraryError> {
203        info!("Reclustering collections");
204
205        if self.collection_recluster_lock.try_lock().is_err() {
206            warn!("Collection reclustering already in progress");
207            return Err(SerializableLibraryError::ReclusterInProgress);
208        }
209
210        let span = tracing::Span::current();
211
212        tokio::task::spawn(
213            async move {
214                let _guard = self.collection_recluster_lock.lock().await;
215                match services::library::recluster(
216                    &self.db,
217                    self.settings.reclustering,
218                    self.interrupt.resubscribe(),
219                )
220                .await
221                {
222                    Ok(()) => info!("Collection reclustering complete"),
223                    Err(e) => error!("Error in library_recluster: {e}"),
224                }
225
226                let result = self.publish(Event::LibraryReclusterFinished).await;
227                if let Err(e) = result {
228                    error!("Error notifying clients that library_recluster_finished: {e}");
229                }
230            }
231            .instrument(span),
232        );
233
234        Ok(())
235    }
236    /// Check if a recluster is in progress.
237    #[instrument]
238    async fn library_recluster_in_progress(self, context: Context) -> bool {
239        self.collection_recluster_lock.try_lock().is_err()
240    }
241    /// Returns brief information about the music library.
242    #[instrument]
243    async fn library_brief(
244        self,
245        context: Context,
246    ) -> Result<LibraryBrief, SerializableLibraryError> {
247        info!("Creating library brief");
248        Ok(services::library::brief(&self.db)
249            .await
250            .tap_err(|e| warn!("Error in library_brief: {e}"))?)
251    }
252    /// Returns full information about the music library. (all songs, artists, albums, etc.)
253    #[instrument]
254    async fn library_full(self, context: Context) -> Result<LibraryFull, SerializableLibraryError> {
255        info!("Creating library full");
256        Ok(services::library::full(&self.db)
257            .await
258            .tap_err(|e| warn!("Error in library_full: {e}"))?)
259    }
260    /// Returns brief information about the music library's artists.
261    #[instrument]
262    async fn library_artists_brief(
263        self,
264        context: Context,
265    ) -> Result<Box<[ArtistBrief]>, SerializableLibraryError> {
266        info!("Creating library artists brief");
267        Ok(Artist::read_all_brief(&self.db)
268            .await
269            .tap_err(|e| warn!("Error in library_artists_brief: {e}"))?
270            .into_boxed_slice())
271    }
272    /// Returns full information about the music library's artists.
273    #[instrument]
274    async fn library_artists_full(
275        self,
276        context: Context,
277    ) -> Result<Box<[Artist]>, SerializableLibraryError> {
278        info!("Creating library artists full");
279        Ok(Artist::read_all(&self.db)
280            .await
281            .tap_err(|e| warn!("Error in library_artists_brief: {e}"))?
282            .into_boxed_slice())
283    }
284    /// Returns brief information about the music library's albums.
285    #[instrument]
286    async fn library_albums_brief(
287        self,
288        context: Context,
289    ) -> Result<Box<[AlbumBrief]>, SerializableLibraryError> {
290        info!("Creating library albums brief");
291        Ok(Album::read_all_brief(&self.db)
292            .await
293            .tap_err(|e| warn!("Error in library_albums_brief: {e}"))?
294            .into_boxed_slice())
295    }
296    /// Returns full information about the music library's albums.
297    #[instrument]
298    async fn library_albums_full(
299        self,
300        context: Context,
301    ) -> Result<Box<[Album]>, SerializableLibraryError> {
302        info!("Creating library albums full");
303        Ok(Album::read_all(&self.db)
304            .await
305            .map(std::vec::Vec::into_boxed_slice)
306            .tap_err(|e| warn!("Error in library_albums_full: {e}"))?)
307    }
308    /// Returns brief information about the music library's songs.
309    #[instrument]
310    async fn library_songs_brief(
311        self,
312        context: Context,
313    ) -> Result<Box<[SongBrief]>, SerializableLibraryError> {
314        info!("Creating library songs brief");
315        Ok(Song::read_all_brief(&self.db)
316            .await
317            .tap_err(|e| warn!("Error in library_songs_brief: {e}"))?
318            .into_boxed_slice())
319    }
320    /// Returns full information about the music library's songs.
321    #[instrument]
322    async fn library_songs_full(
323        self,
324        context: Context,
325    ) -> Result<Box<[Song]>, SerializableLibraryError> {
326        info!("Creating library songs full");
327        Ok(Song::read_all(&self.db)
328            .await
329            .tap_err(|e| warn!("Error in library_songs_full: {e}"))?
330            .into_boxed_slice())
331    }
332    /// Returns brief information about the users playlists.
333    #[instrument]
334    async fn library_playlists_brief(
335        self,
336        context: Context,
337    ) -> Result<Box<[PlaylistBrief]>, SerializableLibraryError> {
338        info!("Creating library playlists brief");
339        Ok(Playlist::read_all_brief(&self.db)
340            .await
341            .tap_err(|e| warn!("Error in library_playlists_brief: {e}"))?
342            .into_boxed_slice())
343    }
344    /// Returns full information about the users playlists.
345    #[instrument]
346    async fn library_playlists_full(
347        self,
348        context: Context,
349    ) -> Result<Box<[Playlist]>, SerializableLibraryError> {
350        info!("Creating library playlists full");
351        Ok(Playlist::read_all(&self.db)
352            .await
353            .tap_err(|e| warn!("Error in library_playlists_full: {e}"))?
354            .into_boxed_slice())
355    }
356    /// Return brief information about the users collections.
357    #[instrument]
358    async fn library_collections_brief(
359        self,
360        context: Context,
361    ) -> Result<Box<[CollectionBrief]>, SerializableLibraryError> {
362        info!("Creating library collections brief");
363        Ok(Collection::read_all_brief(&self.db)
364            .await
365            .tap_err(|e| warn!("Error in library_collections_brief: {e}"))?
366            .into_boxed_slice())
367    }
368    /// Return full information about the users collections.
369    #[instrument]
370    async fn library_collections_full(
371        self,
372        context: Context,
373    ) -> Result<Box<[Collection]>, SerializableLibraryError> {
374        info!("Creating library collections full");
375        Ok(Collection::read_all(&self.db)
376            .await
377            .tap_err(|e| warn!("Error in library_collections_full: {e}"))?
378            .into_boxed_slice())
379    }
380    /// Returns information about the health of the music library (are there any missing files, etc.)
381    #[instrument]
382    async fn library_health(
383        self,
384        context: Context,
385    ) -> Result<LibraryHealth, SerializableLibraryError> {
386        info!("Creating library health");
387        Ok(services::library::health(&self.db)
388            .await
389            .tap_err(|e| warn!("Error in library_health: {e}"))?)
390    }
391    /// Get a song by its ID.
392    #[instrument]
393    async fn library_song_get(self, context: Context, id: SongId) -> Option<Song> {
394        let id = id.into();
395        info!("Getting song by ID: {id}");
396        Song::read(&self.db, id)
397            .await
398            .tap_err(|e| warn!("Error in library_song_get: {e}"))
399            .unwrap_or_default()
400    }
401    /// Get a song by its file path.
402    #[instrument]
403    async fn library_song_get_by_path(self, context: Context, path: PathBuf) -> Option<Song> {
404        info!("Getting song by path: {}", path.display());
405        Song::read_by_path(&self.db, path)
406            .await
407            .tap_err(|e| warn!("Error in library_song_get_by_path: {e}"))
408            .unwrap_or_default()
409    }
410    /// Get the artists of a song.
411    #[instrument]
412    async fn library_song_get_artist(self, context: Context, id: SongId) -> OneOrMany<Artist> {
413        let id = id.into();
414        info!("Getting artist of: {id}");
415        Song::read_artist(&self.db, id)
416            .await
417            .tap_err(|e| warn!("Error in library_song_get_artist: {e}"))
418            .unwrap_or_default()
419    }
420    /// Get the album of a song.
421    #[instrument]
422    async fn library_song_get_album(self, context: Context, id: SongId) -> Option<Album> {
423        let id = id.into();
424        info!("Getting album of: {id}");
425        Song::read_album(&self.db, id)
426            .await
427            .tap_err(|e| warn!("Error in library_song_get_album: {e}"))
428            .unwrap_or_default()
429    }
430    /// Get the Playlists a song is in.
431    #[instrument]
432    async fn library_song_get_playlists(self, context: Context, id: SongId) -> Box<[Playlist]> {
433        let id = id.into();
434        info!("Getting playlists of: {id}");
435        Song::read_playlists(&self.db, id)
436            .await
437            .tap_err(|e| warn!("Error in library_song_get_playlists: {e}"))
438            .ok()
439            .unwrap_or_default()
440            .into()
441    }
442    /// Get the Collections a song is in.
443    #[instrument]
444    async fn library_song_get_collections(self, context: Context, id: SongId) -> Box<[Collection]> {
445        let id = id.into();
446        info!("Getting collections of: {id}");
447        Song::read_collections(&self.db, id)
448            .await
449            .tap_err(|e| warn!("Error in library_song_get_collections: {e}"))
450            .ok()
451            .unwrap_or_default()
452            .into()
453    }
454
455    /// Get an album by its ID.
456    #[instrument]
457    async fn library_album_get(self, context: Context, id: AlbumId) -> Option<Album> {
458        let id = id.into();
459        info!("Getting album by ID: {id}");
460        Album::read(&self.db, id)
461            .await
462            .tap_err(|e| warn!("Error in library_album_get: {e}"))
463            .ok()
464            .flatten()
465    }
466    /// Get the artists of an album
467    #[instrument]
468    async fn library_album_get_artist(self, context: Context, id: AlbumId) -> OneOrMany<Artist> {
469        let id = id.into();
470        info!("Getting artists of: {id}");
471        Album::read_artist(&self.db, id)
472            .await
473            .tap_err(|e| warn!("Error in library_album_get_artist: {e}"))
474            .ok()
475            .into()
476    }
477    /// Get the songs of an album
478    #[instrument]
479    async fn library_album_get_songs(self, context: Context, id: AlbumId) -> Option<Box<[Song]>> {
480        let id = id.into();
481        info!("Getting songs of: {id}");
482        Album::read_songs(&self.db, id)
483            .await
484            .tap_err(|e| warn!("Error in library_album_get_songs: {e}"))
485            .ok()
486            .map(Vec::into_boxed_slice)
487    }
488    /// Get an artist by its ID.
489    #[instrument]
490    async fn library_artist_get(self, context: Context, id: ArtistId) -> Option<Artist> {
491        let id = id.into();
492        info!("Getting artist by ID: {id}");
493        Artist::read(&self.db, id)
494            .await
495            .tap_err(|e| warn!("Error in library_artist_get: {e}"))
496            .ok()
497            .flatten()
498    }
499    /// Get the songs of an artist
500    #[instrument]
501    async fn library_artist_get_songs(self, context: Context, id: ArtistId) -> Option<Box<[Song]>> {
502        let id = id.into();
503        info!("Getting songs of: {id}");
504        Artist::read_songs(&self.db, id)
505            .await
506            .tap_err(|e| warn!("Error in library_artist_get_songs: {e}"))
507            .ok()
508            .map(Vec::into_boxed_slice)
509    }
510    /// Get the albums of an artist
511    #[instrument]
512    async fn library_artist_get_albums(
513        self,
514        context: Context,
515        id: ArtistId,
516    ) -> Option<Box<[Album]>> {
517        let id = id.into();
518        info!("Getting albums of: {id}");
519        Artist::read_albums(&self.db, id)
520            .await
521            .tap_err(|e| warn!("Error in library_artist_get_albums: {e}"))
522            .ok()
523            .map(Vec::into_boxed_slice)
524    }
525
526    /// tells the daemon to shutdown.
527    #[instrument]
528    async fn daemon_shutdown(self, context: Context) {
529        let terminator = self.terminator.clone();
530        std::thread::Builder::new()
531            .name(String::from("Daemon Shutdown"))
532            .spawn(move || {
533                std::thread::sleep(std::time::Duration::from_secs(1));
534                let terminate_result = terminator
535                    .blocking_lock()
536                    .terminate(termination::Interrupted::UserInt);
537                if let Err(e) = terminate_result {
538                    error!("Error terminating daemon, panicking instead: {e}");
539                    panic!("Error terminating daemon: {e}");
540                }
541            })
542            .unwrap();
543        info!("Shutting down daemon in 1 second");
544    }
545
546    /// returns full information about the current state of the audio player (queue, current song, etc.)
547    #[instrument]
548    async fn state_audio(self, context: Context) -> Option<StateAudio> {
549        debug!("Getting state of audio player");
550        let (tx, rx) = tokio::sync::oneshot::channel();
551
552        self.audio_kernel.send(AudioCommand::ReportStatus(tx));
553
554        rx.await
555            .tap_err(|e| warn!("Error in state_audio: {e}"))
556            .ok()
557    }
558
559    /// returns the current artist.
560    #[instrument]
561    async fn current_artist(self, context: Context) -> OneOrMany<Artist> {
562        info!("Getting current artist");
563        let (tx, rx) = tokio::sync::oneshot::channel();
564
565        self.audio_kernel.send(AudioCommand::ReportStatus(tx));
566
567        if let Some(song) = rx
568            .await
569            .tap_err(|e| warn!("Error in current_artist: {e}"))
570            .ok()
571            .and_then(|state| state.current_song)
572        {
573            Song::read_artist(&self.db, song.id)
574                .await
575                .tap_err(|e| warn!("Error in current_album: {e}"))
576                .unwrap_or_default()
577        } else {
578            OneOrMany::None
579        }
580    }
581    /// returns the current album.
582    #[instrument]
583    async fn current_album(self, context: Context) -> Option<Album> {
584        info!("Getting current album");
585        let (tx, rx) = tokio::sync::oneshot::channel();
586
587        self.audio_kernel.send(AudioCommand::ReportStatus(tx));
588
589        if let Some(song) = rx
590            .await
591            .tap_err(|e| warn!("Error in current_album: {e}"))
592            .ok()
593            .and_then(|state| state.current_song)
594        {
595            Song::read_album(&self.db, song.id)
596                .await
597                .tap_err(|e| warn!("Error in current_album: {e}"))
598                .unwrap_or_default()
599        } else {
600            None
601        }
602    }
603    /// returns the current song.
604    #[instrument]
605    async fn current_song(self, context: Context) -> Option<SongBrief> {
606        info!("Getting current song");
607        let (tx, rx) = tokio::sync::oneshot::channel();
608
609        self.audio_kernel.send(AudioCommand::ReportStatus(tx));
610
611        rx.await
612            .tap_err(|e| warn!("Error in current_song: {e}"))
613            .ok()
614            .and_then(|state| state.current_song)
615    }
616
617    /// returns a random artist.
618    #[instrument]
619    async fn rand_artist(self, context: Context) -> Option<ArtistBrief> {
620        info!("Getting random artist");
621        Artist::read_rand(&self.db, 1)
622            .await
623            .tap_err(|e| warn!("Error in rand_artist: {e}"))
624            .ok()
625            .and_then(|results| results.first().cloned())
626    }
627    /// returns a random album.
628    #[instrument]
629    async fn rand_album(self, context: Context) -> Option<AlbumBrief> {
630        info!("Getting random album");
631        Album::read_rand(&self.db, 1)
632            .await
633            .tap_err(|e| warn!("Error in rand_album: {e}"))
634            .ok()
635            .and_then(|results| results.first().cloned())
636    }
637    /// returns a random song.
638    #[instrument]
639    async fn rand_song(self, context: Context) -> Option<SongBrief> {
640        info!("Getting random song");
641        Song::read_rand(&self.db, 1)
642            .await
643            .tap_err(|e| warn!("Error in rand_song: {e}"))
644            .ok()
645            .and_then(|results| results.first().cloned())
646    }
647
648    /// returns a list of artists, albums, and songs matching the given search query.
649    #[instrument]
650    async fn search(self, context: Context, query: String, limit: usize) -> SearchResult {
651        info!("Searching for: {query}");
652        // basic idea:
653        // 1. search for songs
654        // 2. search for albums
655        // 3. search for artists
656        // 4. return the results
657        let songs = Song::search(&self.db, &query, limit)
658            .await
659            .tap_err(|e| warn!("Error in search: {e}"))
660            .unwrap_or_default()
661            .into();
662
663        let albums = Album::search(&self.db, &query, limit)
664            .await
665            .tap_err(|e| warn!("Error in search: {e}"))
666            .unwrap_or_default()
667            .into();
668
669        let artists = Artist::search(&self.db, &query, limit)
670            .await
671            .tap_err(|e| warn!("Error in search: {e}"))
672            .unwrap_or_default()
673            .into();
674        SearchResult {
675            songs,
676            albums,
677            artists,
678        }
679    }
680    /// returns a list of artists matching the given search query.
681    #[instrument]
682    async fn search_artist(
683        self,
684        context: Context,
685        query: String,
686        limit: usize,
687    ) -> Box<[ArtistBrief]> {
688        info!("Searching for artist: {query}");
689        Artist::search(&self.db, &query, limit)
690            .await
691            .tap_err(|e| {
692                warn!("Error in search_artist: {e}");
693            })
694            .unwrap_or_default()
695            .into()
696    }
697    /// returns a list of albums matching the given search query.
698    #[instrument]
699    async fn search_album(
700        self,
701        context: Context,
702        query: String,
703        limit: usize,
704    ) -> Box<[AlbumBrief]> {
705        info!("Searching for album: {query}");
706        Album::search(&self.db, &query, limit)
707            .await
708            .tap_err(|e| {
709                warn!("Error in search_album: {e}");
710            })
711            .unwrap_or_default()
712            .into()
713    }
714    /// returns a list of songs matching the given search query.
715    #[instrument]
716    async fn search_song(self, context: Context, query: String, limit: usize) -> Box<[SongBrief]> {
717        info!("Searching for song: {query}");
718        Song::search(&self.db, &query, limit)
719            .await
720            .tap_err(|e| {
721                warn!("Error in search_song: {e}");
722            })
723            .unwrap_or_default()
724            .into()
725    }
726
727    /// toggles playback (play/pause).
728    #[instrument]
729    async fn playback_toggle(self, context: Context) {
730        info!("Toggling playback");
731        self.audio_kernel.send(AudioCommand::TogglePlayback);
732    }
733    /// start playback (unpause).
734    #[instrument]
735    async fn playback_play(self, context: Context) {
736        info!("Starting playback");
737        self.audio_kernel.send(AudioCommand::Play);
738    }
739    /// pause playback.
740    #[instrument]
741    async fn playback_pause(self, context: Context) {
742        info!("Pausing playback");
743        self.audio_kernel.send(AudioCommand::Pause);
744    }
745    /// stop playback.
746    #[instrument]
747    async fn playback_stop(self, context: Context) {
748        info!("Stopping playback");
749        self.audio_kernel.send(AudioCommand::Stop);
750    }
751    /// restart the current song.
752    #[instrument]
753    async fn playback_restart(self, context: Context) {
754        info!("Restarting current song");
755        self.audio_kernel.send(AudioCommand::RestartSong);
756    }
757    /// skip forward by the given amount of songs
758    #[instrument]
759    async fn playback_skip_forward(self, context: Context, amount: usize) {
760        info!("Skipping forward by {amount} songs");
761        self.audio_kernel
762            .send(AudioCommand::Queue(QueueCommand::SkipForward(amount)));
763    }
764    /// go backwards by the given amount of songs.
765    #[instrument]
766    async fn playback_skip_backward(self, context: Context, amount: usize) {
767        info!("Going back by {amount} songs");
768        self.audio_kernel
769            .send(AudioCommand::Queue(QueueCommand::SkipBackward(amount)));
770    }
771    /// stop playback.
772    /// (clears the queue and stops playback)
773    #[instrument]
774    async fn playback_clear_player(self, context: Context) {
775        info!("Stopping playback");
776        self.audio_kernel.send(AudioCommand::ClearPlayer);
777    }
778    /// clear the queue.
779    #[instrument]
780    async fn playback_clear(self, context: Context) {
781        info!("Clearing queue and stopping playback");
782        self.audio_kernel
783            .send(AudioCommand::Queue(QueueCommand::Clear));
784    }
785    /// seek forwards, backwards, or to an absolute second in the current song.
786    #[instrument]
787    async fn playback_seek(self, context: Context, seek: SeekType, duration: Duration) {
788        info!("Seeking {seek} by {:.2}s", duration.as_secs_f32());
789        self.audio_kernel.send(AudioCommand::Seek(seek, duration));
790    }
791    /// set the repeat mode.
792    #[instrument]
793    async fn playback_repeat(self, context: Context, mode: RepeatMode) {
794        info!("Setting repeat mode to: {mode}");
795        self.audio_kernel
796            .send(AudioCommand::Queue(QueueCommand::SetRepeatMode(mode)));
797    }
798    /// Shuffle the current queue, then start playing from the 1st Song in the queue.
799    #[instrument]
800    async fn playback_shuffle(self, context: Context) {
801        info!("Shuffling queue");
802        self.audio_kernel
803            .send(AudioCommand::Queue(QueueCommand::Shuffle));
804    }
805    /// set the volume to the given value
806    /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` will multiply each sample by this value.
807    #[instrument]
808    async fn playback_volume(self, context: Context, volume: f32) {
809        info!("Setting volume to: {volume}",);
810        self.audio_kernel
811            .send(AudioCommand::Volume(VolumeCommand::Set(volume)));
812    }
813    /// increase the volume by the given amount
814    #[instrument]
815    async fn playback_volume_up(self, context: Context, amount: f32) {
816        info!("Increasing volume by: {amount}",);
817        self.audio_kernel
818            .send(AudioCommand::Volume(VolumeCommand::Up(amount)));
819    }
820    /// decrease the volume by the given amount
821    #[instrument]
822    async fn playback_volume_down(self, context: Context, amount: f32) {
823        info!("Decreasing volume by: {amount}",);
824        self.audio_kernel
825            .send(AudioCommand::Volume(VolumeCommand::Down(amount)));
826    }
827    /// toggle the volume mute.
828    #[instrument]
829    async fn playback_volume_toggle_mute(self, context: Context) {
830        info!("Toggling volume mute");
831        self.audio_kernel
832            .send(AudioCommand::Volume(VolumeCommand::ToggleMute));
833    }
834    /// mute the volume.
835    #[instrument]
836    async fn playback_mute(self, context: Context) {
837        info!("Muting volume");
838        self.audio_kernel
839            .send(AudioCommand::Volume(VolumeCommand::Mute));
840    }
841    /// unmute the volume.
842    #[instrument]
843    async fn playback_unmute(self, context: Context) {
844        info!("Unmuting volume");
845        self.audio_kernel
846            .send(AudioCommand::Volume(VolumeCommand::Unmute));
847    }
848
849    /// add a song to the queue.
850    /// (if the queue is empty, it will start playing the song.)
851    #[instrument]
852    async fn queue_add(
853        self,
854        context: Context,
855        thing: schemas::RecordId,
856    ) -> Result<(), SerializableLibraryError> {
857        info!("Adding thing to queue: {thing}");
858
859        let songs = services::get_songs_from_things(&self.db, &[thing]).await?;
860
861        if songs.is_empty() {
862            return Err(Error::NotFound.into());
863        }
864
865        self.audio_kernel
866            .send(AudioCommand::Queue(QueueCommand::AddToQueue(
867                songs.into_iter().map(Into::into).collect(),
868            )));
869
870        Ok(())
871    }
872    /// add a list of things to the queue.
873    /// (if the queue is empty, it will start playing the first thing in the list.)
874    #[instrument]
875    async fn queue_add_list(
876        self,
877        context: Context,
878        list: Vec<schemas::RecordId>,
879    ) -> Result<(), SerializableLibraryError> {
880        info!(
881            "Adding list to queue: ({})",
882            list.iter()
883                .map(ToString::to_string)
884                .collect::<Vec<_>>()
885                .join(", ")
886        );
887
888        // go through the list, and get songs for each thing (depending on what it is)
889        let songs: OneOrMany<Song> = services::get_songs_from_things(&self.db, &list).await?;
890
891        self.audio_kernel
892            .send(AudioCommand::Queue(QueueCommand::AddToQueue(
893                songs.into_iter().map(Into::into).collect(),
894            )));
895
896        Ok(())
897    }
898    /// set the current song to a queue index.
899    /// if the index is out of bounds, it will be clamped to the nearest valid index.
900    #[instrument]
901    async fn queue_set_index(self, context: Context, index: usize) {
902        info!("Setting queue index to: {index}");
903
904        self.audio_kernel
905            .send(AudioCommand::Queue(QueueCommand::SetPosition(index)));
906    }
907    /// remove a range of songs from the queue.
908    /// if the range is out of bounds, it will be clamped to the nearest valid range.
909    #[instrument]
910    async fn queue_remove_range(self, context: Context, range: Range<usize>) {
911        info!("Removing queue range: {range:?}");
912
913        self.audio_kernel
914            .send(AudioCommand::Queue(QueueCommand::RemoveRange(range)));
915    }
916
917    /// create a new playlist.
918    /// if a playlist with the same name already exists, this will return that playlist's id in the error variant
919    #[instrument]
920    async fn playlist_get_or_create(
921        self,
922        context: Context,
923        name: String,
924    ) -> Result<PlaylistId, SerializableLibraryError> {
925        info!("Creating new playlist: {name}");
926
927        // see if a playlist with that name already exists
928        match Playlist::read_by_name(&self.db, name.clone()).await {
929            Ok(Some(playlist)) => return Ok(playlist.id.into()),
930            Err(e) => warn!("Error in playlist_new (looking for existing playlist): {e}"),
931            _ => {}
932        }
933        // if it doesn't, create a new playlist with that name
934        match Playlist::create(
935            &self.db,
936            Playlist {
937                id: Playlist::generate_id(),
938                name,
939                runtime: Duration::from_secs(0),
940                song_count: 0,
941            },
942        )
943        .await
944        .tap_err(|e| warn!("Error in playlist_new (creating new playlist): {e}"))?
945        {
946            Some(playlist) => Ok(playlist.id.into()),
947            None => Err(Error::NotCreated.into()),
948        }
949    }
950    /// remove a playlist.
951    #[instrument]
952    async fn playlist_remove(
953        self,
954        context: Context,
955        id: PlaylistId,
956    ) -> Result<(), SerializableLibraryError> {
957        let id = id.into();
958        info!("Removing playlist with id: {id}");
959
960        Playlist::delete(&self.db, id)
961            .await?
962            .ok_or(Error::NotFound)?;
963
964        Ok(())
965    }
966    /// clone a playlist.
967    /// (creates a new playlist with the same name (append " (copy)") and contents as the given playlist.)
968    /// returns the id of the new playlist
969    #[instrument]
970    async fn playlist_clone(
971        self,
972        context: Context,
973        id: PlaylistId,
974    ) -> Result<PlaylistId, SerializableLibraryError> {
975        let id = id.into();
976        info!("Cloning playlist with id: {id}");
977
978        let new_playlist = Playlist::create_copy(&self.db, id)
979            .await?
980            .ok_or(Error::NotFound)?;
981
982        Ok(new_playlist.id.into())
983    }
984    /// get the id of a playlist.
985    /// returns none if the playlist does not exist.
986    #[instrument]
987    async fn playlist_get_id(self, context: Context, name: String) -> Option<PlaylistId> {
988        info!("Getting playlist ID: {name}");
989
990        Playlist::read_by_name(&self.db, name)
991            .await
992            .tap_err(|e| warn!("Error in playlist_get_id: {e}"))
993            .ok()
994            .flatten()
995            .map(|playlist| playlist.id.into())
996    }
997    /// remove a list of songs from a playlist.
998    /// if the songs are not in the playlist, this will do nothing.
999    #[instrument]
1000    async fn playlist_remove_songs(
1001        self,
1002        context: Context,
1003        playlist: PlaylistId,
1004        songs: Vec<SongId>,
1005    ) -> Result<(), SerializableLibraryError> {
1006        let playlist = playlist.into();
1007        let songs = songs.into_iter().map(Into::into).collect::<Vec<_>>();
1008        info!("Removing song from playlist: {playlist} ({songs:?})");
1009
1010        Ok(Playlist::remove_songs(&self.db, playlist, songs).await?)
1011    }
1012    /// Add a thing to a playlist.
1013    /// If the thing is something that has songs (an album, artist, etc.), it will add all the songs.
1014    #[instrument]
1015    async fn playlist_add(
1016        self,
1017        context: Context,
1018        playlist: PlaylistId,
1019        thing: schemas::RecordId,
1020    ) -> Result<(), SerializableLibraryError> {
1021        let playlist = playlist.into();
1022        info!("Adding thing to playlist: {playlist} ({thing})");
1023
1024        // get songs for the thing
1025        let songs: OneOrMany<Song> = services::get_songs_from_things(&self.db, &[thing]).await?;
1026
1027        Ok(Playlist::add_songs(
1028            &self.db,
1029            playlist,
1030            songs.into_iter().map(|s| s.id).collect::<Vec<_>>(),
1031        )
1032        .await?)
1033    }
1034    /// Add a list of things to a playlist.
1035    /// If the things are something that have songs (an album, artist, etc.), it will add all the songs.
1036    #[instrument]
1037    async fn playlist_add_list(
1038        self,
1039        context: Context,
1040        playlist: PlaylistId,
1041        list: Vec<schemas::RecordId>,
1042    ) -> Result<(), SerializableLibraryError> {
1043        let playlist = playlist.into();
1044        info!(
1045            "Adding list to playlist: {playlist} ({})",
1046            list.iter()
1047                .map(ToString::to_string)
1048                .collect::<Vec<_>>()
1049                .join(", ")
1050        );
1051
1052        // go through the list, and get songs for each thing (depending on what it is)
1053        let songs: OneOrMany<Song> = services::get_songs_from_things(&self.db, &list).await?;
1054
1055        Ok(Playlist::add_songs(
1056            &self.db,
1057            playlist,
1058            songs.into_iter().map(|s| s.id).collect::<Vec<_>>(),
1059        )
1060        .await?)
1061    }
1062    /// Get a playlist by its ID.
1063    #[instrument]
1064    async fn playlist_get(self, context: Context, id: PlaylistId) -> Option<Playlist> {
1065        let id = id.into();
1066        info!("Getting playlist by ID: {id}");
1067
1068        Playlist::read(&self.db, id)
1069            .await
1070            .tap_err(|e| warn!("Error in playlist_get: {e}"))
1071            .ok()
1072            .flatten()
1073    }
1074    /// Get the songs of a playlist
1075    #[instrument]
1076    async fn playlist_get_songs(self, context: Context, id: PlaylistId) -> Option<Box<[Song]>> {
1077        let id = id.into();
1078        info!("Getting songs in: {id}");
1079        Playlist::read_songs(&self.db, id)
1080            .await
1081            .tap_err(|e| warn!("Error in playlist_get_songs: {e}"))
1082            .ok()
1083            .map(Into::into)
1084    }
1085    /// Rename a playlist.
1086    #[instrument]
1087    async fn playlist_rename(
1088        self,
1089        context: Context,
1090        id: PlaylistId,
1091        name: String,
1092    ) -> Result<Playlist, SerializableLibraryError> {
1093        let id = id.into();
1094        info!("Renaming playlist: {id} ({name})");
1095        Playlist::update(&self.db, id, PlaylistChangeSet::new().name(name))
1096            .await?
1097            .ok_or(Error::NotFound.into())
1098    }
1099    /// Export a playlist to a .m3u file
1100    #[instrument]
1101    async fn playlist_export(
1102        self,
1103        context: Context,
1104        id: PlaylistId,
1105        path: PathBuf,
1106    ) -> Result<(), SerializableLibraryError> {
1107        info!("Exporting playlist to: {}", path.display());
1108
1109        // validate the path
1110        validate_file_path(&path, "m3u", false)?;
1111
1112        // read the playlist
1113        let playlist = Playlist::read(&self.db, id.into())
1114            .await
1115            .tap_err(|e| warn!("Error in playlist_export: {e}"))
1116            .ok()
1117            .flatten()
1118            .ok_or(Error::NotFound)?;
1119        // get the songs in the playlist
1120        let songs = Playlist::read_songs(&self.db, playlist.id)
1121            .await
1122            .tap_err(|e| warn!("Error in playlist_export: {e}"))
1123            .ok()
1124            .unwrap_or_default();
1125
1126        // create the file
1127        let file = File::create(&path).tap_err(|e| warn!("Error in playlist_export: {e}"))?;
1128        // write the playlist to the file
1129        export_playlist(&playlist.name, &songs, file)
1130            .tap_err(|e| warn!("Error in playlist_export: {e}"))?;
1131        info!("Exported playlist to: {path:?}");
1132        Ok(())
1133    }
1134    /// Import a playlist from a .m3u file
1135    #[instrument]
1136    async fn playlist_import(
1137        self,
1138        context: Context,
1139        path: PathBuf,
1140        name: Option<String>,
1141    ) -> Result<PlaylistId, SerializableLibraryError> {
1142        info!("Importing playlist from: {}", path.display());
1143
1144        // validate the path
1145        validate_file_path(&path, "m3u", true)?;
1146
1147        // read file
1148        let file = File::open(&path).tap_err(|e| warn!("Error in playlist_import: {e}"))?;
1149        let (parsed_name, song_paths) =
1150            import_playlist(file).tap_err(|e| warn!("Error in playlist_import: {e}"))?;
1151
1152        log::debug!("Parsed playlist name: {parsed_name:?}");
1153        log::debug!("Parsed song paths: {song_paths:?}");
1154
1155        let name = match (name, parsed_name) {
1156            (Some(name), _) | (None, Some(name)) => name,
1157            (None, None) => "Imported Playlist".to_owned(),
1158        };
1159
1160        // check if the playlist already exists
1161        if let Ok(Some(playlist)) = Playlist::read_by_name(&self.db, name.clone()).await {
1162            // if it does, return the id
1163            info!("Playlist \"{name}\" already exists, will not import");
1164            return Ok(playlist.id.into());
1165        }
1166
1167        // create the playlist
1168        let playlist = Playlist::create(
1169            &self.db,
1170            Playlist {
1171                id: Playlist::generate_id(),
1172                name,
1173                runtime: Duration::from_secs(0),
1174                song_count: 0,
1175            },
1176        )
1177        .await
1178        .tap_err(|e| warn!("Error in playlist_import: {e}"))?
1179        .ok_or(Error::NotCreated)?;
1180
1181        // lookup all the songs
1182        let mut songs = Vec::new();
1183        for path in &song_paths {
1184            let Some(song) = Song::read_by_path(&self.db, path.clone())
1185                .await
1186                .tap_err(|e| warn!("Error in playlist_import: {e}"))?
1187            else {
1188                warn!("Song at {} not found in the library", path.display());
1189                continue;
1190            };
1191
1192            songs.push(song.id);
1193        }
1194
1195        if songs.is_empty() {
1196            return Err(BackupError::NoValidSongs(song_paths.len()).into());
1197        }
1198
1199        // add the songs to the playlist
1200        Playlist::add_songs(&self.db, playlist.id.clone(), songs)
1201            .await
1202            .tap_err(|e| {
1203                warn!("Error in playlist_import: {e}");
1204            })?;
1205
1206        // return the playlist id
1207        Ok(playlist.id.into())
1208    }
1209
1210    /// Collections: get a collection by its ID.
1211    #[instrument]
1212    async fn collection_get(self, context: Context, id: CollectionId) -> Option<Collection> {
1213        info!("Getting collection by ID: {id:?}");
1214        Collection::read(&self.db, id.into())
1215            .await
1216            .tap_err(|e| warn!("Error in collection_get: {e}"))
1217            .ok()
1218            .flatten()
1219    }
1220    /// Collections: freeze a collection (convert it to a playlist).
1221    #[instrument]
1222    async fn collection_freeze(
1223        self,
1224        context: Context,
1225        id: CollectionId,
1226        name: String,
1227    ) -> Result<PlaylistId, SerializableLibraryError> {
1228        info!("Freezing collection: {id:?} ({name})");
1229        Ok(Collection::freeze(&self.db, id.into(), name)
1230            .await
1231            .map(|p| p.id.into())?)
1232    }
1233    /// Get the songs of a collection
1234    #[instrument]
1235    async fn collection_get_songs(self, context: Context, id: CollectionId) -> Option<Box<[Song]>> {
1236        let id = id.into();
1237        info!("Getting songs in: {id}");
1238        Collection::read_songs(&self.db, id)
1239            .await
1240            .tap_err(|e| warn!("Error in collection_get_songs: {e}"))
1241            .ok()
1242            .map(Into::into)
1243    }
1244
1245    /// Radio: get the `n` most similar songs to the given things.
1246    #[instrument]
1247    async fn radio_get_similar(
1248        self,
1249        context: Context,
1250        things: Vec<schemas::RecordId>,
1251        n: u32,
1252    ) -> Result<Box<[Song]>, SerializableLibraryError> {
1253        info!("Getting the {n} most similar songs to: {things:?}");
1254        Ok(services::radio::get_similar(&self.db, things, n)
1255            .await
1256            .map(Vec::into_boxed_slice)
1257            .tap_err(|e| warn!("Error in radio_get_similar: {e}"))?)
1258    }
1259    /// Radio: get the ids of the `n` most similar songs to the given things.
1260    #[instrument]
1261    async fn radio_get_similar_ids(
1262        self,
1263        context: Context,
1264        things: Vec<schemas::RecordId>,
1265        n: u32,
1266    ) -> Result<Box<[SongId]>, SerializableLibraryError> {
1267        info!("Getting the {n} most similar songs to: {things:?}");
1268        Ok(services::radio::get_similar(&self.db, things, n)
1269            .await
1270            .map(|songs| songs.into_iter().map(|song| song.id.into()).collect())
1271            .tap_err(|e| warn!("Error in radio_get_similar_songs: {e}"))?)
1272    }
1273
1274    // Dynamic playlist commands
1275    /// Dynamic Playlists: create a new DP with the given name and query
1276    #[instrument]
1277    async fn dynamic_playlist_create(
1278        self,
1279        context: Context,
1280        name: String,
1281        query: Query,
1282    ) -> Result<DynamicPlaylistId, SerializableLibraryError> {
1283        let id = DynamicPlaylist::generate_id();
1284        info!("Creating new DP: {id:?} ({name})");
1285
1286        match DynamicPlaylist::create(&self.db, DynamicPlaylist { id, name, query })
1287            .await
1288            .tap_err(|e| warn!("Error in dynamic_playlist_create: {e}"))?
1289        {
1290            Some(dp) => Ok(dp.id.into()),
1291            None => Err(Error::NotCreated.into()),
1292        }
1293    }
1294    /// Dynamic Playlists: list all DPs
1295    #[instrument]
1296    async fn dynamic_playlist_list(self, context: Context) -> Box<[DynamicPlaylist]> {
1297        info!("Listing DPs");
1298        DynamicPlaylist::read_all(&self.db)
1299            .await
1300            .tap_err(|e| warn!("Error in dynamic_playlist_list: {e}"))
1301            .ok()
1302            .map(Into::into)
1303            .unwrap_or_default()
1304    }
1305    /// Dynamic Playlists: update a DP
1306    #[instrument]
1307    async fn dynamic_playlist_update(
1308        self,
1309        context: Context,
1310        id: DynamicPlaylistId,
1311        changes: DynamicPlaylistChangeSet,
1312    ) -> Result<DynamicPlaylist, SerializableLibraryError> {
1313        info!("Updating DP: {id:?}, {changes:?}");
1314        DynamicPlaylist::update(&self.db, id.into(), changes)
1315            .await
1316            .tap_err(|e| warn!("Error in dynamic_playlist_update: {e}"))?
1317            .ok_or(Error::NotFound.into())
1318    }
1319    /// Dynamic Playlists: remove a DP
1320    #[instrument]
1321    async fn dynamic_playlist_remove(
1322        self,
1323        context: Context,
1324        id: DynamicPlaylistId,
1325    ) -> Result<(), SerializableLibraryError> {
1326        info!("Removing DP with id: {id:?}");
1327        DynamicPlaylist::delete(&self.db, id.into())
1328            .await?
1329            .ok_or(Error::NotFound)?;
1330        Ok(())
1331    }
1332    /// Dynamic Playlists: get a DP by its ID
1333    #[instrument]
1334    async fn dynamic_playlist_get(
1335        self,
1336        context: Context,
1337        id: DynamicPlaylistId,
1338    ) -> Option<DynamicPlaylist> {
1339        info!("Getting DP by ID: {id:?}");
1340        DynamicPlaylist::read(&self.db, id.into())
1341            .await
1342            .tap_err(|e| warn!("Error in dynamic_playlist_get: {e}"))
1343            .ok()
1344            .flatten()
1345    }
1346    /// Dynamic Playlists: get the songs of a DP
1347    #[instrument]
1348    async fn dynamic_playlist_get_songs(
1349        self,
1350        context: Context,
1351        id: DynamicPlaylistId,
1352    ) -> Option<Box<[Song]>> {
1353        info!("Getting songs in DP: {id:?}");
1354        DynamicPlaylist::run_query_by_id(&self.db, id.into())
1355            .await
1356            .tap_err(|e| warn!("Error in dynamic_playlist_get_songs: {e}"))
1357            .ok()
1358            .flatten()
1359            .map(Into::into)
1360    }
1361    /// Dynamic Playlists: export dynamic playlists to a csv file
1362    #[instrument]
1363    async fn dynamic_playlist_export(
1364        self,
1365        context: Context,
1366        path: PathBuf,
1367    ) -> Result<(), SerializableLibraryError> {
1368        info!("Exporting dynamic playlists to: {path:?}");
1369
1370        // validate the path
1371        validate_file_path(&path, "csv", false)?;
1372
1373        // read the playlists
1374        let playlists = DynamicPlaylist::read_all(&self.db)
1375            .await
1376            .tap_err(|e| warn!("Error in dynamic_playlist_export: {e}"))?;
1377
1378        // create the file
1379        let file =
1380            File::create(&path).tap_err(|e| warn!("Error in dynamic_playlist_export: {e}"))?;
1381        let writer = csv::Writer::from_writer(std::io::BufWriter::new(file));
1382        // write the playlists to the file
1383        export_dynamic_playlists(&playlists, writer)
1384            .tap_err(|e| warn!("Error in dynamic_playlist_export: {e}"))?;
1385        info!("Exported dynamic playlists to: {path:?}");
1386        Ok(())
1387    }
1388    /// Dynamic Playlists: import dynamic playlists from a csv file
1389    #[instrument]
1390    async fn dynamic_playlist_import(
1391        self,
1392        context: Context,
1393        path: PathBuf,
1394    ) -> Result<Vec<DynamicPlaylist>, SerializableLibraryError> {
1395        info!("Importing dynamic playlists from: {path:?}");
1396
1397        // validate the path
1398        validate_file_path(&path, "csv", true)?;
1399
1400        // read file
1401        let file = File::open(&path).tap_err(|e| warn!("Error in dynamic_playlist_import: {e}"))?;
1402        let reader = csv::ReaderBuilder::new()
1403            .has_headers(true)
1404            .from_reader(std::io::BufReader::new(file));
1405
1406        // read the playlists from the file
1407        let playlists = import_dynamic_playlists(reader)
1408            .tap_err(|e| warn!("Error in dynamic_playlist_import: {e}"))?;
1409
1410        if playlists.is_empty() {
1411            return Err(BackupError::NoValidPlaylists.into());
1412        }
1413
1414        // create the playlists
1415        let mut ids = Vec::new();
1416        for playlist in playlists {
1417            // if a playlist with the same name already exists, skip this one
1418            if let Ok(Some(existing_playlist)) =
1419                DynamicPlaylist::read_by_name(&self.db, playlist.name.clone()).await
1420            {
1421                info!(
1422                    "Dynamic Playlist \"{}\" already exists, will not import",
1423                    existing_playlist.name
1424                );
1425                continue;
1426            }
1427
1428            ids.push(
1429                DynamicPlaylist::create(&self.db, playlist)
1430                    .await
1431                    .tap_err(|e| warn!("Error in dynamic_playlist_import: {e}"))?
1432                    .ok_or(Error::NotCreated)?,
1433            );
1434        }
1435
1436        Ok(ids)
1437    }
1438}