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