mecomp_daemon/
controller.rs

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