1#![cfg_attr(
73 feature = "ffmpeg",
74 doc = r##"
75```no_run
76 use anyhow::{Error, Result};
77 use bliss_audio::library::{BaseConfig, Library};
78 use bliss_audio::decoder::ffmpeg::FFmpegDecoder;
79 use std::path::PathBuf;
80
81 let config_path = Some(PathBuf::from("path/to/config/config.json"));
82 let database_path = Some(PathBuf::from("path/to/config/bliss.db"));
83 let config = BaseConfig::new(config_path, database_path, None)?;
84 let library: Library<BaseConfig, FFmpegDecoder> = Library::new(config)?;
85 # Ok::<(), Error>(())
86```"##
87)]
88use crate::cue::CueInfo;
119use crate::playlist::closest_album_to_group;
120use crate::playlist::closest_to_songs;
121use crate::playlist::dedup_playlist_custom_distance;
122use crate::playlist::euclidean_distance;
123use crate::playlist::DistanceMetricBuilder;
124use crate::song::AnalysisOptions;
125use crate::FeaturesVersion;
126use anyhow::{bail, Context, Result};
127#[cfg(all(not(test), not(feature = "integration-tests")))]
128use dirs::config_local_dir;
129#[cfg(all(not(test), not(feature = "integration-tests")))]
130use dirs::data_local_dir;
131use indicatif::{ProgressBar, ProgressStyle};
132use ndarray::Array2;
133use rusqlite::params;
134use rusqlite::params_from_iter;
135use rusqlite::Connection;
136use rusqlite::Params;
137use rusqlite::Row;
138use serde::de::DeserializeOwned;
139use serde::Deserialize;
140use serde::Serialize;
141use std::collections::{HashMap, HashSet};
142use std::env;
143use std::fs;
144use std::fs::create_dir_all;
145use std::marker::PhantomData;
146use std::num::NonZeroUsize;
147use std::path::{Path, PathBuf};
148use std::sync::Arc;
149use std::sync::Mutex;
150
151use crate::decoder::Decoder as DecoderTrait;
152use crate::Song;
153use crate::{Analysis, BlissError, NUMBER_FEATURES};
154use rusqlite::types::ToSqlOutput;
155use rusqlite::Error as RusqliteError;
156use rusqlite::{
157 types::{FromSql, FromSqlResult, ValueRef},
158 ToSql,
159};
160use std::convert::TryInto;
161use std::time::Duration;
162
163impl ToSql for FeaturesVersion {
164 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
165 Ok(ToSqlOutput::from(*self as u16))
166 }
167}
168
169impl FromSql for FeaturesVersion {
170 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
171 let value = value.as_i64()?;
172 FeaturesVersion::try_from(u16::try_from(value).unwrap())
173 .map_err(|e| rusqlite::types::FromSqlError::Other(Box::new(e)))
174 }
175}
176
177pub trait AppConfigTrait: Serialize + Sized + DeserializeOwned {
180 fn base_config(&self) -> &BaseConfig;
184
185 fn base_config_mut(&mut self) -> &mut BaseConfig;
189
190 fn serialize_config(&self) -> Result<String> {
198 Ok(serde_json::to_string_pretty(&self)?)
199 }
200
201 fn set_number_cores(&mut self, number_cores: NonZeroUsize) -> Result<()> {
204 self.base_config_mut().analysis_options.number_cores = number_cores;
205 self.write()
206 }
207
208 fn set_features_version(&mut self, features_version: FeaturesVersion) -> Result<()> {
211 self.base_config_mut().analysis_options.features_version = features_version;
212 self.write()
213 }
214
215 fn get_features_version(&self) -> FeaturesVersion {
218 self.base_config().analysis_options.features_version
219 }
220
221 fn get_number_cores(&self) -> NonZeroUsize {
224 self.base_config().analysis_options.number_cores
225 }
226
227 fn deserialize_config(data: &str) -> Result<Self> {
234 Ok(serde_json::from_str(data)?)
235 }
236
237 fn from_path(path: &str) -> Result<Self> {
242 let data = fs::read_to_string(path)?;
243 Self::deserialize_config(&data)
244 }
245
246 fn write(&self) -> Result<()> {
254 let serialized = self.serialize_config()?;
255 fs::write(&self.base_config().config_path, serialized)?;
256 Ok(())
257 }
258}
259
260#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
261pub struct BaseConfig {
264 pub config_path: PathBuf,
267 pub database_path: PathBuf,
270 #[serde(flatten)]
273 pub analysis_options: AnalysisOptions,
274 #[serde(default = "default_m")]
280 pub m: Array2<f32>,
281}
282
283fn default_m() -> Array2<f32> {
284 Array2::eye(NUMBER_FEATURES)
285}
286
287impl BaseConfig {
288 pub(crate) fn get_default_data_folder() -> Result<PathBuf> {
295 let error_message = "No suitable path found to store bliss' song database. Consider specifying such a path.";
296 let default_folder = env::var("XDG_CONFIG_HOME")
297 .map(|path| Path::new(&path).join("bliss-rs"))
298 .or_else(|_| {
299 config_local_dir()
300 .map(|p| p.join("bliss-rs"))
301 .with_context(|| error_message)
302 });
303
304 if let Ok(folder) = &default_folder {
305 if folder.exists() {
306 return Ok(folder.clone());
307 }
308 }
309
310 if let Ok(legacy_folder) = BaseConfig::get_legacy_data_folder() {
311 if legacy_folder.exists() {
312 return Ok(legacy_folder);
313 }
314 }
315
316 default_folder
318 }
319
320 fn get_legacy_data_folder() -> Result<PathBuf> {
321 let path = match env::var("XDG_DATA_HOME") {
322 Ok(path) => Path::new(&path).join("bliss-rs"),
323 Err(_) => data_local_dir().with_context(|| "No suitable path found to store bliss' song database. Consider specifying such a path.")?.join("bliss-rs"),
324 };
325 Ok(path)
326 }
327
328 pub fn new(
344 config_path: Option<PathBuf>,
345 database_path: Option<PathBuf>,
346 analysis_options: Option<AnalysisOptions>,
347 ) -> Result<Self> {
348 let provided_database_path = database_path.is_some();
349 let provided_config_path = config_path.is_some();
350 let mut final_config_path = {
351 if let Some(path) = config_path {
354 path
355 } else {
356 Self::get_default_data_folder()?.join(Path::new("config.json"))
357 }
358 };
359
360 let mut final_database_path = {
361 if let Some(path) = database_path {
362 path
363 } else {
364 Self::get_default_data_folder()?.join(Path::new("songs.db"))
365 }
366 };
367
368 if provided_database_path && !provided_config_path {
369 final_config_path = final_database_path
370 .parent()
371 .ok_or(BlissError::ProviderError(String::from(
372 "provided database path was invalid.",
373 )))?
374 .join(Path::new("config.json"))
375 } else if !provided_database_path && provided_config_path {
376 final_database_path = final_config_path
377 .parent()
378 .ok_or(BlissError::ProviderError(String::from(
379 "provided config path was invalid.",
380 )))?
381 .join(Path::new("songs.db"))
382 }
383
384 Ok(Self {
385 config_path: final_config_path,
386 database_path: final_database_path,
387 analysis_options: analysis_options.unwrap_or_default(),
388 m: Array2::eye(NUMBER_FEATURES),
389 })
390 }
391}
392
393impl AppConfigTrait for BaseConfig {
394 fn base_config(&self) -> &BaseConfig {
395 self
396 }
397
398 fn base_config_mut(&mut self) -> &mut BaseConfig {
399 self
400 }
401}
402
403pub struct Library<Config, D: ?Sized> {
410 pub config: Config,
413 pub sqlite_conn: Arc<Mutex<Connection>>,
415 decoder: PhantomData<D>,
416}
417
418#[derive(Debug, Eq, PartialEq)]
420pub struct ProcessingError {
421 pub song_path: PathBuf,
423 pub error: String,
425 pub features_version: FeaturesVersion,
427}
428
429#[derive(Debug, PartialEq, Clone)]
446pub struct LibrarySong<T: Serialize + DeserializeOwned + Clone> {
447 pub bliss_song: Song,
450 pub extra_info: T,
452}
453
454impl<T: Serialize + DeserializeOwned + Clone> AsRef<Song> for LibrarySong<T> {
455 fn as_ref(&self) -> &Song {
456 &self.bliss_song
457 }
458}
459
460#[derive(Debug, PartialEq)]
463pub enum SanityError {
464 MultipleVersionsInDB {
467 versions: Vec<FeaturesVersion>,
469 },
470 OldFeaturesVersionInDB {
473 version: FeaturesVersion,
475 },
476}
477
478impl<Config: AppConfigTrait, D: ?Sized + DecoderTrait> Library<Config, D> {
484 const SQLITE_SCHEMA: &'static str = "
485 create table song (
486 id integer primary key,
487 path text not null unique,
488 duration float,
489 album_artist text,
490 artist text,
491 title text,
492 album text,
493 track_number integer,
494 disc_number integer,
495 genre text,
496 cue_path text,
497 audio_file_path text,
498 stamp timestamp default current_timestamp,
499 version integer not null,
500 analyzed boolean default false,
501 extra_info json,
502 error text
503 );
504 pragma foreign_keys = on;
505 create table feature (
506 id integer primary key,
507 song_id integer not null,
508 feature real not null,
509 feature_index integer not null,
510 unique(song_id, feature_index),
511 foreign key(song_id) references song(id) on delete cascade
512 )
513 ";
514 const SQLITE_MIGRATIONS: &'static [&'static str] = &[
515 "",
516 "
517 alter table song add column track_number_1 integer;
518 update song set track_number_1 = s1.cast_track_number from (
519 select cast(track_number as int) as cast_track_number, id from song
520 ) as s1 where s1.id = song.id and cast(track_number as int) != 0;
521 alter table song drop column track_number;
522 alter table song rename column track_number_1 to track_number;
523 ",
524 "alter table song add column disc_number integer;",
525 "
526 -- Training triplets used to do metric learning, in conjunction with
527 -- a human-processed survey. In this table, songs pointed to
528 -- by song_1_id and song_2_id are closer together than they
529 -- are to the song pointed to by odd_one_out_id, i.e.
530 -- d(s1, s2) < d(s1, odd_one_out) and d(s1, s2) < d(s2, odd_one_out)
531 create table training_triplet (
532 id integer primary key,
533 song_1_id integer not null,
534 song_2_id integer not null,
535 odd_one_out_id integer not null,
536 stamp timestamp default current_timestamp,
537 foreign key(song_1_id) references song(id) on delete cascade,
538 foreign key(song_2_id) references song(id) on delete cascade,
539 foreign key(odd_one_out_id) references song(id) on delete cascade
540 )
541 ",
542 "
544 create table song_bak (
545 id integer primary key,
546 path text not null unique,
547 duration float,
548 album_artist text,
549 artist text,
550 title text,
551 album text,
552 track_number integer,
553 disc_number integer,
554 genre text,
555 cue_path text,
556 audio_file_path text,
557 stamp timestamp default current_timestamp,
558 version integer not null,
559 analyzed boolean default false,
560 extra_info json,
561 error text
562 );
563 insert into song_bak (
564 id, path, duration, album_artist, artist, title, album, track_number,
565 disc_number,genre, cue_path, audio_file_path, stamp, version,
566 analyzed, extra_info, error
567 ) select
568 id, path, duration, album_artist, artist, title, album, track_number,
569 disc_number,genre, cue_path, audio_file_path, stamp,
570 coalesce(version, 1), analyzed, extra_info, error
571 from song;
572 drop table song;
573 alter table song_bak rename to song;
574 ",
575 ];
576
577 pub fn new(config: Config) -> Result<Self> {
587 if !config
588 .base_config()
589 .config_path
590 .parent()
591 .ok_or_else(|| {
592 BlissError::ProviderError(format!(
593 "specified path {} is not a valid file path.",
594 config.base_config().config_path.display()
595 ))
596 })?
597 .is_dir()
598 {
599 create_dir_all(config.base_config().config_path.parent().unwrap())?;
600 }
601 let sqlite_conn = Connection::open(&config.base_config().database_path)?;
602
603 Library::<Config, D>::upgrade(&sqlite_conn).map_err(|e| {
604 BlissError::ProviderError(format!("Could not run database upgrade: {e}"))
605 })?;
606
607 config.write()?;
608 Ok(Self {
609 config,
610 sqlite_conn: Arc::new(Mutex::new(sqlite_conn)),
611 decoder: PhantomData,
612 })
613 }
614
615 fn upgrade(sqlite_conn: &Connection) -> Result<()> {
616 let version: u32 = sqlite_conn
617 .query_row("pragma user_version", [], |row| row.get(0))
618 .map_err(|e| {
619 BlissError::ProviderError(format!("Could not get database version: {e}."))
620 })?;
621
622 let migrations = Library::<Config, D>::SQLITE_MIGRATIONS;
623 match version.cmp(&(migrations.len() as u32)) {
624 std::cmp::Ordering::Equal => return Ok(()),
625 std::cmp::Ordering::Greater => bail!(format!(
626 "bliss-rs version {} is older than the schema version {}",
627 version,
628 migrations.len()
629 )),
630 _ => (),
631 };
632
633 let number_tables: u32 = sqlite_conn
634 .query_row("select count(*) from pragma_table_list", [], |row| {
635 row.get(0)
636 })
637 .map_err(|e| {
638 BlissError::ProviderError(format!(
639 "Could not query initial database information: {e}",
640 ))
641 })?;
642 let is_database_new = number_tables <= 2;
643
644 if version == 0 && is_database_new {
645 sqlite_conn
646 .execute_batch(Library::<Config, D>::SQLITE_SCHEMA)
647 .map_err(|e| {
648 BlissError::ProviderError(format!("Could not initialize schema: {e}."))
649 })?;
650 } else {
651 for migration in migrations.iter().skip(version as usize) {
652 sqlite_conn.execute_batch(migration).map_err(|e| {
653 BlissError::ProviderError(format!("Could not execute migration: {e}."))
654 })?;
655 }
656 }
657
658 sqlite_conn
659 .execute(&format!("pragma user_version = {}", migrations.len()), [])
660 .map_err(|e| {
661 BlissError::ProviderError(format!("Could not update database version: {e}."))
662 })?;
663
664 Ok(())
665 }
666
667 pub fn from_config_path(config_path: Option<PathBuf>) -> Result<Self> {
672 let config_path: Result<PathBuf> =
673 config_path.map_or_else(|| Ok(BaseConfig::new(None, None, None)?.config_path), Ok);
674 let config_path = config_path?;
675 let data = fs::read_to_string(config_path)?;
676 let config = Config::deserialize_config(&data)?;
677 let sqlite_conn = Connection::open(&config.base_config().database_path)?;
678 Library::<Config, D>::upgrade(&sqlite_conn)?;
679 let library = Self {
680 config,
681 sqlite_conn: Arc::new(Mutex::new(sqlite_conn)),
682 decoder: PhantomData,
683 };
684 Ok(library)
685 }
686
687 pub fn version_sanity_check(&mut self) -> Result<Vec<SanityError>> {
693 let mut errors = vec![];
694 let connection = self
695 .sqlite_conn
696 .lock()
697 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
698 let mut stmt = connection.prepare("select distinct version from song")?;
699
700 let mut features_version: Vec<FeaturesVersion> = stmt
701 .query_map([], |row| row.get::<_, FeaturesVersion>(0))?
702 .collect::<rusqlite::Result<Vec<_>>>()?;
703
704 features_version.sort();
705 if features_version.len() > 1 {
706 errors.push(SanityError::MultipleVersionsInDB {
707 versions: features_version.to_owned(),
708 })
709 }
710 if features_version
711 .iter()
712 .any(|features_version_in_db| features_version_in_db != &FeaturesVersion::LATEST)
713 {
714 errors.push(SanityError::OldFeaturesVersionInDB {
715 version: features_version[0],
716 });
717 }
718 Ok(errors)
719 }
720
721 pub fn new_from_base(
724 config_path: Option<PathBuf>,
725 database_path: Option<PathBuf>,
726 analysis_options: Option<AnalysisOptions>,
727 ) -> Result<Self>
728 where
729 BaseConfig: Into<Config>,
730 {
731 let base = BaseConfig::new(config_path, database_path, analysis_options)?;
732 let config = base.into();
733 Self::new(config)
734 }
735
736 pub fn playlist_from<'a, T: Serialize + DeserializeOwned + Clone + 'a>(
745 &self,
746 song_paths: &[&str],
747 ) -> Result<impl Iterator<Item = LibrarySong<T>> + 'a> {
748 self.playlist_from_custom(song_paths, &euclidean_distance, closest_to_songs, true)
749 }
750
751 pub fn playlist_from_custom<'a, T, F, I>(
785 &self,
786 initial_song_paths: &[&str],
787 distance: &'a dyn DistanceMetricBuilder,
788 sort_by: F,
789 deduplicate: bool,
790 ) -> Result<impl Iterator<Item = LibrarySong<T>> + 'a>
791 where
792 T: Serialize + DeserializeOwned + Clone + 'a,
793 F: Fn(&[LibrarySong<T>], &[LibrarySong<T>], &'a dyn DistanceMetricBuilder) -> I,
794 I: Iterator<Item = LibrarySong<T>> + 'a,
795 {
796 let initial_songs: Vec<LibrarySong<T>> = initial_song_paths
797 .iter()
798 .map(|s| {
799 self.song_from_path(s).map_err(|_| {
800 BlissError::ProviderError(format!("song '{s}' has not been analyzed"))
801 })
802 })
803 .collect::<Result<Vec<_>, BlissError>>()?;
804 let songs = self
807 .songs_from_library()?
808 .into_iter()
809 .filter(|s| {
810 !initial_song_paths.contains(&&*s.bliss_song.path.to_string_lossy().to_string())
811 })
812 .collect::<Vec<_>>();
813
814 let iterator = sort_by(&initial_songs, &songs, distance);
815 let mut iterator: Box<dyn Iterator<Item = LibrarySong<T>>> =
816 Box::new(initial_songs.into_iter().chain(iterator));
817 if deduplicate {
818 iterator = Box::new(dedup_playlist_custom_distance(iterator, None, distance));
819 }
820 Ok(iterator)
821 }
822
823 pub fn album_playlist_from<T: Serialize + DeserializeOwned + Clone + PartialEq>(
830 &self,
831 album_title: String,
832 number_albums: usize,
833 ) -> Result<Vec<LibrarySong<T>>> {
834 let album = self.songs_from_album(&album_title)?;
835 let songs = self.songs_from_library()?;
837 let playlist = closest_album_to_group(album, songs)?;
838
839 let mut album_count = 0;
840 let mut index = 0;
841 let mut current_album = Some(album_title);
842 for song in playlist.iter() {
843 if song.bliss_song.album != current_album {
844 album_count += 1;
845 if album_count > number_albums {
846 break;
847 }
848 song.bliss_song.album.clone_into(&mut current_album);
849 }
850 index += 1;
851 }
852 let playlist = &playlist[..index];
853 Ok(playlist.to_vec())
854 }
855
856 pub fn update_library<P: Into<PathBuf>>(
875 &mut self,
876 paths: Vec<P>,
877 delete_everything_else: bool,
878 show_progress_bar: bool,
879 ) -> Result<()> {
880 let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
881 self.update_library_convert_extra_info(
882 paths_extra_info,
883 delete_everything_else,
884 show_progress_bar,
885 |x, _, _| x,
886 self.config.base_config().analysis_options,
887 )
888 }
889
890 pub fn update_library_with_options<P: Into<PathBuf>>(
910 &mut self,
911 paths: Vec<P>,
912 delete_everything_else: bool,
913 show_progress_bar: bool,
914 analysis_options: AnalysisOptions,
915 ) -> Result<()> {
916 let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
917 self.update_library_convert_extra_info(
918 paths_extra_info,
919 delete_everything_else,
920 show_progress_bar,
921 |x, _, _| x,
922 analysis_options,
923 )
924 }
925
926 pub fn update_library_extra_info<T: Serialize + DeserializeOwned + Clone, P: Into<PathBuf>>(
936 &mut self,
937 paths_extra_info: Vec<(P, T)>,
938 delete_everything_else: bool,
939 show_progress_bar: bool,
940 ) -> Result<()> {
941 self.update_library_convert_extra_info(
942 paths_extra_info,
943 delete_everything_else,
944 show_progress_bar,
945 |extra_info, _, _| extra_info,
946 self.config.base_config().analysis_options,
947 )
948 }
949
950 pub fn update_library_convert_extra_info<
980 T: Serialize + DeserializeOwned + Clone,
981 U,
982 P: Into<PathBuf>,
983 >(
984 &mut self,
985 paths_extra_info: Vec<(P, U)>,
986 delete_everything_else: bool,
987 show_progress_bar: bool,
988 convert_extra_info: fn(U, &Song, &Self) -> T,
989 analysis_options: AnalysisOptions,
990 ) -> Result<()> {
991 let existing_paths = {
992 let connection = self
993 .sqlite_conn
994 .lock()
995 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
996 let mut path_statement = connection.prepare(
997 "
998 select
999 path
1000 from song where analyzed = true and version = ? order by id
1001 ",
1002 )?;
1003 #[allow(clippy::let_and_return)]
1004 let return_value = path_statement
1005 .query_map([analysis_options.features_version], |row| {
1006 Ok(row.get_unwrap::<usize, String>(0))
1007 })?
1008 .map(|x| PathBuf::from(x.unwrap()))
1009 .collect::<HashSet<PathBuf>>();
1010 return_value
1011 };
1012
1013 let paths_extra_info: Vec<_> = paths_extra_info
1014 .into_iter()
1015 .map(|(x, y)| (x.into(), y))
1016 .collect();
1017 let paths: HashSet<_> = paths_extra_info.iter().map(|(p, _)| p.to_owned()).collect();
1018
1019 if delete_everything_else {
1020 let existing_paths_old_features_version = {
1021 let connection = self
1022 .sqlite_conn
1023 .lock()
1024 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1025 let mut path_statement = connection.prepare(
1026 "
1027 select
1028 path
1029 from song where analyzed = true order by id
1030 ",
1031 )?;
1032 #[allow(clippy::let_and_return)]
1033 let return_value = path_statement
1034 .query_map([], |row| Ok(row.get_unwrap::<usize, String>(0)))?
1035 .map(|x| PathBuf::from(x.unwrap()))
1036 .collect::<HashSet<PathBuf>>();
1037 return_value
1038 };
1039
1040 let paths_to_delete = existing_paths_old_features_version.difference(&paths);
1041
1042 self.delete_paths(paths_to_delete)?;
1043 }
1044
1045 let paths_to_analyze = paths_extra_info
1048 .into_iter()
1049 .filter(|(path, _)| !existing_paths.contains(path))
1050 .collect::<Vec<(PathBuf, U)>>();
1051
1052 {
1053 let connection = self
1054 .sqlite_conn
1055 .lock()
1056 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1057
1058 if !paths_to_analyze.is_empty() {
1059 connection.execute(
1060 "delete from song where version != ?",
1061 params![analysis_options.features_version],
1062 )?;
1063 }
1064 }
1065
1066 self.analyze_paths_convert_extra_info(
1067 paths_to_analyze,
1068 show_progress_bar,
1069 convert_extra_info,
1070 analysis_options,
1071 )
1072 }
1073
1074 pub fn analyze_paths<P: Into<PathBuf>>(
1082 &mut self,
1083 paths: Vec<P>,
1084 show_progress_bar: bool,
1085 ) -> Result<()> {
1086 let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
1087 let analysis_options = self.config.base_config().analysis_options;
1088 self.analyze_paths_convert_extra_info(
1089 paths_extra_info,
1090 show_progress_bar,
1091 |x, _, _| x,
1092 analysis_options,
1093 )
1094 }
1095
1096 pub fn analyze_paths_with_options<P: Into<PathBuf>>(
1107 &mut self,
1108 paths: Vec<P>,
1109 show_progress_bar: bool,
1110 analysis_options: AnalysisOptions,
1111 ) -> Result<()> {
1112 let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
1113 self.analyze_paths_convert_extra_info(
1114 paths_extra_info,
1115 show_progress_bar,
1116 |x, _, _| x,
1117 analysis_options,
1118 )
1119 }
1120
1121 pub fn analyze_paths_extra_info<
1131 T: Serialize + DeserializeOwned + std::fmt::Debug + Clone,
1132 P: Into<PathBuf>,
1133 >(
1134 &mut self,
1135 paths_extra_info: Vec<(P, T)>,
1136 show_progress_bar: bool,
1137 analysis_options: AnalysisOptions,
1138 ) -> Result<()> {
1139 self.analyze_paths_convert_extra_info(
1140 paths_extra_info,
1141 show_progress_bar,
1142 |extra_info, _, _| extra_info,
1143 analysis_options,
1144 )
1145 }
1146
1147 pub fn analyze_paths_convert_extra_info<
1167 T: Serialize + DeserializeOwned + Clone,
1168 U,
1169 P: Into<PathBuf>,
1170 >(
1171 &mut self,
1172 paths_extra_info: Vec<(P, U)>,
1173 show_progress_bar: bool,
1174 convert_extra_info: fn(U, &Song, &Self) -> T,
1175 analysis_options: AnalysisOptions,
1176 ) -> Result<()> {
1177 let number_songs = paths_extra_info.len();
1178 if number_songs == 0 {
1179 log::info!("No (new) songs found.");
1180 return Ok(());
1181 }
1182 log::info!("Analyzing {number_songs} song(s), this might take some time…",);
1183 let pb = if show_progress_bar {
1184 ProgressBar::new(number_songs.try_into().unwrap())
1185 } else {
1186 ProgressBar::hidden()
1187 };
1188 let style = ProgressStyle::default_bar()
1189 .template("[{elapsed_precise}] {bar:40} {pos:>7}/{len:7} {wide_msg}")?
1190 .progress_chars("##-");
1191 pb.set_style(style);
1192
1193 let mut paths_extra_info: HashMap<PathBuf, U> = paths_extra_info
1194 .into_iter()
1195 .map(|(x, y)| (x.into(), y))
1196 .collect();
1197 let mut cue_extra_info: HashMap<PathBuf, String> = HashMap::new();
1198
1199 let results = D::analyze_paths_with_options(paths_extra_info.keys(), analysis_options);
1200 let mut success_count = 0;
1201 let mut failure_count = 0;
1202 for (path, result) in results {
1203 if show_progress_bar {
1204 pb.set_message(format!("Analyzing {}", path.display()));
1205 }
1206 match result {
1207 Ok(song) => {
1208 let is_cue = song.cue_info.is_some();
1209 let path = {
1213 if let Some(cue_info) = song.cue_info.to_owned() {
1214 cue_info.cue_path
1215 } else {
1216 path
1217 }
1218 };
1219 let extra = {
1224 if is_cue && paths_extra_info.contains_key(&path) {
1225 let extra = paths_extra_info.remove(&path).unwrap();
1226 let e = convert_extra_info(extra, &song, self);
1227 cue_extra_info.insert(
1228 path,
1229 serde_json::to_string(&e)
1230 .map_err(|e| BlissError::ProviderError(e.to_string()))?,
1231 );
1232 e
1233 } else if is_cue {
1234 let serialized_extra_info =
1235 cue_extra_info.get(&path).unwrap().to_owned();
1236 serde_json::from_str(&serialized_extra_info).unwrap()
1237 } else {
1238 let extra = paths_extra_info.remove(&path).unwrap();
1239 convert_extra_info(extra, &song, self)
1240 }
1241 };
1242 let library_song = LibrarySong::<T> {
1243 bliss_song: song,
1244 extra_info: extra,
1245 };
1246 self.store_song(&library_song)?;
1247 success_count += 1;
1248 }
1249 Err(e) => {
1250 log::error!(
1251 "Analysis of song '{}' failed: {} The error has been stored.",
1252 path.display(),
1253 e
1254 );
1255
1256 self.store_failed_song(path, e, analysis_options.features_version)?;
1257 failure_count += 1;
1258 }
1259 };
1260 pb.inc(1);
1261 }
1262 pb.finish_with_message(format!(
1263 "Analyzed {success_count} song(s) successfully. {failure_count} Failure(s).",
1264 ));
1265
1266 log::info!("Analyzed {success_count} song(s) successfully. {failure_count} Failure(s).",);
1267
1268 self.config.base_config_mut().analysis_options = analysis_options;
1269 self.config.write()?;
1270
1271 Ok(())
1272 }
1273
1274 fn _songs_from_statement<T: Serialize + DeserializeOwned + Clone, P: Params + Clone>(
1277 &self,
1278 songs_statement: &str,
1279 features_statement: &str,
1280 params: P,
1281 ) -> Result<Vec<LibrarySong<T>>> {
1282 let connection = self
1283 .sqlite_conn
1284 .lock()
1285 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1286 let mut songs_statement = connection.prepare(songs_statement)?;
1287 let mut features_statement = connection.prepare(features_statement)?;
1288 let song_rows = songs_statement.query_map(params.to_owned(), |row| {
1289 Ok((row.get(13)?, Self::_song_from_row_closure(row)?))
1290 })?;
1291 let feature_rows =
1292 features_statement.query_map(params, |row| Ok((row.get(1)?, row.get(0)?)))?;
1293
1294 let mut feature_iterator = feature_rows.into_iter().peekable();
1295 let mut songs = Vec::new();
1296 for row in song_rows {
1299 let song_id: u32 = row.as_ref().unwrap().0;
1300 let mut chunk: Vec<f32> = Vec::with_capacity(NUMBER_FEATURES);
1301
1302 while let Some(first_value) = feature_iterator.peek() {
1303 let (song_feature_id, feature): (u32, f32) = *first_value.as_ref().unwrap();
1304 if song_feature_id == song_id {
1305 chunk.push(feature);
1306 feature_iterator.next();
1307 } else {
1308 break;
1309 };
1310 }
1311 let mut song = row.unwrap().1;
1312 song.bliss_song.analysis = Analysis::new(chunk, song.bliss_song.features_version)
1313 .map_err(|_| {
1314 BlissError::ProviderError(format!(
1315 "Song with ID {} and path {} has a different feature \
1316 number than expected. Please rescan or update \
1317 the song library.",
1318 song_id,
1319 song.bliss_song.path.display(),
1320 ))
1321 })?;
1322 songs.push(song);
1323 }
1324 Ok(songs)
1325 }
1326
1327 pub fn songs_from_library<T: Serialize + DeserializeOwned + Clone>(
1336 &self,
1337 ) -> Result<Vec<LibrarySong<T>>> {
1338 let songs_statement = "
1339 select
1340 path, artist, title, album, album_artist,
1341 track_number, disc_number, genre, duration, version, extra_info, cue_path,
1342 audio_file_path, id
1343 from song where analyzed = true and version = ? order by id
1344 ";
1345 let features_statement = "
1346 select
1347 feature, song.id from feature join song on song.id = feature.song_id
1348 where song.analyzed = true and song.version = ? order by song_id, feature_index
1349 ";
1350 let params = params![self.config.base_config().analysis_options.features_version];
1351 self._songs_from_statement(songs_statement, features_statement, params)
1352 }
1353
1354 pub fn songs_from_album<T: Serialize + DeserializeOwned + Clone>(
1359 &self,
1360 album_title: &str,
1361 ) -> Result<Vec<LibrarySong<T>>> {
1362 let params = params![
1363 album_title,
1364 self.config.base_config().analysis_options.features_version
1365 ];
1366 let songs_statement = "
1367 select
1368 path, artist, title, album, album_artist,
1369 track_number, disc_number, genre, duration, version, extra_info, cue_path,
1370 audio_file_path, id
1371 from song where album = ? and analyzed = true and version = ?
1372 order
1373 by disc_number, track_number;
1374 ";
1375
1376 let features_statement = "
1378 select
1379 feature, song.id from feature join song on song.id = feature.song_id
1380 where album=? and analyzed = true and version = ?
1381 order by disc_number, track_number;
1382 ";
1383 let songs = self._songs_from_statement(songs_statement, features_statement, params)?;
1384 if songs.is_empty() {
1385 bail!(BlissError::ProviderError(String::from(
1386 "target album was not found in the database.",
1387 )));
1388 };
1389 Ok(songs)
1390 }
1391
1392 pub fn song_from_path<T: Serialize + DeserializeOwned + Clone>(
1395 &self,
1396 song_path: &str,
1397 ) -> Result<LibrarySong<T>> {
1398 let connection = self
1399 .sqlite_conn
1400 .lock()
1401 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1402 let mut song = connection.query_row(
1404 "
1405 select
1406 path, artist, title, album, album_artist,
1407 track_number, disc_number, genre, duration, version, extra_info,
1408 cue_path, audio_file_path
1409 from song where path=? and analyzed = true
1410 ",
1411 params![song_path],
1412 Self::_song_from_row_closure,
1413 )?;
1414
1415 let mut stmt = connection.prepare(
1417 "
1418 select
1419 feature from feature join song on song.id = feature.song_id
1420 where song.path = ? order by feature_index
1421 ",
1422 )?;
1423 let analysis = Analysis::new(
1424 stmt.query_map(params![song_path], |row| row.get(0))
1425 .unwrap()
1426 .map(|x| x.unwrap())
1427 .collect::<Vec<f32>>(),
1428 song.bliss_song.features_version,
1429 )
1430 .map_err(|_| {
1431 BlissError::ProviderError(format!(
1432 "song has more or less than {NUMBER_FEATURES} features",
1433 ))
1434 })?;
1435 song.bliss_song.analysis = analysis;
1436 Ok(song)
1437 }
1438
1439 fn _song_from_row_closure<T: Serialize + DeserializeOwned + Clone>(
1440 row: &Row,
1441 ) -> Result<LibrarySong<T>, RusqliteError> {
1442 let path: String = row.get(0)?;
1443
1444 let cue_path: Option<String> = row.get(11)?;
1445 let audio_file_path: Option<String> = row.get(12)?;
1446 let mut cue_info = None;
1447 if let Some(cue_path) = cue_path {
1448 cue_info = Some(CueInfo {
1449 cue_path: PathBuf::from(cue_path),
1450 audio_file_path: PathBuf::from(audio_file_path.unwrap()),
1451 })
1452 };
1453
1454 let song = Song {
1455 path: PathBuf::from(path),
1456 artist: row
1457 .get_ref(1)
1458 .unwrap()
1459 .as_bytes_or_null()
1460 .unwrap()
1461 .map(|v| String::from_utf8_lossy(v).to_string()),
1462 title: row
1463 .get_ref(2)
1464 .unwrap()
1465 .as_bytes_or_null()
1466 .unwrap()
1467 .map(|v| String::from_utf8_lossy(v).to_string()),
1468 album: row
1469 .get_ref(3)
1470 .unwrap()
1471 .as_bytes_or_null()
1472 .unwrap()
1473 .map(|v| String::from_utf8_lossy(v).to_string()),
1474 album_artist: row
1475 .get_ref(4)
1476 .unwrap()
1477 .as_bytes_or_null()
1478 .unwrap()
1479 .map(|v| String::from_utf8_lossy(v).to_string()),
1480 track_number: row
1481 .get_ref(5)
1482 .unwrap()
1483 .as_i64_or_null()
1484 .unwrap()
1485 .map(|v| v as i32),
1486 disc_number: row
1487 .get_ref(6)
1488 .unwrap()
1489 .as_i64_or_null()
1490 .unwrap()
1491 .map(|v| v as i32),
1492 genre: row
1493 .get_ref(7)
1494 .unwrap()
1495 .as_bytes_or_null()
1496 .unwrap()
1497 .map(|v| String::from_utf8_lossy(v).to_string()),
1498 analysis: Analysis {
1499 internal_analysis: vec![0.; NUMBER_FEATURES],
1500 features_version: row.get(9).unwrap(),
1501 },
1502 duration: Duration::from_secs_f64(row.get(8).unwrap()),
1503 features_version: row.get(9).unwrap(),
1504 cue_info,
1505 };
1506
1507 let serialized: Option<String> = row.get(10).unwrap();
1508 let serialized = serialized.unwrap_or_else(|| "null".into());
1509 let extra_info = serde_json::from_str(&serialized).unwrap();
1510 Ok(LibrarySong {
1511 bliss_song: song,
1512 extra_info,
1513 })
1514 }
1515
1516 pub fn store_song<T: Serialize + DeserializeOwned + Clone>(
1520 &mut self,
1521 library_song: &LibrarySong<T>,
1522 ) -> Result<(), BlissError> {
1523 let mut sqlite_conn = self.sqlite_conn.lock().unwrap();
1524 let tx = sqlite_conn
1525 .transaction()
1526 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1527 let song = &library_song.bliss_song;
1528 let (cue_path, audio_file_path) = match &song.cue_info {
1529 Some(c) => (
1530 Some(c.cue_path.to_string_lossy()),
1531 Some(c.audio_file_path.to_string_lossy()),
1532 ),
1533 None => (None, None),
1534 };
1535 tx.execute(
1536 "
1537 insert into song (
1538 path, artist, title, album, album_artist,
1539 duration, track_number, disc_number, genre, analyzed, version, extra_info,
1540 cue_path, audio_file_path
1541 )
1542 values (
1543 ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14
1544 )
1545 on conflict(path)
1546 do update set
1547 artist=excluded.artist,
1548 title=excluded.title,
1549 album=excluded.album,
1550 track_number=excluded.track_number,
1551 disc_number=excluded.disc_number,
1552 album_artist=excluded.album_artist,
1553 duration=excluded.duration,
1554 genre=excluded.genre,
1555 analyzed=excluded.analyzed,
1556 version=excluded.version,
1557 extra_info=excluded.extra_info,
1558 cue_path=excluded.cue_path,
1559 audio_file_path=excluded.audio_file_path
1560 ",
1561 params![
1562 song.path.to_str(),
1563 song.artist,
1564 song.title,
1565 song.album,
1566 song.album_artist,
1567 song.duration.as_secs_f64(),
1568 song.track_number,
1569 song.disc_number,
1570 song.genre,
1571 true,
1572 song.features_version,
1573 serde_json::to_string(&library_song.extra_info)
1574 .map_err(|e| BlissError::ProviderError(e.to_string()))?,
1575 cue_path,
1576 audio_file_path,
1577 ],
1578 )
1579 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1580
1581 tx.execute(
1583 "delete from feature where song_id in (select id from song where path = ?1);",
1584 params![song.path.to_str()],
1585 )
1586 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1587
1588 for (index, feature) in song.analysis.as_vec().iter().enumerate() {
1589 tx.execute(
1590 "
1591 insert into feature (song_id, feature, feature_index)
1592 values ((select id from song where path = ?1), ?2, ?3)
1593 on conflict(song_id, feature_index) do update set feature=excluded.feature;
1594 ",
1595 params![song.path.to_str(), feature, index],
1596 )
1597 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1598 }
1599 tx.commit()
1600 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1601 Ok(())
1602 }
1603
1604 pub fn store_failed_song<P: Into<PathBuf>>(
1609 &mut self,
1610 song_path: P,
1611 e: BlissError,
1612 features_version: FeaturesVersion,
1613 ) -> Result<()> {
1614 self.sqlite_conn
1615 .lock()
1616 .unwrap()
1617 .execute(
1618 "
1619 insert or replace into song (path, error, version) values (?1, ?2, ?3)
1620 ",
1621 params![
1622 song_path.into().to_string_lossy().to_string(),
1623 e.to_string(),
1624 features_version,
1627 ],
1628 )
1629 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1630 Ok(())
1631 }
1632
1633 pub fn get_failed_songs(&self) -> Result<Vec<ProcessingError>> {
1635 let conn = self.sqlite_conn.lock().unwrap();
1636 let mut stmt = conn.prepare(
1637 "
1638 select path, error, version
1639 from song where error is not null order by id
1640 ",
1641 )?;
1642 let rows = stmt.query_map([], |row| {
1643 Ok(ProcessingError {
1644 song_path: row.get::<_, String>(0)?.into(),
1645 error: row.get(1)?,
1646 features_version: row.get(2)?,
1647 })
1648 })?;
1649 Ok(rows
1650 .into_iter()
1651 .map(|r| r.unwrap())
1652 .collect::<Vec<ProcessingError>>())
1653 }
1654
1655 pub fn delete_path<P: Into<PathBuf>>(&mut self, song_path: P) -> Result<()> {
1659 let song_path = song_path.into();
1660 let count = self
1661 .sqlite_conn
1662 .lock()
1663 .unwrap()
1664 .execute(
1665 "
1666 delete from song where path = ?1;
1667 ",
1668 [song_path.to_str()],
1669 )
1670 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1671 if count == 0 {
1672 bail!(BlissError::ProviderError(format!(
1673 "tried to delete song {}, not existing in the database.",
1674 song_path.display(),
1675 )));
1676 }
1677 Ok(())
1678 }
1679
1680 pub fn delete_paths<P: Into<PathBuf>, I: IntoIterator<Item = P>>(
1684 &mut self,
1685 paths: I,
1686 ) -> Result<usize> {
1687 let song_paths: Vec<String> = paths
1688 .into_iter()
1689 .map(|x| x.into().to_string_lossy().to_string())
1690 .collect();
1691 if song_paths.is_empty() {
1692 return Ok(0);
1693 };
1694 let count = self
1695 .sqlite_conn
1696 .lock()
1697 .unwrap()
1698 .execute(
1699 &format!(
1700 "delete from song where path in ({})",
1701 repeat_vars(song_paths.len()),
1702 ),
1703 params_from_iter(song_paths),
1704 )
1705 .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1706 Ok(count)
1707 }
1708}
1709
1710fn repeat_vars(count: usize) -> String {
1713 assert_ne!(count, 0);
1714 let mut s = "?,".repeat(count);
1715 s.pop();
1717 s
1718}
1719
1720#[cfg(any(test, feature = "integration-tests"))]
1721fn data_local_dir() -> Option<PathBuf> {
1722 Some(PathBuf::from("/tmp/data"))
1723}
1724
1725#[cfg(any(test, feature = "integration-tests"))]
1726fn config_local_dir() -> Option<PathBuf> {
1727 Some(PathBuf::from("/tmp/"))
1728}
1729
1730#[cfg(test)]
1731mod test {
1735 use super::*;
1736 use crate::{decoder::PreAnalyzedSong, Analysis, NUMBER_FEATURES};
1737 use ndarray::Array1;
1738 use pretty_assertions::assert_eq;
1739 use serde::{de::DeserializeOwned, Deserialize};
1740 use serde_json::Value;
1741 use std::thread;
1742 use std::{convert::TryInto, fmt::Debug, str::FromStr, sync::MutexGuard, time::Duration};
1743 use tempdir::TempDir;
1744
1745 #[cfg(feature = "ffmpeg")]
1746 use crate::song::decoder::ffmpeg::FFmpegDecoder as Decoder;
1747 use crate::song::decoder::Decoder as DecoderTrait;
1748
1749 struct DummyDecoder;
1750
1751 impl DecoderTrait for DummyDecoder {
1753 fn decode(_: &Path) -> crate::BlissResult<crate::decoder::PreAnalyzedSong> {
1754 Ok(PreAnalyzedSong::default())
1755 }
1756 }
1757
1758 #[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)]
1759 struct ExtraInfo {
1760 ignore: bool,
1761 metadata_bliss_does_not_have: String,
1762 }
1763
1764 #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
1765 struct CustomConfig {
1766 #[serde(flatten)]
1767 base_config: BaseConfig,
1768 second_path_to_music_library: String,
1769 ignore_wav_files: bool,
1770 }
1771
1772 impl AppConfigTrait for CustomConfig {
1773 fn base_config(&self) -> &BaseConfig {
1774 &self.base_config
1775 }
1776
1777 fn base_config_mut(&mut self) -> &mut BaseConfig {
1778 &mut self.base_config
1779 }
1780 }
1781
1782 fn nzus(i: usize) -> NonZeroUsize {
1783 NonZeroUsize::new(i).unwrap()
1784 }
1785
1786 #[cfg(feature = "ffmpeg")]
1795 fn setup_test_library() -> (
1796 Library<BaseConfig, Decoder>,
1797 TempDir,
1798 (
1799 LibrarySong<ExtraInfo>,
1800 LibrarySong<ExtraInfo>,
1801 LibrarySong<ExtraInfo>,
1802 LibrarySong<ExtraInfo>,
1803 LibrarySong<ExtraInfo>,
1804 LibrarySong<ExtraInfo>,
1805 LibrarySong<ExtraInfo>,
1806 LibrarySong<ExtraInfo>,
1807 ),
1808 ) {
1809 let config_dir = TempDir::new("coucou").unwrap();
1810 let config_file = config_dir.path().join("config.json");
1811 let database_file = config_dir.path().join("bliss.db");
1812 let library = Library::<BaseConfig, Decoder>::new_from_base(
1813 Some(config_file),
1814 Some(database_file),
1815 None,
1816 )
1817 .unwrap();
1818
1819 let analysis_vector = (0..NUMBER_FEATURES)
1820 .map(|x| x as f32 / 10.)
1821 .collect::<Vec<f32>>();
1822
1823 let song = Song {
1824 path: "/path/to/song1001".into(),
1825 artist: Some("Artist1001".into()),
1826 title: Some("Title1001".into()),
1827 album: Some("An Album1001".into()),
1828 album_artist: Some("An Album Artist1001".into()),
1829 track_number: Some(3),
1830 disc_number: Some(1),
1831 genre: Some("Electronica1001".into()),
1832 analysis: Analysis {
1833 internal_analysis: analysis_vector,
1834 features_version: FeaturesVersion::LATEST,
1835 },
1836 duration: Duration::from_secs(310),
1837 features_version: FeaturesVersion::LATEST,
1838 cue_info: None,
1839 };
1840 let first_song = LibrarySong {
1841 bliss_song: song,
1842 extra_info: ExtraInfo {
1843 ignore: true,
1844 metadata_bliss_does_not_have: String::from("/path/to/charlie1001"),
1845 },
1846 };
1847
1848 let analysis_vector = (0..NUMBER_FEATURES)
1849 .map(|x| x as f32 + 10.)
1850 .collect::<Vec<f32>>();
1851
1852 let song = Song {
1853 path: "/path/to/song2001".into(),
1854 artist: Some("Artist2001".into()),
1855 title: Some("Title2001".into()),
1856 album: Some("An Album2001".into()),
1857 album_artist: Some("An Album Artist2001".into()),
1858 track_number: Some(2),
1859 disc_number: Some(1),
1860 genre: Some("Electronica2001".into()),
1861 analysis: Analysis {
1862 internal_analysis: analysis_vector,
1863 features_version: FeaturesVersion::LATEST,
1864 },
1865 duration: Duration::from_secs(410),
1866 features_version: FeaturesVersion::LATEST,
1867 cue_info: None,
1868 };
1869 let second_song = LibrarySong {
1870 bliss_song: song,
1871 extra_info: ExtraInfo {
1872 ignore: false,
1873 metadata_bliss_does_not_have: String::from("/path/to/charlie2001"),
1874 },
1875 };
1876
1877 let analysis_vector = (0..NUMBER_FEATURES)
1878 .map(|x| x as f32 + 10.)
1879 .collect::<Vec<f32>>();
1880
1881 let song = Song {
1882 path: "/path/to/song2201".into(),
1883 artist: Some("Artist2001".into()),
1884 title: Some("Title2001".into()),
1885 album: Some("An Album2001".into()),
1886 album_artist: Some("An Album Artist2001".into()),
1887 track_number: Some(1),
1888 disc_number: Some(2),
1889 genre: Some("Electronica2001".into()),
1890 analysis: Analysis {
1891 internal_analysis: analysis_vector,
1892 features_version: FeaturesVersion::LATEST,
1893 },
1894 duration: Duration::from_secs(410),
1895 features_version: FeaturesVersion::LATEST,
1896 cue_info: None,
1897 };
1898 let second_song_dupe = LibrarySong {
1899 bliss_song: song,
1900 extra_info: ExtraInfo {
1901 ignore: false,
1902 metadata_bliss_does_not_have: String::from("/path/to/charlie2201"),
1903 },
1904 };
1905
1906 let analysis_vector = (0..NUMBER_FEATURES)
1907 .map(|x| x as f32 / 2.)
1908 .collect::<Vec<f32>>();
1909
1910 let song = Song {
1911 path: "/path/to/song5001".into(),
1912 artist: Some("Artist5001".into()),
1913 title: Some("Title5001".into()),
1914 album: Some("An Album1001".into()),
1915 album_artist: Some("An Album Artist5001".into()),
1916 track_number: Some(1),
1917 disc_number: Some(1),
1918 genre: Some("Electronica5001".into()),
1919 analysis: Analysis {
1920 internal_analysis: analysis_vector,
1921 features_version: FeaturesVersion::LATEST,
1922 },
1923 duration: Duration::from_secs(610),
1924 features_version: FeaturesVersion::LATEST,
1925 cue_info: None,
1926 };
1927 let third_song = LibrarySong {
1928 bliss_song: song,
1929 extra_info: ExtraInfo {
1930 ignore: false,
1931 metadata_bliss_does_not_have: String::from("/path/to/charlie5001"),
1932 },
1933 };
1934
1935 let analysis_vector = (0..NUMBER_FEATURES)
1936 .map(|x| x as f32 * 0.9)
1937 .collect::<Vec<f32>>();
1938
1939 let song = Song {
1940 path: "/path/to/song6001".into(),
1941 artist: Some("Artist6001".into()),
1942 title: Some("Title6001".into()),
1943 album: Some("An Album2001".into()),
1944 album_artist: Some("An Album Artist6001".into()),
1945 track_number: Some(1),
1946 disc_number: Some(1),
1947 genre: Some("Electronica6001".into()),
1948 analysis: Analysis {
1949 internal_analysis: analysis_vector,
1950 features_version: FeaturesVersion::LATEST,
1951 },
1952 duration: Duration::from_secs(710),
1953 features_version: FeaturesVersion::LATEST,
1954 cue_info: None,
1955 };
1956 let fourth_song = LibrarySong {
1957 bliss_song: song,
1958 extra_info: ExtraInfo {
1959 ignore: false,
1960 metadata_bliss_does_not_have: String::from("/path/to/charlie6001"),
1961 },
1962 };
1963
1964 let analysis_vector = (0..NUMBER_FEATURES)
1965 .map(|x| x as f32 * 50.)
1966 .collect::<Vec<f32>>();
1967
1968 let song = Song {
1969 path: "/path/to/song7001".into(),
1970 artist: Some("Artist7001".into()),
1971 title: Some("Title7001".into()),
1972 album: Some("An Album7001".into()),
1973 album_artist: Some("An Album Artist7001".into()),
1974 track_number: Some(1),
1975 disc_number: Some(1),
1976 genre: Some("Electronica7001".into()),
1977 analysis: Analysis {
1978 internal_analysis: analysis_vector,
1979 features_version: FeaturesVersion::LATEST,
1980 },
1981 duration: Duration::from_secs(810),
1982 features_version: FeaturesVersion::LATEST,
1983 cue_info: None,
1984 };
1985 let fifth_song = LibrarySong {
1986 bliss_song: song,
1987 extra_info: ExtraInfo {
1988 ignore: false,
1989 metadata_bliss_does_not_have: String::from("/path/to/charlie7001"),
1990 },
1991 };
1992
1993 let analysis_vector = (0..NUMBER_FEATURES)
1994 .map(|x| x as f32 * 100.)
1995 .collect::<Vec<f32>>();
1996
1997 let song = Song {
1998 path: "/path/to/cuetrack.cue/CUE_TRACK001".into(),
1999 artist: Some("CUE Artist".into()),
2000 title: Some("CUE Title 01".into()),
2001 album: Some("CUE Album".into()),
2002 album_artist: Some("CUE Album Artist".into()),
2003 track_number: Some(1),
2004 disc_number: Some(1),
2005 genre: None,
2006 analysis: Analysis {
2007 internal_analysis: analysis_vector,
2008 features_version: FeaturesVersion::LATEST,
2009 },
2010 duration: Duration::from_secs(810),
2011 features_version: FeaturesVersion::LATEST,
2012 cue_info: Some(CueInfo {
2013 cue_path: PathBuf::from("/path/to/cuetrack.cue"),
2014 audio_file_path: PathBuf::from("/path/to/cuetrack.flac"),
2015 }),
2016 };
2017 let sixth_song = LibrarySong {
2018 bliss_song: song,
2019 extra_info: ExtraInfo {
2020 ignore: false,
2021 metadata_bliss_does_not_have: String::from("/path/to/charlie7001"),
2022 },
2023 };
2024
2025 let analysis_vector = (0..NUMBER_FEATURES)
2026 .map(|x| x as f32 * 101.)
2027 .collect::<Vec<f32>>();
2028
2029 let song = Song {
2030 path: "/path/to/cuetrack.cue/CUE_TRACK002".into(),
2031 artist: Some("CUE Artist".into()),
2032 title: Some("CUE Title 02".into()),
2033 album: Some("CUE Album".into()),
2034 album_artist: Some("CUE Album Artist".into()),
2035 track_number: Some(2),
2036 disc_number: Some(1),
2037 genre: None,
2038 analysis: Analysis {
2039 internal_analysis: analysis_vector,
2040 features_version: FeaturesVersion::LATEST,
2041 },
2042 duration: Duration::from_secs(910),
2043 features_version: FeaturesVersion::LATEST,
2044 cue_info: Some(CueInfo {
2045 cue_path: PathBuf::from("/path/to/cuetrack.cue"),
2046 audio_file_path: PathBuf::from("/path/to/cuetrack.flac"),
2047 }),
2048 };
2049 let seventh_song = LibrarySong {
2050 bliss_song: song,
2051 extra_info: ExtraInfo {
2052 ignore: false,
2053 metadata_bliss_does_not_have: String::from("/path/to/charlie7001"),
2054 },
2055 };
2056
2057 {
2058 let connection = library.sqlite_conn.lock().unwrap();
2059 connection
2060 .execute(
2061 &format!(
2062 "
2063 insert into song (
2064 id, path, artist, title, album, album_artist, track_number,
2065 disc_number, genre, duration, analyzed, version, extra_info,
2066 cue_path, audio_file_path, error
2067 ) values (
2068 1001, '/path/to/song1001', 'Artist1001', 'Title1001', 'An Album1001',
2069 'An Album Artist1001', 3, 1, 'Electronica1001', 310, true,
2070 {new_version}, '{{\"ignore\": true, \"metadata_bliss_does_not_have\":
2071 \"/path/to/charlie1001\"}}', null, null, null
2072 ),
2073 (
2074 2001, '/path/to/song2001', 'Artist2001', 'Title2001', 'An Album2001',
2075 'An Album Artist2001', 2, 1, 'Electronica2001', 410, true,
2076 {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2077 \"/path/to/charlie2001\"}}', null, null, null
2078 ),
2079 (
2080 2201, '/path/to/song2201', 'Artist2001', 'Title2001', 'An Album2001',
2081 'An Album Artist2001', 1, 2, 'Electronica2001', 410, true,
2082 {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2083 \"/path/to/charlie2201\"}}', null, null, null
2084 ),
2085 (
2086 3001, '/path/to/song3001', null, null, null,
2087 null, null, null, null, null, false, {new_version}, '{{}}', null, null, null
2088 ),
2089 (
2090 4001, '/path/to/song4001', 'Artist4001', 'Title4001', 'An Album4001',
2091 'An Album Artist4001', 1, 1, 'Electronica4001', 510, true,
2092 {old_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2093 \"/path/to/charlie4001\"}}', null, null, null
2094 ),
2095 (
2096 5001, '/path/to/song5001', 'Artist5001', 'Title5001', 'An Album1001',
2097 'An Album Artist5001', 1, 1, 'Electronica5001', 610, true,
2098 {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2099 \"/path/to/charlie5001\"}}', null, null, null
2100 ),
2101 (
2102 6001, '/path/to/song6001', 'Artist6001', 'Title6001', 'An Album2001',
2103 'An Album Artist6001', 1, 1, 'Electronica6001', 710, true,
2104 {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2105 \"/path/to/charlie6001\"}}', null, null, null
2106 ),
2107 (
2108 7001, '/path/to/song7001', 'Artist7001', 'Title7001', 'An Album7001',
2109 'An Album Artist7001', 1, 1, 'Electronica7001', 810, true,
2110 {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2111 \"/path/to/charlie7001\"}}', null, null, null
2112 ),
2113 (
2114 7002, '/path/to/cuetrack.cue/CUE_TRACK001', 'CUE Artist',
2115 'CUE Title 01', 'CUE Album',
2116 'CUE Album Artist', 1, 1, null, 810, true,
2117 {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2118 \"/path/to/charlie7001\"}}', '/path/to/cuetrack.cue',
2119 '/path/to/cuetrack.flac', null
2120 ),
2121 (
2122 7003, '/path/to/cuetrack.cue/CUE_TRACK002', 'CUE Artist',
2123 'CUE Title 02', 'CUE Album',
2124 'CUE Album Artist', 2, 1, null, 910, true,
2125 {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2126 \"/path/to/charlie7001\"}}', '/path/to/cuetrack.cue',
2127 '/path/to/cuetrack.flac', null
2128 ),
2129 (
2130 8001, '/path/to/song8001', 'Artist8001', 'Title8001', 'An Album1001',
2131 'An Album Artist8001', 3, 1, 'Electronica8001', 910, true,
2132 {old_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2133 \"/path/to/charlie8001\"}}', null, null, null
2134 ),
2135 (
2136 9001, './data/s16_stereo_22_5kHz.flac', 'Artist9001', 'Title9001',
2137 'An Album9001', 'An Album Artist8001', 3, 1, 'Electronica8001',
2138 1010, true, {old_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2139 \"/path/to/charlie7001\"}}', null, null, null
2140 ),
2141 (
2142 404, './data/not-existing.m4a', null, null,
2143 null, null, null, null, null,
2144 null, false, {old_version}, null, null, null, 'error finding the file'
2145 ),
2146 (
2147 502, './data/invalid-file.m4a', null, null,
2148 null, null, null, null, null,
2149 null, false, {old_version}, null, null, null, 'error decoding the file'
2150 );
2151 ",
2152 new_version = FeaturesVersion::LATEST as u16,
2153 old_version = FeaturesVersion::Version1 as u16,
2154 ),
2155 [],
2156 )
2157 .unwrap();
2158 for index in 0..NUMBER_FEATURES {
2159 connection
2160 .execute(
2161 "
2162 insert into feature(song_id, feature, feature_index)
2163 values
2164 (1001, ?2, ?1),
2165 (2001, ?3, ?1),
2166 (3001, ?4, ?1),
2167 (5001, ?5, ?1),
2168 (6001, ?6, ?1),
2169 (7001, ?7, ?1),
2170 (7002, ?8, ?1),
2171 (7003, ?9, ?1),
2172 (2201, ?10, ?1);
2173 ",
2174 params![
2175 index,
2176 index as f32 / 10.,
2177 index as f32 + 10.,
2178 index as f32 / 10. + 1.,
2179 index as f32 / 2.,
2180 index as f32 * 0.9,
2181 index as f32 * 50.,
2182 index as f32 * 100.,
2183 index as f32 * 101.,
2184 index as f32 + 10.,
2185 ],
2186 )
2187 .unwrap();
2188 }
2189 for index in 0..NUMBER_FEATURES - 5 {
2191 connection
2192 .execute(
2193 "
2194 insert into feature(song_id, feature, feature_index)
2195 values
2196 (8001, ?2, ?1),
2197 (9001, ?3, ?1);
2198 ",
2199 params![index, index as f32 / 20., index + 1],
2200 )
2201 .unwrap();
2202 }
2203 }
2204 (
2205 library,
2206 config_dir,
2207 (
2208 first_song,
2209 second_song,
2210 second_song_dupe,
2211 third_song,
2212 fourth_song,
2213 fifth_song,
2214 sixth_song,
2215 seventh_song,
2216 ),
2217 )
2218 }
2219
2220 fn _library_song_from_database<T: DeserializeOwned + Serialize + Clone + Debug>(
2221 connection: MutexGuard<Connection>,
2222 song_path: &str,
2223 ) -> LibrarySong<T> {
2224 let mut song = connection
2225 .query_row(
2226 "
2227 select
2228 path, artist, title, album, album_artist,
2229 track_number, disc_number, genre, duration, version, extra_info,
2230 cue_path, audio_file_path
2231 from song where path=?
2232 ",
2233 params![song_path],
2234 |row| {
2235 let path: String = row.get(0)?;
2236 let cue_path: Option<String> = row.get(11)?;
2237 let audio_file_path: Option<String> = row.get(12)?;
2238 let mut cue_info = None;
2239 if let Some(cue_path) = cue_path {
2240 cue_info = Some(CueInfo {
2241 cue_path: PathBuf::from(cue_path),
2242 audio_file_path: PathBuf::from(audio_file_path.unwrap()),
2243 })
2244 };
2245 let features_version: FeaturesVersion = row.get(9).unwrap();
2246 let song = Song {
2247 path: PathBuf::from(path),
2248 artist: row.get(1).unwrap(),
2249 title: row.get(2).unwrap(),
2250 album: row.get(3).unwrap(),
2251 album_artist: row.get(4).unwrap(),
2252 track_number: row.get(5).unwrap(),
2253 disc_number: row.get(6).unwrap(),
2254 genre: row.get(7).unwrap(),
2255 analysis: Analysis {
2256 internal_analysis: vec![0.; features_version.feature_count()],
2257 features_version: features_version,
2258 },
2259 duration: Duration::from_secs_f64(row.get(8).unwrap()),
2260 features_version: features_version,
2261 cue_info,
2262 };
2263
2264 let serialized: String = row.get(10).unwrap();
2265 let extra_info = serde_json::from_str(&serialized).unwrap();
2266 Ok(LibrarySong {
2267 bliss_song: song,
2268 extra_info,
2269 })
2270 },
2271 )
2272 .expect("Song does not exist in the database");
2273 let mut stmt = connection
2274 .prepare(
2275 "
2276 select
2277 feature from feature join song on song.id = feature.song_id
2278 where song.path = ? order by feature_index
2279 ",
2280 )
2281 .unwrap();
2282 let analysis_vector = Analysis {
2283 internal_analysis: stmt
2284 .query_map(params![song_path], |row| row.get(0))
2285 .unwrap()
2286 .into_iter()
2287 .map(|x| x.unwrap())
2288 .collect::<Vec<f32>>()
2289 .try_into()
2290 .unwrap(),
2291 features_version: song.bliss_song.analysis.features_version,
2292 };
2293 song.bliss_song.analysis = analysis_vector;
2294 song
2295 }
2296
2297 fn _basic_song_from_database(connection: MutexGuard<Connection>, song_path: &str) -> Song {
2298 let mut expected_song = connection
2299 .query_row(
2300 "
2301 select
2302 path, artist, title, album, album_artist,
2303 track_number, disc_number, genre, duration, version
2304 from song where path=? and analyzed = true
2305 ",
2306 params![song_path],
2307 |row| {
2308 let path: String = row.get(0)?;
2309 Ok(Song {
2310 path: PathBuf::from(path),
2311 artist: row.get(1).unwrap(),
2312 title: row.get(2).unwrap(),
2313 album: row.get(3).unwrap(),
2314 album_artist: row.get(4).unwrap(),
2315 track_number: row.get(5).unwrap(),
2316 disc_number: row.get(6).unwrap(),
2317 genre: row.get(7).unwrap(),
2318 analysis: Analysis {
2319 internal_analysis: vec![0.; NUMBER_FEATURES],
2320 features_version: FeaturesVersion::Version2,
2321 },
2322 duration: Duration::from_secs_f64(row.get(8).unwrap()),
2323 features_version: row.get(9).unwrap(),
2324 cue_info: None,
2325 })
2326 },
2327 )
2328 .expect("Song is probably not in the db");
2329 let mut stmt = connection
2330 .prepare(
2331 "
2332 select
2333 feature from feature join song on song.id = feature.song_id
2334 where song.path = ? order by feature_index
2335 ",
2336 )
2337 .unwrap();
2338 let expected_analysis_vector = Analysis {
2339 internal_analysis: stmt
2340 .query_map(params![song_path], |row| row.get(0))
2341 .unwrap()
2342 .into_iter()
2343 .map(|x| x.unwrap())
2344 .collect::<Vec<f32>>()
2345 .try_into()
2346 .map_err(|v| {
2347 BlissError::ProviderError(format!("Could not retrieve analysis for song {} that was supposed to be analyzed: {:?}.", song_path, v))
2348 })
2349 .unwrap(),
2350 features_version: FeaturesVersion::Version2,
2351 };
2352 expected_song.analysis = expected_analysis_vector;
2353 expected_song
2354 }
2355
2356 fn _generate_basic_song(path: Option<String>) -> Song {
2357 let path = path.unwrap_or_else(|| "/path/to/song".into());
2358 let analysis_vector = (0..NUMBER_FEATURES)
2360 .map(|x| x as f32 + 0.1)
2361 .collect::<Vec<f32>>();
2362 Song {
2363 path: path.into(),
2364 artist: Some("An Artist".into()),
2365 title: Some("Title".into()),
2366 album: Some("An Album".into()),
2367 album_artist: Some("An Album Artist".into()),
2368 track_number: Some(3),
2369 disc_number: Some(1),
2370 genre: Some("Electronica".into()),
2371 analysis: Analysis {
2372 internal_analysis: analysis_vector,
2373 features_version: FeaturesVersion::Version2,
2374 },
2375 duration: Duration::from_secs(80),
2376 features_version: FeaturesVersion::LATEST,
2377 cue_info: None,
2378 }
2379 }
2380
2381 fn _generate_library_song(path: Option<String>) -> LibrarySong<ExtraInfo> {
2382 let song = _generate_basic_song(path);
2383 let extra_info = ExtraInfo {
2384 ignore: true,
2385 metadata_bliss_does_not_have: "FoobarIze".into(),
2386 };
2387 LibrarySong {
2388 bliss_song: song,
2389 extra_info,
2390 }
2391 }
2392
2393 fn first_factor_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
2394 (a[1] - b[1]).abs()
2395 }
2396
2397 #[test]
2398 #[cfg(feature = "ffmpeg")]
2399 fn test_library_playlist_song_not_existing() {
2400 let (library, _temp_dir, _) = setup_test_library();
2401 assert!(library
2402 .playlist_from::<ExtraInfo>(&["not-existing"])
2403 .is_err());
2404 }
2405
2406 #[test]
2407 #[cfg(feature = "ffmpeg")]
2408 fn test_library_simple_playlist() {
2409 let (library, _temp_dir, _) = setup_test_library();
2410 let songs: Vec<LibrarySong<ExtraInfo>> = library
2411 .playlist_from(&["/path/to/song2001"])
2412 .unwrap()
2413 .collect();
2414 assert_eq!(
2415 vec![
2416 "/path/to/song2001",
2417 "/path/to/song6001",
2418 "/path/to/song5001",
2419 "/path/to/song1001",
2420 "/path/to/song7001",
2421 "/path/to/cuetrack.cue/CUE_TRACK001",
2422 "/path/to/cuetrack.cue/CUE_TRACK002",
2423 ],
2424 songs
2425 .into_iter()
2426 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2427 .collect::<Vec<String>>(),
2428 )
2429 }
2430
2431 #[test]
2432 #[cfg(feature = "ffmpeg")]
2433 fn test_library_playlist_dupe_order_preserved() {
2434 let (library, _temp_dir, _) = setup_test_library();
2435 let songs: Vec<LibrarySong<ExtraInfo>> = library
2436 .playlist_from_custom(
2437 &["/path/to/song2201"],
2438 &euclidean_distance,
2439 closest_to_songs,
2440 false,
2441 )
2442 .unwrap()
2443 .collect();
2444 assert_eq!(
2445 vec![
2446 "/path/to/song2201",
2447 "/path/to/song2001",
2448 "/path/to/song6001",
2449 "/path/to/song5001",
2450 "/path/to/song1001",
2451 "/path/to/song7001",
2452 "/path/to/cuetrack.cue/CUE_TRACK001",
2453 "/path/to/cuetrack.cue/CUE_TRACK002",
2454 ],
2455 songs
2456 .into_iter()
2457 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2458 .collect::<Vec<String>>(),
2459 )
2460 }
2461
2462 fn first_factor_divided_by_30_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
2463 ((a[1] - b[1]).abs() / 30.).floor()
2464 }
2465
2466 #[test]
2467 #[cfg(feature = "ffmpeg")]
2468 fn test_library_playlist_deduplication() {
2469 let (library, _temp_dir, _) = setup_test_library();
2470 let songs: Vec<LibrarySong<ExtraInfo>> = library
2471 .playlist_from_custom(
2472 &["/path/to/song2001"],
2473 &first_factor_divided_by_30_distance,
2474 closest_to_songs,
2475 true,
2476 )
2477 .unwrap()
2478 .collect();
2479 assert_eq!(
2480 vec![
2481 "/path/to/song2001",
2482 "/path/to/song7001",
2483 "/path/to/cuetrack.cue/CUE_TRACK001",
2484 ],
2485 songs
2486 .into_iter()
2487 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2488 .collect::<Vec<String>>(),
2489 );
2490
2491 let songs: Vec<LibrarySong<ExtraInfo>> = library
2492 .playlist_from_custom(
2493 &["/path/to/song2001"],
2494 &first_factor_distance,
2495 &closest_to_songs,
2496 true,
2497 )
2498 .unwrap()
2499 .collect();
2500 assert_eq!(
2501 vec![
2502 "/path/to/song2001",
2503 "/path/to/song6001",
2504 "/path/to/song5001",
2505 "/path/to/song1001",
2506 "/path/to/song7001",
2507 "/path/to/cuetrack.cue/CUE_TRACK001",
2508 "/path/to/cuetrack.cue/CUE_TRACK002",
2509 ],
2510 songs
2511 .into_iter()
2512 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2513 .collect::<Vec<String>>(),
2514 )
2515 }
2516
2517 #[test]
2518 #[cfg(feature = "ffmpeg")]
2519 fn test_library_playlist_take() {
2520 let (library, _temp_dir, _) = setup_test_library();
2521 let songs: Vec<LibrarySong<ExtraInfo>> = library
2522 .playlist_from(&["/path/to/song2001"])
2523 .unwrap()
2524 .take(4)
2525 .collect();
2526 assert_eq!(
2527 vec![
2528 "/path/to/song2001",
2529 "/path/to/song6001",
2530 "/path/to/song5001",
2531 "/path/to/song1001",
2532 ],
2533 songs
2534 .into_iter()
2535 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2536 .collect::<Vec<String>>(),
2537 )
2538 }
2539
2540 #[test]
2541 #[cfg(feature = "ffmpeg")]
2542 fn test_library_custom_playlist_distance() {
2543 let (library, _temp_dir, _) = setup_test_library();
2544 let songs: Vec<LibrarySong<ExtraInfo>> = library
2545 .playlist_from_custom(
2546 &["/path/to/song2001"],
2547 &first_factor_distance,
2548 closest_to_songs,
2549 false,
2550 )
2551 .unwrap()
2552 .collect();
2553 assert_eq!(
2554 vec![
2555 "/path/to/song2001",
2556 "/path/to/song2201",
2557 "/path/to/song6001",
2558 "/path/to/song5001",
2559 "/path/to/song1001",
2560 "/path/to/song7001",
2561 "/path/to/cuetrack.cue/CUE_TRACK001",
2562 "/path/to/cuetrack.cue/CUE_TRACK002",
2563 ],
2564 songs
2565 .into_iter()
2566 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2567 .collect::<Vec<String>>(),
2568 )
2569 }
2570
2571 fn custom_sort(
2572 _: &[LibrarySong<ExtraInfo>],
2573 songs: &[LibrarySong<ExtraInfo>],
2574 _distance: &dyn DistanceMetricBuilder,
2575 ) -> impl Iterator<Item = LibrarySong<ExtraInfo>> {
2576 let mut songs = songs.to_vec();
2577 songs.sort_by(|s1, s2| s1.bliss_song.path.cmp(&s2.bliss_song.path));
2578 songs.to_vec().into_iter()
2579 }
2580
2581 #[test]
2582 #[cfg(feature = "ffmpeg")]
2583 fn test_library_custom_playlist_sort() {
2584 let (library, _temp_dir, _) = setup_test_library();
2585 let songs: Vec<LibrarySong<ExtraInfo>> = library
2586 .playlist_from_custom(
2587 &["/path/to/song2001"],
2588 &euclidean_distance,
2589 custom_sort,
2590 false,
2591 )
2592 .unwrap()
2593 .collect();
2594 assert_eq!(
2595 vec![
2596 "/path/to/song2001",
2597 "/path/to/cuetrack.cue/CUE_TRACK001",
2598 "/path/to/cuetrack.cue/CUE_TRACK002",
2599 "/path/to/song1001",
2600 "/path/to/song2201",
2601 "/path/to/song5001",
2602 "/path/to/song6001",
2603 "/path/to/song7001",
2604 ],
2605 songs
2606 .into_iter()
2607 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2608 .collect::<Vec<String>>(),
2609 )
2610 }
2611
2612 #[test]
2613 #[cfg(feature = "ffmpeg")]
2614 fn test_library_album_playlist() {
2615 let (library, _temp_dir, _) = setup_test_library();
2616 let album: Vec<LibrarySong<ExtraInfo>> = library
2617 .album_playlist_from("An Album1001".to_string(), 20)
2618 .unwrap();
2619 assert_eq!(
2620 vec![
2621 "/path/to/song5001".to_string(),
2623 "/path/to/song1001".to_string(),
2624 "/path/to/song6001".to_string(),
2626 "/path/to/song2001".to_string(),
2627 "/path/to/song2201".to_string(),
2629 "/path/to/song7001".to_string(),
2631 "/path/to/cuetrack.cue/CUE_TRACK001".to_string(),
2633 "/path/to/cuetrack.cue/CUE_TRACK002".to_string(),
2634 ],
2635 album
2636 .into_iter()
2637 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2638 .collect::<Vec<_>>(),
2639 )
2640 }
2641
2642 #[test]
2643 #[cfg(feature = "ffmpeg")]
2644 fn test_library_album_playlist_crop() {
2645 let (library, _temp_dir, _) = setup_test_library();
2646 let album: Vec<LibrarySong<ExtraInfo>> = library
2647 .album_playlist_from("An Album1001".to_string(), 1)
2648 .unwrap();
2649 assert_eq!(
2650 vec![
2651 "/path/to/song5001".to_string(),
2653 "/path/to/song1001".to_string(),
2654 "/path/to/song6001".to_string(),
2656 "/path/to/song2001".to_string(),
2657 "/path/to/song2201".to_string(),
2658 ],
2659 album
2660 .into_iter()
2661 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2662 .collect::<Vec<_>>(),
2663 )
2664 }
2665
2666 #[test]
2667 #[cfg(feature = "ffmpeg")]
2668 fn test_library_songs_from_album() {
2669 let (library, _temp_dir, _) = setup_test_library();
2670 let album: Vec<LibrarySong<ExtraInfo>> = library.songs_from_album("An Album1001").unwrap();
2671 assert_eq!(
2672 vec![
2673 "/path/to/song5001".to_string(),
2674 "/path/to/song1001".to_string()
2675 ],
2676 album
2677 .into_iter()
2678 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2679 .collect::<Vec<_>>(),
2680 )
2681 }
2682
2683 #[test]
2684 #[cfg(feature = "ffmpeg")]
2685 fn test_library_songs_from_album_proper_features_version() {
2686 let (library, _temp_dir, _) = setup_test_library();
2687 let album: Vec<LibrarySong<ExtraInfo>> = library.songs_from_album("An Album1001").unwrap();
2688 assert_eq!(
2689 vec![
2690 "/path/to/song5001".to_string(),
2691 "/path/to/song1001".to_string()
2692 ],
2693 album
2694 .into_iter()
2695 .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2696 .collect::<Vec<_>>(),
2697 )
2698 }
2699
2700 #[test]
2701 #[cfg(feature = "ffmpeg")]
2702 fn test_library_songs_from_album_not_existing() {
2703 let (library, _temp_dir, _) = setup_test_library();
2704 assert!(library
2705 .songs_from_album::<ExtraInfo>("not-existing")
2706 .is_err());
2707 }
2708
2709 #[test]
2710 #[cfg(feature = "ffmpeg")]
2711 fn test_library_delete_path_non_existing() {
2712 let (mut library, _temp_dir, _) = setup_test_library();
2713 {
2714 let connection = library.sqlite_conn.lock().unwrap();
2715 let count: u32 = connection
2716 .query_row(
2717 "select count(*) from feature join song on song.id = feature.song_id where song.path = ?",
2718 ["not-existing"],
2719 |row| row.get(0),
2720 )
2721 .unwrap();
2722 assert_eq!(count, 0);
2723 let count: u32 = connection
2724 .query_row(
2725 "select count(*) from song where path = ?",
2726 ["not-existing"],
2727 |row| row.get(0),
2728 )
2729 .unwrap();
2730 assert_eq!(count, 0);
2731 }
2732 assert!(library.delete_path("not-existing").is_err());
2733 }
2734
2735 #[test]
2736 #[cfg(feature = "ffmpeg")]
2737 fn test_library_delete_path() {
2738 let (mut library, _temp_dir, _) = setup_test_library();
2739 {
2740 let connection = library.sqlite_conn.lock().unwrap();
2741 let count: u32 = connection
2742 .query_row(
2743 "select count(*) from feature join song on song.id = feature.song_id where song.path = ?",
2744 ["/path/to/song1001"],
2745 |row| row.get(0),
2746 )
2747 .unwrap();
2748 assert!(count >= 1);
2749 let count: u32 = connection
2750 .query_row(
2751 "select count(*) from song where path = ?",
2752 ["/path/to/song1001"],
2753 |row| row.get(0),
2754 )
2755 .unwrap();
2756 assert!(count >= 1);
2757 }
2758
2759 library.delete_path("/path/to/song1001").unwrap();
2760
2761 {
2762 let connection = library.sqlite_conn.lock().unwrap();
2763 let count: u32 = connection
2764 .query_row(
2765 "select count(*) from feature join song on song.id = feature.song_id where song.path = ?",
2766 ["/path/to/song1001"],
2767 |row| row.get(0),
2768 )
2769 .unwrap();
2770 assert_eq!(0, count);
2771 let count: u32 = connection
2772 .query_row(
2773 "select count(*) from song where path = ?",
2774 ["/path/to/song1001"],
2775 |row| row.get(0),
2776 )
2777 .unwrap();
2778 assert_eq!(0, count);
2779 }
2780 }
2781
2782 #[test]
2783 #[cfg(feature = "ffmpeg")]
2784 fn test_library_delete_paths() {
2785 let (mut library, _temp_dir, _) = setup_test_library();
2786 {
2787 let connection = library.sqlite_conn.lock().unwrap();
2788 let count: u32 = connection
2789 .query_row(
2790 "select count(*) from feature join song on song.id = feature.song_id where song.path in (?1, ?2)",
2791 ["/path/to/song1001", "/path/to/song2001"],
2792 |row| row.get(0),
2793 )
2794 .unwrap();
2795 assert!(count >= 1);
2796 let count: u32 = connection
2797 .query_row(
2798 "select count(*) from song where path in (?1, ?2)",
2799 ["/path/to/song1001", "/path/to/song2001"],
2800 |row| row.get(0),
2801 )
2802 .unwrap();
2803 assert!(count >= 1);
2804 }
2805
2806 library
2807 .delete_paths(vec!["/path/to/song1001", "/path/to/song2001"])
2808 .unwrap();
2809
2810 {
2811 let connection = library.sqlite_conn.lock().unwrap();
2812 let count: u32 = connection
2813 .query_row(
2814 "select count(*) from feature join song on song.id = feature.song_id where song.path in (?1, ?2)",
2815 ["/path/to/song1001", "/path/to/song2001"],
2816 |row| row.get(0),
2817 )
2818 .unwrap();
2819 assert_eq!(0, count);
2820 let count: u32 = connection
2821 .query_row(
2822 "select count(*) from song where path in (?1, ?2)",
2823 ["/path/to/song1001", "/path/to/song2001"],
2824 |row| row.get(0),
2825 )
2826 .unwrap();
2827 assert_eq!(0, count);
2828 let count: u32 = connection
2830 .query_row("select count(*) from feature", [], |row| row.get(0))
2831 .unwrap();
2832 assert!(count >= 1);
2833 let count: u32 = connection
2834 .query_row("select count(*) from song", [], |row| row.get(0))
2835 .unwrap();
2836 assert!(count >= 1);
2837 }
2838 }
2839
2840 #[test]
2841 #[cfg(feature = "ffmpeg")]
2842 fn test_library_delete_paths_empty() {
2843 let (mut library, _temp_dir, _) = setup_test_library();
2844 assert_eq!(library.delete_paths::<String, _>([]).unwrap(), 0);
2845 }
2846
2847 #[test]
2848 #[cfg(feature = "ffmpeg")]
2849 fn test_library_delete_paths_non_existing() {
2850 let (mut library, _temp_dir, _) = setup_test_library();
2851 assert_eq!(library.delete_paths(["not-existing"]).unwrap(), 0);
2852 }
2853
2854 #[test]
2855 #[cfg(feature = "ffmpeg")]
2856 fn test_analyze_paths_cue() {
2857 let (mut library, _temp_dir, _) = setup_test_library();
2858 library
2859 .config
2860 .base_config_mut()
2861 .analysis_options
2862 .features_version = FeaturesVersion::Version1;
2863 {
2864 let sqlite_conn =
2865 Connection::open(&library.config.base_config().database_path).unwrap();
2866 sqlite_conn.execute("delete from song", []).unwrap();
2867 }
2868
2869 let paths = vec![
2870 "./data/s16_mono_22_5kHz.flac",
2871 "./data/testcue.cue",
2872 "non-existing",
2873 ];
2874 library
2875 .analyze_paths_with_options(
2876 paths.to_owned(),
2877 false,
2878 AnalysisOptions {
2879 features_version: FeaturesVersion::Version2,
2880 ..Default::default()
2881 },
2882 )
2883 .unwrap();
2884 let expected_analyzed_paths = vec![
2885 "./data/s16_mono_22_5kHz.flac",
2886 "./data/testcue.cue/CUE_TRACK001",
2887 "./data/testcue.cue/CUE_TRACK002",
2888 "./data/testcue.cue/CUE_TRACK003",
2889 ];
2890 {
2891 let connection = library.sqlite_conn.lock().unwrap();
2892 let mut stmt = connection
2893 .prepare(
2894 "
2895 select
2896 path from song where analyzed = true and path not like '%song%'
2897 order by path
2898 ",
2899 )
2900 .unwrap();
2901 let paths = stmt
2902 .query_map(params![], |row| row.get(0))
2903 .unwrap()
2904 .map(|x| x.unwrap())
2905 .collect::<Vec<String>>();
2906
2907 assert_eq!(paths, expected_analyzed_paths);
2908 }
2909 {
2910 let connection = library.sqlite_conn.lock().unwrap();
2911 let song: LibrarySong<()> =
2912 _library_song_from_database(connection, "./data/testcue.cue/CUE_TRACK001");
2913 assert!(song.bliss_song.cue_info.is_some());
2914 }
2915 }
2916
2917 #[test]
2918 #[cfg(feature = "ffmpeg")]
2919 fn test_analyze_paths() {
2920 let (mut library, _temp_dir, _) = setup_test_library();
2921 library
2922 .config
2923 .base_config_mut()
2924 .analysis_options
2925 .features_version = FeaturesVersion::LATEST;
2926
2927 let paths = vec![
2928 "./data/s16_mono_22_5kHz.flac",
2929 "./data/s16_stereo_22_5kHz.flac",
2930 "non-existing",
2931 ];
2932 library.analyze_paths(paths.to_owned(), false).unwrap();
2933 let songs = paths[..2]
2934 .iter()
2935 .map(|path| {
2936 let connection = library.sqlite_conn.lock().unwrap();
2937 _library_song_from_database(connection, path)
2938 })
2939 .collect::<Vec<LibrarySong<()>>>();
2940 let expected_songs = paths[..2]
2941 .iter()
2942 .zip(vec![(), ()].into_iter())
2943 .map(|(path, expected_extra_info)| LibrarySong {
2944 bliss_song: Decoder::song_from_path(path).unwrap(),
2945 extra_info: expected_extra_info,
2946 })
2947 .collect::<Vec<LibrarySong<()>>>();
2948 assert_eq!(songs, expected_songs);
2949 assert_eq!(
2950 library
2951 .config
2952 .base_config_mut()
2953 .analysis_options
2954 .features_version,
2955 FeaturesVersion::LATEST
2956 );
2957 }
2958
2959 #[test]
2960 #[cfg(feature = "ffmpeg")]
2961 fn test_analyze_paths_convert_extra_info() {
2962 let (mut library, _temp_dir, _) = setup_test_library();
2963 library
2964 .config
2965 .base_config_mut()
2966 .analysis_options
2967 .features_version = FeaturesVersion::Version1;
2968 let paths = vec![
2969 ("./data/s16_mono_22_5kHz.flac", true),
2970 ("./data/s16_stereo_22_5kHz.flac", false),
2971 ("non-existing", false),
2972 ];
2973 library
2974 .analyze_paths_convert_extra_info(
2975 paths.to_owned(),
2976 true,
2977 |b, _, _| ExtraInfo {
2978 ignore: b,
2979 metadata_bliss_does_not_have: String::from("coucou"),
2980 },
2981 AnalysisOptions::default(),
2982 )
2983 .unwrap();
2984 library
2985 .analyze_paths_convert_extra_info(
2986 paths.to_owned(),
2987 false,
2988 |b, _, _| ExtraInfo {
2989 ignore: b,
2990 metadata_bliss_does_not_have: String::from("coucou"),
2991 },
2992 AnalysisOptions::default(),
2993 )
2994 .unwrap();
2995 let songs = paths[..2]
2996 .iter()
2997 .map(|(path, _)| {
2998 let connection = library.sqlite_conn.lock().unwrap();
2999 _library_song_from_database(connection, path)
3000 })
3001 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3002 let expected_songs = paths[..2]
3003 .iter()
3004 .zip(
3005 vec![
3006 ExtraInfo {
3007 ignore: true,
3008 metadata_bliss_does_not_have: String::from("coucou"),
3009 },
3010 ExtraInfo {
3011 ignore: false,
3012 metadata_bliss_does_not_have: String::from("coucou"),
3013 },
3014 ]
3015 .into_iter(),
3016 )
3017 .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3018 bliss_song: Decoder::song_from_path(path).unwrap(),
3019 extra_info: expected_extra_info,
3020 })
3021 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3022 assert_eq!(songs, expected_songs);
3023 assert_eq!(
3024 library
3025 .config
3026 .base_config_mut()
3027 .analysis_options
3028 .features_version,
3029 FeaturesVersion::LATEST
3030 );
3031 }
3032
3033 #[test]
3034 #[cfg(feature = "ffmpeg")]
3035 fn test_analyze_paths_extra_info() {
3036 let (mut library, _temp_dir, _) = setup_test_library();
3037
3038 let paths = vec![
3039 (
3040 "./data/s16_mono_22_5kHz.flac",
3041 ExtraInfo {
3042 ignore: true,
3043 metadata_bliss_does_not_have: String::from("hey"),
3044 },
3045 ),
3046 (
3047 "./data/s16_stereo_22_5kHz.flac",
3048 ExtraInfo {
3049 ignore: false,
3050 metadata_bliss_does_not_have: String::from("hello"),
3051 },
3052 ),
3053 (
3054 "non-existing",
3055 ExtraInfo {
3056 ignore: true,
3057 metadata_bliss_does_not_have: String::from("coucou"),
3058 },
3059 ),
3060 ];
3061 library
3062 .analyze_paths_extra_info(paths.to_owned(), false, AnalysisOptions::default())
3063 .unwrap();
3064 let songs = paths[..2]
3065 .iter()
3066 .map(|(path, _)| {
3067 let connection = library.sqlite_conn.lock().unwrap();
3068 _library_song_from_database(connection, path)
3069 })
3070 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3071 let expected_songs = paths[..2]
3072 .iter()
3073 .zip(
3074 vec![
3075 ExtraInfo {
3076 ignore: true,
3077 metadata_bliss_does_not_have: String::from("hey"),
3078 },
3079 ExtraInfo {
3080 ignore: false,
3081 metadata_bliss_does_not_have: String::from("hello"),
3082 },
3083 ]
3084 .into_iter(),
3085 )
3086 .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3087 bliss_song: Decoder::song_from_path(path).unwrap(),
3088 extra_info: expected_extra_info,
3089 })
3090 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3091 assert_eq!(songs, expected_songs);
3092 }
3093
3094 #[test]
3095 #[cfg(feature = "ffmpeg")]
3096 fn test_update_skip_analyzed() {
3099 let (mut library, _temp_dir, _) = setup_test_library();
3100 library
3101 .config
3102 .base_config_mut()
3103 .analysis_options
3104 .features_version = FeaturesVersion::Version1;
3105 for input in vec![
3106 ("./data/s16_mono_22_5kHz.flac", true),
3107 ("./data/s16_mono_22_5kHz.flac", false),
3108 ]
3109 .into_iter()
3110 {
3111 let paths = vec![input.to_owned()];
3112 library
3113 .update_library_convert_extra_info(
3114 paths.to_owned(),
3115 true,
3116 false,
3117 |b, _, _| ExtraInfo {
3118 ignore: b,
3119 metadata_bliss_does_not_have: String::from("coucou"),
3120 },
3121 AnalysisOptions {
3122 features_version: FeaturesVersion::Version1,
3123 ..Default::default()
3124 },
3125 )
3126 .unwrap();
3127 let song = {
3128 let connection = library.sqlite_conn.lock().unwrap();
3129 _library_song_from_database::<ExtraInfo>(connection, "./data/s16_mono_22_5kHz.flac")
3130 };
3131 let expected_song = {
3132 LibrarySong {
3133 bliss_song: Decoder::song_from_path_with_options(
3134 "./data/s16_mono_22_5kHz.flac",
3135 AnalysisOptions {
3136 features_version: FeaturesVersion::Version1,
3137 ..Default::default()
3138 },
3139 )
3140 .unwrap(),
3141 extra_info: ExtraInfo {
3142 ignore: true,
3143 metadata_bliss_does_not_have: String::from("coucou"),
3144 },
3145 }
3146 };
3147 assert_eq!(song, expected_song);
3148 assert_eq!(
3149 library
3150 .config
3151 .base_config_mut()
3152 .analysis_options
3153 .features_version,
3154 FeaturesVersion::Version1
3155 );
3156 }
3157 }
3158
3159 fn _get_song_analyzed(
3160 connection: MutexGuard<Connection>,
3161 path: String,
3162 ) -> Result<bool, RusqliteError> {
3163 let mut stmt = connection.prepare(
3164 "
3165 select
3166 analyzed from song
3167 where song.path = ?
3168 ",
3169 )?;
3170 stmt.query_row([path], |row| (row.get(0)))
3171 }
3172
3173 #[test]
3174 #[cfg(feature = "ffmpeg")]
3175 fn test_update_library_override_old_features() {
3180 let (mut library, _temp_dir, _) = setup_test_library();
3181 let path: String = "./data/s16_stereo_22_5kHz.flac".into();
3182
3183 {
3185 let connection = library.sqlite_conn.lock().unwrap();
3186 let song: LibrarySong<ExtraInfo> = _library_song_from_database(connection, &path);
3187 assert_eq!(
3188 song.bliss_song.analysis,
3189 Analysis {
3190 internal_analysis: vec![
3191 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17.,
3192 18.
3193 ],
3194 features_version: FeaturesVersion::Version1,
3195 }
3196 )
3197 }
3198 {
3200 let connection = library.sqlite_conn.lock().unwrap();
3201 let count_old_features_version: u32 = connection
3202 .query_row(
3203 "select count(*) from song where version = ? and analyzed = true",
3204 params![FeaturesVersion::Version1],
3205 |row| row.get(0),
3206 )
3207 .unwrap();
3208 assert!(count_old_features_version > 0);
3209 }
3210
3211 library
3212 .update_library(vec![path.to_owned()], true, false)
3213 .unwrap();
3214
3215 {
3217 let connection = library.sqlite_conn.lock().unwrap();
3218 let count_old_features_version: u32 = connection
3219 .query_row(
3220 "select count(*) from song where version = ? and analyzed = true",
3221 params![FeaturesVersion::Version1],
3222 |row| row.get(0),
3223 )
3224 .unwrap();
3225 assert_eq!(count_old_features_version, 0);
3226 }
3227
3228 let connection = library.sqlite_conn.lock().unwrap();
3229 let song: LibrarySong<()> = _library_song_from_database(connection, &path);
3230 let expected_analysis_vector = Decoder::song_from_path(path).unwrap().analysis;
3232 assert_eq!(song.bliss_song.analysis, expected_analysis_vector);
3233 assert_eq!(
3234 song.bliss_song.analysis.features_version,
3235 FeaturesVersion::LATEST
3236 );
3237 }
3238
3239 #[test]
3240 #[cfg(feature = "ffmpeg")]
3241 fn test_update_library() {
3243 let (mut library, _temp_dir, _) = setup_test_library();
3244 library
3245 .config
3246 .base_config_mut()
3247 .analysis_options
3248 .features_version = FeaturesVersion::LATEST;
3249
3250 {
3251 let connection = library.sqlite_conn.lock().unwrap();
3252 assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3254 }
3255
3256 let paths = vec![
3257 "./data/s16_mono_22_5kHz.flac",
3258 "./data/s16_stereo_22_5kHz.flac",
3259 "/path/to/song4001",
3260 "non-existing",
3261 ];
3262 library
3263 .update_library(paths.to_owned(), true, false)
3264 .unwrap();
3265 library
3266 .update_library(paths.to_owned(), true, true)
3267 .unwrap();
3268
3269 let songs = paths[..2]
3270 .iter()
3271 .map(|path| {
3272 let connection = library.sqlite_conn.lock().unwrap();
3273 _library_song_from_database(connection, path)
3274 })
3275 .collect::<Vec<LibrarySong<()>>>();
3276 let expected_songs = paths[..2]
3277 .iter()
3278 .zip(vec![(), ()].into_iter())
3279 .map(|(path, expected_extra_info)| LibrarySong {
3280 bliss_song: Decoder::song_from_path(path).unwrap(),
3281 extra_info: expected_extra_info,
3282 })
3283 .collect::<Vec<LibrarySong<()>>>();
3284
3285 assert_eq!(songs, expected_songs);
3286 {
3287 let connection = library.sqlite_conn.lock().unwrap();
3288 assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3290 }
3291 assert_eq!(
3292 library
3293 .config
3294 .base_config_mut()
3295 .analysis_options
3296 .features_version,
3297 FeaturesVersion::LATEST
3298 );
3299 }
3300
3301 #[test]
3302 #[cfg(feature = "ffmpeg")]
3303 fn test_update_library_with_options() {
3305 let (mut library, _temp_dir, _) = setup_test_library();
3306 library
3307 .config
3308 .base_config_mut()
3309 .analysis_options
3310 .features_version = FeaturesVersion::LATEST;
3311
3312 {
3313 let connection = library.sqlite_conn.lock().unwrap();
3314 assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3316 }
3317 {
3318 let connection = library.sqlite_conn.lock().unwrap();
3319 connection
3321 .execute("update song set extra_info = \"null\";", [])
3322 .unwrap();
3323 }
3324
3325 let paths = vec![
3326 "./data/s16_mono_22_5kHz.flac",
3327 "./data/s16_stereo_22_5kHz.flac",
3328 "/path/to/song4001",
3329 "non-existing",
3330 ];
3331 library
3332 .update_library_with_options(
3333 paths.to_owned(),
3334 true,
3335 false,
3336 AnalysisOptions {
3337 features_version: FeaturesVersion::Version1,
3338 ..Default::default()
3339 },
3340 )
3341 .unwrap();
3342 library
3343 .update_library_with_options(
3344 paths.to_owned(),
3345 true,
3346 false,
3347 AnalysisOptions {
3348 features_version: FeaturesVersion::Version1,
3349 ..Default::default()
3350 },
3351 )
3352 .unwrap();
3353
3354 let first_song = {
3355 let connection = library.sqlite_conn.lock().unwrap();
3356 _library_song_from_database(connection, paths[0])
3357 };
3358 let expected_song = LibrarySong {
3359 bliss_song: Decoder::song_from_path_with_options(
3360 paths[0],
3361 AnalysisOptions {
3362 features_version: FeaturesVersion::Version1,
3363 ..Default::default()
3364 },
3365 )
3366 .unwrap(),
3367 extra_info: (),
3368 };
3369
3370 assert_eq!(first_song, expected_song);
3371 {
3372 let connection = library.sqlite_conn.lock().unwrap();
3373 assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3376 }
3377 assert_eq!(
3378 library
3379 .config
3380 .base_config_mut()
3381 .analysis_options
3382 .features_version,
3383 FeaturesVersion::Version1
3384 );
3385 }
3386
3387 #[test]
3388 #[cfg(feature = "ffmpeg")]
3389 fn test_update_extra_info() {
3390 let (mut library, _temp_dir, _) = setup_test_library();
3391 library
3392 .config
3393 .base_config_mut()
3394 .analysis_options
3395 .features_version = FeaturesVersion::LATEST;
3396
3397 {
3398 let connection = library.sqlite_conn.lock().unwrap();
3399 assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3401 }
3402
3403 let paths = vec![
3404 ("./data/s16_mono_22_5kHz.flac", true),
3405 ("./data/s16_stereo_22_5kHz.flac", false),
3406 ("/path/to/song4001", false),
3407 ("non-existing", false),
3408 ];
3409 library
3410 .update_library_extra_info(paths.to_owned(), true, false)
3411 .unwrap();
3412 let songs = paths[..2]
3413 .iter()
3414 .map(|(path, _)| {
3415 let connection = library.sqlite_conn.lock().unwrap();
3416 _library_song_from_database(connection, path)
3417 })
3418 .collect::<Vec<LibrarySong<bool>>>();
3419 let expected_songs = paths[..2]
3420 .iter()
3421 .zip(vec![true, false].into_iter())
3422 .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3423 bliss_song: Decoder::song_from_path(path).unwrap(),
3424 extra_info: expected_extra_info,
3425 })
3426 .collect::<Vec<LibrarySong<bool>>>();
3427 assert_eq!(songs, expected_songs);
3428 {
3429 let connection = library.sqlite_conn.lock().unwrap();
3430 assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3432 }
3433 assert_eq!(
3434 library
3435 .config
3436 .base_config_mut()
3437 .analysis_options
3438 .features_version,
3439 FeaturesVersion::LATEST
3440 );
3441 }
3442
3443 #[test]
3444 #[cfg(feature = "ffmpeg")]
3445 fn test_update_convert_extra_info() {
3446 let (mut library, _temp_dir, _) = setup_test_library();
3447 library
3448 .config
3449 .base_config_mut()
3450 .analysis_options
3451 .features_version = FeaturesVersion::Version1;
3452
3453 {
3454 let connection = library.sqlite_conn.lock().unwrap();
3455 assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3457 }
3458 {
3459 let connection = library.sqlite_conn.lock().unwrap();
3460 assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap());
3462 }
3463
3464 let paths = vec![
3465 ("./data/s16_mono_22_5kHz.flac", true),
3466 ("./data/s16_stereo_22_5kHz.flac", false),
3467 ("/path/to/song4001", false),
3468 ("non-existing", false),
3469 ];
3470 library
3471 .update_library_convert_extra_info(
3472 paths.to_owned(),
3473 true,
3474 false,
3475 |b, _, _| ExtraInfo {
3476 ignore: b,
3477 metadata_bliss_does_not_have: String::from("coucou"),
3478 },
3479 AnalysisOptions::default(),
3480 )
3481 .unwrap();
3482 let songs = paths[..2]
3483 .iter()
3484 .map(|(path, _)| {
3485 let connection = library.sqlite_conn.lock().unwrap();
3486 _library_song_from_database(connection, path)
3487 })
3488 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3489 let expected_songs = paths[..2]
3490 .iter()
3491 .zip(
3492 vec![
3493 ExtraInfo {
3494 ignore: true,
3495 metadata_bliss_does_not_have: String::from("coucou"),
3496 },
3497 ExtraInfo {
3498 ignore: false,
3499 metadata_bliss_does_not_have: String::from("coucou"),
3500 },
3501 ]
3502 .into_iter(),
3503 )
3504 .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3505 bliss_song: Decoder::song_from_path(path).unwrap(),
3506 extra_info: expected_extra_info,
3507 })
3508 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3509 assert_eq!(songs, expected_songs);
3510 {
3511 let connection = library.sqlite_conn.lock().unwrap();
3512 assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3514 }
3515 {
3516 let connection = library.sqlite_conn.lock().unwrap();
3517 assert_eq!(
3519 rusqlite::Error::QueryReturnedNoRows,
3520 _get_song_analyzed(connection, "/path/to/song2001".into()).unwrap_err(),
3521 );
3522 }
3523 assert_eq!(
3524 library
3525 .config
3526 .base_config_mut()
3527 .analysis_options
3528 .features_version,
3529 FeaturesVersion::LATEST
3530 );
3531 }
3532
3533 #[test]
3534 #[cfg(feature = "ffmpeg")]
3535 fn test_update_convert_extra_info_do_not_delete() {
3537 let (mut library, _temp_dir, _) = setup_test_library();
3538 library
3539 .config
3540 .base_config_mut()
3541 .analysis_options
3542 .features_version = FeaturesVersion::Version1;
3543
3544 {
3545 let connection = library.sqlite_conn.lock().unwrap();
3546 assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3548 }
3549 {
3550 let connection = library.sqlite_conn.lock().unwrap();
3551 assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap());
3553 }
3554
3555 let paths = vec![
3556 ("./data/s16_mono_22_5kHz.flac", true),
3557 ("./data/s16_stereo_22_5kHz.flac", false),
3558 ("/path/to/song4001", false),
3559 ("non-existing", false),
3560 ];
3561 library
3562 .update_library_convert_extra_info(
3563 paths.to_owned(),
3564 false,
3565 false,
3566 |b, _, _| ExtraInfo {
3567 ignore: b,
3568 metadata_bliss_does_not_have: String::from("coucou"),
3569 },
3570 AnalysisOptions::default(),
3571 )
3572 .unwrap();
3573 let songs = paths[..2]
3574 .iter()
3575 .map(|(path, _)| {
3576 let connection = library.sqlite_conn.lock().unwrap();
3577 _library_song_from_database(connection, path)
3578 })
3579 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3580 let expected_songs = paths[..2]
3581 .iter()
3582 .zip(
3583 vec![
3584 ExtraInfo {
3585 ignore: true,
3586 metadata_bliss_does_not_have: String::from("coucou"),
3587 },
3588 ExtraInfo {
3589 ignore: false,
3590 metadata_bliss_does_not_have: String::from("coucou"),
3591 },
3592 ]
3593 .into_iter(),
3594 )
3595 .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3596 bliss_song: Decoder::song_from_path(path).unwrap(),
3597 extra_info: expected_extra_info,
3598 })
3599 .collect::<Vec<LibrarySong<ExtraInfo>>>();
3600 assert_eq!(songs, expected_songs);
3601 {
3602 let connection = library.sqlite_conn.lock().unwrap();
3603 assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3605 }
3606 {
3607 let connection = library.sqlite_conn.lock().unwrap();
3608 assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap());
3610 }
3611 assert_eq!(
3612 library
3613 .config
3614 .base_config_mut()
3615 .analysis_options
3616 .features_version,
3617 FeaturesVersion::LATEST
3618 );
3619 }
3620
3621 #[test]
3622 #[cfg(feature = "ffmpeg")]
3623 fn test_song_from_path() {
3624 let (library, _temp_dir, _) = setup_test_library();
3625 let analysis_vector = (0..NUMBER_FEATURES)
3626 .map(|x| x as f32 + 10.)
3627 .collect::<Vec<f32>>();
3628
3629 let song = Song {
3630 path: "/path/to/song2001".into(),
3631 artist: Some("Artist2001".into()),
3632 title: Some("Title2001".into()),
3633 album: Some("An Album2001".into()),
3634 album_artist: Some("An Album Artist2001".into()),
3635 track_number: Some(2),
3636 disc_number: Some(1),
3637 genre: Some("Electronica2001".into()),
3638 analysis: Analysis {
3639 internal_analysis: analysis_vector,
3640 features_version: FeaturesVersion::Version2,
3641 },
3642 duration: Duration::from_secs(410),
3643 features_version: FeaturesVersion::Version2,
3644 cue_info: None,
3645 };
3646 let expected_song = LibrarySong {
3647 bliss_song: song,
3648 extra_info: ExtraInfo {
3649 ignore: false,
3650 metadata_bliss_does_not_have: String::from("/path/to/charlie2001"),
3651 },
3652 };
3653
3654 let song = library
3655 .song_from_path::<ExtraInfo>("/path/to/song2001")
3656 .unwrap();
3657 assert_eq!(song, expected_song)
3658 }
3659
3660 #[test]
3661 #[cfg(feature = "ffmpeg")]
3662 fn test_store_failed_song() {
3663 let (mut library, _temp_dir, _) = setup_test_library();
3664 library
3665 .store_failed_song(
3666 "/some/failed/path",
3667 BlissError::ProviderError("error with the analysis".into()),
3668 FeaturesVersion::Version1,
3669 )
3670 .unwrap();
3671 let connection = library.sqlite_conn.lock().unwrap();
3672 let (error, analyzed, features_version): (String, bool, FeaturesVersion) = connection
3673 .query_row(
3674 "
3675 select
3676 error, analyzed, version
3677 from song where path=?
3678 ",
3679 params!["/some/failed/path"],
3680 |row| Ok((row.get_unwrap(0), row.get_unwrap(1), row.get_unwrap(2))),
3681 )
3682 .unwrap();
3683 assert_eq!(
3684 error,
3685 String::from(
3686 "error happened with the music library provider - error with the analysis"
3687 )
3688 );
3689 assert_eq!(analyzed, false);
3690 assert_eq!(features_version, FeaturesVersion::Version1);
3691 let count_features: u32 = connection
3692 .query_row(
3693 "
3694 select
3695 count(*) from feature join song
3696 on song.id = feature.song_id where path=?
3697 ",
3698 params!["/some/failed/path"],
3699 |row| Ok(row.get_unwrap(0)),
3700 )
3701 .unwrap();
3702 assert_eq!(count_features, 0);
3703 }
3704
3705 #[test]
3706 #[cfg(feature = "ffmpeg")]
3707 fn test_songs_from_library() {
3708 let (library, _temp_dir, expected_library_songs) = setup_test_library();
3709
3710 let library_songs = library.songs_from_library::<ExtraInfo>().unwrap();
3711 assert_eq!(library_songs.len(), 8);
3712 assert_eq!(
3713 expected_library_songs,
3714 (
3715 library_songs[0].to_owned(),
3716 library_songs[1].to_owned(),
3717 library_songs[2].to_owned(),
3718 library_songs[3].to_owned(),
3719 library_songs[4].to_owned(),
3720 library_songs[5].to_owned(),
3721 library_songs[6].to_owned(),
3722 library_songs[7].to_owned(),
3723 )
3724 );
3725 }
3726
3727 #[test]
3728 #[cfg(feature = "ffmpeg")]
3729 fn test_songs_from_library_screwed_db() {
3730 let (library, _temp_dir, _) = setup_test_library();
3731 {
3732 let connection = library.sqlite_conn.lock().unwrap();
3733 connection
3734 .execute(
3735 "insert into feature (song_id, feature, feature_index)
3736 values (2001, 1.5, 29)
3737 ",
3738 [],
3739 )
3740 .unwrap();
3741 }
3742
3743 let error = library.songs_from_library::<ExtraInfo>().unwrap_err();
3744 assert_eq!(
3745 error.to_string(),
3746 String::from(
3747 "error happened with the music library provider - \
3748 Song with ID 2001 and path /path/to/song2001 has a \
3749 different feature number than expected. Please rescan or \
3750 update the song library.",
3751 ),
3752 );
3753 }
3754
3755 #[test]
3756 #[cfg(feature = "ffmpeg")]
3757 fn test_song_from_path_not_analyzed() {
3758 let (library, _temp_dir, _) = setup_test_library();
3759 let error = library.song_from_path::<ExtraInfo>("/path/to/song404");
3760 assert!(error.is_err());
3761 }
3762
3763 #[test]
3764 #[cfg(feature = "ffmpeg")]
3765 fn test_song_from_path_not_found() {
3766 let (library, _temp_dir, _) = setup_test_library();
3767 let error = library.song_from_path::<ExtraInfo>("/path/to/randomsong");
3768 assert!(error.is_err());
3769 }
3770
3771 #[test]
3772 fn test_get_default_data_folder_no_default_path() {
3773 env::set_var("XDG_CONFIG_HOME", "/home/foo/.config");
3780 env::set_var("XDG_DATA_HOME", "/home/foo/.local/share");
3781 assert_eq!(
3782 PathBuf::from("/home/foo/.config/bliss-rs"),
3783 BaseConfig::get_default_data_folder().unwrap()
3784 );
3785 env::remove_var("XDG_CONFIG_HOME");
3786 env::remove_var("XDG_DATA_HOME");
3787
3788 let existing_legacy_folder = TempDir::new("tmp").unwrap();
3790 create_dir_all(existing_legacy_folder.path().join("bliss-rs")).unwrap();
3791 env::set_var("XDG_CONFIG_HOME", "/home/foo/.config");
3792 env::set_var("XDG_DATA_HOME", existing_legacy_folder.path().as_os_str());
3793 assert_eq!(
3794 existing_legacy_folder.path().join("bliss-rs"),
3795 BaseConfig::get_default_data_folder().unwrap()
3796 );
3797
3798 let existing_folder = TempDir::new("tmp").unwrap();
3800 create_dir_all(existing_folder.path().join("bliss-rs")).unwrap();
3801 env::set_var("XDG_CONFIG_HOME", existing_folder.path().as_os_str());
3802 assert_eq!(
3803 existing_folder.path().join("bliss-rs"),
3804 BaseConfig::get_default_data_folder().unwrap()
3805 );
3806
3807 env::remove_var("XDG_DATA_HOME");
3808 env::remove_var("XDG_CONFIG_HOME");
3809
3810 assert_eq!(
3811 PathBuf::from("/tmp/bliss-rs/"),
3812 BaseConfig::get_default_data_folder().unwrap()
3813 );
3814 }
3815
3816 #[test]
3817 #[cfg(feature = "ffmpeg")]
3818 fn test_library_new_default_write() {
3819 let (library, _temp_dir, _) = setup_test_library();
3820 let config_content = fs::read_to_string(&library.config.base_config().config_path)
3821 .unwrap()
3822 .replace(' ', "")
3823 .replace('\n', "");
3824 assert_eq!(
3825 config_content,
3826 format!(
3827 "{{\"config_path\":\"{}\",\"database_path\":\"{}\",\"\
3828 features_version\":{},\"number_cores\":{},\
3829 \"m\":{{\"v\":1,\"dim\":[{},{}],\"data\":{}}}}}",
3830 library.config.base_config().config_path.display(),
3831 library.config.base_config().database_path.display(),
3832 FeaturesVersion::LATEST as u16,
3833 thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()),
3834 NUMBER_FEATURES,
3835 NUMBER_FEATURES,
3836 format!(
3838 "{:?}",
3839 Array2::<f32>::eye(NUMBER_FEATURES).as_slice().unwrap()
3840 )
3841 .replace(" ", ""),
3842 )
3843 );
3844 }
3845
3846 #[test]
3847 #[cfg(feature = "ffmpeg")]
3848 fn test_library_new_create_database() {
3849 let (library, _temp_dir, _) = setup_test_library();
3850 let sqlite_conn = Connection::open(&library.config.base_config().database_path).unwrap();
3851 sqlite_conn
3852 .execute(
3853 "
3854 insert into song (
3855 id, path, artist, title, album, album_artist,
3856 track_number, disc_number, genre, stamp, version, duration, analyzed,
3857 extra_info
3858 )
3859 values (
3860 1, '/random/path', 'Some Artist', 'A Title', 'Some Album',
3861 'Some Album Artist', 1, 1, 'Electronica', '2022-01-01',
3862 1, 250, true, '{\"key\": \"value\"}'
3863 );
3864 ",
3865 [],
3866 )
3867 .unwrap();
3868 sqlite_conn
3869 .execute(
3870 "
3871 insert into feature(id, song_id, feature, feature_index)
3872 values (2000, 1, 1.1, 1)
3873 on conflict(song_id, feature_index) do update set feature=excluded.feature;
3874 ",
3875 [],
3876 )
3877 .unwrap();
3878 }
3879
3880 #[test]
3881 #[cfg(feature = "ffmpeg")]
3882 fn test_library_new_database_upgrade() {
3883 let config_dir = TempDir::new("tmp").unwrap();
3884 let sqlite_db_path = config_dir.path().join("test.db");
3885 {
3888 let sqlite_conn = Connection::open(sqlite_db_path.clone()).unwrap();
3889 let sql_statements = fs::read_to_string("data/old_database.sql").unwrap();
3890 sqlite_conn.execute_batch(&sql_statements).unwrap();
3891 let track_number: String = sqlite_conn
3892 .query_row("select track_number from song where id = 1", [], |row| {
3893 row.get(0)
3894 })
3895 .unwrap();
3896 assert_eq!(track_number, "01");
3898 let version: u32 = sqlite_conn
3899 .query_row("pragma user_version", [], |row| row.get(0))
3900 .unwrap();
3901 assert_eq!(version, 0);
3902 }
3903
3904 let library = Library::<BaseConfig, DummyDecoder>::new_from_base(
3905 Some(config_dir.path().join("config.txt")),
3906 Some(sqlite_db_path.clone()),
3907 Some(AnalysisOptions {
3908 number_cores: nzus(1),
3909 features_version: FeaturesVersion::Version1,
3910 }),
3911 )
3912 .unwrap();
3913 let sqlite_conn = library.sqlite_conn.lock().unwrap();
3914 let mut query = sqlite_conn
3915 .prepare("select track_number from song where id = ?1")
3916 .unwrap();
3917
3918 let first_song_track_number: Option<u32> = query.query_row([1], |row| row.get(0)).unwrap();
3919 assert_eq!(first_song_track_number, Some(1));
3920
3921 let second_song_track_number: Option<u32> = query.query_row([2], |row| row.get(0)).unwrap();
3922 assert_eq!(None, second_song_track_number);
3923
3924 let third_song_track_number: Option<u32> = query.query_row([3], |row| row.get(0)).unwrap();
3925 assert_eq!(None, third_song_track_number);
3926
3927 let fourth_song_track_number: Option<u32> = query.query_row([4], |row| row.get(0)).unwrap();
3928 assert_eq!(None, fourth_song_track_number);
3929
3930 let version: u32 = sqlite_conn
3931 .query_row("pragma user_version", [], |row| row.get(0))
3932 .unwrap();
3933 assert_eq!(version, 5);
3934 Library::<BaseConfig, DummyDecoder>::new_from_base(
3936 Some(config_dir.path().join("config.txt")),
3937 Some(sqlite_db_path),
3938 Some(AnalysisOptions {
3939 number_cores: NonZeroUsize::new(1).unwrap(),
3940 ..Default::default()
3941 }),
3942 )
3943 .unwrap();
3944 let version: u32 = sqlite_conn
3945 .query_row("pragma user_version", [], |row| row.get(0))
3946 .unwrap();
3947 assert_eq!(version, 5);
3948 }
3949
3950 #[test]
3951 #[cfg(feature = "ffmpeg")]
3952 fn test_library_new_database_already_last_version() {
3953 let config_dir = TempDir::new("tmp").unwrap();
3954 let sqlite_db_path = config_dir.path().join("test.db");
3955 Library::<BaseConfig, DummyDecoder>::new_from_base(
3956 Some(config_dir.path().join("config.txt")),
3957 Some(sqlite_db_path.clone()),
3958 Some(AnalysisOptions {
3959 number_cores: NonZeroUsize::new(1).unwrap(),
3960 ..Default::default()
3961 }),
3962 )
3963 .unwrap();
3964 let library = Library::<BaseConfig, DummyDecoder>::new_from_base(
3965 Some(config_dir.path().join("config.txt")),
3966 Some(sqlite_db_path.clone()),
3967 Some(AnalysisOptions {
3968 number_cores: NonZeroUsize::new(1).unwrap(),
3969 ..Default::default()
3970 }),
3971 )
3972 .unwrap();
3973 let sqlite_conn = library.sqlite_conn.lock().unwrap();
3974 let version: u32 = sqlite_conn
3975 .query_row("pragma user_version", [], |row| row.get(0))
3976 .unwrap();
3977 assert_eq!(version, 5);
3978 }
3979
3980 #[test]
3981 #[cfg(feature = "ffmpeg")]
3982 fn test_library_store_song() {
3983 let (mut library, _temp_dir, _) = setup_test_library();
3984 let song = _generate_basic_song(None);
3985 let library_song = LibrarySong {
3986 bliss_song: song.to_owned(),
3987 extra_info: (),
3988 };
3989 library.store_song(&library_song).unwrap();
3990 let connection = library.sqlite_conn.lock().unwrap();
3991 let expected_song = _basic_song_from_database(connection, &song.path.to_string_lossy());
3992 assert_eq!(expected_song, song);
3993 }
3994
3995 #[test]
3996 fn test_base_config_new() {
3999 let random_config_home = TempDir::new("config").unwrap();
4000 let config_path = random_config_home.path().join("test.json");
4001 let database_path = random_config_home.path().join("database.db");
4002 let base_config = BaseConfig::new(
4003 Some(config_path.to_owned()),
4004 Some(database_path.to_owned()),
4005 Some(AnalysisOptions {
4006 number_cores: NonZeroUsize::new(4).unwrap(),
4007 features_version: FeaturesVersion::Version1,
4008 }),
4009 )
4010 .unwrap();
4011 base_config.write().unwrap();
4012 let data = fs::read_to_string(&config_path).unwrap();
4013 let config = BaseConfig::deserialize_config(&data).unwrap();
4014
4015 assert_eq!(
4016 config,
4017 BaseConfig {
4018 config_path: config_path,
4019 database_path: database_path,
4020 analysis_options: AnalysisOptions {
4021 number_cores: NonZeroUsize::new(4).unwrap(),
4022 features_version: FeaturesVersion::Version1
4023 },
4024 m: default_m(),
4025 }
4026 );
4027
4028 let v: Value = serde_json::from_str(&data).unwrap();
4029 let obj = v.as_object().expect("top-level JSON must be an object");
4030 assert!(obj.contains_key("config_path"));
4031 assert!(obj.contains_key("database_path"));
4032 assert!(obj.contains_key("m"));
4033 assert!(obj.contains_key("features_version"));
4034 assert!(obj.contains_key("number_cores"));
4035 }
4036
4037 #[test]
4038 fn test_base_config_new_default() {
4040 let random_config_home = TempDir::new("config").unwrap();
4041 let config_path = random_config_home.path().join("test.json");
4042 let base_config = BaseConfig::new(Some(config_path.to_owned()), None, None).unwrap();
4043 base_config.write().unwrap();
4044 let data = fs::read_to_string(&config_path).unwrap();
4045 let config = BaseConfig::deserialize_config(&data).unwrap();
4046
4047 let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
4048
4049 assert_eq!(
4050 config,
4051 BaseConfig {
4052 config_path: config_path,
4053 database_path: random_config_home.path().join("songs.db"),
4054 analysis_options: AnalysisOptions {
4055 number_cores: cores,
4056 features_version: FeaturesVersion::LATEST,
4057 },
4058 m: default_m(),
4059 }
4060 );
4061
4062 let v: Value = serde_json::from_str(&data).unwrap();
4063 let obj = v.as_object().expect("top-level JSON must be an object");
4064 assert!(obj.contains_key("config_path"));
4065 assert!(obj.contains_key("database_path"));
4066 assert!(obj.contains_key("m"));
4067 assert!(obj.contains_key("features_version"));
4068 assert!(obj.contains_key("number_cores"));
4069 }
4070
4071 #[test]
4072 fn test_path_base_config_new() {
4073 {
4074 let xdg_config_home = TempDir::new("test-bliss").unwrap();
4075 fs::create_dir_all(xdg_config_home.path().join("bliss-rs")).unwrap();
4076 env::set_var("XDG_CONFIG_HOME", xdg_config_home.path());
4077
4078 let base_config = BaseConfig::new(None, None, None).unwrap();
4080
4081 assert_eq!(
4082 base_config.config_path,
4083 xdg_config_home.path().join("bliss-rs/config.json"),
4084 );
4085 assert_eq!(
4086 base_config.database_path,
4087 xdg_config_home.path().join("bliss-rs/songs.db"),
4088 );
4089 base_config.write().unwrap();
4090 assert!(xdg_config_home.path().join("bliss-rs/config.json").exists());
4091 }
4092
4093 {
4095 let random_config_home = TempDir::new("config").unwrap();
4096 let base_config = BaseConfig::new(
4097 Some(random_config_home.path().join("test.json")),
4098 None,
4099 None,
4100 )
4101 .unwrap();
4102 base_config.write().unwrap();
4103
4104 assert_eq!(
4105 base_config.config_path,
4106 random_config_home.path().join("test.json"),
4107 );
4108 assert_eq!(
4109 base_config.database_path,
4110 random_config_home.path().join("songs.db")
4111 );
4112 assert!(random_config_home.path().join("test.json").exists());
4113 }
4114
4115 {
4117 let random_config_home = TempDir::new("database").unwrap();
4118 let base_config =
4119 BaseConfig::new(None, Some(random_config_home.path().join("test.db")), None)
4120 .unwrap();
4121 base_config.write().unwrap();
4122
4123 assert_eq!(
4124 base_config.config_path,
4125 random_config_home.path().join("config.json"),
4126 );
4127 assert_eq!(
4128 base_config.database_path,
4129 random_config_home.path().join("test.db"),
4130 );
4131 }
4132 {
4134 let random_config_home = TempDir::new("config").unwrap();
4135 let random_database_home = TempDir::new("database").unwrap();
4136 fs::create_dir_all(random_config_home.path().join("bliss-rs")).unwrap();
4137 let base_config = BaseConfig::new(
4138 Some(random_config_home.path().join("config_test.json")),
4139 Some(random_database_home.path().join("test-database.db")),
4140 None,
4141 )
4142 .unwrap();
4143 base_config.write().unwrap();
4144
4145 assert_eq!(
4146 base_config.config_path,
4147 random_config_home.path().join("config_test.json"),
4148 );
4149 assert_eq!(
4150 base_config.database_path,
4151 random_database_home.path().join("test-database.db"),
4152 );
4153 assert!(random_config_home.path().join("config_test.json").exists());
4154 }
4155 }
4156
4157 #[test]
4158 #[cfg(feature = "ffmpeg")]
4159 fn test_library_extra_info() {
4160 let (mut library, _temp_dir, _) = setup_test_library();
4161 let song = _generate_library_song(None);
4162 library.store_song(&song).unwrap();
4163 let connection = library.sqlite_conn.lock().unwrap();
4164 let returned_song =
4165 _library_song_from_database(connection, &song.bliss_song.path.to_string_lossy());
4166 assert_eq!(returned_song, song);
4167 }
4168
4169 #[test]
4170 fn test_from_config_path_non_existing() {
4171 assert!(
4172 Library::<CustomConfig, DummyDecoder>::from_config_path(Some(PathBuf::from(
4173 "non-existing"
4174 )))
4175 .is_err()
4176 );
4177 }
4178
4179 #[test]
4180 fn test_from_config_path() {
4181 let config_dir = TempDir::new("coucou").unwrap();
4182 let config_file = config_dir.path().join("config.json");
4183 let database_file = config_dir.path().join("bliss.db");
4184
4185 let base_config = BaseConfig::new(
4188 Some(config_file.to_owned()),
4189 Some(database_file),
4190 Some(AnalysisOptions {
4191 number_cores: nzus(1),
4192 ..Default::default()
4193 }),
4194 )
4195 .unwrap();
4196
4197 let config = CustomConfig {
4198 base_config,
4199 second_path_to_music_library: "/path/to/somewhere".into(),
4200 ignore_wav_files: true,
4201 };
4202 let song = _generate_library_song(None);
4206 {
4207 let mut library = Library::<_, DummyDecoder>::new(config.to_owned()).unwrap();
4208 library.store_song(&song).unwrap();
4209 }
4210
4211 let library: Library<CustomConfig, DummyDecoder> =
4212 Library::from_config_path(Some(config_file)).unwrap();
4213 let connection = library.sqlite_conn.lock().unwrap();
4214 let returned_song =
4215 _library_song_from_database(connection, &song.bliss_song.path.to_string_lossy());
4216
4217 assert_eq!(library.config, config);
4218 assert_eq!(song, returned_song);
4219 }
4220
4221 #[test]
4222 fn test_config_from_file() {
4223 let config = BaseConfig::from_path("./data/sample-config.json").unwrap();
4224 let mut m: Array2<f32> = Array2::eye(FeaturesVersion::Version1.feature_count());
4225 m[[0, 1]] = 1.;
4226 assert_eq!(
4227 config,
4228 BaseConfig {
4229 config_path: PathBuf::from_str("/tmp/bliss-rs/config.json").unwrap(),
4230 database_path: PathBuf::from_str("/tmp/bliss-rs/songs.db").unwrap(),
4231 analysis_options: AnalysisOptions {
4232 features_version: FeaturesVersion::Version1,
4233 number_cores: NonZeroUsize::new(8).unwrap()
4234 },
4235 m,
4236 }
4237 );
4238 }
4239
4240 #[test]
4241 fn test_config_old_existing() {
4242 let config = BaseConfig::from_path("./data/old_config.json").unwrap();
4243 assert_eq!(
4244 config,
4245 BaseConfig {
4246 config_path: PathBuf::from_str("/tmp/bliss-rs/config.json").unwrap(),
4247 database_path: PathBuf::from_str("/tmp/bliss-rs/songs.db").unwrap(),
4248 analysis_options: AnalysisOptions {
4249 features_version: FeaturesVersion::Version1,
4250 number_cores: NonZeroUsize::new(8).unwrap()
4251 },
4252 m: Array2::eye(NUMBER_FEATURES),
4253 }
4254 );
4255 }
4256
4257 #[test]
4258 fn test_config_serialize_deserialize() {
4259 let config_dir = TempDir::new("coucou").unwrap();
4260 let config_file = config_dir.path().join("config.json");
4261 let database_file = config_dir.path().join("bliss.db");
4262
4263 let base_config = BaseConfig::new(
4266 Some(config_file.to_owned()),
4267 Some(database_file),
4268 Some(AnalysisOptions {
4269 number_cores: nzus(1),
4270 features_version: FeaturesVersion::Version1,
4271 }),
4272 )
4273 .unwrap();
4274
4275 let config = CustomConfig {
4276 base_config,
4277 second_path_to_music_library: "/path/to/somewhere".into(),
4278 ignore_wav_files: true,
4279 };
4280 config.write().unwrap();
4281
4282 assert_eq!(
4283 config,
4284 CustomConfig::from_path(&config_file.to_string_lossy()).unwrap(),
4285 );
4286 }
4287
4288 #[test]
4289 #[cfg(feature = "ffmpeg")]
4290 fn test_library_sanity_check_fail() {
4291 let (mut library, _temp_dir, _) = setup_test_library();
4292 assert_eq!(
4293 library.version_sanity_check().unwrap(),
4294 vec![
4295 SanityError::MultipleVersionsInDB {
4296 versions: vec![FeaturesVersion::Version1, FeaturesVersion::Version2]
4297 },
4298 SanityError::OldFeaturesVersionInDB {
4299 version: FeaturesVersion::Version1
4300 }
4301 ],
4302 );
4303 }
4304
4305 #[test]
4306 #[cfg(feature = "ffmpeg")]
4307 fn test_library_sanity_check_ok() {
4308 let (mut library, _temp_dir, _) = setup_test_library();
4309 {
4310 let sqlite_conn =
4311 Connection::open(&library.config.base_config().database_path).unwrap();
4312 sqlite_conn
4313 .execute(
4314 "delete from song where version != ?1",
4315 [FeaturesVersion::LATEST],
4316 )
4317 .unwrap();
4318 }
4319 assert!(library.version_sanity_check().unwrap().is_empty());
4320 }
4321
4322 #[test]
4323 fn test_config_number_cpus() {
4324 let config_dir = TempDir::new("coucou").unwrap();
4325 let config_file = config_dir.path().join("config.json");
4326 let database_file = config_dir.path().join("bliss.db");
4327
4328 let base_config = BaseConfig::new(
4329 Some(config_file.to_owned()),
4330 Some(database_file.to_owned()),
4331 None,
4332 )
4333 .unwrap();
4334 let config = CustomConfig {
4335 base_config,
4336 second_path_to_music_library: "/path/to/somewhere".into(),
4337 ignore_wav_files: true,
4338 };
4339
4340 assert_eq!(
4341 config.get_number_cores().get(),
4342 usize::from(thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap())),
4343 );
4344
4345 let base_config = BaseConfig::new(
4346 Some(config_file),
4347 Some(database_file),
4348 Some(AnalysisOptions {
4349 number_cores: nzus(1),
4350 ..Default::default()
4351 }),
4352 )
4353 .unwrap();
4354 let mut config = CustomConfig {
4355 base_config,
4356 second_path_to_music_library: "/path/to/somewhere".into(),
4357 ignore_wav_files: true,
4358 };
4359
4360 assert_eq!(config.get_number_cores().get(), 1);
4361 config.set_number_cores(nzus(2)).unwrap();
4362 assert_eq!(config.get_number_cores().get(), 2);
4363 }
4364
4365 #[test]
4366 fn test_config_features_version() {
4367 let config_dir = TempDir::new("coucou").unwrap();
4368 let config_file = config_dir.path().join("config.json");
4369 let database_file = config_dir.path().join("bliss.db");
4370
4371 let base_config = BaseConfig::new(
4372 Some(config_file.to_owned()),
4373 Some(database_file.to_owned()),
4374 None,
4375 )
4376 .unwrap();
4377 let config = CustomConfig {
4378 base_config,
4379 second_path_to_music_library: "/path/to/somewhere".into(),
4380 ignore_wav_files: true,
4381 };
4382
4383 assert_eq!(config.get_features_version(), FeaturesVersion::LATEST,);
4384
4385 let base_config = BaseConfig::new(
4386 Some(config_file),
4387 Some(database_file),
4388 Some(AnalysisOptions {
4389 features_version: FeaturesVersion::Version1,
4390 ..Default::default()
4391 }),
4392 )
4393 .unwrap();
4394 let mut config = CustomConfig {
4395 base_config,
4396 second_path_to_music_library: "/path/to/somewhere".into(),
4397 ignore_wav_files: true,
4398 };
4399
4400 assert_eq!(config.get_features_version(), FeaturesVersion::Version1);
4401 config
4402 .set_features_version(FeaturesVersion::Version2)
4403 .unwrap();
4404 assert_eq!(config.get_features_version(), FeaturesVersion::Version2);
4405 }
4406
4407 #[test]
4408 fn test_library_create_all_dirs() {
4409 let config_dir = TempDir::new("coucou")
4410 .unwrap()
4411 .path()
4412 .join("path")
4413 .join("to");
4414 assert!(!config_dir.is_dir());
4415 let config_file = config_dir.join("config.json");
4416 let database_file = config_dir.join("bliss.db");
4417 Library::<BaseConfig, DummyDecoder>::new_from_base(
4418 Some(config_file),
4419 Some(database_file),
4420 Some(AnalysisOptions {
4421 number_cores: nzus(1),
4422 ..Default::default()
4423 }),
4424 )
4425 .unwrap();
4426 assert!(config_dir.is_dir());
4427 }
4428
4429 #[test]
4430 #[cfg(feature = "ffmpeg")]
4431 fn test_library_get_failed_songs() {
4432 let (library, _temp_dir, _) = setup_test_library();
4433 let failed_songs = library.get_failed_songs().unwrap();
4434 assert_eq!(
4435 failed_songs,
4436 vec![
4437 ProcessingError {
4438 song_path: PathBuf::from("./data/not-existing.m4a"),
4439 error: String::from("error finding the file"),
4440 features_version: FeaturesVersion::Version1,
4441 },
4442 ProcessingError {
4443 song_path: PathBuf::from("./data/invalid-file.m4a"),
4444 error: String::from("error decoding the file"),
4445 features_version: FeaturesVersion::Version1,
4446 }
4447 ]
4448 );
4449 }
4450
4451 #[test]
4452 #[cfg(feature = "ffmpeg")]
4453 fn test_analyze_store_failed_songs() {
4454 let (mut library, _temp_dir, _) = setup_test_library();
4455 library
4456 .config
4457 .base_config_mut()
4458 .analysis_options
4459 .features_version = FeaturesVersion::Version1;
4460
4461 let paths = vec![
4462 "./data/s16_mono_22_5kHz.flac",
4463 "./data/s16_stereo_22_5kHz.flac",
4464 "non-existing",
4465 ];
4466 library.analyze_paths(paths.to_owned(), false).unwrap();
4467 let failed_songs = library.get_failed_songs().unwrap();
4468 assert!(failed_songs.contains(&ProcessingError {
4469 song_path: PathBuf::from("non-existing"),
4470 error: String::from("error happened while decoding file - while opening format for file 'non-existing': ffmpeg::Error(2: No such file or directory)."),
4471 features_version: FeaturesVersion::Version1,
4472 }));
4473 }
4474}