1#![deny(clippy::missing_inline_in_public_items)]
2
3use std::{
5 net::{IpAddr, Ipv4Addr},
6 sync::Arc,
7};
8use 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};
21use 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#[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 let settings = Arc::new(settings);
76
77 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 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 #[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 let (event_tx, event_rx) = std::sync::mpsc::channel();
103 let event_publisher = Arc::new(RwLock::new(Sender::new().await?));
104
105 let (terminator, mut interrupt_rx) = termination::create_termination();
107
108 let audio_kernel = AudioKernelSender::start(event_tx);
110
111 let server = MusicPlayerServer::new(
113 db.clone(),
114 settings.clone(),
115 audio_kernel.clone(),
116 event_publisher.clone(),
117 terminator.clone(),
118 );
119
120 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 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 .filter_map(|r| future::ready(r.ok()))
146 .map(BaseChannel::with_defaults)
147 .max_channels_per_key(10, |t| t.transport().peer_addr().unwrap().ip())
149 .map(|channel| channel.execute(server.clone().serve()).for_each(spawn))
153 .buffer_unordered(10)
158 .for_each(async |()| {})
159 .fuse();
161 let (abort_handle, abort_registration) = AbortHandle::new_pair();
163 let abortable_server_handle = Abortable::new(server_handle, abort_registration);
164
165 tokio::select! {
167 _ = abortable_server_handle => {
168 error!("Server stopped unexpectedly");
169 },
170 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_handle.abort();
185
186 audio_kernel.send(AudioCommand::Exit);
188
189 #[cfg(feature = "dynamic_updates")]
190 guard.stop();
191
192 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#[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 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 .for_each(async |response| {
234 tokio::spawn(response);
235 }) => {},
236 _ = interrupt_rx.recv() => {
238 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 Ok(MusicPlayerClient::new(tarpc::client::Config::default(), client_transport).spawn())
250}
251
252#[cfg(test)]
253mod test_client_tests {
254 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 let song_case = SongCase::new(0, vec![0], vec![0], 0, 0);
286
287 let song = create_song_with_overrides(
289 &db,
290 song_case,
291 SongChangeSet {
292 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 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 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 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 let playlist_id = client
552 .playlist_get_or_create(ctx, name.clone())
553 .await?
554 .unwrap();
555
556 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 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 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 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 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 #[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 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 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 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 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 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 #[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 #[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 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}