1use std::{
2 collections::{HashMap, HashSet},
3 path::PathBuf,
4 time::Duration,
5};
6
7use log::{debug, error, info, warn};
8use mecomp_analysis::{
9 clustering::{ClusteringHelper, KOptimal, NotInitialized},
10 decoder::{Decoder, MecompDecoder},
11};
12use mecomp_core::{
13 config::ReclusterSettings,
14 state::library::{LibraryBrief, LibraryFull, LibraryHealth},
15};
16use one_or_many::OneOrMany;
17use surrealdb::{Connection, Surreal};
18use tap::TapFallible;
19use tracing::{Instrument, instrument};
20use walkdir::WalkDir;
21
22use mecomp_storage::{
23 db::{
24 health::{
25 count_albums, count_artists, count_collections, count_dynamic_playlists,
26 count_orphaned_albums, count_orphaned_artists, count_orphaned_collections,
27 count_orphaned_playlists, count_playlists, count_songs, count_unanalyzed_songs,
28 },
29 schemas::{
30 album::Album,
31 analysis::Analysis,
32 artist::Artist,
33 collection::Collection,
34 dynamic::DynamicPlaylist,
35 playlist::Playlist,
36 song::{Song, SongMetadata},
37 },
38 },
39 errors::Error,
40 util::MetadataConflictResolution,
41};
42
43use crate::termination::InterruptReceiver;
44
45#[instrument]
53#[inline]
54pub async fn rescan<C: Connection>(
55 db: &Surreal<C>,
56 paths: &[PathBuf],
57 artist_name_separator: &OneOrMany<String>,
58 protected_artist_names: &OneOrMany<String>,
59 genre_separator: Option<&str>,
60 conflict_resolution_mode: MetadataConflictResolution,
61) -> Result<(), Error> {
62 let songs = Song::read_all(db).await?;
64 let mut paths_to_skip = HashSet::new(); async {
68 for song in songs {
69 let path = song.path.clone();
70 if !path.exists() { warn!("Song {} no longer exists, deleting", path.to_string_lossy());
72 Song::delete(db, song.id).await?;
73 continue;
74 }
75
76 debug!("loading metadata for {}", path.to_string_lossy());
77 match SongMetadata::load_from_path(path.clone(), artist_name_separator,protected_artist_names, genre_separator) {
79 Ok(metadata) if metadata != SongMetadata::from(&song) => {
81 let log_postfix = if conflict_resolution_mode == MetadataConflictResolution::Skip {
82 "but conflict resolution mode is \"skip\", so we do nothing"
83 } else {
84 "resolving conflict"
85 };
86 info!(
87 "{} has conflicting metadata with index, {log_postfix}",
88 path.display(),
89 );
90
91 match conflict_resolution_mode {
92 MetadataConflictResolution::Overwrite => {
94 Song::update(db, song.id.clone(), metadata.merge_with_song(&song)).await?;
96 }
97 MetadataConflictResolution::Skip => {}
99 }
100 }
101 Err(e) => {
103 warn!(
104 "Error reading metadata for {}: {e}",
105 path.display()
106 );
107 info!("assuming the file isn't a song or doesn't exist anymore, removing from library");
108 Song::delete(db, song.id).await?;
109 }
110 _ => {}
112 }
113
114 paths_to_skip.insert(path);
116 }
117
118 <Result<(), Error>>::Ok(())
119 }.instrument(tracing::info_span!("Checking library for missing or outdated songs")).await?;
120
121 let mut visited_paths = paths_to_skip;
123
124 debug!("Indexing paths: {paths:?}");
125 async {
126 for path in paths
127 .iter()
128 .filter_map(|p| {
129 p.canonicalize()
130 .tap_err(|e| warn!("Error canonicalizing path: {e}"))
131 .ok()
132 })
133 .flat_map(|x| WalkDir::new(x).into_iter())
134 .filter_map(|x| x.tap_err(|e| warn!("Error reading path: {e}")).ok())
135 .filter_map(|x| x.file_type().is_file().then_some(x))
136 {
137 if !visited_paths.insert(path.path().to_owned()) {
138 continue;
139 }
140
141 let displayable_path = path.path().display();
142
143 match SongMetadata::load_from_path(
145 path.path().to_owned(),
146 artist_name_separator,
147 protected_artist_names,
148 genre_separator,
149 ) {
150 Ok(metadata) => Song::try_load_into_db(db, metadata).await.map_or_else(
151 |e| warn!("Error indexing {displayable_path}: {e}"),
152 |_| debug!("Indexed {displayable_path}"),
153 ),
154 Err(e) => warn!("Error reading metadata for {displayable_path}: {e}"),
155 }
156 }
157
158 <Result<(), Error>>::Ok(())
159 }
160 .instrument(tracing::info_span!("Indexing new songs"))
161 .await?;
162
163 macro_rules! delete_orphans {
165 ($model:ident, $db:expr) => {
166 let orphans = $model::delete_orphaned($db)
167 .instrument(tracing::info_span!(concat!(
168 "Deleting orphaned ",
169 stringify!($model)
170 )))
171 .await?;
172 if !orphans.is_empty() {
173 info!("Deleted orphaned {}: {orphans:?}", stringify!($model));
174 }
175 };
176 }
177
178 delete_orphans!(Album, db);
179 delete_orphans!(Artist, db);
180 delete_orphans!(Collection, db);
181 delete_orphans!(Playlist, db);
182
183 info!("Library rescan complete");
184 info!("Library health: {:?}", health(db).await?);
185
186 Ok(())
187}
188
189#[instrument]
205#[inline]
206pub async fn analyze<C: Connection>(
207 db: &Surreal<C>,
208 mut interrupt: InterruptReceiver,
209 overwrite: bool,
210) -> Result<(), Error> {
211 if overwrite {
212 async {
214 for analysis in Analysis::read_all(db).await? {
215 Analysis::delete(db, analysis.id.clone()).await?;
216 }
217 <Result<(), Error>>::Ok(())
218 }
219 .instrument(tracing::info_span!("Deleting existing analyses"))
220 .await?;
221 }
222
223 let songs_to_analyze: Vec<Song> = Analysis::read_songs_without_analysis(db).await?;
225 let paths = songs_to_analyze
227 .iter()
228 .map(|song| (song.path.clone(), song.id.clone()))
229 .collect::<HashMap<_, _>>();
230
231 let keys = paths.keys().cloned().collect::<Vec<_>>();
232
233 let (tx, rx) = std::sync::mpsc::channel();
234
235 let Ok(decoder) = MecompDecoder::new() else {
236 error!("Error creating decoder");
237 return Ok(());
238 };
239
240 let handle = tokio::task::spawn_blocking(move || decoder.analyze_paths_with_callback(keys, tx));
242 let abort = handle.abort_handle();
243
244 async {
245 for (song_path, maybe_analysis) in rx {
246 if interrupt.is_stopped() {
247 info!("Analysis interrupted");
248 break;
249 }
250
251 let displayable_path = song_path.display();
252
253 let Some(song_id) = paths.get(&song_path) else {
254 error!("No song id found for path: {displayable_path}");
255 continue;
256 };
257
258 match maybe_analysis {
259 Ok(analysis) => Analysis::create(
260 db,
261 song_id.clone(),
262 Analysis {
263 id: Analysis::generate_id(),
264 features: *analysis.inner(),
265 },
266 )
267 .await?
268 .map_or_else(
269 || {
270 warn!(
271 "Error analyzing {displayable_path}: song either wasn't found or already has an analysis"
272 );
273 },
274 |_| debug!("Analyzed {displayable_path}"),
275 ),
276 Err(e) => {
277 error!("Error analyzing {displayable_path}: {e}");
278 }
279 }
280 }
281
282 <Result<(), Error>>::Ok(())
283 }
284 .instrument(tracing::info_span!("Adding analyses to database"))
285 .await?;
286
287 tokio::select! {
288 _ = interrupt.wait() => {
290 info!("Analysis interrupted");
291 abort.abort();
292 }
293 result = handle => match result {
295 Ok(Ok(())) => {
296 info!("Analysis complete");
297 info!("Library health: {:?}", health(db).await?);
298 }
299 Ok(Err(e)) => {
300 error!("Error analyzing songs: {e}");
301 }
302 Err(e) => {
303 error!("Error joining task: {e}");
304 }
305 }
306 }
307
308 Ok(())
309}
310
311#[instrument]
319#[inline]
320pub async fn recluster<C: Connection>(
321 db: &Surreal<C>,
322 settings: ReclusterSettings,
323 mut interrupt: InterruptReceiver,
324) -> Result<(), Error> {
325 let samples = Analysis::read_all(db).await?;
327
328 if samples.is_empty() {
329 info!("No analyses found, nothing to recluster");
330 return Ok(());
331 }
332
333 let samples_ref = samples.clone();
334
335 let clustering = move || {
337 let model: ClusteringHelper<NotInitialized> = match ClusteringHelper::new(
338 samples_ref
339 .iter()
340 .map(Into::into)
341 .collect::<Vec<mecomp_analysis::Analysis>>()
342 .into(),
343 settings.max_clusters,
344 KOptimal::GapStatistic {
345 b: settings.gap_statistic_reference_datasets,
346 },
347 settings.algorithm.into(),
348 settings.projection_method.into(),
349 ) {
350 Err(e) => {
351 error!("There was an error creating the clustering helper: {e}",);
352 return None;
353 }
354 Ok(kmeans) => kmeans,
355 };
356
357 let model = match model.initialize() {
358 Err(e) => {
359 error!("There was an error initializing the clustering helper: {e}",);
360 return None;
361 }
362 Ok(kmeans) => kmeans.cluster(),
363 };
364
365 Some(model)
366 };
367
368 let handle = tokio::task::spawn_blocking(clustering)
370 .instrument(tracing::info_span!("Clustering library"));
371 let abort = handle.inner().abort_handle();
372
373 let model = tokio::select! {
375 _ = interrupt.wait() => {
376 info!("Reclustering interrupted");
377 abort.abort();
378 return Ok(());
379 }
380 result = handle => match result {
381 Ok(Some(model)) => model,
382 Ok(None) => {
383 return Ok(());
384 }
385 Err(e) => {
386 error!("Error joining task: {e}");
387 return Ok(());
388 }
389 }
390 };
391
392 async {
394 for collection in Collection::read_all(db).await? {
397 Collection::delete(db, collection.id.clone()).await?;
398 }
399
400 <Result<(), Error>>::Ok(())
401 }
402 .instrument(tracing::info_span!("Deleting old collections"))
403 .await?;
404
405 async {
407 let clusters = model.extract_analysis_clusters(samples);
408
409 for (i, cluster) in clusters.iter().filter(|c| !c.is_empty()).enumerate() {
411 let collection = Collection::create(
412 db,
413 Collection {
414 id: Collection::generate_id(),
415 name: format!("Collection {i}"),
416 runtime: Duration::default(),
417 song_count: Default::default(),
418 },
419 )
420 .await?
421 .ok_or(Error::NotCreated)?;
422
423 let mut songs = Vec::with_capacity(cluster.len());
424
425 async {
426 for analysis in cluster {
427 songs.push(Analysis::read_song(db, analysis.id.clone()).await?.id);
428 }
429
430 Collection::add_songs(db, collection.id.clone(), songs).await?;
431
432 <Result<(), Error>>::Ok(())
433 }
434 .instrument(tracing::info_span!("Adding songs to collection"))
435 .await?;
436 }
437 Ok::<(), Error>(())
438 }
439 .instrument(tracing::info_span!("Creating new collections"))
440 .await?;
441
442 info!("Library recluster complete");
443 info!("Library health: {:?}", health(db).await?);
444
445 Ok(())
446}
447
448#[instrument]
454#[inline]
455pub async fn brief<C: Connection>(db: &Surreal<C>) -> Result<LibraryBrief, Error> {
456 Ok(LibraryBrief {
457 artists: Artist::read_all_brief(db).await?.into_boxed_slice(),
458 albums: Album::read_all_brief(db).await?.into_boxed_slice(),
459 songs: Song::read_all_brief(db).await?.into_boxed_slice(),
460 playlists: Playlist::read_all_brief(db).await?.into_boxed_slice(),
461 collections: Collection::read_all_brief(db).await?.into_boxed_slice(),
462 dynamic_playlists: DynamicPlaylist::read_all(db).await?.into_boxed_slice(),
463 })
464}
465
466#[instrument]
472#[inline]
473pub async fn full<C: Connection>(db: &Surreal<C>) -> Result<LibraryFull, Error> {
474 Ok(LibraryFull {
475 artists: Artist::read_all(db).await?.into(),
476 albums: Album::read_all(db).await?.into(),
477 songs: Song::read_all(db).await?.into(),
478 playlists: Playlist::read_all(db).await?.into(),
479 collections: Collection::read_all(db).await?.into(),
480 dynamic_playlists: DynamicPlaylist::read_all(db).await?.into(),
481 })
482}
483
484#[instrument]
492#[inline]
493pub async fn health<C: Connection>(db: &Surreal<C>) -> Result<LibraryHealth, Error> {
494 Ok(LibraryHealth {
495 artists: count_artists(db).await?,
496 albums: count_albums(db).await?,
497 songs: count_songs(db).await?,
498 unanalyzed_songs: Some(count_unanalyzed_songs(db).await?),
499 playlists: count_playlists(db).await?,
500 collections: count_collections(db).await?,
501 dynamic_playlists: count_dynamic_playlists(db).await?,
502 orphaned_artists: count_orphaned_artists(db).await?,
503 orphaned_albums: count_orphaned_albums(db).await?,
504 orphaned_playlists: count_orphaned_playlists(db).await?,
505 orphaned_collections: count_orphaned_collections(db).await?,
506 })
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::test_utils::init;
513
514 use mecomp_core::config::{ClusterAlgorithm, ProjectionMethod};
515 use mecomp_storage::db::schemas::song::{SongChangeSet, SongMetadata};
516 use mecomp_storage::test_utils::{
517 ARTIST_NAME_SEPARATOR, SongCase, arb_analysis_features, arb_song_case, arb_vec,
518 create_song_metadata, create_song_with_overrides, init_test_database,
519 };
520 use one_or_many::OneOrMany;
521 use pretty_assertions::assert_eq;
522 use rstest::rstest;
523
524 #[tokio::test]
525 #[allow(clippy::too_many_lines)]
526 async fn test_rescan() {
527 init();
528 let tempdir = tempfile::tempdir().unwrap();
529 let db = init_test_database().await.unwrap();
530
531 let song_cases = arb_vec(&arb_song_case(), 10..=15)();
533 let metadatas = song_cases
534 .into_iter()
535 .map(|song_case| create_song_metadata(&tempdir, song_case))
536 .collect::<Result<Vec<_>, _>>()
537 .unwrap();
538 let song_with_nonexistent_path = create_song_with_overrides(
541 &db,
542 arb_song_case()(),
543 SongChangeSet {
544 path: Some(tempdir.path().join("nonexistent.mp3")),
545 ..Default::default()
546 },
547 )
548 .await
549 .unwrap();
550 let mut metadata_of_song_with_outdated_metadata =
551 create_song_metadata(&tempdir, arb_song_case()()).unwrap();
552 metadata_of_song_with_outdated_metadata.genre = OneOrMany::None;
553 let song_with_outdated_metadata =
554 Song::try_load_into_db(&db, metadata_of_song_with_outdated_metadata)
555 .await
556 .unwrap();
557 let invalid_song_path = tempdir.path().join("invalid1.mp3");
559 std::fs::write(&invalid_song_path, "this is not a song").unwrap();
560 let invalid_song_path = tempdir.path().join("invalid2.mp3");
562 std::fs::write(&invalid_song_path, "this is not a song").unwrap();
563 let song_with_invalid_metadata = create_song_with_overrides(
564 &db,
565 arb_song_case()(),
566 SongChangeSet {
567 path: Some(tempdir.path().join("invalid2.mp3")),
568 ..Default::default()
569 },
570 )
571 .await
572 .unwrap();
573
574 rescan(
576 &db,
577 &[tempdir.path().to_owned()],
578 &ARTIST_NAME_SEPARATOR.to_string().into(),
579 &OneOrMany::None,
580 Some(ARTIST_NAME_SEPARATOR),
581 MetadataConflictResolution::Overwrite,
582 )
583 .await
584 .unwrap();
585
586 assert_eq!(
589 Song::read(&db, song_with_nonexistent_path.id)
590 .await
591 .unwrap(),
592 None
593 );
594 assert_eq!(
596 Song::read(&db, song_with_invalid_metadata.id)
597 .await
598 .unwrap(),
599 None
600 );
601 assert!(
603 Song::read(&db, song_with_outdated_metadata.id)
604 .await
605 .unwrap()
606 .unwrap()
607 .genre
608 .is_some()
609 );
610 for metadata in metadatas {
613 let song = Song::read_by_path(&db, metadata.path.clone())
615 .await
616 .unwrap();
617 assert!(song.is_some());
618 let song = song.unwrap();
619
620 assert_eq!(SongMetadata::from(&song), metadata);
622
623 let artists = Artist::read_by_names(&db, Vec::from(metadata.artist.clone()))
625 .await
626 .unwrap();
627 assert_eq!(artists.len(), metadata.artist.len());
628 for artist in &artists {
630 assert!(metadata.artist.contains(&artist.name));
631 assert!(
632 Artist::read_songs(&db, artist.id.clone())
633 .await
634 .unwrap()
635 .contains(&song)
636 );
637 }
638 if let Ok(song_artists) = Song::read_artist(&db, song.id.clone()).await {
640 for artist in artists {
641 assert!(song_artists.contains(&artist));
642 }
643 } else {
644 panic!("Error reading song artists");
645 }
646
647 let album = Album::read_by_name_and_album_artist(
649 &db,
650 &metadata.album,
651 metadata.album_artist.clone(),
652 )
653 .await
654 .unwrap();
655 assert!(album.is_some());
656 let album = album.unwrap();
657 assert_eq!(
659 Song::read_album(&db, song.id.clone()).await.unwrap(),
660 Some(album.clone())
661 );
662 assert!(
664 Album::read_songs(&db, album.id.clone())
665 .await
666 .unwrap()
667 .contains(&song)
668 );
669
670 let album_artists =
672 Artist::read_by_names(&db, Vec::from(metadata.album_artist.clone()))
673 .await
674 .unwrap();
675 assert_eq!(album_artists.len(), metadata.album_artist.len());
676 for album_artist in album_artists {
678 assert!(metadata.album_artist.contains(&album_artist.name));
679 assert!(
680 Artist::read_albums(&db, album_artist.id.clone())
681 .await
682 .unwrap()
683 .contains(&album)
684 );
685 }
686 }
687 }
688
689 #[tokio::test]
690 async fn rescan_deletes_preexisting_orphans() {
691 init();
692 let tempdir = tempfile::tempdir().unwrap();
693 let db = init_test_database().await.unwrap();
694
695 let metadata = create_song_metadata(&tempdir, arb_song_case()()).unwrap();
697 let song = Song::try_load_into_db(&db, metadata.clone()).await.unwrap();
698
699 std::fs::remove_file(&song.path).unwrap();
701 Song::delete(&db, (song.id.clone(), false)).await.unwrap();
702
703 rescan(
705 &db,
706 &[tempdir.path().to_owned()],
707 &ARTIST_NAME_SEPARATOR.to_string().into(),
708 &OneOrMany::None,
709 Some(ARTIST_NAME_SEPARATOR),
710 MetadataConflictResolution::Overwrite,
711 )
712 .await
713 .unwrap();
714
715 assert_eq!(Song::read_all(&db).await.unwrap().len(), 0);
717 assert_eq!(Album::read_all(&db).await.unwrap().len(), 0);
718 let artists = Artist::read_all(&db).await.unwrap();
719 for artist in artists {
720 assert_eq!(artist.album_count, 0);
721 assert_eq!(artist.song_count, 0);
722 }
723 assert_eq!(Artist::read_all(&db).await.unwrap().len(), 0);
724 }
725
726 #[tokio::test]
727 async fn rescan_deletes_orphaned_albums_and_artists() {
728 init();
729 let tempdir = tempfile::tempdir().unwrap();
730 let db = init_test_database().await.unwrap();
731
732 let metadata = create_song_metadata(&tempdir, arb_song_case()()).unwrap();
734 let song = Song::try_load_into_db(&db, metadata.clone()).await.unwrap();
735 let artist = Artist::read_by_names(&db, Vec::from(metadata.artist.clone()))
736 .await
737 .unwrap()
738 .pop()
739 .unwrap();
740 let album = Album::read_by_name_and_album_artist(
741 &db,
742 &metadata.album,
743 metadata.album_artist.clone(),
744 )
745 .await
746 .unwrap()
747 .unwrap();
748
749 std::fs::remove_file(&song.path).unwrap();
751
752 rescan(
754 &db,
755 &[tempdir.path().to_owned()],
756 &ARTIST_NAME_SEPARATOR.to_string().into(),
757 &OneOrMany::None,
758 Some(ARTIST_NAME_SEPARATOR),
759 MetadataConflictResolution::Overwrite,
760 )
761 .await
762 .unwrap();
763
764 assert_eq!(Artist::read(&db, artist.id.clone()).await.unwrap(), None);
766 assert_eq!(Album::read(&db, album.id.clone()).await.unwrap(), None);
767 }
768
769 #[tokio::test]
770 async fn test_analyze() {
771 init();
772 let dir = tempfile::tempdir().unwrap();
773 let db = init_test_database().await.unwrap();
774 let interrupt = InterruptReceiver::dummy();
775
776 let song_cases = arb_vec(&arb_song_case(), 10..=15)();
778 let song_cases = song_cases.into_iter().enumerate().map(|(i, sc)| SongCase {
779 song: u8::try_from(i).unwrap(),
780 ..sc
781 });
782 let metadatas = song_cases
783 .into_iter()
784 .map(|song_case| create_song_metadata(&dir, song_case))
785 .collect::<Result<Vec<_>, _>>()
786 .unwrap();
787 for metadata in &metadatas {
788 Song::try_load_into_db(&db, metadata.clone()).await.unwrap();
789 }
790
791 assert_eq!(
793 Analysis::read_songs_without_analysis(&db)
794 .await
795 .unwrap()
796 .len(),
797 metadatas.len()
798 );
799
800 analyze(&db, interrupt, true).await.unwrap();
802
803 assert_eq!(
805 Analysis::read_songs_without_analysis(&db)
806 .await
807 .unwrap()
808 .len(),
809 0
810 );
811 for metadata in &metadatas {
812 let song = Song::read_by_path(&db, metadata.path.clone())
813 .await
814 .unwrap()
815 .unwrap();
816 let analysis = Analysis::read_for_song(&db, song.id.clone()).await.unwrap();
817 assert!(analysis.is_some());
818 }
819
820 for analysis in Analysis::read_all(&db).await.unwrap() {
822 let neighbors = Analysis::nearest_neighbors(&db, analysis.id.clone(), 100)
823 .await
824 .unwrap();
825 assert!(!neighbors.contains(&analysis));
826 assert_eq!(neighbors.len(), metadatas.len() - 1);
827 assert_eq!(
828 neighbors.len(),
829 neighbors
830 .iter()
831 .map(|n| n.id.clone())
832 .collect::<HashSet<_>>()
833 .len()
834 );
835 }
836 }
837
838 #[rstest]
839 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
840 async fn test_recluster(
841 #[values(ProjectionMethod::TSne, ProjectionMethod::None, ProjectionMethod::Pca)]
842 projection_method: ProjectionMethod,
843 ) {
844 init();
845 let dir = tempfile::tempdir().unwrap();
846 let db = init_test_database().await.unwrap();
847 let settings = ReclusterSettings {
848 gap_statistic_reference_datasets: 5,
849 max_clusters: 18,
850 algorithm: ClusterAlgorithm::GMM,
851 projection_method,
852 };
853
854 let song_cases = arb_vec(&arb_song_case(), 32..=32)();
856 let song_cases = song_cases.into_iter().enumerate().map(|(i, sc)| SongCase {
857 song: u8::try_from(i).unwrap(),
858 ..sc
859 });
860 let metadatas = song_cases
861 .into_iter()
862 .map(|song_case| create_song_metadata(&dir, song_case))
863 .collect::<Result<Vec<_>, _>>()
864 .unwrap();
865 let mut songs = Vec::with_capacity(metadatas.len());
866 for metadata in &metadatas {
867 songs.push(Song::try_load_into_db(&db, metadata.clone()).await.unwrap());
868 }
869
870 for song in &songs {
872 Analysis::create(
873 &db,
874 song.id.clone(),
875 Analysis {
876 id: Analysis::generate_id(),
877 features: arb_analysis_features()(),
878 },
879 )
880 .await
881 .unwrap();
882 }
883
884 recluster(&db, settings, InterruptReceiver::dummy())
886 .await
887 .unwrap();
888
889 let collections = Collection::read_all(&db).await.unwrap();
891 assert!(!collections.is_empty());
892 for collection in collections {
893 let songs = Collection::read_songs(&db, collection.id.clone())
894 .await
895 .unwrap();
896 assert!(!songs.is_empty());
897 }
898 }
899
900 #[tokio::test]
901 async fn test_brief() {
902 init();
903 let db = init_test_database().await.unwrap();
904 let brief = brief(&db).await.unwrap();
905 assert_eq!(brief.artists, Box::default());
906 assert_eq!(brief.albums, Box::default());
907 assert_eq!(brief.songs, Box::default());
908 assert_eq!(brief.playlists, Box::default());
909 assert_eq!(brief.collections, Box::default());
910 }
911
912 #[tokio::test]
913 async fn test_full() {
914 init();
915 let db = init_test_database().await.unwrap();
916 let full = full(&db).await.unwrap();
917 assert_eq!(full.artists.len(), 0);
918 assert_eq!(full.albums.len(), 0);
919 assert_eq!(full.songs.len(), 0);
920 assert_eq!(full.playlists.len(), 0);
921 assert_eq!(full.collections.len(), 0);
922 }
923
924 #[tokio::test]
925 async fn test_health() {
926 init();
927 let db = init_test_database().await.unwrap();
928 let health = health(&db).await.unwrap();
929 assert_eq!(health.artists, 0);
930 assert_eq!(health.albums, 0);
931 assert_eq!(health.songs, 0);
932 assert_eq!(health.unanalyzed_songs, Some(0));
933 assert_eq!(health.playlists, 0);
934 assert_eq!(health.collections, 0);
935 assert_eq!(health.orphaned_artists, 0);
936 assert_eq!(health.orphaned_albums, 0);
937 assert_eq!(health.orphaned_playlists, 0);
938 assert_eq!(health.orphaned_collections, 0);
939 }
940}