mecomp_daemon/
lib.rs

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