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