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