mecomp_daemon/
lib.rs

1#![deny(clippy::missing_inline_in_public_items)]
2
3//----------------------------------------------------------------------------------------- std lib
4use std::{
5    net::{IpAddr, Ipv4Addr, SocketAddr},
6    path::PathBuf,
7    sync::Arc,
8};
9//--------------------------------------------------------------------------------- other libraries
10use log::{error, info};
11use persistence::QueueState;
12use surrealdb::{Surreal, engine::local::Db};
13use tokio::net::TcpListener;
14use tokio::runtime::Handle;
15use tokio_stream::wrappers::TcpListenerStream;
16use tonic::transport::Server;
17use tracing::Instrument;
18//-------------------------------------------------------------------------------- MECOMP libraries
19use mecomp_core::{
20    audio::{AudioKernelSender, commands::AudioCommand},
21    config::Settings,
22    logger::{init_logger, init_tracing},
23    udp::{Message, Sender, StateChange},
24};
25use mecomp_prost::{MusicPlayerClient, TraceInterceptor, server::MusicPlayerServer};
26use mecomp_storage::db::{init_database, set_database_path};
27
28pub mod controller;
29#[cfg(feature = "dynamic_updates")]
30pub mod dynamic_updates;
31pub mod persistence;
32pub mod services;
33pub mod termination;
34#[cfg(test)]
35pub use mecomp_core::test_utils;
36
37use crate::{controller::MusicPlayer, termination::InterruptReceiver};
38
39/// The maximum number of concurrent requests.
40pub const MAX_CONCURRENT_REQUESTS: usize = 4;
41
42/// Event Publisher guard
43///
44/// This is a newtype for the event publisher that ensures it is stopped when the guard is dropped.
45struct EventPublisher {
46    dispatcher: Arc<Sender<Message>>,
47    event_tx: std::sync::mpsc::Sender<StateChange>,
48    handle: tokio::task::JoinHandle<()>,
49}
50
51impl EventPublisher {
52    /// Start the event publisher
53    pub async fn new() -> Self {
54        let (event_tx, event_rx) = std::sync::mpsc::channel();
55        let event_publisher = Arc::new(Sender::new().await.unwrap());
56        let event_publisher_clone = event_publisher.clone();
57
58        let handle = tokio::task::spawn_blocking(move || {
59            while let Ok(event) = event_rx.recv() {
60                // re-enter the async context to send the event over UDP
61                Handle::current().block_on(async {
62                    if let Err(e) = event_publisher_clone
63                        .send(Message::StateChange(event))
64                        .await
65                    {
66                        error!("Failed to send event over UDP: {e}");
67                    }
68                });
69            }
70        })
71        .instrument(tracing::info_span!("event_publisher"));
72
73        Self {
74            dispatcher: event_publisher,
75            event_tx,
76            handle: handle.into_inner(),
77        }
78    }
79}
80
81impl Drop for EventPublisher {
82    fn drop(&mut self) {
83        // Stop the event publisher thread
84        self.handle.abort();
85    }
86}
87
88// TODO: at some point, we should probably add a panic handler to the daemon to ensure graceful shutdown.
89
90/// Run the daemon
91///
92/// also initializes the logger, database, and other necessary components.
93///
94/// # Arguments
95///
96/// * `settings` - The settings to use.
97/// * `db_dir` - The directory where the database is stored.
98///   If the directory does not exist, it will be created.
99/// * `log_file_path` - The path to the file where logs will be written.
100/// * `state_file_path` - The path to the file where the queue state restored from / saved to.
101///
102/// # Errors
103///
104/// If the daemon cannot be started, an error is returned.
105///
106/// # Panics
107///
108/// Panics if the peer address of the underlying TCP transport cannot be determined.
109#[inline]
110#[allow(clippy::redundant_pub_crate)]
111pub async fn start_daemon(
112    settings: Settings,
113    db_dir: PathBuf,
114    log_file_path: Option<PathBuf>,
115    state_file_path: Option<PathBuf>,
116) -> anyhow::Result<()> {
117    // Throw the given settings into an Arc so we can share settings across threads.
118    let settings = Arc::new(settings);
119
120    // Initialize the logger, database, and tracing.
121    init_logger(settings.daemon.log_level, log_file_path);
122    set_database_path(db_dir)?;
123    tracing::subscriber::set_global_default(init_tracing())?;
124    log::debug!("initialized logging");
125
126    // bind to `localhost:{rpc_port}`, we do this as soon as possible to minimize perceived startup delay
127    let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), settings.daemon.rpc_port);
128    let listener = TcpListener::bind(server_addr).await?;
129    info!(
130        "Listening on {}, preparing to handle requests",
131        listener.local_addr()?
132    );
133    let incoming = TcpListenerStream::new(listener);
134
135    // start initializing the database asynchronously
136    let db_task = tokio::task::spawn(init_database());
137
138    // initialize the termination handler
139    let (terminator, interrupt_rx) = termination::create_termination();
140    log::debug!("initialized terminator");
141
142    // initialize the event publisher
143    let event_publisher_guard = EventPublisher::new().await;
144    log::debug!("initialized event publisher");
145
146    // Start the audio kernel.
147    let audio_kernel = AudioKernelSender::start(event_publisher_guard.event_tx.clone());
148    log::debug!("initialized audio kernel");
149
150    // optionally restore the queue state
151    if let Some(state_path) = &state_file_path {
152        info!("Restoring queue state from {}", state_path.display());
153        match QueueState::load_from_file(state_path) {
154            Ok(state) => state.restore_to(&audio_kernel),
155            Err(e) => error!("Failed to restore queue state: {e}"),
156        }
157    }
158
159    // join the db initialization task
160    let db = Arc::new(db_task.await??);
161    log::debug!("initialized database");
162
163    // Start the music library watcher.
164    #[cfg(feature = "dynamic_updates")]
165    let guard = dynamic_updates::init_music_library_watcher(
166        db.clone(),
167        &settings.daemon.library_paths,
168        settings.daemon.artist_separator.clone(),
169        settings.daemon.protected_artist_names.clone(),
170        settings.daemon.genre_separator.clone(),
171        interrupt_rx.resubscribe(),
172    )?;
173
174    // Initialize the server state.
175    let state = MusicPlayer::new(
176        db.clone(),
177        settings.clone(),
178        audio_kernel.clone(),
179        event_publisher_guard.dispatcher.clone(),
180        terminator.clone(),
181        interrupt_rx.resubscribe(),
182    );
183
184    // Start the daemon server.
185    if let Err(e) = run_daemon(incoming, state, interrupt_rx.resubscribe()).await {
186        error!("Error running daemon: {e}");
187    }
188
189    #[cfg(feature = "dynamic_updates")]
190    guard.stop();
191
192    // send a shutdown event to all clients (ignore errors)
193    let _ = event_publisher_guard
194        .dispatcher
195        .send(Message::Event(mecomp_core::udp::Event::DaemonShutdown))
196        .await;
197
198    if let Some(state_path) = &state_file_path {
199        info!("Persisting queue state to {}", state_path.display());
200        let _ = QueueState::retrieve(&audio_kernel)
201            .await
202            .and_then(|state| state.save_to_file(state_path))
203            .inspect_err(|e| error!("Failed to persist queue state: {e}"));
204    }
205
206    log::info!("Cleanup complete");
207
208    Ok(())
209}
210
211/// Run the daemon with the given settings, database, and state file path.
212/// Does not handle setup or teardown, just runs the server.
213async fn run_daemon(
214    incoming: TcpListenerStream,
215    state: MusicPlayer,
216    mut interrupt_rx: InterruptReceiver,
217) -> anyhow::Result<()> {
218    // Start the RPC server listening to the given stream of `incoming` data
219    let svc = MusicPlayerServer::new(state);
220
221    let shutdown_future = async move {
222        // Wait for the server to be stopped.
223        // This will be triggered by the signal handler.
224        match interrupt_rx.wait().await {
225            Ok(termination::Interrupted::UserInt) => info!("Stopping server per user request"),
226            Ok(termination::Interrupted::OsSigInt) => {
227                info!("Stopping server because of an os sig int");
228            }
229            Ok(termination::Interrupted::OsSigTerm) => {
230                info!("Stopping server because of an os sig term");
231            }
232            Ok(termination::Interrupted::OsSigQuit) => {
233                info!("Stopping server because of an os sig quit");
234            }
235            Err(e) => error!("Stopping server because of an unexpected error: {e}"),
236        }
237    };
238
239    info!("Daemon is ready to handle requests");
240
241    Server::builder()
242        .trace_fn(|r| tracing::trace_span!("grpc", "request" = %r.uri()))
243        .add_service(svc)
244        .serve_with_incoming_shutdown(incoming, shutdown_future)
245        .await?;
246
247    Ok(())
248}
249
250/// Initialize a test client, sends and receives messages over a channel / pipe.
251/// This is useful for testing the server without needing to start it.
252///
253/// # Errors
254///
255/// Errors if the event publisher cannot be created.
256#[inline]
257pub async fn init_test_client_server(
258    db: Arc<Surreal<Db>>,
259    settings: Arc<Settings>,
260    audio_kernel: Arc<AudioKernelSender>,
261) -> anyhow::Result<MusicPlayerClient> {
262    // initialize the event publisher
263    let event_publisher = Arc::new(Sender::new().await?);
264    // initialize the termination handler
265    let (terminator, mut interrupt_rx) = termination::create_termination();
266
267    // Build the service implementation
268    let server = MusicPlayer::new(
269        db,
270        settings.clone(),
271        audio_kernel.clone(),
272        event_publisher.clone(),
273        terminator,
274        interrupt_rx.resubscribe(),
275    );
276
277    // Bind an ephemeral local port for the in-process server
278    let listener = TcpListener::bind("127.0.0.1:0").await?;
279    let local_addr = listener.local_addr()?;
280    let incoming = TcpListenerStream::new(listener);
281
282    // Create the gRPC service
283    let svc = MusicPlayerServer::new(server);
284
285    // Spawn the server with shutdown triggered by interrupt receiver
286    tokio::spawn(async move {
287        let shutdown_future = async move {
288            let _ = interrupt_rx.wait().await;
289            info!("Stopping test server...");
290            audio_kernel.send(AudioCommand::Exit);
291            let _ = event_publisher
292                .send(Message::Event(mecomp_core::udp::Event::DaemonShutdown))
293                .await;
294            info!("Test server stopped");
295        };
296
297        if let Err(e) = Server::builder()
298            .add_service(svc)
299            .serve_with_incoming_shutdown(incoming, shutdown_future)
300            .await
301        {
302            error!("Error running test server: {e}");
303        }
304    });
305
306    // Connect a client to the local server
307    let endpoint = format!("http://{local_addr}");
308    let endpoint = tonic::transport::Channel::from_shared(endpoint)?.connect_lazy();
309
310    let client =
311        mecomp_prost::client::MusicPlayerClient::with_interceptor(endpoint, TraceInterceptor {});
312    Ok(client)
313}
314
315#[cfg(test)]
316mod test_client_tests {
317    //! Tests for:
318    //! - the `init_test_client_server` function
319    //! - daemon endpoints that aren't covered in other tests
320
321    use std::io::{Read, Write};
322
323    use super::*;
324    use anyhow::Result;
325    use mecomp_core::errors::{BackupError, SerializableLibraryError};
326    use mecomp_prost::{
327        DynamicPlaylist, DynamicPlaylistChangeSet, DynamicPlaylistCreateRequest,
328        DynamicPlaylistUpdateRequest, LibraryFull, Path, PlaylistExportRequest,
329        PlaylistImportRequest, PlaylistName, PlaylistRenameRequest, RecordIdList,
330    };
331    use mecomp_storage::{
332        db::schemas::{
333            collection::Collection,
334            dynamic::query::{Compile, Query},
335            playlist::Playlist,
336            song::SongChangeSet,
337        },
338        test_utils::{SongCase, create_song_with_overrides, init_test_database},
339    };
340
341    use pretty_assertions::{assert_eq, assert_str_eq};
342    use rstest::{fixture, rstest};
343    use tonic::Code;
344
345    #[fixture]
346    async fn db() -> Arc<Surreal<Db>> {
347        let db = Arc::new(init_test_database().await.unwrap());
348
349        // create a test song, add it to a playlist and collection
350
351        let song_case = SongCase::new(0, vec![0], vec![0], 0, 0);
352
353        // Call the create_song function
354        let song = create_song_with_overrides(
355            &db,
356            song_case,
357            SongChangeSet {
358                // need to specify overrides so that items are created in the db
359                artist: Some("Artist 0".to_string().into()),
360                album_artist: Some("Artist 0".to_string().into()),
361                album: Some("Album 0".into()),
362                path: Some("/path/to/song.mp3".into()),
363                ..Default::default()
364            },
365        )
366        .await
367        .unwrap();
368
369        // create a playlist with the song
370        let playlist = Playlist {
371            id: Playlist::generate_id(),
372            name: "Playlist 0".into(),
373            runtime: song.runtime,
374            song_count: 1,
375        };
376
377        let result = Playlist::create(&db, playlist).await.unwrap().unwrap();
378
379        Playlist::add_songs(&db, result.id, vec![song.id.clone()])
380            .await
381            .unwrap();
382
383        // create a collection with the song
384        let collection = Collection {
385            id: Collection::generate_id(),
386            name: "Collection 0".into(),
387            runtime: song.runtime,
388            song_count: 1,
389        };
390
391        let result = Collection::create(&db, collection).await.unwrap().unwrap();
392
393        Collection::add_songs(&db, result.id, vec![song.id])
394            .await
395            .unwrap();
396
397        return db;
398    }
399
400    #[fixture]
401    async fn client(#[future] db: Arc<Surreal<Db>>) -> MusicPlayerClient {
402        let settings = Arc::new(Settings::default());
403        let (tx, _) = std::sync::mpsc::channel();
404        let audio_kernel = AudioKernelSender::start(tx);
405
406        init_test_client_server(db.await, settings, audio_kernel)
407            .await
408            .unwrap()
409    }
410
411    #[tokio::test]
412    async fn test_init_test_client_server() {
413        let db = Arc::new(init_test_database().await.unwrap());
414        let settings = Arc::new(Settings::default());
415        let (tx, _) = std::sync::mpsc::channel();
416        let audio_kernel = AudioKernelSender::start(tx);
417
418        let mut client = init_test_client_server(db, settings, audio_kernel)
419            .await
420            .unwrap();
421
422        let response = client.ping(()).await.unwrap().into_inner().message;
423
424        assert_eq!(response, "pong");
425
426        // ensure that the client is shutdown properly
427        drop(client);
428    }
429
430    #[rstest]
431    #[tokio::test]
432    async fn test_library_song_get_artist(#[future] client: MusicPlayerClient) -> Result<()> {
433        let mut client = client.await;
434
435        let library_brief = client.library_brief(()).await?.into_inner();
436
437        let response = client
438            .library_song_get_artists(library_brief.songs.first().unwrap().id.ulid())
439            .await?
440            .into_inner()
441            .artists;
442
443        assert_eq!(response, library_brief.artists);
444
445        Ok(())
446    }
447
448    #[rstest]
449    #[tokio::test]
450    async fn test_library_song_get_album(#[future] client: MusicPlayerClient) -> Result<()> {
451        let mut client = client.await;
452
453        let library_brief = client.library_brief(()).await?.into_inner();
454
455        let response = client
456            .library_song_get_album(library_brief.songs.first().unwrap().id.ulid())
457            .await?
458            .into_inner()
459            .album
460            .unwrap();
461
462        assert_eq!(response, library_brief.albums.first().unwrap().clone());
463
464        Ok(())
465    }
466
467    #[rstest]
468    #[tokio::test]
469    async fn test_library_song_get_playlists(#[future] client: MusicPlayerClient) -> Result<()> {
470        let mut client = client.await;
471
472        let library_full: LibraryFull = client.library_full(()).await?.into_inner();
473
474        let response = client
475            .library_song_get_playlists(library_full.songs.first().unwrap().id.ulid())
476            .await?
477            .into_inner()
478            .playlists;
479
480        assert_eq!(response, library_full.playlists);
481
482        Ok(())
483    }
484
485    #[rstest]
486    #[tokio::test]
487    async fn test_library_album_get_artist(#[future] client: MusicPlayerClient) -> Result<()> {
488        let mut client = client.await;
489
490        let library = client.library_brief(()).await?.into_inner();
491
492        let response = client
493            .library_album_get_artists(library.albums.first().unwrap().id.ulid())
494            .await?
495            .into_inner()
496            .artists;
497
498        assert_eq!(response, library.artists);
499
500        Ok(())
501    }
502
503    #[rstest]
504    #[tokio::test]
505    async fn test_library_album_get_songs(#[future] client: MusicPlayerClient) -> Result<()> {
506        let mut client = client.await;
507
508        let library_brief = client.library_brief(()).await?.into_inner();
509
510        let response = client
511            .library_album_get_songs(library_brief.albums.first().unwrap().id.ulid())
512            .await?
513            .into_inner()
514            .songs;
515
516        assert_eq!(response, library_brief.songs);
517
518        Ok(())
519    }
520
521    #[rstest]
522    #[tokio::test]
523    async fn test_library_artist_get_songs(#[future] client: MusicPlayerClient) -> Result<()> {
524        let mut client = client.await;
525
526        let library = client.library_brief(()).await?.into_inner();
527
528        let response = client
529            .library_artist_get_songs(library.artists.first().unwrap().id.ulid())
530            .await?
531            .into_inner()
532            .songs;
533
534        assert_eq!(response, library.songs);
535
536        Ok(())
537    }
538
539    #[rstest]
540    #[tokio::test]
541    async fn test_library_artist_get_albums(#[future] client: MusicPlayerClient) -> Result<()> {
542        let mut client = client.await;
543
544        let library = client.library_brief(()).await?.into_inner();
545
546        let response = client
547            .library_artist_get_albums(library.artists.first().unwrap().id.ulid())
548            .await?
549            .into_inner()
550            .albums;
551
552        assert_eq!(response, library.albums);
553
554        Ok(())
555    }
556
557    #[rstest]
558    #[tokio::test]
559    async fn test_playback_toggle_mute(#[future] client: MusicPlayerClient) -> Result<()> {
560        let mut client = client.await;
561
562        client.playback_toggle_mute(()).await?;
563        Ok(())
564    }
565
566    #[rstest]
567    #[tokio::test]
568    async fn test_playback_stop(#[future] client: MusicPlayerClient) -> Result<()> {
569        let mut client = client.await;
570
571        client.playback_stop(()).await?;
572        Ok(())
573    }
574
575    #[rstest]
576    #[tokio::test]
577    async fn test_queue_add_list(#[future] client: MusicPlayerClient) -> Result<()> {
578        let mut client = client.await;
579
580        let library_full: LibraryFull = client.library_full(()).await?.into_inner();
581
582        let response = client
583            .queue_add_list(RecordIdList::new(vec![
584                library_full.songs.first().unwrap().id.clone().into(),
585            ]))
586            .await;
587
588        assert!(response.is_ok());
589
590        Ok(())
591    }
592
593    #[rstest]
594    #[case::get(String::from("Playlist 0"))]
595    #[case::create(String::from("Playlist 1"))]
596    #[tokio::test]
597    async fn test_playlist_get_or_create(
598        #[future] client: MusicPlayerClient,
599        #[case] name: String,
600    ) -> Result<()> {
601        let mut client = client.await;
602
603        // get or create the playlist
604        let playlist_id = client
605            .playlist_get_or_create(PlaylistName::new(name.clone()))
606            .await?
607            .into_inner();
608
609        // now get that playlist
610        let playlist = client
611            .library_playlist_get(playlist_id.ulid())
612            .await?
613            .into_inner()
614            .playlist
615            .unwrap();
616
617        assert_eq!(playlist.name, name);
618
619        Ok(())
620    }
621
622    #[rstest]
623    #[tokio::test]
624    async fn test_playlist_clone(#[future] client: MusicPlayerClient) -> Result<()> {
625        let mut client = client.await;
626
627        let library_full: LibraryFull = client.library_full(()).await?.into_inner();
628
629        // clone the only playlist in the db
630        let playlist_id = client
631            .playlist_clone(library_full.playlists.first().unwrap().id.ulid())
632            .await?
633            .into_inner();
634
635        // now get that playlist
636        let playlist = client
637            .library_playlist_get(playlist_id.ulid())
638            .await?
639            .into_inner()
640            .playlist
641            .unwrap();
642
643        assert_eq!(playlist.name, "Playlist 0 (copy)");
644
645        Ok(())
646    }
647
648    #[rstest]
649    #[tokio::test]
650    async fn test_playlist_get_songs(#[future] client: MusicPlayerClient) -> Result<()> {
651        let mut client = client.await;
652
653        let library = client.library_brief(()).await?.into_inner();
654
655        // clone the only playlist in the db
656        let response = client
657            .library_playlist_get_songs(library.playlists.first().unwrap().id.ulid())
658            .await?
659            .into_inner()
660            .songs;
661
662        assert_eq!(response, library.songs);
663
664        Ok(())
665    }
666
667    #[rstest]
668    #[tokio::test]
669    async fn test_playlist_rename(#[future] client: MusicPlayerClient) -> Result<()> {
670        let mut client = client.await;
671
672        let library_full: LibraryFull = client.library_full(()).await?.into_inner();
673
674        let target = library_full.playlists.first().unwrap();
675
676        let response = client
677            .playlist_rename(PlaylistRenameRequest::new(target.id.id.clone(), "New Name"))
678            .await
679            .unwrap()
680            .into_inner();
681
682        let expected = mecomp_prost::Playlist {
683            name: "New Name".into(),
684            ..target.clone()
685        };
686
687        assert_eq!(response, expected.clone().into());
688
689        let response = client
690            .library_playlist_get(target.id.ulid())
691            .await?
692            .into_inner()
693            .playlist
694            .unwrap();
695
696        assert_eq!(response, expected);
697        Ok(())
698    }
699
700    #[rstest]
701    #[tokio::test]
702    async fn test_collection_get_songs(#[future] client: MusicPlayerClient) -> Result<()> {
703        let mut client = client.await;
704
705        let library = client.library_brief(()).await?.into_inner();
706
707        // clone the only playlist in the db
708        let response = client
709            .library_collection_get_songs(library.collections.first().unwrap().id.ulid())
710            .await?
711            .into_inner()
712            .songs;
713
714        assert_eq!(response, library.songs);
715
716        Ok(())
717    }
718
719    #[rstest]
720    #[tokio::test]
721    async fn test_dynamic_playlist_create(#[future] client: MusicPlayerClient) -> Result<()> {
722        let mut client = client.await;
723
724        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
725
726        let response = client
727            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(
728                "Dynamic Playlist 0",
729                query,
730            ))
731            .await;
732
733        assert!(response.is_ok());
734
735        Ok(())
736    }
737
738    #[rstest]
739    #[tokio::test]
740    async fn test_dynamic_playlist_list(#[future] client: MusicPlayerClient) -> Result<()> {
741        let mut client = client.await;
742
743        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
744
745        let dynamic_playlist_id = client
746            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(
747                "Dynamic Playlist 0",
748                query,
749            ))
750            .await?
751            .into_inner();
752
753        let response = client
754            .library_dynamic_playlists(())
755            .await?
756            .into_inner()
757            .playlists;
758
759        assert_eq!(response.len(), 1);
760        assert_eq!(response.first().unwrap().id, dynamic_playlist_id);
761
762        Ok(())
763    }
764
765    #[rstest]
766    #[tokio::test]
767    async fn test_dynamic_playlist_update(#[future] client: MusicPlayerClient) -> Result<()> {
768        let mut client = client.await;
769
770        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
771
772        let dynamic_playlist_id = client
773            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(
774                "Dynamic Playlist 0",
775                &query,
776            ))
777            .await?
778            .into_inner();
779
780        let response = client
781            .dynamic_playlist_update(DynamicPlaylistUpdateRequest::new(
782                dynamic_playlist_id.id.clone(),
783                DynamicPlaylistChangeSet::new().name("Dynamic Playlist 1"),
784            ))
785            .await?
786            .into_inner();
787
788        let expected = DynamicPlaylist {
789            id: dynamic_playlist_id.clone().into(),
790            name: "Dynamic Playlist 1".into(),
791            query: query.clone().compile_for_storage(),
792        };
793
794        assert_eq!(response, expected.clone());
795
796        let response = client
797            .library_dynamic_playlist_get(dynamic_playlist_id.ulid())
798            .await?
799            .into_inner()
800            .playlist
801            .unwrap();
802
803        assert_eq!(response, expected);
804
805        Ok(())
806    }
807
808    #[rstest]
809    #[tokio::test]
810    async fn test_dynamic_playlist_remove(#[future] client: MusicPlayerClient) -> Result<()> {
811        let mut client = client.await;
812
813        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
814
815        let dynamic_playlist_id = client
816            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(
817                "Dynamic Playlist 0",
818                query,
819            ))
820            .await?
821            .into_inner();
822
823        let response = client
824            .dynamic_playlist_remove(dynamic_playlist_id.ulid())
825            .await;
826
827        assert!(response.is_ok());
828
829        let response = client
830            .library_dynamic_playlists(())
831            .await?
832            .into_inner()
833            .playlists;
834
835        assert_eq!(response.len(), 0);
836
837        Ok(())
838    }
839
840    #[rstest]
841    #[tokio::test]
842    async fn test_dynamic_playlist_get(#[future] client: MusicPlayerClient) -> Result<()> {
843        let mut client = client.await;
844
845        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
846
847        let dynamic_playlist_id = client
848            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(
849                "Dynamic Playlist 0",
850                &query,
851            ))
852            .await?
853            .into_inner();
854
855        let response = client
856            .library_dynamic_playlist_get(dynamic_playlist_id.ulid())
857            .await?
858            .into_inner()
859            .playlist
860            .unwrap();
861
862        assert_eq!(response.name, "Dynamic Playlist 0");
863        assert_eq!(response.query, query.compile_for_storage());
864
865        Ok(())
866    }
867
868    #[rstest]
869    #[tokio::test]
870    async fn test_dynamic_playlist_get_songs(#[future] client: MusicPlayerClient) -> Result<()> {
871        let mut client = client.await;
872
873        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
874
875        let dynamic_playlist_id = client
876            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(
877                "Dynamic Playlist 0",
878                query,
879            ))
880            .await?
881            .into_inner();
882
883        let response = client
884            .library_dynamic_playlist_get_songs(dynamic_playlist_id.ulid())
885            .await?
886            .into_inner()
887            .songs;
888
889        assert_eq!(response.len(), 1);
890
891        Ok(())
892    }
893
894    // Dynamic Playlist Import Tests
895    #[rstest]
896    #[tokio::test]
897    async fn test_dynamic_playlist_import(#[future] client: MusicPlayerClient) -> Result<()> {
898        let mut client = client.await;
899
900        let tmpfile = tempfile::NamedTempFile::with_suffix("dps.csv")?;
901
902        // write a csv file to the temp file
903        let mut file = tmpfile.reopen()?;
904        writeln!(file, "dynamic playlist name,query")?;
905        writeln!(file, "Dynamic Playlist 0,artist CONTAINS \"Artist 0\"")?;
906
907        let tmpfile_path = tmpfile.path().to_path_buf();
908
909        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
910
911        let response = client
912            .dynamic_playlist_import(Path::new(tmpfile_path))
913            .await?
914            .into_inner()
915            .playlists;
916
917        let expected = DynamicPlaylist {
918            id: response[0].id.clone(),
919            name: "Dynamic Playlist 0".into(),
920            query: query.compile_for_storage(),
921        };
922
923        assert_eq!(response, vec![expected]);
924
925        Ok(())
926    }
927    #[rstest]
928    #[tokio::test]
929    async fn test_dynamic_playlist_import_file_nonexistent(
930        #[future] client: MusicPlayerClient,
931    ) -> Result<()> {
932        let mut client = client.await;
933
934        let tmpfile = tempfile::NamedTempFile::with_suffix("dps.csv")?;
935
936        // write a csv file to the temp file
937        let mut file = tmpfile.reopen()?;
938        writeln!(file, "artist,album,album_artist,title")?;
939
940        let tmpfile_path = "/this/path/does/not/exist.csv";
941
942        let response = client
943            .dynamic_playlist_import(Path::new(tmpfile_path))
944            .await;
945        assert!(response.is_err(), "response: {response:?}");
946        assert_eq!(
947            response.unwrap_err().message(),
948            format!("Backup Error: The file \"{tmpfile_path}\" does not exist")
949        );
950        Ok(())
951    }
952    #[rstest]
953    #[tokio::test]
954    async fn test_dynamic_playlist_import_file_wrong_extension(
955        #[future] client: MusicPlayerClient,
956    ) -> Result<()> {
957        let mut client = client.await;
958
959        let tmpfile = tempfile::NamedTempFile::with_suffix("dps.txt")?;
960
961        // write a csv file to the temp file
962        let mut file = tmpfile.reopen()?;
963        writeln!(file, "artist,album,album_artist,title")?;
964
965        let response = client
966            .dynamic_playlist_import(Path::new(tmpfile.path()))
967            .await;
968        assert!(response.is_err(), "response: {response:?}");
969        assert_str_eq!(
970            response.unwrap_err().message(),
971            format!(
972                "Backup Error: The file \"{}\" has the wrong extension, expected: csv",
973                tmpfile.path().display()
974            )
975        );
976        Ok(())
977    }
978    #[rstest]
979    #[tokio::test]
980    async fn test_dynamic_playlist_import_file_is_directory(
981        #[future] client: MusicPlayerClient,
982    ) -> Result<()> {
983        let mut client = client.await;
984
985        let tmpfile = tempfile::tempdir()?;
986
987        let response = client
988            .dynamic_playlist_import(Path::new(tmpfile.path()))
989            .await;
990        assert!(response.is_err());
991        let response = response.unwrap_err();
992        assert_eq!(response.code(), Code::InvalidArgument);
993        assert_str_eq!(
994            response.message(),
995            format!(
996                "Backup Error: {} is a directory, not a file",
997                tmpfile.path().display()
998            )
999        );
1000        Ok(())
1001    }
1002    #[rstest]
1003    #[tokio::test]
1004    async fn test_dynamic_playlist_import_file_invalid_format(
1005        #[future] client: MusicPlayerClient,
1006    ) -> Result<()> {
1007        let mut client = client.await;
1008
1009        let tmpfile = tempfile::NamedTempFile::with_suffix("dps.csv")?;
1010
1011        // write a csv file to the temp file
1012        let mut file = tmpfile.reopen()?;
1013        writeln!(file, "artist,album,album_artist,title")?;
1014
1015        let tmpfile_path = tmpfile.path().to_path_buf();
1016
1017        let response = client
1018            .dynamic_playlist_import(Path::new(tmpfile_path))
1019            .await;
1020        assert!(response.is_err());
1021        let response = response.unwrap_err();
1022        assert_eq!(response.code(), Code::InvalidArgument);
1023        assert_str_eq!(
1024            response.message(),
1025            "Backup Error: No valid playlists were found in the csv file."
1026        );
1027        Ok(())
1028    }
1029    #[rstest]
1030    #[tokio::test]
1031    async fn test_dynamic_playlist_import_file_invalid_query(
1032        #[future] client: MusicPlayerClient,
1033    ) -> Result<()> {
1034        let mut client = client.await;
1035
1036        let tmpfile = tempfile::NamedTempFile::with_suffix("dps.csv")?;
1037
1038        // write a csv file to the temp file
1039        let mut file = tmpfile.reopen()?;
1040        writeln!(file, "dynamic playlist name,query")?;
1041        writeln!(file, "Dynamic Playlist 0,artist CONTAINS \"Artist 0\"")?;
1042        writeln!(file, "Dynamic Playlist 1,artist CONTAINS \"")?;
1043
1044        let response = client
1045            .dynamic_playlist_import(Path::new(tmpfile.path()))
1046            .await;
1047        assert!(response.is_err());
1048        let response = response.unwrap_err();
1049        let expected = SerializableLibraryError::BackupError(
1050            BackupError::InvalidDynamicPlaylistQuery(
1051                String::from(
1052                    "failed to parse field at 16, (inner: Mismatch at 16: seq [114, 101, 108, 101, 97, 115, 101, 95, 121, 101, 97, 114] expect: 114, found: 34)",
1053                ),
1054                2,
1055            ),
1056        );
1057        assert_eq!(response.code(), Code::Internal);
1058        assert_str_eq!(
1059            response.message(),
1060            expected.to_string(),
1061            "response: {response:?}"
1062        );
1063        Ok(())
1064    }
1065
1066    // Dynamic Playlist Export Tests
1067    #[rstest]
1068    #[tokio::test]
1069    async fn test_dynamic_playlist_export(#[future] client: MusicPlayerClient) -> Result<()> {
1070        let mut client = client.await;
1071
1072        let tmpdir = tempfile::tempdir()?;
1073        let path = tmpdir.path().join("test.csv");
1074
1075        let query: Query = "artist CONTAINS \"Artist 0\"".parse()?;
1076        let _ = client
1077            .dynamic_playlist_create(DynamicPlaylistCreateRequest::new(
1078                "Dynamic Playlist 0",
1079                query.clone(),
1080            ))
1081            .await?;
1082
1083        let expected = r#"dynamic playlist name,query
1084Dynamic Playlist 0,"artist CONTAINS ""Artist 0"""
1085"#;
1086
1087        let response = client
1088            .dynamic_playlist_export(Path::new(path.clone()))
1089            .await;
1090        assert!(response.is_ok(), "response: {response:?}");
1091
1092        let mut file = std::fs::File::open(path.clone())?;
1093        let mut contents = String::new();
1094        file.read_to_string(&mut contents)?;
1095        assert_str_eq!(contents, expected);
1096
1097        Ok(())
1098    }
1099    #[rstest]
1100    #[tokio::test]
1101    async fn test_dynamic_playlist_export_file_exists(
1102        #[future] client: MusicPlayerClient,
1103    ) -> Result<()> {
1104        let mut client = client.await;
1105
1106        let tmpfile = tempfile::NamedTempFile::with_suffix("dps.csv")?;
1107
1108        let response = client
1109            .dynamic_playlist_export(Path::new(tmpfile.path()))
1110            .await;
1111        assert!(response.is_ok(), "response: {response:?}");
1112        Ok(())
1113    }
1114    #[rstest]
1115    #[tokio::test]
1116    async fn test_dynamic_playlist_export_file_is_directory(
1117        #[future] client: MusicPlayerClient,
1118    ) -> Result<()> {
1119        let mut client = client.await;
1120
1121        let tmpfile = tempfile::tempdir()?;
1122
1123        let response = client
1124            .dynamic_playlist_export(Path::new(tmpfile.path()))
1125            .await;
1126        assert!(response.is_err());
1127        let response = response.unwrap_err();
1128        assert_eq!(response.code(), Code::InvalidArgument);
1129        let expected = SerializableLibraryError::BackupError(BackupError::PathIsDirectory(
1130            tmpfile.path().to_path_buf(),
1131        ));
1132        assert_str_eq!(
1133            response.message(),
1134            expected.to_string(),
1135            "response: {response:?}"
1136        );
1137
1138        Ok(())
1139    }
1140    #[rstest]
1141    #[tokio::test]
1142    async fn test_dynamic_playlist_export_file_invalid_extension(
1143        #[future] client: MusicPlayerClient,
1144    ) -> Result<()> {
1145        let mut client = client.await;
1146
1147        let tmpfile = tempfile::NamedTempFile::with_suffix("dps.txt")?;
1148
1149        let response = client
1150            .dynamic_playlist_export(Path::new(tmpfile.path()))
1151            .await;
1152        assert!(response.is_err(), "response: {response:?}");
1153        let err = response.unwrap_err();
1154        let expected = SerializableLibraryError::BackupError(BackupError::WrongExtension(
1155            tmpfile.path().to_path_buf(),
1156            String::from("csv"),
1157        ))
1158        .to_string();
1159        assert_str_eq!(&err.message(), &expected,);
1160
1161        Ok(())
1162    }
1163
1164    // Playlist import test
1165    #[rstest]
1166    #[tokio::test]
1167    async fn test_playlist_import(#[future] client: MusicPlayerClient) -> Result<()> {
1168        let mut client = client.await;
1169
1170        let tmpfile = tempfile::NamedTempFile::with_suffix("pl.m3u")?;
1171
1172        // write a csv file to the temp file
1173        let mut file = tmpfile.reopen()?;
1174        write!(
1175            file,
1176            r"#EXTM3U
1177#EXTINF:123,Sample Artist - Sample title
1178/path/to/song.mp3
1179"
1180        )?;
1181
1182        let tmpfile_path = tmpfile.path().to_path_buf();
1183
1184        let response = client
1185            .playlist_import(PlaylistImportRequest::new(tmpfile_path))
1186            .await;
1187        assert!(response.is_ok());
1188        let response = response.unwrap().into_inner();
1189
1190        let playlist = client
1191            .library_playlist_get(response.ulid())
1192            .await?
1193            .into_inner()
1194            .playlist
1195            .unwrap();
1196
1197        assert_eq!(playlist.name, "Imported Playlist");
1198        assert_eq!(playlist.song_count, 1);
1199
1200        let songs = client
1201            .library_playlist_get_songs(response.ulid())
1202            .await?
1203            .into_inner()
1204            .songs;
1205        assert_eq!(songs.len(), 1);
1206        assert_eq!(songs[0].path, "/path/to/song.mp3");
1207
1208        Ok(())
1209    }
1210
1211    #[rstest]
1212    #[tokio::test]
1213    async fn test_playlist_export(#[future] client: MusicPlayerClient) -> Result<()> {
1214        let mut client = client.await;
1215
1216        let tmpdir = tempfile::tempdir()?;
1217        let path = tmpdir.path().join("test.m3u");
1218
1219        let library_full: LibraryFull = client.library_full(()).await?.into_inner();
1220
1221        let playlist = library_full.playlists[0].clone();
1222
1223        let response = client
1224            .playlist_export(PlaylistExportRequest::new(
1225                playlist.id.clone(),
1226                path.clone(),
1227            ))
1228            .await;
1229        assert!(response.is_ok(), "response: {response:?}");
1230
1231        let mut file = std::fs::File::open(path.clone())?;
1232        let mut contents = String::new();
1233        file.read_to_string(&mut contents)?;
1234        assert_str_eq!(
1235            contents,
1236            r"#EXTM3U
1237
1238#PLAYLIST:Playlist 0
1239
1240#EXTINF:120,Song 0 - Artist 0
1241#EXTGENRE:Genre 0
1242#EXTALB:Artist 0
1243/path/to/song.mp3
1244
1245"
1246        );
1247
1248        Ok(())
1249    }
1250}