1#![deny(clippy::missing_inline_in_public_items)]
2
3use std::{
5 net::{IpAddr, Ipv4Addr, SocketAddr},
6 path::PathBuf,
7 sync::Arc,
8};
9use 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;
18use 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
39pub const MAX_CONCURRENT_REQUESTS: usize = 4;
41
42struct EventPublisher {
46 dispatcher: Arc<Sender<Message>>,
47 event_tx: std::sync::mpsc::Sender<StateChange>,
48 handle: tokio::task::JoinHandle<()>,
49}
50
51impl EventPublisher {
52 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 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 self.handle.abort();
85 }
86}
87
88#[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 let settings = Arc::new(settings);
119
120 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 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 let db_task = tokio::task::spawn(init_database());
137
138 let (terminator, interrupt_rx) = termination::create_termination();
140 log::debug!("initialized terminator");
141
142 let event_publisher_guard = EventPublisher::new().await;
144 log::debug!("initialized event publisher");
145
146 let audio_kernel = AudioKernelSender::start(event_publisher_guard.event_tx.clone());
148 log::debug!("initialized audio kernel");
149
150 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 let db = Arc::new(db_task.await??);
161 log::debug!("initialized database");
162
163 #[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 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 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 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
211async fn run_daemon(
214 incoming: TcpListenerStream,
215 state: MusicPlayer,
216 mut interrupt_rx: InterruptReceiver,
217) -> anyhow::Result<()> {
218 let svc = MusicPlayerServer::new(state);
220
221 let shutdown_future = async move {
222 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#[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 let event_publisher = Arc::new(Sender::new().await?);
264 let (terminator, mut interrupt_rx) = termination::create_termination();
266
267 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 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 let svc = MusicPlayerServer::new(server);
284
285 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 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 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 let song_case = SongCase::new(0, vec![0], vec![0], 0, 0);
352
353 let song = create_song_with_overrides(
355 &db,
356 song_case,
357 SongChangeSet {
358 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 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 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 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 let playlist_id = client
605 .playlist_get_or_create(PlaylistName::new(name.clone()))
606 .await?
607 .into_inner();
608
609 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 let playlist_id = client
631 .playlist_clone(library_full.playlists.first().unwrap().id.ulid())
632 .await?
633 .into_inner();
634
635 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 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 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 #[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 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 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 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 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 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 #[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 #[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 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}