bliss_audio/
library.rs

1//! Module containing utilities to properly manage a library of [Song]s,
2//! for people wanting to e.g. implement a bliss plugin for an existing
3//! audio player. A good resource to look at for inspiration is
4//! [blissify](https://github.com/Polochon-street/blissify-rs)'s source code.
5//!
6//! Useful to have direct and easy access to functions that analyze
7//! and store analysis of songs in a SQLite database, as well as retrieve it,
8//! and make playlists directly from analyzed songs. All functions are as
9//! thoroughly tested as possible, so you don't have to do it yourself,
10//! including for instance bliss features version handling, etc.
11//!
12//! It works in three parts:
13//! * The first part is the configuration part, which allows you to
14//!   specify extra information that your plugin might need that will
15//!   be automatically stored / retrieved when you instanciate a
16//!   [Library] (the core of your plugin).
17//!
18//!   To do so implies specifying a configuration struct, that will implement
19//!   [AppConfigTrait], i.e. implement `Serialize`, `Deserialize`, and a
20//!   function to retrieve the [BaseConfig] (which is just a structure
21//!   holding the path to the configuration file and the path to the database).
22//!
23//!   The most straightforward way to do so is to have something like this (
24//!   in this example, we assume that `path_to_extra_information` is something
25//!   you would want stored in your configuration file, path to a second music
26//!   folder for instance:
27//!   ```
28//!     use anyhow::Result;
29//!     use serde::{Deserialize, Serialize};
30//!     use std::path::PathBuf;
31//!     use bliss_audio::{BlissError};
32//!     use bliss_audio::AnalysisOptions;
33//!     use bliss_audio::library::{AppConfigTrait, BaseConfig};
34//!
35//!     #[derive(Serialize, Deserialize, Clone, Debug)]
36//!     pub struct Config {
37//!         #[serde(flatten)]
38//!         pub base_config: BaseConfig,
39//!         pub music_library_path: PathBuf,
40//!     }
41//!
42//!     impl AppConfigTrait for Config {
43//!         fn base_config(&self) -> &BaseConfig {
44//!             &self.base_config
45//!         }
46//!
47//!         fn base_config_mut(&mut self) -> &mut BaseConfig {
48//!             &mut self.base_config
49//!         }
50//!     }
51//!     impl Config {
52//!         pub fn new(
53//!             music_library_path: PathBuf,
54//!             config_path: Option<PathBuf>,
55//!             database_path: Option<PathBuf>,
56//!             analysis_options: Option<AnalysisOptions>,
57//!         ) -> Result<Self> {
58//!             // Note that by passing `(None, None)` here, the paths will
59//!             // be inferred automatically using user data dirs.
60//!             let base_config = BaseConfig::new(config_path, database_path, analysis_options)?;
61//!             Ok(Self {
62//!                 base_config,
63//!                 music_library_path,
64//!             })
65//!         }
66//!     }
67//!   ```
68//!
69//! * The second part is the actual [Library] structure, that makes the
70//!   bulk of the plug-in. To initialize a library once with a given config,
71//!   you can do (here with a base configuration, requiring ffmpeg):
72#![cfg_attr(
73    feature = "ffmpeg",
74    doc = r##"
75```no_run
76  use anyhow::{Error, Result};
77  use bliss_audio::library::{BaseConfig, Library};
78  use bliss_audio::decoder::ffmpeg::FFmpegDecoder;
79  use std::path::PathBuf;
80
81  let config_path = Some(PathBuf::from("path/to/config/config.json"));
82  let database_path = Some(PathBuf::from("path/to/config/bliss.db"));
83  let config = BaseConfig::new(config_path, database_path, None)?;
84  let library: Library<BaseConfig, FFmpegDecoder> = Library::new(config)?;
85  # Ok::<(), Error>(())
86```"##
87)]
88//!   Once this is done, you can simply load the library by doing
89//!   `Library::from_config_path(config_path);`
90//! * The third part is using the [Library] itself: it provides you with
91//!   utilies such as [Library::analyze_paths], which analyzes all songs
92//!   in given paths and stores it in the databases, as well as
93//!   [Library::playlist_from], which allows you to generate a playlist
94//!   from any given analyzed song(s), and [Library::playlist_from_custom],
95//!   which allows you to customize the way you generate playlists.
96//!
97//!   The [Library] structure also comes with a [LibrarySong] song struct,
98//!   which represents a song stored in the database.
99//!
100//!   It is made of a `bliss_song` field, containing the analyzed bliss
101//!   song (with the normal metatada such as the artist, etc), and an
102//!   `extra_info` field, which can be any user-defined serialized struct.
103//!   For most use cases, it would just be the unit type `()` (which is no
104//!   extra info), that would be used like
105//!   `library.playlist_from<()>(song, path, playlist_length)`,
106//!   but functions such as [Library::analyze_paths_extra_info] and
107//!   [Library::analyze_paths_convert_extra_info] let you customize what
108//!   information you store for each song.
109//!
110//! The files in
111//! [examples/library.rs](https://github.com/Polochon-street/bliss-rs/blob/master/examples/library.rs)
112//! and
113//! [examples/libray_extra_info.rs](https://github.com/Polochon-street/bliss-rs/blob/master/examples/library_extra_info.rs)
114//! should provide the user with enough information to start with. For a more
115//! "real-life" example, the
116//! [blissify](https://github.com/Polochon-street/blissify-rs)'s code is using
117//! [Library] to implement bliss for a MPD player.
118use 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
177/// Configuration trait, used for instance to customize
178/// the format in which the configuration file should be written.
179pub trait AppConfigTrait: Serialize + Sized + DeserializeOwned {
180    // Implementers have to provide these.
181    /// This trait should return the [BaseConfig] from the parent,
182    /// user-created `Config`.
183    fn base_config(&self) -> &BaseConfig;
184
185    // Implementers have to provide these.
186    /// This trait should return the [BaseConfig] from the parent,
187    /// user-created `Config`.
188    fn base_config_mut(&mut self) -> &mut BaseConfig;
189
190    // Default implementation to output the config as a JSON file.
191    /// Convert the current config to a [String], to be written to
192    /// a file.
193    ///
194    /// The default writes a JSON file, but any format can be used,
195    /// using for example the various Serde libraries (`serde_yaml`, etc) -
196    /// just overwrite this method.
197    fn serialize_config(&self) -> Result<String> {
198        Ok(serde_json::to_string_pretty(&self)?)
199    }
200
201    /// Set the number of desired cores for analysis, and write it to the
202    /// configuration file.
203    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    /// Set the desired version for analysis, and write it to the
209    /// configuration file.
210    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    /// Get the number of desired cores for analysis, and write it to the
216    /// configuration file.
217    fn get_features_version(&self) -> FeaturesVersion {
218        self.base_config().analysis_options.features_version
219    }
220
221    /// Get the number of desired cores for analysis, and write it to the
222    /// configuration file.
223    fn get_number_cores(&self) -> NonZeroUsize {
224        self.base_config().analysis_options.number_cores
225    }
226
227    /// Default implementation to load a config from a JSON file.
228    /// Reads from a string.
229    ///
230    /// If you change the serialization format to use something else
231    /// than JSON, you need to also overwrite that function with the
232    /// format you chose.
233    fn deserialize_config(data: &str) -> Result<Self> {
234        Ok(serde_json::from_str(data)?)
235    }
236
237    /// Load a config from the specified path, using `deserialize_config`.
238    ///
239    /// This method can be overriden in the very unlikely case
240    /// the user wants to do something Serde cannot.
241    fn from_path(path: &str) -> Result<Self> {
242        let data = fs::read_to_string(path)?;
243        Self::deserialize_config(&data)
244    }
245
246    // This default impl is what requires the `Serialize` supertrait
247    /// Write the configuration to a file using
248    /// [AppConfigTrait::serialize_config].
249    ///
250    /// This method can be overriden
251    /// to not use [AppConfigTrait::serialize_config], in the very unlikely
252    /// case the user wants to do something Serde cannot.
253    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)]
261/// The minimum configuration an application needs to work with
262/// a [Library].
263pub struct BaseConfig {
264    /// The path to where the configuration file should be stored,
265    /// e.g. `/home/foo/.local/share/bliss-rs/config.json`
266    pub config_path: PathBuf,
267    /// The path to where the database file should be stored,
268    /// e.g. `/home/foo/.local/share/bliss-rs/bliss.db`
269    pub database_path: PathBuf,
270    /// The analysis options set in the database (number of CPU cores for the
271    /// analysis, desired feature version...)
272    #[serde(flatten)]
273    pub analysis_options: AnalysisOptions,
274    /// The mahalanobis matrix used for mahalanobis distance.
275    /// Used to customize the distance metric beyond simple euclidean distance.
276    /// Uses ndarray's `serde` feature for serialization / deserialization.
277    /// Field would look like this:
278    /// "m": {"v": 1, "dim": [20, 20], "data": [1.0, 0.0, ..., 1.0]}
279    #[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    /// Because we spent some time using XDG_DATA_HOME instead of XDG_CONFIG_HOME
289    /// as the default folder, we have to jump through some hoops:
290    ///
291    /// - Legacy path exists, new path doesn't exist => legacy path should be returned
292    /// - Legacy path exists, new path exists => new path should be returned
293    /// - Legacy path doesn't exist => new path should be returned
294    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        // If neither default_folder nor legacy_folder exist, return the default folder
317        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    /// Create a new, basic config. Upon calls of `Config.write()`, it will be
329    /// written to `config_path`.
330    //
331    /// Any path omitted will instead default to a "clever" path using
332    /// data directory inference. The "clever" thinking is as follows:
333    /// - If the user specified only one of the paths, it will put the other
334    ///   file in the same folder as the given path.
335    /// - If the user specified both paths, it will go with what the user
336    ///   chose.
337    /// - If the user didn't select any path, it will try to put everything in
338    ///   the user's configuration directory, i.e. XDG_CONFIG_HOME.
339    ///
340    /// The number of cores is the number of cores that should be used for
341    /// any analysis. If not provided, it will default to the computer's
342    /// number of cores.
343    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            // User provided a path; let the future file creation determine
352            // whether it points to something valid or not
353            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
403/// A struct used to hold a collection of [Song]s, with convenience
404/// methods to add, remove and update songs.
405///
406/// Provide it either the `BaseConfig`, or a `Config` extending
407/// `BaseConfig`.
408/// TODO code example
409pub struct Library<Config, D: ?Sized> {
410    /// The configuration struct, containing both information
411    /// from `BaseConfig` as well as user-defined values.
412    pub config: Config,
413    /// SQL connection to the database.
414    pub sqlite_conn: Arc<Mutex<Connection>>,
415    decoder: PhantomData<D>,
416}
417
418/// Hold an error that happened while processing songs during analysis.
419#[derive(Debug, Eq, PartialEq)]
420pub struct ProcessingError {
421    /// The path of the song whose analysis was attempted.
422    pub song_path: PathBuf,
423    /// The actual error string.
424    pub error: String,
425    /// Features version the analysis was attempted with.
426    pub features_version: FeaturesVersion,
427}
428
429/// Struct holding both a Bliss song, as well as any extra info
430/// that a user would want to store in the database related to that
431/// song.
432///
433/// The only constraint is that `extra_info` must be serializable, so,
434/// something like
435/// ```no_compile
436/// #[derive(Serialize)]
437/// struct ExtraInfo {
438///     ignore: bool,
439///     unique_id: i64,
440/// }
441/// let extra_info = ExtraInfo { ignore: true, unique_id = 123 };
442/// let song = LibrarySong { bliss_song: song, extra_info };
443/// ```
444/// is totally possible.
445#[derive(Debug, PartialEq, Clone)]
446pub struct LibrarySong<T: Serialize + DeserializeOwned + Clone> {
447    /// Actual bliss song, containing the song's metadata, as well
448    /// as the bliss analysis.
449    pub bliss_song: Song,
450    /// User-controlled information regarding that specific song.
451    pub extra_info: T,
452}
453
454impl<T: Serialize + DeserializeOwned + Clone> AsRef<Song> for LibrarySong<T> {
455    fn as_ref(&self) -> &Song {
456        &self.bliss_song
457    }
458}
459
460/// An enum containing potential sanity errors wrt. database and
461/// songs' features version.
462#[derive(Debug, PartialEq)]
463pub enum SanityError {
464    /// If there are songs analyzed with different features version in the
465    /// database.
466    MultipleVersionsInDB {
467        /// The FeaturesVersion found in the database.
468        versions: Vec<FeaturesVersion>,
469    },
470    /// If songs in the database are analyzed with a lower features version
471    /// number than the latest version advertised by bliss.
472    OldFeaturesVersionInDB {
473        /// The oldest version of the features in the database.
474        version: FeaturesVersion,
475    },
476}
477
478// TODO add logging statement
479// TODO maybe return number of elements updated / deleted / whatev in analysis
480//      functions?
481// TODO should it really use anyhow errors?
482// TODO make sure that the path to string is consistent
483impl<Config: AppConfigTrait, D: ?Sized + DecoderTrait> Library<Config, D> {
484    const SQLITE_SCHEMA: &'static str = "
485        create table song (
486                id integer primary key,
487                path text not null unique,
488                duration float,
489                album_artist text,
490                artist text,
491                title text,
492                album text,
493                track_number integer,
494                disc_number integer,
495                genre text,
496                cue_path text,
497                audio_file_path text,
498                stamp timestamp default current_timestamp,
499                version integer not null,
500                analyzed boolean default false,
501                extra_info json,
502                error text
503            );
504            pragma foreign_keys = on;
505            create table feature (
506                id integer primary key,
507                song_id integer not null,
508                feature real not null,
509                feature_index integer not null,
510                unique(song_id, feature_index),
511                foreign key(song_id) references song(id) on delete cascade
512            )
513        ";
514    const SQLITE_MIGRATIONS: &'static [&'static str] = &[
515        "",
516        "
517            alter table song add column track_number_1 integer;
518            update song set track_number_1 = s1.cast_track_number from (
519                select cast(track_number as int) as cast_track_number, id from song
520            ) as s1 where s1.id = song.id and cast(track_number as int) != 0;
521            alter table song drop column track_number;
522            alter table song rename column track_number_1 to track_number;
523        ",
524        "alter table song add column disc_number integer;",
525        "
526            -- Training triplets used to do metric learning, in conjunction with
527            -- a human-processed survey. In this table, songs pointed to
528            -- by song_1_id and song_2_id are closer together than they
529            -- are to the song pointed to by odd_one_out_id, i.e.
530            -- d(s1, s2) < d(s1, odd_one_out) and d(s1, s2) < d(s2, odd_one_out)
531            create table training_triplet (
532                id integer primary key,
533                song_1_id integer not null,
534                song_2_id integer not null,
535                odd_one_out_id integer not null,
536                stamp timestamp default current_timestamp,
537                foreign key(song_1_id) references song(id) on delete cascade,
538                foreign key(song_2_id) references song(id) on delete cascade,
539                foreign key(odd_one_out_id) references song(id) on delete cascade
540            )
541        ",
542        // Add the "not null" constraint to the "version" column
543        "
544            create table song_bak (
545                id integer primary key,
546                path text not null unique,
547                duration float,
548                album_artist text,
549                artist text,
550                title text,
551                album text,
552                track_number integer,
553                disc_number integer,
554                genre text,
555                cue_path text,
556                audio_file_path text,
557                stamp timestamp default current_timestamp,
558                version integer not null,
559                analyzed boolean default false,
560                extra_info json,
561                error text
562            );
563            insert into song_bak (
564                id, path, duration, album_artist, artist, title, album, track_number,
565                disc_number,genre, cue_path, audio_file_path, stamp, version,
566                analyzed, extra_info, error
567            ) select
568                id, path, duration, album_artist, artist, title, album, track_number,
569                disc_number,genre, cue_path, audio_file_path, stamp,
570                coalesce(version, 1), analyzed, extra_info, error
571            from song;
572            drop table song;
573            alter table song_bak rename to song;
574        ",
575    ];
576
577    /// Create a new [Library] object from the given Config struct that
578    /// implements the [AppConfigTrait].
579    /// writing the configuration to the file given in
580    /// `config.config_path`.
581    ///
582    /// This function should only be called once, when a user wishes to
583    /// create a completely new "library".
584    /// Otherwise, load an existing library file using
585    /// [Library::from_config_path].
586    pub fn new(config: Config) -> Result<Self> {
587        if !config
588            .base_config()
589            .config_path
590            .parent()
591            .ok_or_else(|| {
592                BlissError::ProviderError(format!(
593                    "specified path {} is not a valid file path.",
594                    config.base_config().config_path.display()
595                ))
596            })?
597            .is_dir()
598        {
599            create_dir_all(config.base_config().config_path.parent().unwrap())?;
600        }
601        let sqlite_conn = Connection::open(&config.base_config().database_path)?;
602
603        Library::<Config, D>::upgrade(&sqlite_conn).map_err(|e| {
604            BlissError::ProviderError(format!("Could not run database upgrade: {e}"))
605        })?;
606
607        config.write()?;
608        Ok(Self {
609            config,
610            sqlite_conn: Arc::new(Mutex::new(sqlite_conn)),
611            decoder: PhantomData,
612        })
613    }
614
615    fn upgrade(sqlite_conn: &Connection) -> Result<()> {
616        let version: u32 = sqlite_conn
617            .query_row("pragma user_version", [], |row| row.get(0))
618            .map_err(|e| {
619                BlissError::ProviderError(format!("Could not get database version: {e}."))
620            })?;
621
622        let migrations = Library::<Config, D>::SQLITE_MIGRATIONS;
623        match version.cmp(&(migrations.len() as u32)) {
624            std::cmp::Ordering::Equal => return Ok(()),
625            std::cmp::Ordering::Greater => bail!(format!(
626                "bliss-rs version {} is older than the schema version {}",
627                version,
628                migrations.len()
629            )),
630            _ => (),
631        };
632
633        let number_tables: u32 = sqlite_conn
634            .query_row("select count(*) from pragma_table_list", [], |row| {
635                row.get(0)
636            })
637            .map_err(|e| {
638                BlissError::ProviderError(format!(
639                    "Could not query initial database information: {e}",
640                ))
641            })?;
642        let is_database_new = number_tables <= 2;
643
644        if version == 0 && is_database_new {
645            sqlite_conn
646                .execute_batch(Library::<Config, D>::SQLITE_SCHEMA)
647                .map_err(|e| {
648                    BlissError::ProviderError(format!("Could not initialize schema: {e}."))
649                })?;
650        } else {
651            for migration in migrations.iter().skip(version as usize) {
652                sqlite_conn.execute_batch(migration).map_err(|e| {
653                    BlissError::ProviderError(format!("Could not execute migration: {e}."))
654                })?;
655            }
656        }
657
658        sqlite_conn
659            .execute(&format!("pragma user_version = {}", migrations.len()), [])
660            .map_err(|e| {
661                BlissError::ProviderError(format!("Could not update database version: {e}."))
662            })?;
663
664        Ok(())
665    }
666
667    /// Load a library from a configuration path.
668    ///
669    /// If no configuration path is provided, the path is
670    /// set using default data folder path.
671    pub fn from_config_path(config_path: Option<PathBuf>) -> Result<Self> {
672        let config_path: Result<PathBuf> =
673            config_path.map_or_else(|| Ok(BaseConfig::new(None, None, None)?.config_path), Ok);
674        let config_path = config_path?;
675        let data = fs::read_to_string(config_path)?;
676        let config = Config::deserialize_config(&data)?;
677        let sqlite_conn = Connection::open(&config.base_config().database_path)?;
678        Library::<Config, D>::upgrade(&sqlite_conn)?;
679        let library = Self {
680            config,
681            sqlite_conn: Arc::new(Mutex::new(sqlite_conn)),
682            decoder: PhantomData,
683        };
684        Ok(library)
685    }
686
687    /// Check whether the library contains songs analyzed with different,
688    /// incompatible versions of bliss.
689    ///
690    /// Returns a vector filled with potential errors. A sane database would return
691    /// Ok() with an empty vector.
692    pub fn version_sanity_check(&mut self) -> Result<Vec<SanityError>> {
693        let mut errors = vec![];
694        let connection = self
695            .sqlite_conn
696            .lock()
697            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
698        let mut stmt = connection.prepare("select distinct version from song")?;
699
700        let mut features_version: Vec<FeaturesVersion> = stmt
701            .query_map([], |row| row.get::<_, FeaturesVersion>(0))?
702            .collect::<rusqlite::Result<Vec<_>>>()?;
703
704        features_version.sort();
705        if features_version.len() > 1 {
706            errors.push(SanityError::MultipleVersionsInDB {
707                versions: features_version.to_owned(),
708            })
709        }
710        if features_version
711            .iter()
712            .any(|features_version_in_db| features_version_in_db != &FeaturesVersion::LATEST)
713        {
714            errors.push(SanityError::OldFeaturesVersionInDB {
715                version: features_version[0],
716            });
717        }
718        Ok(errors)
719    }
720
721    /// Create a new [Library] object from a minimal configuration setup,
722    /// writing it to `config_path`.
723    pub fn new_from_base(
724        config_path: Option<PathBuf>,
725        database_path: Option<PathBuf>,
726        analysis_options: Option<AnalysisOptions>,
727    ) -> Result<Self>
728    where
729        BaseConfig: Into<Config>,
730    {
731        let base = BaseConfig::new(config_path, database_path, analysis_options)?;
732        let config = base.into();
733        Self::new(config)
734    }
735
736    /// Build a playlist of `playlist_length` items from a set of already analyzed
737    /// songs in the library at `song_path`.
738    ///
739    /// It uses the ExentedIsolationForest score as a distance between songs, and deduplicates
740    /// songs that are too close.
741    ///
742    /// Generating a playlist from a single song is also possible, and is just the special case
743    /// where song_paths is a slice of length 1.
744    pub fn playlist_from<'a, T: Serialize + DeserializeOwned + Clone + 'a>(
745        &self,
746        song_paths: &[&str],
747    ) -> Result<impl Iterator<Item = LibrarySong<T>> + 'a> {
748        self.playlist_from_custom(song_paths, &euclidean_distance, closest_to_songs, true)
749    }
750
751    /// Build a playlist of `playlist_length` items from a set of already analyzed
752    /// song(s) in the library `initial_song_paths`, using distance metric `distance`,
753    /// and sorting function `sort_by`.
754    /// Note: The resulting playlist includes the songs specified in `initial_song_paths`
755    /// at the beginning. Use [Iterator::skip] on the resulting iterator to avoid it.
756    ///
757    /// You can use ready-to-use distance metrics such as
758    /// [ExtendedIsolationForest](extended_isolation_forest::Forest) or [euclidean_distance],
759    /// and ready-to-use sorting functions like [closest_to_songs] or
760    /// [crate::playlist::song_to_song].
761    ///
762    /// If you want to use the sorting functions in a uniform manner, you can do something like
763    /// this:
764    /// ```
765    /// use bliss_audio::library::LibrarySong;
766    /// use bliss_audio::playlist::{closest_to_songs, song_to_song};
767    ///
768    /// // The user would be choosing this
769    /// let use_closest_to_songs = true;
770    /// let sort = |x: &[LibrarySong<()>],
771    ///             y: &[LibrarySong<()>],
772    ///             z|
773    ///  -> Box<dyn Iterator<Item = LibrarySong<()>>> {
774    ///     match use_closest_to_songs {
775    ///         false => Box::new(closest_to_songs(x, y, z)),
776    ///         true => Box::new(song_to_song(x, y, z)),
777    ///     }
778    /// };
779    /// ```
780    /// and use `playlist_from_custom` with that sort as `sort_by`.
781    ///
782    /// Generating a playlist from a single song is also possible, and is just the special case
783    /// where song_paths is a slice with a single song.
784    pub fn playlist_from_custom<'a, T, F, I>(
785        &self,
786        initial_song_paths: &[&str],
787        distance: &'a dyn DistanceMetricBuilder,
788        sort_by: F,
789        deduplicate: bool,
790    ) -> Result<impl Iterator<Item = LibrarySong<T>> + 'a>
791    where
792        T: Serialize + DeserializeOwned + Clone + 'a,
793        F: Fn(&[LibrarySong<T>], &[LibrarySong<T>], &'a dyn DistanceMetricBuilder) -> I,
794        I: Iterator<Item = LibrarySong<T>> + 'a,
795    {
796        let initial_songs: Vec<LibrarySong<T>> = initial_song_paths
797            .iter()
798            .map(|s| {
799                self.song_from_path(s).map_err(|_| {
800                    BlissError::ProviderError(format!("song '{s}' has not been analyzed"))
801                })
802            })
803            .collect::<Result<Vec<_>, BlissError>>()?;
804        // Remove the initial songs, so they don't get
805        // sorted in the mess.
806        let songs = self
807            .songs_from_library()?
808            .into_iter()
809            .filter(|s| {
810                !initial_song_paths.contains(&&*s.bliss_song.path.to_string_lossy().to_string())
811            })
812            .collect::<Vec<_>>();
813
814        let iterator = sort_by(&initial_songs, &songs, distance);
815        let mut iterator: Box<dyn Iterator<Item = LibrarySong<T>>> =
816            Box::new(initial_songs.into_iter().chain(iterator));
817        if deduplicate {
818            iterator = Box::new(dedup_playlist_custom_distance(iterator, None, distance));
819        }
820        Ok(iterator)
821    }
822
823    /// Make a playlist of `number_albums` albums closest to the album
824    /// with title `album_title`.
825    /// The playlist starts with the album with `album_title`, and contains
826    /// `number_albums` on top of that one.
827    ///
828    /// Returns the songs of each album ordered by bliss' `track_number`.
829    pub fn album_playlist_from<T: Serialize + DeserializeOwned + Clone + PartialEq>(
830        &self,
831        album_title: String,
832        number_albums: usize,
833    ) -> Result<Vec<LibrarySong<T>>> {
834        let album = self.songs_from_album(&album_title)?;
835        // Every song should be from the same album. Hopefully...
836        let songs = self.songs_from_library()?;
837        let playlist = closest_album_to_group(album, songs)?;
838
839        let mut album_count = 0;
840        let mut index = 0;
841        let mut current_album = Some(album_title);
842        for song in playlist.iter() {
843            if song.bliss_song.album != current_album {
844                album_count += 1;
845                if album_count > number_albums {
846                    break;
847                }
848                song.bliss_song.album.clone_into(&mut current_album);
849            }
850            index += 1;
851        }
852        let playlist = &playlist[..index];
853        Ok(playlist.to_vec())
854    }
855
856    /// Analyze and store all songs in `paths` that haven't been already analyzed,
857    /// re-analyzing songs with newer feature versions if there was an update
858    /// of bliss features.
859    ///
860    /// Use this function if you don't have any extra data to bundle with each song.
861    ///
862    /// Setting `delete_everything_else` to true will delete the paths that are
863    /// not mentionned in `paths_extra_info` from the database. If you do not
864    /// use it, because you only pass the new paths that need to be analyzed to
865    /// this function, make sure to delete yourself from the database the songs
866    /// that have been deleted from storage.
867    ///
868    /// If your library
869    /// contains CUE files, pass the CUE file path only, and not individual
870    /// CUE track names: passing `vec![file.cue]` will add
871    /// individual tracks with the `cue_info` field set in the database.
872    // TODO: align these functions using maybe a struct. And make it more coherent,
873    // we shouldn't be feeding paths to this one...
874    pub fn update_library<P: Into<PathBuf>>(
875        &mut self,
876        paths: Vec<P>,
877        delete_everything_else: bool,
878        show_progress_bar: bool,
879    ) -> Result<()> {
880        let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
881        self.update_library_convert_extra_info(
882            paths_extra_info,
883            delete_everything_else,
884            show_progress_bar,
885            |x, _, _| x,
886            self.config.base_config().analysis_options,
887        )
888    }
889
890    /// Analyze and store all songs in `paths` that haven't been already analyzed,
891    /// with analysis options (including features version). If the features
892    /// version in the analysis options are newer than the ones in the song
893    /// database, all those songs are updated.
894    ///
895    /// Use this function if you don't have any extra data to bundle with each song.
896    ///
897    /// Setting `delete_everything_else` to true will delete the paths that are
898    /// not mentionned in `paths_extra_info` from the database. If you do not
899    /// use it, because you only pass the new paths that need to be analyzed to
900    /// this function, make sure to delete yourself from the database the songs
901    /// that have been deleted from storage.
902    ///
903    /// If your library
904    /// contains CUE files, pass the CUE file path only, and not individual
905    /// CUE track names: passing `vec![file.cue]` will add
906    /// individual tracks with the `cue_info` field set in the database.
907    // TODO: align these functions using maybe a struct. And make it more coherent,
908    // we shouldn't be feeding paths to this one...
909    pub fn update_library_with_options<P: Into<PathBuf>>(
910        &mut self,
911        paths: Vec<P>,
912        delete_everything_else: bool,
913        show_progress_bar: bool,
914        analysis_options: AnalysisOptions,
915    ) -> Result<()> {
916        let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
917        self.update_library_convert_extra_info(
918            paths_extra_info,
919            delete_everything_else,
920            show_progress_bar,
921            |x, _, _| x,
922            analysis_options,
923        )
924    }
925
926    /// Analyze and store all songs in `paths_extra_info` that haven't already
927    /// been analyzed, along with some extra metadata serializable, and known
928    /// before song analysis.
929    ///
930    /// Setting `delete_everything_else` to true will delete the paths that are
931    /// not mentionned in `paths_extra_info` from the database. If you do not
932    /// use it, because you only pass the new paths that need to be analyzed to
933    /// this function, make sure to delete yourself from the database the songs
934    /// that have been deleted from storage.
935    pub fn update_library_extra_info<T: Serialize + DeserializeOwned + Clone, P: Into<PathBuf>>(
936        &mut self,
937        paths_extra_info: Vec<(P, T)>,
938        delete_everything_else: bool,
939        show_progress_bar: bool,
940    ) -> Result<()> {
941        self.update_library_convert_extra_info(
942            paths_extra_info,
943            delete_everything_else,
944            show_progress_bar,
945            |extra_info, _, _| extra_info,
946            self.config.base_config().analysis_options,
947        )
948    }
949
950    /// Analyze and store all songs in `paths_extra_info` that haven't
951    /// been already analyzed, as well as handling extra, user-specified metadata,
952    /// that can't directly be serializable,
953    /// or that need input from the analyzed Song to be processed. If you
954    /// just want to analyze and store songs along with some directly
955    /// serializable values, consider using [Library::update_library_extra_info],
956    /// or [Library::update_library] if you just want the analyzed songs
957    /// stored as is.
958    ///
959    /// `paths_extra_info` is a tuple made out of song paths, along
960    /// with any extra info you want to store for each song.
961    /// If your library
962    /// contains CUE files, pass the CUE file path only, and not individual
963    /// CUE track names: passing `vec![file.cue]` will add
964    /// individual tracks with the `cue_info` field set in the database.
965    ///
966    /// Setting `delete_everything_else` to true will delete the paths that are
967    /// not mentionned in `paths_extra_info` from the database. If you do not
968    /// use it, because you only pass the new paths that need to be analyzed to
969    /// this function, make sure to delete yourself from the database the songs
970    /// that have been deleted from storage.
971    ///
972    /// `convert_extra_info` is a function that you should specify how
973    /// to convert that extra info to something serializable.
974    ///
975    /// `analysis_options` contains the desired analysis options, i.e. number
976    /// of cores and the version of the features you want your library to be
977    /// analyzed with. It will reanalyze songs that have features version older
978    /// than latest's, and set the config file's features_version to the specified version.
979    pub fn update_library_convert_extra_info<
980        T: Serialize + DeserializeOwned + Clone,
981        U,
982        P: Into<PathBuf>,
983    >(
984        &mut self,
985        paths_extra_info: Vec<(P, U)>,
986        delete_everything_else: bool,
987        show_progress_bar: bool,
988        convert_extra_info: fn(U, &Song, &Self) -> T,
989        analysis_options: AnalysisOptions,
990    ) -> Result<()> {
991        let existing_paths = {
992            let connection = self
993                .sqlite_conn
994                .lock()
995                .map_err(|e| BlissError::ProviderError(e.to_string()))?;
996            let mut path_statement = connection.prepare(
997                "
998                select
999                    path
1000                    from song where analyzed = true and version = ? order by id
1001                ",
1002            )?;
1003            #[allow(clippy::let_and_return)]
1004            let return_value = path_statement
1005                .query_map([analysis_options.features_version], |row| {
1006                    Ok(row.get_unwrap::<usize, String>(0))
1007                })?
1008                .map(|x| PathBuf::from(x.unwrap()))
1009                .collect::<HashSet<PathBuf>>();
1010            return_value
1011        };
1012
1013        let paths_extra_info: Vec<_> = paths_extra_info
1014            .into_iter()
1015            .map(|(x, y)| (x.into(), y))
1016            .collect();
1017        let paths: HashSet<_> = paths_extra_info.iter().map(|(p, _)| p.to_owned()).collect();
1018
1019        if delete_everything_else {
1020            let existing_paths_old_features_version = {
1021                let connection = self
1022                    .sqlite_conn
1023                    .lock()
1024                    .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1025                let mut path_statement = connection.prepare(
1026                    "
1027                select
1028                    path
1029                    from song where analyzed = true order by id
1030                ",
1031                )?;
1032                #[allow(clippy::let_and_return)]
1033                let return_value = path_statement
1034                    .query_map([], |row| Ok(row.get_unwrap::<usize, String>(0)))?
1035                    .map(|x| PathBuf::from(x.unwrap()))
1036                    .collect::<HashSet<PathBuf>>();
1037                return_value
1038            };
1039
1040            let paths_to_delete = existing_paths_old_features_version.difference(&paths);
1041
1042            self.delete_paths(paths_to_delete)?;
1043        }
1044
1045        // Can't use hashsets because we need the extra info here too,
1046        // and U might not be hashable.
1047        let paths_to_analyze = paths_extra_info
1048            .into_iter()
1049            .filter(|(path, _)| !existing_paths.contains(path))
1050            .collect::<Vec<(PathBuf, U)>>();
1051
1052        {
1053            let connection = self
1054                .sqlite_conn
1055                .lock()
1056                .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1057
1058            if !paths_to_analyze.is_empty() {
1059                connection.execute(
1060                    "delete from song where version != ?",
1061                    params![analysis_options.features_version],
1062                )?;
1063            }
1064        }
1065
1066        self.analyze_paths_convert_extra_info(
1067            paths_to_analyze,
1068            show_progress_bar,
1069            convert_extra_info,
1070            analysis_options,
1071        )
1072    }
1073
1074    /// Analyze and store all songs in `paths`.
1075    ///
1076    /// Use this function if you don't have any extra data to bundle with each song.
1077    ///
1078    /// If your library contains CUE files, pass the CUE file path only, and not individual
1079    /// CUE track names: passing `vec![file.cue]` will add
1080    /// individual tracks with the `cue_info` field set in the database.
1081    pub fn analyze_paths<P: Into<PathBuf>>(
1082        &mut self,
1083        paths: Vec<P>,
1084        show_progress_bar: bool,
1085    ) -> Result<()> {
1086        let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
1087        let analysis_options = self.config.base_config().analysis_options;
1088        self.analyze_paths_convert_extra_info(
1089            paths_extra_info,
1090            show_progress_bar,
1091            |x, _, _| x,
1092            analysis_options,
1093        )
1094    }
1095
1096    /// Analyze and store all songs in `paths`, setting analysis options such
1097    /// as features version and the number and cores.
1098    /// Be careful not to analyze
1099    /// some songs with a different features version than the rest of the database!
1100    ///
1101    /// Use this function if you don't have any extra data to bundle with each song.
1102    ///
1103    /// If your library contains CUE files, pass the CUE file path only, and not individual
1104    /// CUE track names: passing `vec![file.cue]` will add
1105    /// individual tracks with the `cue_info` field set in the database.
1106    pub fn analyze_paths_with_options<P: Into<PathBuf>>(
1107        &mut self,
1108        paths: Vec<P>,
1109        show_progress_bar: bool,
1110        analysis_options: AnalysisOptions,
1111    ) -> Result<()> {
1112        let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::<Vec<_>>();
1113        self.analyze_paths_convert_extra_info(
1114            paths_extra_info,
1115            show_progress_bar,
1116            |x, _, _| x,
1117            analysis_options,
1118        )
1119    }
1120
1121    /// Analyze and store all songs in `paths_extra_info`, along with some
1122    /// extra metadata serializable, and known before song analysis.
1123    ///
1124    /// Updates the value of `features_version` in the config, using bliss'
1125    /// latest version.
1126    /// If your library
1127    /// contains CUE files, pass the CUE file path only, and not individual
1128    /// CUE track names: passing `vec![file.cue]` will add
1129    /// individual tracks with the `cue_info` field set in the database.
1130    pub fn analyze_paths_extra_info<
1131        T: Serialize + DeserializeOwned + std::fmt::Debug + Clone,
1132        P: Into<PathBuf>,
1133    >(
1134        &mut self,
1135        paths_extra_info: Vec<(P, T)>,
1136        show_progress_bar: bool,
1137        analysis_options: AnalysisOptions,
1138    ) -> Result<()> {
1139        self.analyze_paths_convert_extra_info(
1140            paths_extra_info,
1141            show_progress_bar,
1142            |extra_info, _, _| extra_info,
1143            analysis_options,
1144        )
1145    }
1146
1147    /// Analyze and store all songs in `paths_extra_info`, along with some
1148    /// extra, user-specified metadata, that can't directly be serializable,
1149    /// or that need input from the analyzed Song to be processed.
1150    /// If you just want to analyze and store songs, along with some
1151    /// directly serializable metadata values, consider using
1152    /// [Library::analyze_paths_extra_info], or [Library::analyze_paths] for
1153    /// the simpler use cases.
1154    ///
1155    /// Updates the value of `features_version` in the config, using bliss'
1156    /// latest version.
1157    ///
1158    /// `paths_extra_info` is a tuple made out of song paths, along
1159    /// with any extra info you want to store for each song. If your library
1160    /// contains CUE files, pass the CUE file path only, and not individual
1161    /// CUE track names: passing `vec![file.cue]` will add
1162    /// individual tracks with the `cue_info` field set in the database.
1163    ///
1164    /// `convert_extra_info` is a function that you should specify
1165    /// to convert that extra info to something serializable.
1166    pub fn analyze_paths_convert_extra_info<
1167        T: Serialize + DeserializeOwned + Clone,
1168        U,
1169        P: Into<PathBuf>,
1170    >(
1171        &mut self,
1172        paths_extra_info: Vec<(P, U)>,
1173        show_progress_bar: bool,
1174        convert_extra_info: fn(U, &Song, &Self) -> T,
1175        analysis_options: AnalysisOptions,
1176    ) -> Result<()> {
1177        let number_songs = paths_extra_info.len();
1178        if number_songs == 0 {
1179            log::info!("No (new) songs found.");
1180            return Ok(());
1181        }
1182        log::info!("Analyzing {number_songs} song(s), this might take some time…",);
1183        let pb = if show_progress_bar {
1184            ProgressBar::new(number_songs.try_into().unwrap())
1185        } else {
1186            ProgressBar::hidden()
1187        };
1188        let style = ProgressStyle::default_bar()
1189            .template("[{elapsed_precise}] {bar:40} {pos:>7}/{len:7} {wide_msg}")?
1190            .progress_chars("##-");
1191        pb.set_style(style);
1192
1193        let mut paths_extra_info: HashMap<PathBuf, U> = paths_extra_info
1194            .into_iter()
1195            .map(|(x, y)| (x.into(), y))
1196            .collect();
1197        let mut cue_extra_info: HashMap<PathBuf, String> = HashMap::new();
1198
1199        let results = D::analyze_paths_with_options(paths_extra_info.keys(), analysis_options);
1200        let mut success_count = 0;
1201        let mut failure_count = 0;
1202        for (path, result) in results {
1203            if show_progress_bar {
1204                pb.set_message(format!("Analyzing {}", path.display()));
1205            }
1206            match result {
1207                Ok(song) => {
1208                    let is_cue = song.cue_info.is_some();
1209                    // If it's a song that's part of a CUE, its path will be
1210                    // something like `testcue.flac/CUE_TRACK001`, so we need
1211                    // to get the path of the main CUE file.
1212                    let path = {
1213                        if let Some(cue_info) = song.cue_info.to_owned() {
1214                            cue_info.cue_path
1215                        } else {
1216                            path
1217                        }
1218                    };
1219                    // Some magic to avoid having to depend on T: Clone, because
1220                    // all CUE tracks on a CUE file have the same extra_info.
1221                    // This serializes the data, store the serialized version
1222                    // in a hashmap, and then deserializes that when needed.
1223                    let extra = {
1224                        if is_cue && paths_extra_info.contains_key(&path) {
1225                            let extra = paths_extra_info.remove(&path).unwrap();
1226                            let e = convert_extra_info(extra, &song, self);
1227                            cue_extra_info.insert(
1228                                path,
1229                                serde_json::to_string(&e)
1230                                    .map_err(|e| BlissError::ProviderError(e.to_string()))?,
1231                            );
1232                            e
1233                        } else if is_cue {
1234                            let serialized_extra_info =
1235                                cue_extra_info.get(&path).unwrap().to_owned();
1236                            serde_json::from_str(&serialized_extra_info).unwrap()
1237                        } else {
1238                            let extra = paths_extra_info.remove(&path).unwrap();
1239                            convert_extra_info(extra, &song, self)
1240                        }
1241                    };
1242                    let library_song = LibrarySong::<T> {
1243                        bliss_song: song,
1244                        extra_info: extra,
1245                    };
1246                    self.store_song(&library_song)?;
1247                    success_count += 1;
1248                }
1249                Err(e) => {
1250                    log::error!(
1251                        "Analysis of song '{}' failed: {} The error has been stored.",
1252                        path.display(),
1253                        e
1254                    );
1255
1256                    self.store_failed_song(path, e, analysis_options.features_version)?;
1257                    failure_count += 1;
1258                }
1259            };
1260            pb.inc(1);
1261        }
1262        pb.finish_with_message(format!(
1263            "Analyzed {success_count} song(s) successfully. {failure_count} Failure(s).",
1264        ));
1265
1266        log::info!("Analyzed {success_count} song(s) successfully. {failure_count} Failure(s).",);
1267
1268        self.config.base_config_mut().analysis_options = analysis_options;
1269        self.config.write()?;
1270
1271        Ok(())
1272    }
1273
1274    // Get songs from a songs / features statement.
1275    // BEWARE that the two songs and features query MUST be the same
1276    fn _songs_from_statement<T: Serialize + DeserializeOwned + Clone, P: Params + Clone>(
1277        &self,
1278        songs_statement: &str,
1279        features_statement: &str,
1280        params: P,
1281    ) -> Result<Vec<LibrarySong<T>>> {
1282        let connection = self
1283            .sqlite_conn
1284            .lock()
1285            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1286        let mut songs_statement = connection.prepare(songs_statement)?;
1287        let mut features_statement = connection.prepare(features_statement)?;
1288        let song_rows = songs_statement.query_map(params.to_owned(), |row| {
1289            Ok((row.get(13)?, Self::_song_from_row_closure(row)?))
1290        })?;
1291        let feature_rows =
1292            features_statement.query_map(params, |row| Ok((row.get(1)?, row.get(0)?)))?;
1293
1294        let mut feature_iterator = feature_rows.into_iter().peekable();
1295        let mut songs = Vec::new();
1296        // Poor man's way to double check that each feature correspond to the
1297        // right song, and group them.
1298        for row in song_rows {
1299            let song_id: u32 = row.as_ref().unwrap().0;
1300            let mut chunk: Vec<f32> = Vec::with_capacity(NUMBER_FEATURES);
1301
1302            while let Some(first_value) = feature_iterator.peek() {
1303                let (song_feature_id, feature): (u32, f32) = *first_value.as_ref().unwrap();
1304                if song_feature_id == song_id {
1305                    chunk.push(feature);
1306                    feature_iterator.next();
1307                } else {
1308                    break;
1309                };
1310            }
1311            let mut song = row.unwrap().1;
1312            song.bliss_song.analysis = Analysis::new(chunk, song.bliss_song.features_version)
1313                .map_err(|_| {
1314                    BlissError::ProviderError(format!(
1315                        "Song with ID {} and path {} has a different feature \
1316                        number than expected. Please rescan or update \
1317                        the song library.",
1318                        song_id,
1319                        song.bliss_song.path.display(),
1320                    ))
1321                })?;
1322            songs.push(song);
1323        }
1324        Ok(songs)
1325    }
1326
1327    /// Retrieve all songs which have been analyzed with
1328    /// the bliss version specified in the configuration.
1329    ///
1330    /// Returns an error if one or several songs have a different number of
1331    /// features than they should, indicating the offending song id.
1332    ///
1333    // TODO maybe the error should make the song id / song path
1334    // accessible easily?
1335    pub fn songs_from_library<T: Serialize + DeserializeOwned + Clone>(
1336        &self,
1337    ) -> Result<Vec<LibrarySong<T>>> {
1338        let songs_statement = "
1339            select
1340                path, artist, title, album, album_artist,
1341                track_number, disc_number, genre, duration, version, extra_info, cue_path,
1342                audio_file_path, id
1343                from song where analyzed = true and version = ? order by id
1344            ";
1345        let features_statement = "
1346            select
1347                feature, song.id from feature join song on song.id = feature.song_id
1348                where song.analyzed = true and song.version = ? order by song_id, feature_index
1349                ";
1350        let params = params![self.config.base_config().analysis_options.features_version];
1351        self._songs_from_statement(songs_statement, features_statement, params)
1352    }
1353
1354    /// Get a LibrarySong from a given album title.
1355    ///
1356    /// This will return all songs with corresponding bliss "album" tag,
1357    /// and will order them by track number.
1358    pub fn songs_from_album<T: Serialize + DeserializeOwned + Clone>(
1359        &self,
1360        album_title: &str,
1361    ) -> Result<Vec<LibrarySong<T>>> {
1362        let params = params![
1363            album_title,
1364            self.config.base_config().analysis_options.features_version
1365        ];
1366        let songs_statement = "
1367            select
1368                path, artist, title, album, album_artist,
1369                track_number, disc_number, genre, duration, version, extra_info, cue_path,
1370                audio_file_path, id
1371                from song where album = ? and analyzed = true and version = ?
1372                order
1373                by disc_number, track_number;
1374            ";
1375
1376        // Get the song's analysis, and attach it to the existing song.
1377        let features_statement = "
1378            select
1379                feature, song.id from feature join song on song.id = feature.song_id
1380                where album=? and analyzed = true and version = ?
1381                order by disc_number, track_number;
1382            ";
1383        let songs = self._songs_from_statement(songs_statement, features_statement, params)?;
1384        if songs.is_empty() {
1385            bail!(BlissError::ProviderError(String::from(
1386                "target album was not found in the database.",
1387            )));
1388        };
1389        Ok(songs)
1390    }
1391
1392    /// Get a LibrarySong from a given file path.
1393    /// TODO pathbuf here too
1394    pub fn song_from_path<T: Serialize + DeserializeOwned + Clone>(
1395        &self,
1396        song_path: &str,
1397    ) -> Result<LibrarySong<T>> {
1398        let connection = self
1399            .sqlite_conn
1400            .lock()
1401            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1402        // Get the song's metadata. The analysis is populated yet.
1403        let mut song = connection.query_row(
1404            "
1405            select
1406                path, artist, title, album, album_artist,
1407                track_number, disc_number, genre, duration, version, extra_info,
1408                cue_path, audio_file_path
1409                from song where path=? and analyzed = true
1410            ",
1411            params![song_path],
1412            Self::_song_from_row_closure,
1413        )?;
1414
1415        // Get the song's analysis, and attach it to the existing song.
1416        let mut stmt = connection.prepare(
1417            "
1418            select
1419                feature from feature join song on song.id = feature.song_id
1420                where song.path = ? order by feature_index
1421            ",
1422        )?;
1423        let analysis = Analysis::new(
1424            stmt.query_map(params![song_path], |row| row.get(0))
1425                .unwrap()
1426                .map(|x| x.unwrap())
1427                .collect::<Vec<f32>>(),
1428            song.bliss_song.features_version,
1429        )
1430        .map_err(|_| {
1431            BlissError::ProviderError(format!(
1432                "song has more or less than {NUMBER_FEATURES} features",
1433            ))
1434        })?;
1435        song.bliss_song.analysis = analysis;
1436        Ok(song)
1437    }
1438
1439    fn _song_from_row_closure<T: Serialize + DeserializeOwned + Clone>(
1440        row: &Row,
1441    ) -> Result<LibrarySong<T>, RusqliteError> {
1442        let path: String = row.get(0)?;
1443
1444        let cue_path: Option<String> = row.get(11)?;
1445        let audio_file_path: Option<String> = row.get(12)?;
1446        let mut cue_info = None;
1447        if let Some(cue_path) = cue_path {
1448            cue_info = Some(CueInfo {
1449                cue_path: PathBuf::from(cue_path),
1450                audio_file_path: PathBuf::from(audio_file_path.unwrap()),
1451            })
1452        };
1453
1454        let song = Song {
1455            path: PathBuf::from(path),
1456            artist: row
1457                .get_ref(1)
1458                .unwrap()
1459                .as_bytes_or_null()
1460                .unwrap()
1461                .map(|v| String::from_utf8_lossy(v).to_string()),
1462            title: row
1463                .get_ref(2)
1464                .unwrap()
1465                .as_bytes_or_null()
1466                .unwrap()
1467                .map(|v| String::from_utf8_lossy(v).to_string()),
1468            album: row
1469                .get_ref(3)
1470                .unwrap()
1471                .as_bytes_or_null()
1472                .unwrap()
1473                .map(|v| String::from_utf8_lossy(v).to_string()),
1474            album_artist: row
1475                .get_ref(4)
1476                .unwrap()
1477                .as_bytes_or_null()
1478                .unwrap()
1479                .map(|v| String::from_utf8_lossy(v).to_string()),
1480            track_number: row
1481                .get_ref(5)
1482                .unwrap()
1483                .as_i64_or_null()
1484                .unwrap()
1485                .map(|v| v as i32),
1486            disc_number: row
1487                .get_ref(6)
1488                .unwrap()
1489                .as_i64_or_null()
1490                .unwrap()
1491                .map(|v| v as i32),
1492            genre: row
1493                .get_ref(7)
1494                .unwrap()
1495                .as_bytes_or_null()
1496                .unwrap()
1497                .map(|v| String::from_utf8_lossy(v).to_string()),
1498            analysis: Analysis {
1499                internal_analysis: vec![0.; NUMBER_FEATURES],
1500                features_version: row.get(9).unwrap(),
1501            },
1502            duration: Duration::from_secs_f64(row.get(8).unwrap()),
1503            features_version: row.get(9).unwrap(),
1504            cue_info,
1505        };
1506
1507        let serialized: Option<String> = row.get(10).unwrap();
1508        let serialized = serialized.unwrap_or_else(|| "null".into());
1509        let extra_info = serde_json::from_str(&serialized).unwrap();
1510        Ok(LibrarySong {
1511            bliss_song: song,
1512            extra_info,
1513        })
1514    }
1515
1516    /// Store a [Song] in the database, overidding any existing
1517    /// song with the same path by that one.
1518    // TODO to_str() returns an option; return early and avoid panicking
1519    pub fn store_song<T: Serialize + DeserializeOwned + Clone>(
1520        &mut self,
1521        library_song: &LibrarySong<T>,
1522    ) -> Result<(), BlissError> {
1523        let mut sqlite_conn = self.sqlite_conn.lock().unwrap();
1524        let tx = sqlite_conn
1525            .transaction()
1526            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1527        let song = &library_song.bliss_song;
1528        let (cue_path, audio_file_path) = match &song.cue_info {
1529            Some(c) => (
1530                Some(c.cue_path.to_string_lossy()),
1531                Some(c.audio_file_path.to_string_lossy()),
1532            ),
1533            None => (None, None),
1534        };
1535        tx.execute(
1536            "
1537            insert into song (
1538                path, artist, title, album, album_artist,
1539                duration, track_number, disc_number, genre, analyzed, version, extra_info,
1540                cue_path, audio_file_path
1541            )
1542            values (
1543                ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14
1544            )
1545            on conflict(path)
1546            do update set
1547                artist=excluded.artist,
1548                title=excluded.title,
1549                album=excluded.album,
1550                track_number=excluded.track_number,
1551                disc_number=excluded.disc_number,
1552                album_artist=excluded.album_artist,
1553                duration=excluded.duration,
1554                genre=excluded.genre,
1555                analyzed=excluded.analyzed,
1556                version=excluded.version,
1557                extra_info=excluded.extra_info,
1558                cue_path=excluded.cue_path,
1559                audio_file_path=excluded.audio_file_path
1560            ",
1561            params![
1562                song.path.to_str(),
1563                song.artist,
1564                song.title,
1565                song.album,
1566                song.album_artist,
1567                song.duration.as_secs_f64(),
1568                song.track_number,
1569                song.disc_number,
1570                song.genre,
1571                true,
1572                song.features_version,
1573                serde_json::to_string(&library_song.extra_info)
1574                    .map_err(|e| BlissError::ProviderError(e.to_string()))?,
1575                cue_path,
1576                audio_file_path,
1577            ],
1578        )
1579        .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1580
1581        // Override existing features.
1582        tx.execute(
1583            "delete from feature where song_id in (select id from song where path = ?1);",
1584            params![song.path.to_str()],
1585        )
1586        .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1587
1588        for (index, feature) in song.analysis.as_vec().iter().enumerate() {
1589            tx.execute(
1590                "
1591                insert into feature (song_id, feature, feature_index)
1592                values ((select id from song where path = ?1), ?2, ?3)
1593                on conflict(song_id, feature_index) do update set feature=excluded.feature;
1594                ",
1595                params![song.path.to_str(), feature, index],
1596            )
1597            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1598        }
1599        tx.commit()
1600            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1601        Ok(())
1602    }
1603
1604    /// Store an errored [Song] in the SQLite database.
1605    ///
1606    /// If there already is an existing song with that path, replace it by
1607    /// the latest failed result.
1608    pub fn store_failed_song<P: Into<PathBuf>>(
1609        &mut self,
1610        song_path: P,
1611        e: BlissError,
1612        features_version: FeaturesVersion,
1613    ) -> Result<()> {
1614        self.sqlite_conn
1615            .lock()
1616            .unwrap()
1617            .execute(
1618                "
1619            insert or replace into song (path, error, version) values (?1, ?2, ?3)
1620            ",
1621                params![
1622                    song_path.into().to_string_lossy().to_string(),
1623                    e.to_string(),
1624                    // At this point, FeaturesVersion::LATEST is the best indicator we have
1625                    // of the version (since we don't have a proper Song).
1626                    features_version,
1627                ],
1628            )
1629            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1630        Ok(())
1631    }
1632
1633    /// Return all the songs that failed the analysis.
1634    pub fn get_failed_songs(&self) -> Result<Vec<ProcessingError>> {
1635        let conn = self.sqlite_conn.lock().unwrap();
1636        let mut stmt = conn.prepare(
1637            "
1638            select path, error, version
1639                from song where error is not null order by id
1640            ",
1641        )?;
1642        let rows = stmt.query_map([], |row| {
1643            Ok(ProcessingError {
1644                song_path: row.get::<_, String>(0)?.into(),
1645                error: row.get(1)?,
1646                features_version: row.get(2)?,
1647            })
1648        })?;
1649        Ok(rows
1650            .into_iter()
1651            .map(|r| r.unwrap())
1652            .collect::<Vec<ProcessingError>>())
1653    }
1654
1655    /// Delete a song with path `song_path` from the database.
1656    ///
1657    /// Errors out if the song is not in the database.
1658    pub fn delete_path<P: Into<PathBuf>>(&mut self, song_path: P) -> Result<()> {
1659        let song_path = song_path.into();
1660        let count = self
1661            .sqlite_conn
1662            .lock()
1663            .unwrap()
1664            .execute(
1665                "
1666                delete from song where path = ?1;
1667            ",
1668                [song_path.to_str()],
1669            )
1670            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1671        if count == 0 {
1672            bail!(BlissError::ProviderError(format!(
1673                "tried to delete song {}, not existing in the database.",
1674                song_path.display(),
1675            )));
1676        }
1677        Ok(())
1678    }
1679
1680    /// Delete a set of songs with paths `song_paths` from the database.
1681    ///
1682    /// Will return Ok(count) even if less songs than expected were deleted from the database.
1683    pub fn delete_paths<P: Into<PathBuf>, I: IntoIterator<Item = P>>(
1684        &mut self,
1685        paths: I,
1686    ) -> Result<usize> {
1687        let song_paths: Vec<String> = paths
1688            .into_iter()
1689            .map(|x| x.into().to_string_lossy().to_string())
1690            .collect();
1691        if song_paths.is_empty() {
1692            return Ok(0);
1693        };
1694        let count = self
1695            .sqlite_conn
1696            .lock()
1697            .unwrap()
1698            .execute(
1699                &format!(
1700                    "delete from song where path in ({})",
1701                    repeat_vars(song_paths.len()),
1702                ),
1703                params_from_iter(song_paths),
1704            )
1705            .map_err(|e| BlissError::ProviderError(e.to_string()))?;
1706        Ok(count)
1707    }
1708}
1709
1710// Copied from
1711// https://docs.rs/rusqlite/latest/rusqlite/struct.ParamsFromIter.html#realistic-use-case
1712fn repeat_vars(count: usize) -> String {
1713    assert_ne!(count, 0);
1714    let mut s = "?,".repeat(count);
1715    // Remove trailing comma
1716    s.pop();
1717    s
1718}
1719
1720#[cfg(any(test, feature = "integration-tests"))]
1721fn data_local_dir() -> Option<PathBuf> {
1722    Some(PathBuf::from("/tmp/data"))
1723}
1724
1725#[cfg(any(test, feature = "integration-tests"))]
1726fn config_local_dir() -> Option<PathBuf> {
1727    Some(PathBuf::from("/tmp/"))
1728}
1729
1730#[cfg(test)]
1731// TODO refactor (especially the helper functions)
1732// TODO the tests should really open a songs.db
1733// TODO test with invalid UTF-8
1734mod test {
1735    use super::*;
1736    use crate::{decoder::PreAnalyzedSong, Analysis, NUMBER_FEATURES};
1737    use ndarray::Array1;
1738    use pretty_assertions::assert_eq;
1739    use serde::{de::DeserializeOwned, Deserialize};
1740    use serde_json::Value;
1741    use std::thread;
1742    use std::{convert::TryInto, fmt::Debug, str::FromStr, sync::MutexGuard, time::Duration};
1743    use tempdir::TempDir;
1744
1745    #[cfg(feature = "ffmpeg")]
1746    use crate::song::decoder::ffmpeg::FFmpegDecoder as Decoder;
1747    use crate::song::decoder::Decoder as DecoderTrait;
1748
1749    struct DummyDecoder;
1750
1751    // Here to test an ffmpeg-agnostic library
1752    impl DecoderTrait for DummyDecoder {
1753        fn decode(_: &Path) -> crate::BlissResult<crate::decoder::PreAnalyzedSong> {
1754            Ok(PreAnalyzedSong::default())
1755        }
1756    }
1757
1758    #[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)]
1759    struct ExtraInfo {
1760        ignore: bool,
1761        metadata_bliss_does_not_have: String,
1762    }
1763
1764    #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
1765    struct CustomConfig {
1766        #[serde(flatten)]
1767        base_config: BaseConfig,
1768        second_path_to_music_library: String,
1769        ignore_wav_files: bool,
1770    }
1771
1772    impl AppConfigTrait for CustomConfig {
1773        fn base_config(&self) -> &BaseConfig {
1774            &self.base_config
1775        }
1776
1777        fn base_config_mut(&mut self) -> &mut BaseConfig {
1778            &mut self.base_config
1779        }
1780    }
1781
1782    fn nzus(i: usize) -> NonZeroUsize {
1783        NonZeroUsize::new(i).unwrap()
1784    }
1785
1786    // Returning the TempDir here, so it doesn't go out of scope, removing
1787    // the directory.
1788    //
1789    // Setup a test library made of analyzed songs, with every field being different,
1790    // as well as an unanalyzed song and a song analyzed with a previous version.
1791    //
1792    // TODO the SQL database should be populated with the actual songs created here using
1793    // format strings
1794    #[cfg(feature = "ffmpeg")]
1795    fn setup_test_library() -> (
1796        Library<BaseConfig, Decoder>,
1797        TempDir,
1798        (
1799            LibrarySong<ExtraInfo>,
1800            LibrarySong<ExtraInfo>,
1801            LibrarySong<ExtraInfo>,
1802            LibrarySong<ExtraInfo>,
1803            LibrarySong<ExtraInfo>,
1804            LibrarySong<ExtraInfo>,
1805            LibrarySong<ExtraInfo>,
1806            LibrarySong<ExtraInfo>,
1807        ),
1808    ) {
1809        let config_dir = TempDir::new("coucou").unwrap();
1810        let config_file = config_dir.path().join("config.json");
1811        let database_file = config_dir.path().join("bliss.db");
1812        let library = Library::<BaseConfig, Decoder>::new_from_base(
1813            Some(config_file),
1814            Some(database_file),
1815            None,
1816        )
1817        .unwrap();
1818
1819        let analysis_vector = (0..NUMBER_FEATURES)
1820            .map(|x| x as f32 / 10.)
1821            .collect::<Vec<f32>>();
1822
1823        let song = Song {
1824            path: "/path/to/song1001".into(),
1825            artist: Some("Artist1001".into()),
1826            title: Some("Title1001".into()),
1827            album: Some("An Album1001".into()),
1828            album_artist: Some("An Album Artist1001".into()),
1829            track_number: Some(3),
1830            disc_number: Some(1),
1831            genre: Some("Electronica1001".into()),
1832            analysis: Analysis {
1833                internal_analysis: analysis_vector,
1834                features_version: FeaturesVersion::LATEST,
1835            },
1836            duration: Duration::from_secs(310),
1837            features_version: FeaturesVersion::LATEST,
1838            cue_info: None,
1839        };
1840        let first_song = LibrarySong {
1841            bliss_song: song,
1842            extra_info: ExtraInfo {
1843                ignore: true,
1844                metadata_bliss_does_not_have: String::from("/path/to/charlie1001"),
1845            },
1846        };
1847
1848        let analysis_vector = (0..NUMBER_FEATURES)
1849            .map(|x| x as f32 + 10.)
1850            .collect::<Vec<f32>>();
1851
1852        let song = Song {
1853            path: "/path/to/song2001".into(),
1854            artist: Some("Artist2001".into()),
1855            title: Some("Title2001".into()),
1856            album: Some("An Album2001".into()),
1857            album_artist: Some("An Album Artist2001".into()),
1858            track_number: Some(2),
1859            disc_number: Some(1),
1860            genre: Some("Electronica2001".into()),
1861            analysis: Analysis {
1862                internal_analysis: analysis_vector,
1863                features_version: FeaturesVersion::LATEST,
1864            },
1865            duration: Duration::from_secs(410),
1866            features_version: FeaturesVersion::LATEST,
1867            cue_info: None,
1868        };
1869        let second_song = LibrarySong {
1870            bliss_song: song,
1871            extra_info: ExtraInfo {
1872                ignore: false,
1873                metadata_bliss_does_not_have: String::from("/path/to/charlie2001"),
1874            },
1875        };
1876
1877        let analysis_vector = (0..NUMBER_FEATURES)
1878            .map(|x| x as f32 + 10.)
1879            .collect::<Vec<f32>>();
1880
1881        let song = Song {
1882            path: "/path/to/song2201".into(),
1883            artist: Some("Artist2001".into()),
1884            title: Some("Title2001".into()),
1885            album: Some("An Album2001".into()),
1886            album_artist: Some("An Album Artist2001".into()),
1887            track_number: Some(1),
1888            disc_number: Some(2),
1889            genre: Some("Electronica2001".into()),
1890            analysis: Analysis {
1891                internal_analysis: analysis_vector,
1892                features_version: FeaturesVersion::LATEST,
1893            },
1894            duration: Duration::from_secs(410),
1895            features_version: FeaturesVersion::LATEST,
1896            cue_info: None,
1897        };
1898        let second_song_dupe = LibrarySong {
1899            bliss_song: song,
1900            extra_info: ExtraInfo {
1901                ignore: false,
1902                metadata_bliss_does_not_have: String::from("/path/to/charlie2201"),
1903            },
1904        };
1905
1906        let analysis_vector = (0..NUMBER_FEATURES)
1907            .map(|x| x as f32 / 2.)
1908            .collect::<Vec<f32>>();
1909
1910        let song = Song {
1911            path: "/path/to/song5001".into(),
1912            artist: Some("Artist5001".into()),
1913            title: Some("Title5001".into()),
1914            album: Some("An Album1001".into()),
1915            album_artist: Some("An Album Artist5001".into()),
1916            track_number: Some(1),
1917            disc_number: Some(1),
1918            genre: Some("Electronica5001".into()),
1919            analysis: Analysis {
1920                internal_analysis: analysis_vector,
1921                features_version: FeaturesVersion::LATEST,
1922            },
1923            duration: Duration::from_secs(610),
1924            features_version: FeaturesVersion::LATEST,
1925            cue_info: None,
1926        };
1927        let third_song = LibrarySong {
1928            bliss_song: song,
1929            extra_info: ExtraInfo {
1930                ignore: false,
1931                metadata_bliss_does_not_have: String::from("/path/to/charlie5001"),
1932            },
1933        };
1934
1935        let analysis_vector = (0..NUMBER_FEATURES)
1936            .map(|x| x as f32 * 0.9)
1937            .collect::<Vec<f32>>();
1938
1939        let song = Song {
1940            path: "/path/to/song6001".into(),
1941            artist: Some("Artist6001".into()),
1942            title: Some("Title6001".into()),
1943            album: Some("An Album2001".into()),
1944            album_artist: Some("An Album Artist6001".into()),
1945            track_number: Some(1),
1946            disc_number: Some(1),
1947            genre: Some("Electronica6001".into()),
1948            analysis: Analysis {
1949                internal_analysis: analysis_vector,
1950                features_version: FeaturesVersion::LATEST,
1951            },
1952            duration: Duration::from_secs(710),
1953            features_version: FeaturesVersion::LATEST,
1954            cue_info: None,
1955        };
1956        let fourth_song = LibrarySong {
1957            bliss_song: song,
1958            extra_info: ExtraInfo {
1959                ignore: false,
1960                metadata_bliss_does_not_have: String::from("/path/to/charlie6001"),
1961            },
1962        };
1963
1964        let analysis_vector = (0..NUMBER_FEATURES)
1965            .map(|x| x as f32 * 50.)
1966            .collect::<Vec<f32>>();
1967
1968        let song = Song {
1969            path: "/path/to/song7001".into(),
1970            artist: Some("Artist7001".into()),
1971            title: Some("Title7001".into()),
1972            album: Some("An Album7001".into()),
1973            album_artist: Some("An Album Artist7001".into()),
1974            track_number: Some(1),
1975            disc_number: Some(1),
1976            genre: Some("Electronica7001".into()),
1977            analysis: Analysis {
1978                internal_analysis: analysis_vector,
1979                features_version: FeaturesVersion::LATEST,
1980            },
1981            duration: Duration::from_secs(810),
1982            features_version: FeaturesVersion::LATEST,
1983            cue_info: None,
1984        };
1985        let fifth_song = LibrarySong {
1986            bliss_song: song,
1987            extra_info: ExtraInfo {
1988                ignore: false,
1989                metadata_bliss_does_not_have: String::from("/path/to/charlie7001"),
1990            },
1991        };
1992
1993        let analysis_vector = (0..NUMBER_FEATURES)
1994            .map(|x| x as f32 * 100.)
1995            .collect::<Vec<f32>>();
1996
1997        let song = Song {
1998            path: "/path/to/cuetrack.cue/CUE_TRACK001".into(),
1999            artist: Some("CUE Artist".into()),
2000            title: Some("CUE Title 01".into()),
2001            album: Some("CUE Album".into()),
2002            album_artist: Some("CUE Album Artist".into()),
2003            track_number: Some(1),
2004            disc_number: Some(1),
2005            genre: None,
2006            analysis: Analysis {
2007                internal_analysis: analysis_vector,
2008                features_version: FeaturesVersion::LATEST,
2009            },
2010            duration: Duration::from_secs(810),
2011            features_version: FeaturesVersion::LATEST,
2012            cue_info: Some(CueInfo {
2013                cue_path: PathBuf::from("/path/to/cuetrack.cue"),
2014                audio_file_path: PathBuf::from("/path/to/cuetrack.flac"),
2015            }),
2016        };
2017        let sixth_song = LibrarySong {
2018            bliss_song: song,
2019            extra_info: ExtraInfo {
2020                ignore: false,
2021                metadata_bliss_does_not_have: String::from("/path/to/charlie7001"),
2022            },
2023        };
2024
2025        let analysis_vector = (0..NUMBER_FEATURES)
2026            .map(|x| x as f32 * 101.)
2027            .collect::<Vec<f32>>();
2028
2029        let song = Song {
2030            path: "/path/to/cuetrack.cue/CUE_TRACK002".into(),
2031            artist: Some("CUE Artist".into()),
2032            title: Some("CUE Title 02".into()),
2033            album: Some("CUE Album".into()),
2034            album_artist: Some("CUE Album Artist".into()),
2035            track_number: Some(2),
2036            disc_number: Some(1),
2037            genre: None,
2038            analysis: Analysis {
2039                internal_analysis: analysis_vector,
2040                features_version: FeaturesVersion::LATEST,
2041            },
2042            duration: Duration::from_secs(910),
2043            features_version: FeaturesVersion::LATEST,
2044            cue_info: Some(CueInfo {
2045                cue_path: PathBuf::from("/path/to/cuetrack.cue"),
2046                audio_file_path: PathBuf::from("/path/to/cuetrack.flac"),
2047            }),
2048        };
2049        let seventh_song = LibrarySong {
2050            bliss_song: song,
2051            extra_info: ExtraInfo {
2052                ignore: false,
2053                metadata_bliss_does_not_have: String::from("/path/to/charlie7001"),
2054            },
2055        };
2056
2057        {
2058            let connection = library.sqlite_conn.lock().unwrap();
2059            connection
2060                .execute(
2061                    &format!(
2062                        "
2063                    insert into song (
2064                        id, path, artist, title, album, album_artist, track_number,
2065                        disc_number, genre, duration, analyzed, version, extra_info,
2066                        cue_path, audio_file_path, error
2067                    ) values (
2068                        1001, '/path/to/song1001', 'Artist1001', 'Title1001', 'An Album1001',
2069                        'An Album Artist1001', 3, 1, 'Electronica1001', 310, true,
2070                        {new_version}, '{{\"ignore\": true, \"metadata_bliss_does_not_have\":
2071                        \"/path/to/charlie1001\"}}', null, null, null
2072                    ),
2073                    (
2074                        2001, '/path/to/song2001', 'Artist2001', 'Title2001', 'An Album2001',
2075                        'An Album Artist2001', 2, 1, 'Electronica2001', 410, true,
2076                        {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2077                        \"/path/to/charlie2001\"}}', null, null, null
2078                    ),
2079                    (
2080                        2201, '/path/to/song2201', 'Artist2001', 'Title2001', 'An Album2001',
2081                        'An Album Artist2001', 1, 2, 'Electronica2001', 410, true,
2082                        {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2083                        \"/path/to/charlie2201\"}}', null, null, null
2084                    ),
2085                    (
2086                        3001, '/path/to/song3001', null, null, null,
2087                        null, null, null, null, null, false, {new_version}, '{{}}', null, null, null
2088                    ),
2089                    (
2090                        4001, '/path/to/song4001', 'Artist4001', 'Title4001', 'An Album4001',
2091                        'An Album Artist4001', 1, 1, 'Electronica4001', 510, true,
2092                        {old_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2093                        \"/path/to/charlie4001\"}}', null, null, null
2094                    ),
2095                    (
2096                        5001, '/path/to/song5001', 'Artist5001', 'Title5001', 'An Album1001',
2097                        'An Album Artist5001', 1, 1, 'Electronica5001', 610, true,
2098                        {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2099                        \"/path/to/charlie5001\"}}', null, null, null
2100                    ),
2101                    (
2102                        6001, '/path/to/song6001', 'Artist6001', 'Title6001', 'An Album2001',
2103                        'An Album Artist6001', 1, 1, 'Electronica6001', 710, true,
2104                        {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2105                        \"/path/to/charlie6001\"}}', null, null, null
2106                    ),
2107                    (
2108                        7001, '/path/to/song7001', 'Artist7001', 'Title7001', 'An Album7001',
2109                        'An Album Artist7001', 1, 1, 'Electronica7001', 810, true,
2110                        {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2111                        \"/path/to/charlie7001\"}}', null, null, null
2112                    ),
2113                    (
2114                        7002, '/path/to/cuetrack.cue/CUE_TRACK001', 'CUE Artist',
2115                        'CUE Title 01', 'CUE Album',
2116                        'CUE Album Artist', 1, 1, null, 810, true,
2117                        {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2118                        \"/path/to/charlie7001\"}}', '/path/to/cuetrack.cue',
2119                        '/path/to/cuetrack.flac', null
2120                    ),
2121                    (
2122                        7003, '/path/to/cuetrack.cue/CUE_TRACK002', 'CUE Artist',
2123                        'CUE Title 02', 'CUE Album',
2124                        'CUE Album Artist', 2, 1, null, 910, true,
2125                        {new_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2126                        \"/path/to/charlie7001\"}}', '/path/to/cuetrack.cue',
2127                        '/path/to/cuetrack.flac', null
2128                    ),
2129                    (
2130                        8001, '/path/to/song8001', 'Artist8001', 'Title8001', 'An Album1001',
2131                        'An Album Artist8001', 3, 1, 'Electronica8001', 910, true,
2132                        {old_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2133                        \"/path/to/charlie8001\"}}', null, null, null
2134                    ),
2135                    (
2136                        9001, './data/s16_stereo_22_5kHz.flac', 'Artist9001', 'Title9001',
2137                        'An Album9001', 'An Album Artist8001', 3, 1, 'Electronica8001',
2138                        1010, true, {old_version}, '{{\"ignore\": false, \"metadata_bliss_does_not_have\":
2139                        \"/path/to/charlie7001\"}}', null, null, null
2140                    ),
2141                    (
2142                        404, './data/not-existing.m4a', null, null,
2143                        null, null, null, null, null,
2144                        null, false, {old_version}, null, null, null, 'error finding the file'
2145                    ),
2146                    (
2147                        502, './data/invalid-file.m4a', null, null,
2148                        null, null, null, null, null,
2149                        null, false, {old_version}, null, null, null, 'error decoding the file'
2150                    );
2151                    ",
2152                        new_version = FeaturesVersion::LATEST as u16,
2153                        old_version = FeaturesVersion::Version1 as u16,
2154                    ),
2155                    [],
2156                )
2157                .unwrap();
2158            for index in 0..NUMBER_FEATURES {
2159                connection
2160                    .execute(
2161                        "
2162                            insert into feature(song_id, feature, feature_index)
2163                            values
2164                                (1001, ?2, ?1),
2165                                (2001, ?3, ?1),
2166                                (3001, ?4, ?1),
2167                                (5001, ?5, ?1),
2168                                (6001, ?6, ?1),
2169                                (7001, ?7, ?1),
2170                                (7002, ?8, ?1),
2171                                (7003, ?9, ?1),
2172                                (2201, ?10, ?1);
2173                            ",
2174                        params![
2175                            index,
2176                            index as f32 / 10.,
2177                            index as f32 + 10.,
2178                            index as f32 / 10. + 1.,
2179                            index as f32 / 2.,
2180                            index as f32 * 0.9,
2181                            index as f32 * 50.,
2182                            index as f32 * 100.,
2183                            index as f32 * 101.,
2184                            index as f32 + 10.,
2185                        ],
2186                    )
2187                    .unwrap();
2188            }
2189            // Imaginary version 0 of bliss with less features.
2190            for index in 0..NUMBER_FEATURES - 5 {
2191                connection
2192                    .execute(
2193                        "
2194                            insert into feature(song_id, feature, feature_index)
2195                            values
2196                                (8001, ?2, ?1),
2197                                (9001, ?3, ?1);
2198                            ",
2199                        params![index, index as f32 / 20., index + 1],
2200                    )
2201                    .unwrap();
2202            }
2203        }
2204        (
2205            library,
2206            config_dir,
2207            (
2208                first_song,
2209                second_song,
2210                second_song_dupe,
2211                third_song,
2212                fourth_song,
2213                fifth_song,
2214                sixth_song,
2215                seventh_song,
2216            ),
2217        )
2218    }
2219
2220    fn _library_song_from_database<T: DeserializeOwned + Serialize + Clone + Debug>(
2221        connection: MutexGuard<Connection>,
2222        song_path: &str,
2223    ) -> LibrarySong<T> {
2224        let mut song = connection
2225            .query_row(
2226                "
2227            select
2228                path, artist, title, album, album_artist,
2229                track_number, disc_number, genre, duration, version, extra_info,
2230                cue_path, audio_file_path
2231                from song where path=?
2232            ",
2233                params![song_path],
2234                |row| {
2235                    let path: String = row.get(0)?;
2236                    let cue_path: Option<String> = row.get(11)?;
2237                    let audio_file_path: Option<String> = row.get(12)?;
2238                    let mut cue_info = None;
2239                    if let Some(cue_path) = cue_path {
2240                        cue_info = Some(CueInfo {
2241                            cue_path: PathBuf::from(cue_path),
2242                            audio_file_path: PathBuf::from(audio_file_path.unwrap()),
2243                        })
2244                    };
2245                    let features_version: FeaturesVersion = row.get(9).unwrap();
2246                    let song = Song {
2247                        path: PathBuf::from(path),
2248                        artist: row.get(1).unwrap(),
2249                        title: row.get(2).unwrap(),
2250                        album: row.get(3).unwrap(),
2251                        album_artist: row.get(4).unwrap(),
2252                        track_number: row.get(5).unwrap(),
2253                        disc_number: row.get(6).unwrap(),
2254                        genre: row.get(7).unwrap(),
2255                        analysis: Analysis {
2256                            internal_analysis: vec![0.; features_version.feature_count()],
2257                            features_version: features_version,
2258                        },
2259                        duration: Duration::from_secs_f64(row.get(8).unwrap()),
2260                        features_version: features_version,
2261                        cue_info,
2262                    };
2263
2264                    let serialized: String = row.get(10).unwrap();
2265                    let extra_info = serde_json::from_str(&serialized).unwrap();
2266                    Ok(LibrarySong {
2267                        bliss_song: song,
2268                        extra_info,
2269                    })
2270                },
2271            )
2272            .expect("Song does not exist in the database");
2273        let mut stmt = connection
2274            .prepare(
2275                "
2276            select
2277                feature from feature join song on song.id = feature.song_id
2278                where song.path = ? order by feature_index
2279            ",
2280            )
2281            .unwrap();
2282        let analysis_vector = Analysis {
2283            internal_analysis: stmt
2284                .query_map(params![song_path], |row| row.get(0))
2285                .unwrap()
2286                .into_iter()
2287                .map(|x| x.unwrap())
2288                .collect::<Vec<f32>>()
2289                .try_into()
2290                .unwrap(),
2291            features_version: song.bliss_song.analysis.features_version,
2292        };
2293        song.bliss_song.analysis = analysis_vector;
2294        song
2295    }
2296
2297    fn _basic_song_from_database(connection: MutexGuard<Connection>, song_path: &str) -> Song {
2298        let mut expected_song = connection
2299            .query_row(
2300                "
2301            select
2302                path, artist, title, album, album_artist,
2303                track_number, disc_number, genre, duration, version
2304                from song where path=? and analyzed = true
2305            ",
2306                params![song_path],
2307                |row| {
2308                    let path: String = row.get(0)?;
2309                    Ok(Song {
2310                        path: PathBuf::from(path),
2311                        artist: row.get(1).unwrap(),
2312                        title: row.get(2).unwrap(),
2313                        album: row.get(3).unwrap(),
2314                        album_artist: row.get(4).unwrap(),
2315                        track_number: row.get(5).unwrap(),
2316                        disc_number: row.get(6).unwrap(),
2317                        genre: row.get(7).unwrap(),
2318                        analysis: Analysis {
2319                            internal_analysis: vec![0.; NUMBER_FEATURES],
2320                            features_version: FeaturesVersion::Version2,
2321                        },
2322                        duration: Duration::from_secs_f64(row.get(8).unwrap()),
2323                        features_version: row.get(9).unwrap(),
2324                        cue_info: None,
2325                    })
2326                },
2327            )
2328            .expect("Song is probably not in the db");
2329        let mut stmt = connection
2330            .prepare(
2331                "
2332            select
2333                feature from feature join song on song.id = feature.song_id
2334                where song.path = ? order by feature_index
2335            ",
2336            )
2337            .unwrap();
2338        let expected_analysis_vector = Analysis {
2339            internal_analysis: stmt
2340                .query_map(params![song_path], |row| row.get(0))
2341                .unwrap()
2342                .into_iter()
2343                .map(|x| x.unwrap())
2344                .collect::<Vec<f32>>()
2345                .try_into()
2346                .map_err(|v| {
2347                    BlissError::ProviderError(format!("Could not retrieve analysis for song {} that was supposed to be analyzed: {:?}.", song_path, v))
2348                })
2349                .unwrap(),
2350                features_version: FeaturesVersion::Version2,
2351        };
2352        expected_song.analysis = expected_analysis_vector;
2353        expected_song
2354    }
2355
2356    fn _generate_basic_song(path: Option<String>) -> Song {
2357        let path = path.unwrap_or_else(|| "/path/to/song".into());
2358        // Add some "randomness" to the features
2359        let analysis_vector = (0..NUMBER_FEATURES)
2360            .map(|x| x as f32 + 0.1)
2361            .collect::<Vec<f32>>();
2362        Song {
2363            path: path.into(),
2364            artist: Some("An Artist".into()),
2365            title: Some("Title".into()),
2366            album: Some("An Album".into()),
2367            album_artist: Some("An Album Artist".into()),
2368            track_number: Some(3),
2369            disc_number: Some(1),
2370            genre: Some("Electronica".into()),
2371            analysis: Analysis {
2372                internal_analysis: analysis_vector,
2373                features_version: FeaturesVersion::Version2,
2374            },
2375            duration: Duration::from_secs(80),
2376            features_version: FeaturesVersion::LATEST,
2377            cue_info: None,
2378        }
2379    }
2380
2381    fn _generate_library_song(path: Option<String>) -> LibrarySong<ExtraInfo> {
2382        let song = _generate_basic_song(path);
2383        let extra_info = ExtraInfo {
2384            ignore: true,
2385            metadata_bliss_does_not_have: "FoobarIze".into(),
2386        };
2387        LibrarySong {
2388            bliss_song: song,
2389            extra_info,
2390        }
2391    }
2392
2393    fn first_factor_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
2394        (a[1] - b[1]).abs()
2395    }
2396
2397    #[test]
2398    #[cfg(feature = "ffmpeg")]
2399    fn test_library_playlist_song_not_existing() {
2400        let (library, _temp_dir, _) = setup_test_library();
2401        assert!(library
2402            .playlist_from::<ExtraInfo>(&["not-existing"])
2403            .is_err());
2404    }
2405
2406    #[test]
2407    #[cfg(feature = "ffmpeg")]
2408    fn test_library_simple_playlist() {
2409        let (library, _temp_dir, _) = setup_test_library();
2410        let songs: Vec<LibrarySong<ExtraInfo>> = library
2411            .playlist_from(&["/path/to/song2001"])
2412            .unwrap()
2413            .collect();
2414        assert_eq!(
2415            vec![
2416                "/path/to/song2001",
2417                "/path/to/song6001",
2418                "/path/to/song5001",
2419                "/path/to/song1001",
2420                "/path/to/song7001",
2421                "/path/to/cuetrack.cue/CUE_TRACK001",
2422                "/path/to/cuetrack.cue/CUE_TRACK002",
2423            ],
2424            songs
2425                .into_iter()
2426                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2427                .collect::<Vec<String>>(),
2428        )
2429    }
2430
2431    #[test]
2432    #[cfg(feature = "ffmpeg")]
2433    fn test_library_playlist_dupe_order_preserved() {
2434        let (library, _temp_dir, _) = setup_test_library();
2435        let songs: Vec<LibrarySong<ExtraInfo>> = library
2436            .playlist_from_custom(
2437                &["/path/to/song2201"],
2438                &euclidean_distance,
2439                closest_to_songs,
2440                false,
2441            )
2442            .unwrap()
2443            .collect();
2444        assert_eq!(
2445            vec![
2446                "/path/to/song2201",
2447                "/path/to/song2001",
2448                "/path/to/song6001",
2449                "/path/to/song5001",
2450                "/path/to/song1001",
2451                "/path/to/song7001",
2452                "/path/to/cuetrack.cue/CUE_TRACK001",
2453                "/path/to/cuetrack.cue/CUE_TRACK002",
2454            ],
2455            songs
2456                .into_iter()
2457                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2458                .collect::<Vec<String>>(),
2459        )
2460    }
2461
2462    fn first_factor_divided_by_30_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
2463        ((a[1] - b[1]).abs() / 30.).floor()
2464    }
2465
2466    #[test]
2467    #[cfg(feature = "ffmpeg")]
2468    fn test_library_playlist_deduplication() {
2469        let (library, _temp_dir, _) = setup_test_library();
2470        let songs: Vec<LibrarySong<ExtraInfo>> = library
2471            .playlist_from_custom(
2472                &["/path/to/song2001"],
2473                &first_factor_divided_by_30_distance,
2474                closest_to_songs,
2475                true,
2476            )
2477            .unwrap()
2478            .collect();
2479        assert_eq!(
2480            vec![
2481                "/path/to/song2001",
2482                "/path/to/song7001",
2483                "/path/to/cuetrack.cue/CUE_TRACK001",
2484            ],
2485            songs
2486                .into_iter()
2487                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2488                .collect::<Vec<String>>(),
2489        );
2490
2491        let songs: Vec<LibrarySong<ExtraInfo>> = library
2492            .playlist_from_custom(
2493                &["/path/to/song2001"],
2494                &first_factor_distance,
2495                &closest_to_songs,
2496                true,
2497            )
2498            .unwrap()
2499            .collect();
2500        assert_eq!(
2501            vec![
2502                "/path/to/song2001",
2503                "/path/to/song6001",
2504                "/path/to/song5001",
2505                "/path/to/song1001",
2506                "/path/to/song7001",
2507                "/path/to/cuetrack.cue/CUE_TRACK001",
2508                "/path/to/cuetrack.cue/CUE_TRACK002",
2509            ],
2510            songs
2511                .into_iter()
2512                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2513                .collect::<Vec<String>>(),
2514        )
2515    }
2516
2517    #[test]
2518    #[cfg(feature = "ffmpeg")]
2519    fn test_library_playlist_take() {
2520        let (library, _temp_dir, _) = setup_test_library();
2521        let songs: Vec<LibrarySong<ExtraInfo>> = library
2522            .playlist_from(&["/path/to/song2001"])
2523            .unwrap()
2524            .take(4)
2525            .collect();
2526        assert_eq!(
2527            vec![
2528                "/path/to/song2001",
2529                "/path/to/song6001",
2530                "/path/to/song5001",
2531                "/path/to/song1001",
2532            ],
2533            songs
2534                .into_iter()
2535                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2536                .collect::<Vec<String>>(),
2537        )
2538    }
2539
2540    #[test]
2541    #[cfg(feature = "ffmpeg")]
2542    fn test_library_custom_playlist_distance() {
2543        let (library, _temp_dir, _) = setup_test_library();
2544        let songs: Vec<LibrarySong<ExtraInfo>> = library
2545            .playlist_from_custom(
2546                &["/path/to/song2001"],
2547                &first_factor_distance,
2548                closest_to_songs,
2549                false,
2550            )
2551            .unwrap()
2552            .collect();
2553        assert_eq!(
2554            vec![
2555                "/path/to/song2001",
2556                "/path/to/song2201",
2557                "/path/to/song6001",
2558                "/path/to/song5001",
2559                "/path/to/song1001",
2560                "/path/to/song7001",
2561                "/path/to/cuetrack.cue/CUE_TRACK001",
2562                "/path/to/cuetrack.cue/CUE_TRACK002",
2563            ],
2564            songs
2565                .into_iter()
2566                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2567                .collect::<Vec<String>>(),
2568        )
2569    }
2570
2571    fn custom_sort(
2572        _: &[LibrarySong<ExtraInfo>],
2573        songs: &[LibrarySong<ExtraInfo>],
2574        _distance: &dyn DistanceMetricBuilder,
2575    ) -> impl Iterator<Item = LibrarySong<ExtraInfo>> {
2576        let mut songs = songs.to_vec();
2577        songs.sort_by(|s1, s2| s1.bliss_song.path.cmp(&s2.bliss_song.path));
2578        songs.to_vec().into_iter()
2579    }
2580
2581    #[test]
2582    #[cfg(feature = "ffmpeg")]
2583    fn test_library_custom_playlist_sort() {
2584        let (library, _temp_dir, _) = setup_test_library();
2585        let songs: Vec<LibrarySong<ExtraInfo>> = library
2586            .playlist_from_custom(
2587                &["/path/to/song2001"],
2588                &euclidean_distance,
2589                custom_sort,
2590                false,
2591            )
2592            .unwrap()
2593            .collect();
2594        assert_eq!(
2595            vec![
2596                "/path/to/song2001",
2597                "/path/to/cuetrack.cue/CUE_TRACK001",
2598                "/path/to/cuetrack.cue/CUE_TRACK002",
2599                "/path/to/song1001",
2600                "/path/to/song2201",
2601                "/path/to/song5001",
2602                "/path/to/song6001",
2603                "/path/to/song7001",
2604            ],
2605            songs
2606                .into_iter()
2607                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2608                .collect::<Vec<String>>(),
2609        )
2610    }
2611
2612    #[test]
2613    #[cfg(feature = "ffmpeg")]
2614    fn test_library_album_playlist() {
2615        let (library, _temp_dir, _) = setup_test_library();
2616        let album: Vec<LibrarySong<ExtraInfo>> = library
2617            .album_playlist_from("An Album1001".to_string(), 20)
2618            .unwrap();
2619        assert_eq!(
2620            vec![
2621                // First album.
2622                "/path/to/song5001".to_string(),
2623                "/path/to/song1001".to_string(),
2624                // Second album, well ordered, disc 1
2625                "/path/to/song6001".to_string(),
2626                "/path/to/song2001".to_string(),
2627                // Second album disc 2
2628                "/path/to/song2201".to_string(),
2629                // Third album.
2630                "/path/to/song7001".to_string(),
2631                // Fourth album.
2632                "/path/to/cuetrack.cue/CUE_TRACK001".to_string(),
2633                "/path/to/cuetrack.cue/CUE_TRACK002".to_string(),
2634            ],
2635            album
2636                .into_iter()
2637                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2638                .collect::<Vec<_>>(),
2639        )
2640    }
2641
2642    #[test]
2643    #[cfg(feature = "ffmpeg")]
2644    fn test_library_album_playlist_crop() {
2645        let (library, _temp_dir, _) = setup_test_library();
2646        let album: Vec<LibrarySong<ExtraInfo>> = library
2647            .album_playlist_from("An Album1001".to_string(), 1)
2648            .unwrap();
2649        assert_eq!(
2650            vec![
2651                // First album.
2652                "/path/to/song5001".to_string(),
2653                "/path/to/song1001".to_string(),
2654                // Second album, well ordered.
2655                "/path/to/song6001".to_string(),
2656                "/path/to/song2001".to_string(),
2657                "/path/to/song2201".to_string(),
2658            ],
2659            album
2660                .into_iter()
2661                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2662                .collect::<Vec<_>>(),
2663        )
2664    }
2665
2666    #[test]
2667    #[cfg(feature = "ffmpeg")]
2668    fn test_library_songs_from_album() {
2669        let (library, _temp_dir, _) = setup_test_library();
2670        let album: Vec<LibrarySong<ExtraInfo>> = library.songs_from_album("An Album1001").unwrap();
2671        assert_eq!(
2672            vec![
2673                "/path/to/song5001".to_string(),
2674                "/path/to/song1001".to_string()
2675            ],
2676            album
2677                .into_iter()
2678                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2679                .collect::<Vec<_>>(),
2680        )
2681    }
2682
2683    #[test]
2684    #[cfg(feature = "ffmpeg")]
2685    fn test_library_songs_from_album_proper_features_version() {
2686        let (library, _temp_dir, _) = setup_test_library();
2687        let album: Vec<LibrarySong<ExtraInfo>> = library.songs_from_album("An Album1001").unwrap();
2688        assert_eq!(
2689            vec![
2690                "/path/to/song5001".to_string(),
2691                "/path/to/song1001".to_string()
2692            ],
2693            album
2694                .into_iter()
2695                .map(|s| s.bliss_song.path.to_string_lossy().to_string())
2696                .collect::<Vec<_>>(),
2697        )
2698    }
2699
2700    #[test]
2701    #[cfg(feature = "ffmpeg")]
2702    fn test_library_songs_from_album_not_existing() {
2703        let (library, _temp_dir, _) = setup_test_library();
2704        assert!(library
2705            .songs_from_album::<ExtraInfo>("not-existing")
2706            .is_err());
2707    }
2708
2709    #[test]
2710    #[cfg(feature = "ffmpeg")]
2711    fn test_library_delete_path_non_existing() {
2712        let (mut library, _temp_dir, _) = setup_test_library();
2713        {
2714            let connection = library.sqlite_conn.lock().unwrap();
2715            let count: u32 = connection
2716                    .query_row(
2717                        "select count(*) from feature join song on song.id = feature.song_id where song.path = ?",
2718                        ["not-existing"],
2719                        |row| row.get(0),
2720                    )
2721                    .unwrap();
2722            assert_eq!(count, 0);
2723            let count: u32 = connection
2724                .query_row(
2725                    "select count(*) from song where path = ?",
2726                    ["not-existing"],
2727                    |row| row.get(0),
2728                )
2729                .unwrap();
2730            assert_eq!(count, 0);
2731        }
2732        assert!(library.delete_path("not-existing").is_err());
2733    }
2734
2735    #[test]
2736    #[cfg(feature = "ffmpeg")]
2737    fn test_library_delete_path() {
2738        let (mut library, _temp_dir, _) = setup_test_library();
2739        {
2740            let connection = library.sqlite_conn.lock().unwrap();
2741            let count: u32 = connection
2742                    .query_row(
2743                        "select count(*) from feature join song on song.id = feature.song_id where song.path = ?",
2744                        ["/path/to/song1001"],
2745                        |row| row.get(0),
2746                    )
2747                    .unwrap();
2748            assert!(count >= 1);
2749            let count: u32 = connection
2750                .query_row(
2751                    "select count(*) from song where path = ?",
2752                    ["/path/to/song1001"],
2753                    |row| row.get(0),
2754                )
2755                .unwrap();
2756            assert!(count >= 1);
2757        }
2758
2759        library.delete_path("/path/to/song1001").unwrap();
2760
2761        {
2762            let connection = library.sqlite_conn.lock().unwrap();
2763            let count: u32 = connection
2764                .query_row(
2765                    "select count(*) from feature join song on song.id = feature.song_id where song.path = ?",
2766                    ["/path/to/song1001"],
2767                    |row| row.get(0),
2768                )
2769                .unwrap();
2770            assert_eq!(0, count);
2771            let count: u32 = connection
2772                .query_row(
2773                    "select count(*) from song where path = ?",
2774                    ["/path/to/song1001"],
2775                    |row| row.get(0),
2776                )
2777                .unwrap();
2778            assert_eq!(0, count);
2779        }
2780    }
2781
2782    #[test]
2783    #[cfg(feature = "ffmpeg")]
2784    fn test_library_delete_paths() {
2785        let (mut library, _temp_dir, _) = setup_test_library();
2786        {
2787            let connection = library.sqlite_conn.lock().unwrap();
2788            let count: u32 = connection
2789                    .query_row(
2790                        "select count(*) from feature join song on song.id = feature.song_id where song.path in (?1, ?2)",
2791                        ["/path/to/song1001", "/path/to/song2001"],
2792                        |row| row.get(0),
2793                    )
2794                    .unwrap();
2795            assert!(count >= 1);
2796            let count: u32 = connection
2797                .query_row(
2798                    "select count(*) from song where path in (?1, ?2)",
2799                    ["/path/to/song1001", "/path/to/song2001"],
2800                    |row| row.get(0),
2801                )
2802                .unwrap();
2803            assert!(count >= 1);
2804        }
2805
2806        library
2807            .delete_paths(vec!["/path/to/song1001", "/path/to/song2001"])
2808            .unwrap();
2809
2810        {
2811            let connection = library.sqlite_conn.lock().unwrap();
2812            let count: u32 = connection
2813                .query_row(
2814                    "select count(*) from feature join song on song.id = feature.song_id where song.path in (?1, ?2)",
2815                    ["/path/to/song1001", "/path/to/song2001"],
2816                    |row| row.get(0),
2817                )
2818                .unwrap();
2819            assert_eq!(0, count);
2820            let count: u32 = connection
2821                .query_row(
2822                    "select count(*) from song where path in (?1, ?2)",
2823                    ["/path/to/song1001", "/path/to/song2001"],
2824                    |row| row.get(0),
2825                )
2826                .unwrap();
2827            assert_eq!(0, count);
2828            // Make sure we did not delete everything else
2829            let count: u32 = connection
2830                .query_row("select count(*) from feature", [], |row| row.get(0))
2831                .unwrap();
2832            assert!(count >= 1);
2833            let count: u32 = connection
2834                .query_row("select count(*) from song", [], |row| row.get(0))
2835                .unwrap();
2836            assert!(count >= 1);
2837        }
2838    }
2839
2840    #[test]
2841    #[cfg(feature = "ffmpeg")]
2842    fn test_library_delete_paths_empty() {
2843        let (mut library, _temp_dir, _) = setup_test_library();
2844        assert_eq!(library.delete_paths::<String, _>([]).unwrap(), 0);
2845    }
2846
2847    #[test]
2848    #[cfg(feature = "ffmpeg")]
2849    fn test_library_delete_paths_non_existing() {
2850        let (mut library, _temp_dir, _) = setup_test_library();
2851        assert_eq!(library.delete_paths(["not-existing"]).unwrap(), 0);
2852    }
2853
2854    #[test]
2855    #[cfg(feature = "ffmpeg")]
2856    fn test_analyze_paths_cue() {
2857        let (mut library, _temp_dir, _) = setup_test_library();
2858        library
2859            .config
2860            .base_config_mut()
2861            .analysis_options
2862            .features_version = FeaturesVersion::Version1;
2863        {
2864            let sqlite_conn =
2865                Connection::open(&library.config.base_config().database_path).unwrap();
2866            sqlite_conn.execute("delete from song", []).unwrap();
2867        }
2868
2869        let paths = vec![
2870            "./data/s16_mono_22_5kHz.flac",
2871            "./data/testcue.cue",
2872            "non-existing",
2873        ];
2874        library
2875            .analyze_paths_with_options(
2876                paths.to_owned(),
2877                false,
2878                AnalysisOptions {
2879                    features_version: FeaturesVersion::Version2,
2880                    ..Default::default()
2881                },
2882            )
2883            .unwrap();
2884        let expected_analyzed_paths = vec![
2885            "./data/s16_mono_22_5kHz.flac",
2886            "./data/testcue.cue/CUE_TRACK001",
2887            "./data/testcue.cue/CUE_TRACK002",
2888            "./data/testcue.cue/CUE_TRACK003",
2889        ];
2890        {
2891            let connection = library.sqlite_conn.lock().unwrap();
2892            let mut stmt = connection
2893                .prepare(
2894                    "
2895                select
2896                    path from song where analyzed = true and path not like '%song%'
2897                    order by path
2898                ",
2899                )
2900                .unwrap();
2901            let paths = stmt
2902                .query_map(params![], |row| row.get(0))
2903                .unwrap()
2904                .map(|x| x.unwrap())
2905                .collect::<Vec<String>>();
2906
2907            assert_eq!(paths, expected_analyzed_paths);
2908        }
2909        {
2910            let connection = library.sqlite_conn.lock().unwrap();
2911            let song: LibrarySong<()> =
2912                _library_song_from_database(connection, "./data/testcue.cue/CUE_TRACK001");
2913            assert!(song.bliss_song.cue_info.is_some());
2914        }
2915    }
2916
2917    #[test]
2918    #[cfg(feature = "ffmpeg")]
2919    fn test_analyze_paths() {
2920        let (mut library, _temp_dir, _) = setup_test_library();
2921        library
2922            .config
2923            .base_config_mut()
2924            .analysis_options
2925            .features_version = FeaturesVersion::LATEST;
2926
2927        let paths = vec![
2928            "./data/s16_mono_22_5kHz.flac",
2929            "./data/s16_stereo_22_5kHz.flac",
2930            "non-existing",
2931        ];
2932        library.analyze_paths(paths.to_owned(), false).unwrap();
2933        let songs = paths[..2]
2934            .iter()
2935            .map(|path| {
2936                let connection = library.sqlite_conn.lock().unwrap();
2937                _library_song_from_database(connection, path)
2938            })
2939            .collect::<Vec<LibrarySong<()>>>();
2940        let expected_songs = paths[..2]
2941            .iter()
2942            .zip(vec![(), ()].into_iter())
2943            .map(|(path, expected_extra_info)| LibrarySong {
2944                bliss_song: Decoder::song_from_path(path).unwrap(),
2945                extra_info: expected_extra_info,
2946            })
2947            .collect::<Vec<LibrarySong<()>>>();
2948        assert_eq!(songs, expected_songs);
2949        assert_eq!(
2950            library
2951                .config
2952                .base_config_mut()
2953                .analysis_options
2954                .features_version,
2955            FeaturesVersion::LATEST
2956        );
2957    }
2958
2959    #[test]
2960    #[cfg(feature = "ffmpeg")]
2961    fn test_analyze_paths_convert_extra_info() {
2962        let (mut library, _temp_dir, _) = setup_test_library();
2963        library
2964            .config
2965            .base_config_mut()
2966            .analysis_options
2967            .features_version = FeaturesVersion::Version1;
2968        let paths = vec![
2969            ("./data/s16_mono_22_5kHz.flac", true),
2970            ("./data/s16_stereo_22_5kHz.flac", false),
2971            ("non-existing", false),
2972        ];
2973        library
2974            .analyze_paths_convert_extra_info(
2975                paths.to_owned(),
2976                true,
2977                |b, _, _| ExtraInfo {
2978                    ignore: b,
2979                    metadata_bliss_does_not_have: String::from("coucou"),
2980                },
2981                AnalysisOptions::default(),
2982            )
2983            .unwrap();
2984        library
2985            .analyze_paths_convert_extra_info(
2986                paths.to_owned(),
2987                false,
2988                |b, _, _| ExtraInfo {
2989                    ignore: b,
2990                    metadata_bliss_does_not_have: String::from("coucou"),
2991                },
2992                AnalysisOptions::default(),
2993            )
2994            .unwrap();
2995        let songs = paths[..2]
2996            .iter()
2997            .map(|(path, _)| {
2998                let connection = library.sqlite_conn.lock().unwrap();
2999                _library_song_from_database(connection, path)
3000            })
3001            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3002        let expected_songs = paths[..2]
3003            .iter()
3004            .zip(
3005                vec![
3006                    ExtraInfo {
3007                        ignore: true,
3008                        metadata_bliss_does_not_have: String::from("coucou"),
3009                    },
3010                    ExtraInfo {
3011                        ignore: false,
3012                        metadata_bliss_does_not_have: String::from("coucou"),
3013                    },
3014                ]
3015                .into_iter(),
3016            )
3017            .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3018                bliss_song: Decoder::song_from_path(path).unwrap(),
3019                extra_info: expected_extra_info,
3020            })
3021            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3022        assert_eq!(songs, expected_songs);
3023        assert_eq!(
3024            library
3025                .config
3026                .base_config_mut()
3027                .analysis_options
3028                .features_version,
3029            FeaturesVersion::LATEST
3030        );
3031    }
3032
3033    #[test]
3034    #[cfg(feature = "ffmpeg")]
3035    fn test_analyze_paths_extra_info() {
3036        let (mut library, _temp_dir, _) = setup_test_library();
3037
3038        let paths = vec![
3039            (
3040                "./data/s16_mono_22_5kHz.flac",
3041                ExtraInfo {
3042                    ignore: true,
3043                    metadata_bliss_does_not_have: String::from("hey"),
3044                },
3045            ),
3046            (
3047                "./data/s16_stereo_22_5kHz.flac",
3048                ExtraInfo {
3049                    ignore: false,
3050                    metadata_bliss_does_not_have: String::from("hello"),
3051                },
3052            ),
3053            (
3054                "non-existing",
3055                ExtraInfo {
3056                    ignore: true,
3057                    metadata_bliss_does_not_have: String::from("coucou"),
3058                },
3059            ),
3060        ];
3061        library
3062            .analyze_paths_extra_info(paths.to_owned(), false, AnalysisOptions::default())
3063            .unwrap();
3064        let songs = paths[..2]
3065            .iter()
3066            .map(|(path, _)| {
3067                let connection = library.sqlite_conn.lock().unwrap();
3068                _library_song_from_database(connection, path)
3069            })
3070            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3071        let expected_songs = paths[..2]
3072            .iter()
3073            .zip(
3074                vec![
3075                    ExtraInfo {
3076                        ignore: true,
3077                        metadata_bliss_does_not_have: String::from("hey"),
3078                    },
3079                    ExtraInfo {
3080                        ignore: false,
3081                        metadata_bliss_does_not_have: String::from("hello"),
3082                    },
3083                ]
3084                .into_iter(),
3085            )
3086            .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3087                bliss_song: Decoder::song_from_path(path).unwrap(),
3088                extra_info: expected_extra_info,
3089            })
3090            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3091        assert_eq!(songs, expected_songs);
3092    }
3093
3094    #[test]
3095    #[cfg(feature = "ffmpeg")]
3096    // Check that a song already in the database is not
3097    // analyzed again on updates.
3098    fn test_update_skip_analyzed() {
3099        let (mut library, _temp_dir, _) = setup_test_library();
3100        library
3101            .config
3102            .base_config_mut()
3103            .analysis_options
3104            .features_version = FeaturesVersion::Version1;
3105        for input in vec![
3106            ("./data/s16_mono_22_5kHz.flac", true),
3107            ("./data/s16_mono_22_5kHz.flac", false),
3108        ]
3109        .into_iter()
3110        {
3111            let paths = vec![input.to_owned()];
3112            library
3113                .update_library_convert_extra_info(
3114                    paths.to_owned(),
3115                    true,
3116                    false,
3117                    |b, _, _| ExtraInfo {
3118                        ignore: b,
3119                        metadata_bliss_does_not_have: String::from("coucou"),
3120                    },
3121                    AnalysisOptions {
3122                        features_version: FeaturesVersion::Version1,
3123                        ..Default::default()
3124                    },
3125                )
3126                .unwrap();
3127            let song = {
3128                let connection = library.sqlite_conn.lock().unwrap();
3129                _library_song_from_database::<ExtraInfo>(connection, "./data/s16_mono_22_5kHz.flac")
3130            };
3131            let expected_song = {
3132                LibrarySong {
3133                    bliss_song: Decoder::song_from_path_with_options(
3134                        "./data/s16_mono_22_5kHz.flac",
3135                        AnalysisOptions {
3136                            features_version: FeaturesVersion::Version1,
3137                            ..Default::default()
3138                        },
3139                    )
3140                    .unwrap(),
3141                    extra_info: ExtraInfo {
3142                        ignore: true,
3143                        metadata_bliss_does_not_have: String::from("coucou"),
3144                    },
3145                }
3146            };
3147            assert_eq!(song, expected_song);
3148            assert_eq!(
3149                library
3150                    .config
3151                    .base_config_mut()
3152                    .analysis_options
3153                    .features_version,
3154                FeaturesVersion::Version1
3155            );
3156        }
3157    }
3158
3159    fn _get_song_analyzed(
3160        connection: MutexGuard<Connection>,
3161        path: String,
3162    ) -> Result<bool, RusqliteError> {
3163        let mut stmt = connection.prepare(
3164            "
3165                select
3166                    analyzed from song
3167                    where song.path = ?
3168                ",
3169        )?;
3170        stmt.query_row([path], |row| (row.get(0)))
3171    }
3172
3173    #[test]
3174    #[cfg(feature = "ffmpeg")]
3175    // TODO test that LibrarySong<ExtraInfo> and LibrarySong<()> can't cohabitate in the same
3176    // library.
3177    //
3178    // Tests that a song with features version = 1 gets updated to the latest features version.
3179    fn test_update_library_override_old_features() {
3180        let (mut library, _temp_dir, _) = setup_test_library();
3181        let path: String = "./data/s16_stereo_22_5kHz.flac".into();
3182
3183        // Check that s16_stereo_22_5kHz.flac is analyzed with the old features version.
3184        {
3185            let connection = library.sqlite_conn.lock().unwrap();
3186            let song: LibrarySong<ExtraInfo> = _library_song_from_database(connection, &path);
3187            assert_eq!(
3188                song.bliss_song.analysis,
3189                Analysis {
3190                    internal_analysis: vec![
3191                        1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17.,
3192                        18.
3193                    ],
3194                    features_version: FeaturesVersion::Version1,
3195                }
3196            )
3197        }
3198        // Check that there are indeed songs with older features version.
3199        {
3200            let connection = library.sqlite_conn.lock().unwrap();
3201            let count_old_features_version: u32 = connection
3202                .query_row(
3203                    "select count(*) from song where version = ? and analyzed = true",
3204                    params![FeaturesVersion::Version1],
3205                    |row| row.get(0),
3206                )
3207                .unwrap();
3208            assert!(count_old_features_version > 0);
3209        }
3210
3211        library
3212            .update_library(vec![path.to_owned()], true, false)
3213            .unwrap();
3214
3215        // Check that songs with older features version are gone.
3216        {
3217            let connection = library.sqlite_conn.lock().unwrap();
3218            let count_old_features_version: u32 = connection
3219                .query_row(
3220                    "select count(*) from song where version = ? and analyzed = true",
3221                    params![FeaturesVersion::Version1],
3222                    |row| row.get(0),
3223                )
3224                .unwrap();
3225            assert_eq!(count_old_features_version, 0);
3226        }
3227
3228        let connection = library.sqlite_conn.lock().unwrap();
3229        let song: LibrarySong<()> = _library_song_from_database(connection, &path);
3230        // This should give us latest features version, but double-checking.
3231        let expected_analysis_vector = Decoder::song_from_path(path).unwrap().analysis;
3232        assert_eq!(song.bliss_song.analysis, expected_analysis_vector);
3233        assert_eq!(
3234            song.bliss_song.analysis.features_version,
3235            FeaturesVersion::LATEST
3236        );
3237    }
3238
3239    #[test]
3240    #[cfg(feature = "ffmpeg")]
3241    // TODO test when updating the features version also
3242    fn test_update_library() {
3243        let (mut library, _temp_dir, _) = setup_test_library();
3244        library
3245            .config
3246            .base_config_mut()
3247            .analysis_options
3248            .features_version = FeaturesVersion::LATEST;
3249
3250        {
3251            let connection = library.sqlite_conn.lock().unwrap();
3252            // Make sure that we tried to "update" song4001 with the new features.
3253            assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3254        }
3255
3256        let paths = vec![
3257            "./data/s16_mono_22_5kHz.flac",
3258            "./data/s16_stereo_22_5kHz.flac",
3259            "/path/to/song4001",
3260            "non-existing",
3261        ];
3262        library
3263            .update_library(paths.to_owned(), true, false)
3264            .unwrap();
3265        library
3266            .update_library(paths.to_owned(), true, true)
3267            .unwrap();
3268
3269        let songs = paths[..2]
3270            .iter()
3271            .map(|path| {
3272                let connection = library.sqlite_conn.lock().unwrap();
3273                _library_song_from_database(connection, path)
3274            })
3275            .collect::<Vec<LibrarySong<()>>>();
3276        let expected_songs = paths[..2]
3277            .iter()
3278            .zip(vec![(), ()].into_iter())
3279            .map(|(path, expected_extra_info)| LibrarySong {
3280                bliss_song: Decoder::song_from_path(path).unwrap(),
3281                extra_info: expected_extra_info,
3282            })
3283            .collect::<Vec<LibrarySong<()>>>();
3284
3285        assert_eq!(songs, expected_songs);
3286        {
3287            let connection = library.sqlite_conn.lock().unwrap();
3288            // Make sure that we tried to "update" song4001 with the new features.
3289            assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3290        }
3291        assert_eq!(
3292            library
3293                .config
3294                .base_config_mut()
3295                .analysis_options
3296                .features_version,
3297            FeaturesVersion::LATEST
3298        );
3299    }
3300
3301    #[test]
3302    #[cfg(feature = "ffmpeg")]
3303    // TODO test when updating the features version also
3304    fn test_update_library_with_options() {
3305        let (mut library, _temp_dir, _) = setup_test_library();
3306        library
3307            .config
3308            .base_config_mut()
3309            .analysis_options
3310            .features_version = FeaturesVersion::LATEST;
3311
3312        {
3313            let connection = library.sqlite_conn.lock().unwrap();
3314            // Make sure that we tried to "update" song4001 with the new features.
3315            assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3316        }
3317        {
3318            let connection = library.sqlite_conn.lock().unwrap();
3319            // Make sure that we tried to "update" song4001 with the new features.
3320            connection
3321                .execute("update song set extra_info = \"null\";", [])
3322                .unwrap();
3323        }
3324
3325        let paths = vec![
3326            "./data/s16_mono_22_5kHz.flac",
3327            "./data/s16_stereo_22_5kHz.flac",
3328            "/path/to/song4001",
3329            "non-existing",
3330        ];
3331        library
3332            .update_library_with_options(
3333                paths.to_owned(),
3334                true,
3335                false,
3336                AnalysisOptions {
3337                    features_version: FeaturesVersion::Version1,
3338                    ..Default::default()
3339                },
3340            )
3341            .unwrap();
3342        library
3343            .update_library_with_options(
3344                paths.to_owned(),
3345                true,
3346                false,
3347                AnalysisOptions {
3348                    features_version: FeaturesVersion::Version1,
3349                    ..Default::default()
3350                },
3351            )
3352            .unwrap();
3353
3354        let first_song = {
3355            let connection = library.sqlite_conn.lock().unwrap();
3356            _library_song_from_database(connection, paths[0])
3357        };
3358        let expected_song = LibrarySong {
3359            bliss_song: Decoder::song_from_path_with_options(
3360                paths[0],
3361                AnalysisOptions {
3362                    features_version: FeaturesVersion::Version1,
3363                    ..Default::default()
3364                },
3365            )
3366            .unwrap(),
3367            extra_info: (),
3368        };
3369
3370        assert_eq!(first_song, expected_song);
3371        {
3372            let connection = library.sqlite_conn.lock().unwrap();
3373            // Make sure that we did not try to "update" song4001 with the new features, since
3374            // it was analyzed with version 1.
3375            assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3376        }
3377        assert_eq!(
3378            library
3379                .config
3380                .base_config_mut()
3381                .analysis_options
3382                .features_version,
3383            FeaturesVersion::Version1
3384        );
3385    }
3386
3387    #[test]
3388    #[cfg(feature = "ffmpeg")]
3389    fn test_update_extra_info() {
3390        let (mut library, _temp_dir, _) = setup_test_library();
3391        library
3392            .config
3393            .base_config_mut()
3394            .analysis_options
3395            .features_version = FeaturesVersion::LATEST;
3396
3397        {
3398            let connection = library.sqlite_conn.lock().unwrap();
3399            // Make sure that we tried to "update" song4001 with the new features.
3400            assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3401        }
3402
3403        let paths = vec![
3404            ("./data/s16_mono_22_5kHz.flac", true),
3405            ("./data/s16_stereo_22_5kHz.flac", false),
3406            ("/path/to/song4001", false),
3407            ("non-existing", false),
3408        ];
3409        library
3410            .update_library_extra_info(paths.to_owned(), true, false)
3411            .unwrap();
3412        let songs = paths[..2]
3413            .iter()
3414            .map(|(path, _)| {
3415                let connection = library.sqlite_conn.lock().unwrap();
3416                _library_song_from_database(connection, path)
3417            })
3418            .collect::<Vec<LibrarySong<bool>>>();
3419        let expected_songs = paths[..2]
3420            .iter()
3421            .zip(vec![true, false].into_iter())
3422            .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3423                bliss_song: Decoder::song_from_path(path).unwrap(),
3424                extra_info: expected_extra_info,
3425            })
3426            .collect::<Vec<LibrarySong<bool>>>();
3427        assert_eq!(songs, expected_songs);
3428        {
3429            let connection = library.sqlite_conn.lock().unwrap();
3430            // Make sure that we tried to "update" song4001 with the new features.
3431            assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3432        }
3433        assert_eq!(
3434            library
3435                .config
3436                .base_config_mut()
3437                .analysis_options
3438                .features_version,
3439            FeaturesVersion::LATEST
3440        );
3441    }
3442
3443    #[test]
3444    #[cfg(feature = "ffmpeg")]
3445    fn test_update_convert_extra_info() {
3446        let (mut library, _temp_dir, _) = setup_test_library();
3447        library
3448            .config
3449            .base_config_mut()
3450            .analysis_options
3451            .features_version = FeaturesVersion::Version1;
3452
3453        {
3454            let connection = library.sqlite_conn.lock().unwrap();
3455            // Make sure that we tried to "update" song4001 with the new features.
3456            assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3457        }
3458        {
3459            let connection = library.sqlite_conn.lock().unwrap();
3460            // Make sure that all the starting songs are there
3461            assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap());
3462        }
3463
3464        let paths = vec![
3465            ("./data/s16_mono_22_5kHz.flac", true),
3466            ("./data/s16_stereo_22_5kHz.flac", false),
3467            ("/path/to/song4001", false),
3468            ("non-existing", false),
3469        ];
3470        library
3471            .update_library_convert_extra_info(
3472                paths.to_owned(),
3473                true,
3474                false,
3475                |b, _, _| ExtraInfo {
3476                    ignore: b,
3477                    metadata_bliss_does_not_have: String::from("coucou"),
3478                },
3479                AnalysisOptions::default(),
3480            )
3481            .unwrap();
3482        let songs = paths[..2]
3483            .iter()
3484            .map(|(path, _)| {
3485                let connection = library.sqlite_conn.lock().unwrap();
3486                _library_song_from_database(connection, path)
3487            })
3488            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3489        let expected_songs = paths[..2]
3490            .iter()
3491            .zip(
3492                vec![
3493                    ExtraInfo {
3494                        ignore: true,
3495                        metadata_bliss_does_not_have: String::from("coucou"),
3496                    },
3497                    ExtraInfo {
3498                        ignore: false,
3499                        metadata_bliss_does_not_have: String::from("coucou"),
3500                    },
3501                ]
3502                .into_iter(),
3503            )
3504            .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3505                bliss_song: Decoder::song_from_path(path).unwrap(),
3506                extra_info: expected_extra_info,
3507            })
3508            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3509        assert_eq!(songs, expected_songs);
3510        {
3511            let connection = library.sqlite_conn.lock().unwrap();
3512            // Make sure that we tried to "update" song4001 with the new features.
3513            assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3514        }
3515        {
3516            let connection = library.sqlite_conn.lock().unwrap();
3517            // Make sure that we deleted older songs
3518            assert_eq!(
3519                rusqlite::Error::QueryReturnedNoRows,
3520                _get_song_analyzed(connection, "/path/to/song2001".into()).unwrap_err(),
3521            );
3522        }
3523        assert_eq!(
3524            library
3525                .config
3526                .base_config_mut()
3527                .analysis_options
3528                .features_version,
3529            FeaturesVersion::LATEST
3530        );
3531    }
3532
3533    #[test]
3534    #[cfg(feature = "ffmpeg")]
3535    // TODO maybe we can merge / DRY this and the function ⬆
3536    fn test_update_convert_extra_info_do_not_delete() {
3537        let (mut library, _temp_dir, _) = setup_test_library();
3538        library
3539            .config
3540            .base_config_mut()
3541            .analysis_options
3542            .features_version = FeaturesVersion::Version1;
3543
3544        {
3545            let connection = library.sqlite_conn.lock().unwrap();
3546            // Make sure that we tried to "update" song4001 with the new features.
3547            assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3548        }
3549        {
3550            let connection = library.sqlite_conn.lock().unwrap();
3551            // Make sure that all the starting songs are there
3552            assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap());
3553        }
3554
3555        let paths = vec![
3556            ("./data/s16_mono_22_5kHz.flac", true),
3557            ("./data/s16_stereo_22_5kHz.flac", false),
3558            ("/path/to/song4001", false),
3559            ("non-existing", false),
3560        ];
3561        library
3562            .update_library_convert_extra_info(
3563                paths.to_owned(),
3564                false,
3565                false,
3566                |b, _, _| ExtraInfo {
3567                    ignore: b,
3568                    metadata_bliss_does_not_have: String::from("coucou"),
3569                },
3570                AnalysisOptions::default(),
3571            )
3572            .unwrap();
3573        let songs = paths[..2]
3574            .iter()
3575            .map(|(path, _)| {
3576                let connection = library.sqlite_conn.lock().unwrap();
3577                _library_song_from_database(connection, path)
3578            })
3579            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3580        let expected_songs = paths[..2]
3581            .iter()
3582            .zip(
3583                vec![
3584                    ExtraInfo {
3585                        ignore: true,
3586                        metadata_bliss_does_not_have: String::from("coucou"),
3587                    },
3588                    ExtraInfo {
3589                        ignore: false,
3590                        metadata_bliss_does_not_have: String::from("coucou"),
3591                    },
3592                ]
3593                .into_iter(),
3594            )
3595            .map(|((path, _extra_info), expected_extra_info)| LibrarySong {
3596                bliss_song: Decoder::song_from_path(path).unwrap(),
3597                extra_info: expected_extra_info,
3598            })
3599            .collect::<Vec<LibrarySong<ExtraInfo>>>();
3600        assert_eq!(songs, expected_songs);
3601        {
3602            let connection = library.sqlite_conn.lock().unwrap();
3603            // Make sure that we tried to "update" song4001 with the new features.
3604            assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap());
3605        }
3606        {
3607            let connection = library.sqlite_conn.lock().unwrap();
3608            // Make sure that we did not delete older songs
3609            assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap());
3610        }
3611        assert_eq!(
3612            library
3613                .config
3614                .base_config_mut()
3615                .analysis_options
3616                .features_version,
3617            FeaturesVersion::LATEST
3618        );
3619    }
3620
3621    #[test]
3622    #[cfg(feature = "ffmpeg")]
3623    fn test_song_from_path() {
3624        let (library, _temp_dir, _) = setup_test_library();
3625        let analysis_vector = (0..NUMBER_FEATURES)
3626            .map(|x| x as f32 + 10.)
3627            .collect::<Vec<f32>>();
3628
3629        let song = Song {
3630            path: "/path/to/song2001".into(),
3631            artist: Some("Artist2001".into()),
3632            title: Some("Title2001".into()),
3633            album: Some("An Album2001".into()),
3634            album_artist: Some("An Album Artist2001".into()),
3635            track_number: Some(2),
3636            disc_number: Some(1),
3637            genre: Some("Electronica2001".into()),
3638            analysis: Analysis {
3639                internal_analysis: analysis_vector,
3640                features_version: FeaturesVersion::Version2,
3641            },
3642            duration: Duration::from_secs(410),
3643            features_version: FeaturesVersion::Version2,
3644            cue_info: None,
3645        };
3646        let expected_song = LibrarySong {
3647            bliss_song: song,
3648            extra_info: ExtraInfo {
3649                ignore: false,
3650                metadata_bliss_does_not_have: String::from("/path/to/charlie2001"),
3651            },
3652        };
3653
3654        let song = library
3655            .song_from_path::<ExtraInfo>("/path/to/song2001")
3656            .unwrap();
3657        assert_eq!(song, expected_song)
3658    }
3659
3660    #[test]
3661    #[cfg(feature = "ffmpeg")]
3662    fn test_store_failed_song() {
3663        let (mut library, _temp_dir, _) = setup_test_library();
3664        library
3665            .store_failed_song(
3666                "/some/failed/path",
3667                BlissError::ProviderError("error with the analysis".into()),
3668                FeaturesVersion::Version1,
3669            )
3670            .unwrap();
3671        let connection = library.sqlite_conn.lock().unwrap();
3672        let (error, analyzed, features_version): (String, bool, FeaturesVersion) = connection
3673            .query_row(
3674                "
3675            select
3676                error, analyzed, version
3677                from song where path=?
3678            ",
3679                params!["/some/failed/path"],
3680                |row| Ok((row.get_unwrap(0), row.get_unwrap(1), row.get_unwrap(2))),
3681            )
3682            .unwrap();
3683        assert_eq!(
3684            error,
3685            String::from(
3686                "error happened with the music library provider - error with the analysis"
3687            )
3688        );
3689        assert_eq!(analyzed, false);
3690        assert_eq!(features_version, FeaturesVersion::Version1);
3691        let count_features: u32 = connection
3692            .query_row(
3693                "
3694            select
3695                count(*) from feature join song
3696                on song.id = feature.song_id where path=?
3697            ",
3698                params!["/some/failed/path"],
3699                |row| Ok(row.get_unwrap(0)),
3700            )
3701            .unwrap();
3702        assert_eq!(count_features, 0);
3703    }
3704
3705    #[test]
3706    #[cfg(feature = "ffmpeg")]
3707    fn test_songs_from_library() {
3708        let (library, _temp_dir, expected_library_songs) = setup_test_library();
3709
3710        let library_songs = library.songs_from_library::<ExtraInfo>().unwrap();
3711        assert_eq!(library_songs.len(), 8);
3712        assert_eq!(
3713            expected_library_songs,
3714            (
3715                library_songs[0].to_owned(),
3716                library_songs[1].to_owned(),
3717                library_songs[2].to_owned(),
3718                library_songs[3].to_owned(),
3719                library_songs[4].to_owned(),
3720                library_songs[5].to_owned(),
3721                library_songs[6].to_owned(),
3722                library_songs[7].to_owned(),
3723            )
3724        );
3725    }
3726
3727    #[test]
3728    #[cfg(feature = "ffmpeg")]
3729    fn test_songs_from_library_screwed_db() {
3730        let (library, _temp_dir, _) = setup_test_library();
3731        {
3732            let connection = library.sqlite_conn.lock().unwrap();
3733            connection
3734                .execute(
3735                    "insert into feature (song_id, feature, feature_index)
3736                values (2001, 1.5, 29)
3737                ",
3738                    [],
3739                )
3740                .unwrap();
3741        }
3742
3743        let error = library.songs_from_library::<ExtraInfo>().unwrap_err();
3744        assert_eq!(
3745            error.to_string(),
3746            String::from(
3747                "error happened with the music library provider - \
3748                Song with ID 2001 and path /path/to/song2001 has a \
3749                different feature number than expected. Please rescan or \
3750                update the song library.",
3751            ),
3752        );
3753    }
3754
3755    #[test]
3756    #[cfg(feature = "ffmpeg")]
3757    fn test_song_from_path_not_analyzed() {
3758        let (library, _temp_dir, _) = setup_test_library();
3759        let error = library.song_from_path::<ExtraInfo>("/path/to/song404");
3760        assert!(error.is_err());
3761    }
3762
3763    #[test]
3764    #[cfg(feature = "ffmpeg")]
3765    fn test_song_from_path_not_found() {
3766        let (library, _temp_dir, _) = setup_test_library();
3767        let error = library.song_from_path::<ExtraInfo>("/path/to/randomsong");
3768        assert!(error.is_err());
3769    }
3770
3771    #[test]
3772    fn test_get_default_data_folder_no_default_path() {
3773        // Cases to test:
3774        // - Legacy path exists, new path doesn't exist => legacy path should be returned
3775        // - Legacy path exists, new path exists => new path should be returned
3776        // - Legacy path doesn't exist => new path should be returned
3777
3778        // Nothing exists: XDG_CONFIG_HOME takes precedence
3779        env::set_var("XDG_CONFIG_HOME", "/home/foo/.config");
3780        env::set_var("XDG_DATA_HOME", "/home/foo/.local/share");
3781        assert_eq!(
3782            PathBuf::from("/home/foo/.config/bliss-rs"),
3783            BaseConfig::get_default_data_folder().unwrap()
3784        );
3785        env::remove_var("XDG_CONFIG_HOME");
3786        env::remove_var("XDG_DATA_HOME");
3787
3788        // Legacy folder exists, new folder does not exist, it takes precedence
3789        let existing_legacy_folder = TempDir::new("tmp").unwrap();
3790        create_dir_all(existing_legacy_folder.path().join("bliss-rs")).unwrap();
3791        env::set_var("XDG_CONFIG_HOME", "/home/foo/.config");
3792        env::set_var("XDG_DATA_HOME", existing_legacy_folder.path().as_os_str());
3793        assert_eq!(
3794            existing_legacy_folder.path().join("bliss-rs"),
3795            BaseConfig::get_default_data_folder().unwrap()
3796        );
3797
3798        // Both exists, new folder takes precedence
3799        let existing_folder = TempDir::new("tmp").unwrap();
3800        create_dir_all(existing_folder.path().join("bliss-rs")).unwrap();
3801        env::set_var("XDG_CONFIG_HOME", existing_folder.path().as_os_str());
3802        assert_eq!(
3803            existing_folder.path().join("bliss-rs"),
3804            BaseConfig::get_default_data_folder().unwrap()
3805        );
3806
3807        env::remove_var("XDG_DATA_HOME");
3808        env::remove_var("XDG_CONFIG_HOME");
3809
3810        assert_eq!(
3811            PathBuf::from("/tmp/bliss-rs/"),
3812            BaseConfig::get_default_data_folder().unwrap()
3813        );
3814    }
3815
3816    #[test]
3817    #[cfg(feature = "ffmpeg")]
3818    fn test_library_new_default_write() {
3819        let (library, _temp_dir, _) = setup_test_library();
3820        let config_content = fs::read_to_string(&library.config.base_config().config_path)
3821            .unwrap()
3822            .replace(' ', "")
3823            .replace('\n', "");
3824        assert_eq!(
3825            config_content,
3826            format!(
3827                "{{\"config_path\":\"{}\",\"database_path\":\"{}\",\"\
3828                features_version\":{},\"number_cores\":{},\
3829                \"m\":{{\"v\":1,\"dim\":[{},{}],\"data\":{}}}}}",
3830                library.config.base_config().config_path.display(),
3831                library.config.base_config().database_path.display(),
3832                FeaturesVersion::LATEST as u16,
3833                thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()),
3834                NUMBER_FEATURES,
3835                NUMBER_FEATURES,
3836                // Terrible code, but would hardcoding be better?
3837                format!(
3838                    "{:?}",
3839                    Array2::<f32>::eye(NUMBER_FEATURES).as_slice().unwrap()
3840                )
3841                .replace(" ", ""),
3842            )
3843        );
3844    }
3845
3846    #[test]
3847    #[cfg(feature = "ffmpeg")]
3848    fn test_library_new_create_database() {
3849        let (library, _temp_dir, _) = setup_test_library();
3850        let sqlite_conn = Connection::open(&library.config.base_config().database_path).unwrap();
3851        sqlite_conn
3852            .execute(
3853                "
3854            insert into song (
3855                id, path, artist, title, album, album_artist,
3856                track_number, disc_number, genre, stamp, version, duration, analyzed,
3857                extra_info
3858            )
3859            values (
3860                1, '/random/path', 'Some Artist', 'A Title', 'Some Album',
3861                'Some Album Artist', 1, 1, 'Electronica', '2022-01-01',
3862                1, 250, true, '{\"key\": \"value\"}'
3863            );
3864            ",
3865                [],
3866            )
3867            .unwrap();
3868        sqlite_conn
3869            .execute(
3870                "
3871            insert into feature(id, song_id, feature, feature_index)
3872            values (2000, 1, 1.1, 1)
3873            on conflict(song_id, feature_index) do update set feature=excluded.feature;
3874            ",
3875                [],
3876            )
3877            .unwrap();
3878    }
3879
3880    #[test]
3881    #[cfg(feature = "ffmpeg")]
3882    fn test_library_new_database_upgrade() {
3883        let config_dir = TempDir::new("tmp").unwrap();
3884        let sqlite_db_path = config_dir.path().join("test.db");
3885        // Initialize the database with the contents of old_database.sql, without
3886        // having anything to do with Library (yet)
3887        {
3888            let sqlite_conn = Connection::open(sqlite_db_path.clone()).unwrap();
3889            let sql_statements = fs::read_to_string("data/old_database.sql").unwrap();
3890            sqlite_conn.execute_batch(&sql_statements).unwrap();
3891            let track_number: String = sqlite_conn
3892                .query_row("select track_number from song where id = 1", [], |row| {
3893                    row.get(0)
3894                })
3895                .unwrap();
3896            // Check that songs are indeed inserted the old way
3897            assert_eq!(track_number, "01");
3898            let version: u32 = sqlite_conn
3899                .query_row("pragma user_version", [], |row| row.get(0))
3900                .unwrap();
3901            assert_eq!(version, 0);
3902        }
3903
3904        let library = Library::<BaseConfig, DummyDecoder>::new_from_base(
3905            Some(config_dir.path().join("config.txt")),
3906            Some(sqlite_db_path.clone()),
3907            Some(AnalysisOptions {
3908                number_cores: nzus(1),
3909                features_version: FeaturesVersion::Version1,
3910            }),
3911        )
3912        .unwrap();
3913        let sqlite_conn = library.sqlite_conn.lock().unwrap();
3914        let mut query = sqlite_conn
3915            .prepare("select track_number from song where id = ?1")
3916            .unwrap();
3917
3918        let first_song_track_number: Option<u32> = query.query_row([1], |row| row.get(0)).unwrap();
3919        assert_eq!(first_song_track_number, Some(1));
3920
3921        let second_song_track_number: Option<u32> = query.query_row([2], |row| row.get(0)).unwrap();
3922        assert_eq!(None, second_song_track_number);
3923
3924        let third_song_track_number: Option<u32> = query.query_row([3], |row| row.get(0)).unwrap();
3925        assert_eq!(None, third_song_track_number);
3926
3927        let fourth_song_track_number: Option<u32> = query.query_row([4], |row| row.get(0)).unwrap();
3928        assert_eq!(None, fourth_song_track_number);
3929
3930        let version: u32 = sqlite_conn
3931            .query_row("pragma user_version", [], |row| row.get(0))
3932            .unwrap();
3933        assert_eq!(version, 5);
3934        // Make sure we can call this over and over without any problem
3935        Library::<BaseConfig, DummyDecoder>::new_from_base(
3936            Some(config_dir.path().join("config.txt")),
3937            Some(sqlite_db_path),
3938            Some(AnalysisOptions {
3939                number_cores: NonZeroUsize::new(1).unwrap(),
3940                ..Default::default()
3941            }),
3942        )
3943        .unwrap();
3944        let version: u32 = sqlite_conn
3945            .query_row("pragma user_version", [], |row| row.get(0))
3946            .unwrap();
3947        assert_eq!(version, 5);
3948    }
3949
3950    #[test]
3951    #[cfg(feature = "ffmpeg")]
3952    fn test_library_new_database_already_last_version() {
3953        let config_dir = TempDir::new("tmp").unwrap();
3954        let sqlite_db_path = config_dir.path().join("test.db");
3955        Library::<BaseConfig, DummyDecoder>::new_from_base(
3956            Some(config_dir.path().join("config.txt")),
3957            Some(sqlite_db_path.clone()),
3958            Some(AnalysisOptions {
3959                number_cores: NonZeroUsize::new(1).unwrap(),
3960                ..Default::default()
3961            }),
3962        )
3963        .unwrap();
3964        let library = Library::<BaseConfig, DummyDecoder>::new_from_base(
3965            Some(config_dir.path().join("config.txt")),
3966            Some(sqlite_db_path.clone()),
3967            Some(AnalysisOptions {
3968                number_cores: NonZeroUsize::new(1).unwrap(),
3969                ..Default::default()
3970            }),
3971        )
3972        .unwrap();
3973        let sqlite_conn = library.sqlite_conn.lock().unwrap();
3974        let version: u32 = sqlite_conn
3975            .query_row("pragma user_version", [], |row| row.get(0))
3976            .unwrap();
3977        assert_eq!(version, 5);
3978    }
3979
3980    #[test]
3981    #[cfg(feature = "ffmpeg")]
3982    fn test_library_store_song() {
3983        let (mut library, _temp_dir, _) = setup_test_library();
3984        let song = _generate_basic_song(None);
3985        let library_song = LibrarySong {
3986            bliss_song: song.to_owned(),
3987            extra_info: (),
3988        };
3989        library.store_song(&library_song).unwrap();
3990        let connection = library.sqlite_conn.lock().unwrap();
3991        let expected_song = _basic_song_from_database(connection, &song.path.to_string_lossy());
3992        assert_eq!(expected_song, song);
3993    }
3994
3995    #[test]
3996    // Test that while creating a new BaseConfig with custom options,
3997    // the JSON file stores the information correctly.
3998    fn test_base_config_new() {
3999        let random_config_home = TempDir::new("config").unwrap();
4000        let config_path = random_config_home.path().join("test.json");
4001        let database_path = random_config_home.path().join("database.db");
4002        let base_config = BaseConfig::new(
4003            Some(config_path.to_owned()),
4004            Some(database_path.to_owned()),
4005            Some(AnalysisOptions {
4006                number_cores: NonZeroUsize::new(4).unwrap(),
4007                features_version: FeaturesVersion::Version1,
4008            }),
4009        )
4010        .unwrap();
4011        base_config.write().unwrap();
4012        let data = fs::read_to_string(&config_path).unwrap();
4013        let config = BaseConfig::deserialize_config(&data).unwrap();
4014
4015        assert_eq!(
4016            config,
4017            BaseConfig {
4018                config_path: config_path,
4019                database_path: database_path,
4020                analysis_options: AnalysisOptions {
4021                    number_cores: NonZeroUsize::new(4).unwrap(),
4022                    features_version: FeaturesVersion::Version1
4023                },
4024                m: default_m(),
4025            }
4026        );
4027
4028        let v: Value = serde_json::from_str(&data).unwrap();
4029        let obj = v.as_object().expect("top-level JSON must be an object");
4030        assert!(obj.contains_key("config_path"));
4031        assert!(obj.contains_key("database_path"));
4032        assert!(obj.contains_key("m"));
4033        assert!(obj.contains_key("features_version"));
4034        assert!(obj.contains_key("number_cores"));
4035    }
4036
4037    #[test]
4038    // Test that the configuration serializes the default parameters correctly.
4039    fn test_base_config_new_default() {
4040        let random_config_home = TempDir::new("config").unwrap();
4041        let config_path = random_config_home.path().join("test.json");
4042        let base_config = BaseConfig::new(Some(config_path.to_owned()), None, None).unwrap();
4043        base_config.write().unwrap();
4044        let data = fs::read_to_string(&config_path).unwrap();
4045        let config = BaseConfig::deserialize_config(&data).unwrap();
4046
4047        let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
4048
4049        assert_eq!(
4050            config,
4051            BaseConfig {
4052                config_path: config_path,
4053                database_path: random_config_home.path().join("songs.db"),
4054                analysis_options: AnalysisOptions {
4055                    number_cores: cores,
4056                    features_version: FeaturesVersion::LATEST,
4057                },
4058                m: default_m(),
4059            }
4060        );
4061
4062        let v: Value = serde_json::from_str(&data).unwrap();
4063        let obj = v.as_object().expect("top-level JSON must be an object");
4064        assert!(obj.contains_key("config_path"));
4065        assert!(obj.contains_key("database_path"));
4066        assert!(obj.contains_key("m"));
4067        assert!(obj.contains_key("features_version"));
4068        assert!(obj.contains_key("number_cores"));
4069    }
4070
4071    #[test]
4072    fn test_path_base_config_new() {
4073        {
4074            let xdg_config_home = TempDir::new("test-bliss").unwrap();
4075            fs::create_dir_all(xdg_config_home.path().join("bliss-rs")).unwrap();
4076            env::set_var("XDG_CONFIG_HOME", xdg_config_home.path());
4077
4078            // First test case: default options go to the XDG_CONFIG_HOME path.
4079            let base_config = BaseConfig::new(None, None, None).unwrap();
4080
4081            assert_eq!(
4082                base_config.config_path,
4083                xdg_config_home.path().join("bliss-rs/config.json"),
4084            );
4085            assert_eq!(
4086                base_config.database_path,
4087                xdg_config_home.path().join("bliss-rs/songs.db"),
4088            );
4089            base_config.write().unwrap();
4090            assert!(xdg_config_home.path().join("bliss-rs/config.json").exists());
4091        }
4092
4093        // Second test case: config path, no db path.
4094        {
4095            let random_config_home = TempDir::new("config").unwrap();
4096            let base_config = BaseConfig::new(
4097                Some(random_config_home.path().join("test.json")),
4098                None,
4099                None,
4100            )
4101            .unwrap();
4102            base_config.write().unwrap();
4103
4104            assert_eq!(
4105                base_config.config_path,
4106                random_config_home.path().join("test.json"),
4107            );
4108            assert_eq!(
4109                base_config.database_path,
4110                random_config_home.path().join("songs.db")
4111            );
4112            assert!(random_config_home.path().join("test.json").exists());
4113        }
4114
4115        // Third test case: no config path, but db path.
4116        {
4117            let random_config_home = TempDir::new("database").unwrap();
4118            let base_config =
4119                BaseConfig::new(None, Some(random_config_home.path().join("test.db")), None)
4120                    .unwrap();
4121            base_config.write().unwrap();
4122
4123            assert_eq!(
4124                base_config.config_path,
4125                random_config_home.path().join("config.json"),
4126            );
4127            assert_eq!(
4128                base_config.database_path,
4129                random_config_home.path().join("test.db"),
4130            );
4131        }
4132        // Last test case: both paths specified.
4133        {
4134            let random_config_home = TempDir::new("config").unwrap();
4135            let random_database_home = TempDir::new("database").unwrap();
4136            fs::create_dir_all(random_config_home.path().join("bliss-rs")).unwrap();
4137            let base_config = BaseConfig::new(
4138                Some(random_config_home.path().join("config_test.json")),
4139                Some(random_database_home.path().join("test-database.db")),
4140                None,
4141            )
4142            .unwrap();
4143            base_config.write().unwrap();
4144
4145            assert_eq!(
4146                base_config.config_path,
4147                random_config_home.path().join("config_test.json"),
4148            );
4149            assert_eq!(
4150                base_config.database_path,
4151                random_database_home.path().join("test-database.db"),
4152            );
4153            assert!(random_config_home.path().join("config_test.json").exists());
4154        }
4155    }
4156
4157    #[test]
4158    #[cfg(feature = "ffmpeg")]
4159    fn test_library_extra_info() {
4160        let (mut library, _temp_dir, _) = setup_test_library();
4161        let song = _generate_library_song(None);
4162        library.store_song(&song).unwrap();
4163        let connection = library.sqlite_conn.lock().unwrap();
4164        let returned_song =
4165            _library_song_from_database(connection, &song.bliss_song.path.to_string_lossy());
4166        assert_eq!(returned_song, song);
4167    }
4168
4169    #[test]
4170    fn test_from_config_path_non_existing() {
4171        assert!(
4172            Library::<CustomConfig, DummyDecoder>::from_config_path(Some(PathBuf::from(
4173                "non-existing"
4174            )))
4175            .is_err()
4176        );
4177    }
4178
4179    #[test]
4180    fn test_from_config_path() {
4181        let config_dir = TempDir::new("coucou").unwrap();
4182        let config_file = config_dir.path().join("config.json");
4183        let database_file = config_dir.path().join("bliss.db");
4184
4185        // In reality, someone would just do that with `(None, None)` to get the default
4186        // paths.
4187        let base_config = BaseConfig::new(
4188            Some(config_file.to_owned()),
4189            Some(database_file),
4190            Some(AnalysisOptions {
4191                number_cores: nzus(1),
4192                ..Default::default()
4193            }),
4194        )
4195        .unwrap();
4196
4197        let config = CustomConfig {
4198            base_config,
4199            second_path_to_music_library: "/path/to/somewhere".into(),
4200            ignore_wav_files: true,
4201        };
4202        // Test that it is possible to store a song in a library instance,
4203        // make that instance go out of scope, load the library again, and
4204        // get the stored song.
4205        let song = _generate_library_song(None);
4206        {
4207            let mut library = Library::<_, DummyDecoder>::new(config.to_owned()).unwrap();
4208            library.store_song(&song).unwrap();
4209        }
4210
4211        let library: Library<CustomConfig, DummyDecoder> =
4212            Library::from_config_path(Some(config_file)).unwrap();
4213        let connection = library.sqlite_conn.lock().unwrap();
4214        let returned_song =
4215            _library_song_from_database(connection, &song.bliss_song.path.to_string_lossy());
4216
4217        assert_eq!(library.config, config);
4218        assert_eq!(song, returned_song);
4219    }
4220
4221    #[test]
4222    fn test_config_from_file() {
4223        let config = BaseConfig::from_path("./data/sample-config.json").unwrap();
4224        let mut m: Array2<f32> = Array2::eye(FeaturesVersion::Version1.feature_count());
4225        m[[0, 1]] = 1.;
4226        assert_eq!(
4227            config,
4228            BaseConfig {
4229                config_path: PathBuf::from_str("/tmp/bliss-rs/config.json").unwrap(),
4230                database_path: PathBuf::from_str("/tmp/bliss-rs/songs.db").unwrap(),
4231                analysis_options: AnalysisOptions {
4232                    features_version: FeaturesVersion::Version1,
4233                    number_cores: NonZeroUsize::new(8).unwrap()
4234                },
4235                m,
4236            }
4237        );
4238    }
4239
4240    #[test]
4241    fn test_config_old_existing() {
4242        let config = BaseConfig::from_path("./data/old_config.json").unwrap();
4243        assert_eq!(
4244            config,
4245            BaseConfig {
4246                config_path: PathBuf::from_str("/tmp/bliss-rs/config.json").unwrap(),
4247                database_path: PathBuf::from_str("/tmp/bliss-rs/songs.db").unwrap(),
4248                analysis_options: AnalysisOptions {
4249                    features_version: FeaturesVersion::Version1,
4250                    number_cores: NonZeroUsize::new(8).unwrap()
4251                },
4252                m: Array2::eye(NUMBER_FEATURES),
4253            }
4254        );
4255    }
4256
4257    #[test]
4258    fn test_config_serialize_deserialize() {
4259        let config_dir = TempDir::new("coucou").unwrap();
4260        let config_file = config_dir.path().join("config.json");
4261        let database_file = config_dir.path().join("bliss.db");
4262
4263        // In reality, someone would just do that with `(None, None)` to get the default
4264        // paths.
4265        let base_config = BaseConfig::new(
4266            Some(config_file.to_owned()),
4267            Some(database_file),
4268            Some(AnalysisOptions {
4269                number_cores: nzus(1),
4270                features_version: FeaturesVersion::Version1,
4271            }),
4272        )
4273        .unwrap();
4274
4275        let config = CustomConfig {
4276            base_config,
4277            second_path_to_music_library: "/path/to/somewhere".into(),
4278            ignore_wav_files: true,
4279        };
4280        config.write().unwrap();
4281
4282        assert_eq!(
4283            config,
4284            CustomConfig::from_path(&config_file.to_string_lossy()).unwrap(),
4285        );
4286    }
4287
4288    #[test]
4289    #[cfg(feature = "ffmpeg")]
4290    fn test_library_sanity_check_fail() {
4291        let (mut library, _temp_dir, _) = setup_test_library();
4292        assert_eq!(
4293            library.version_sanity_check().unwrap(),
4294            vec![
4295                SanityError::MultipleVersionsInDB {
4296                    versions: vec![FeaturesVersion::Version1, FeaturesVersion::Version2]
4297                },
4298                SanityError::OldFeaturesVersionInDB {
4299                    version: FeaturesVersion::Version1
4300                }
4301            ],
4302        );
4303    }
4304
4305    #[test]
4306    #[cfg(feature = "ffmpeg")]
4307    fn test_library_sanity_check_ok() {
4308        let (mut library, _temp_dir, _) = setup_test_library();
4309        {
4310            let sqlite_conn =
4311                Connection::open(&library.config.base_config().database_path).unwrap();
4312            sqlite_conn
4313                .execute(
4314                    "delete from song where version != ?1",
4315                    [FeaturesVersion::LATEST],
4316                )
4317                .unwrap();
4318        }
4319        assert!(library.version_sanity_check().unwrap().is_empty());
4320    }
4321
4322    #[test]
4323    fn test_config_number_cpus() {
4324        let config_dir = TempDir::new("coucou").unwrap();
4325        let config_file = config_dir.path().join("config.json");
4326        let database_file = config_dir.path().join("bliss.db");
4327
4328        let base_config = BaseConfig::new(
4329            Some(config_file.to_owned()),
4330            Some(database_file.to_owned()),
4331            None,
4332        )
4333        .unwrap();
4334        let config = CustomConfig {
4335            base_config,
4336            second_path_to_music_library: "/path/to/somewhere".into(),
4337            ignore_wav_files: true,
4338        };
4339
4340        assert_eq!(
4341            config.get_number_cores().get(),
4342            usize::from(thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap())),
4343        );
4344
4345        let base_config = BaseConfig::new(
4346            Some(config_file),
4347            Some(database_file),
4348            Some(AnalysisOptions {
4349                number_cores: nzus(1),
4350                ..Default::default()
4351            }),
4352        )
4353        .unwrap();
4354        let mut config = CustomConfig {
4355            base_config,
4356            second_path_to_music_library: "/path/to/somewhere".into(),
4357            ignore_wav_files: true,
4358        };
4359
4360        assert_eq!(config.get_number_cores().get(), 1);
4361        config.set_number_cores(nzus(2)).unwrap();
4362        assert_eq!(config.get_number_cores().get(), 2);
4363    }
4364
4365    #[test]
4366    fn test_config_features_version() {
4367        let config_dir = TempDir::new("coucou").unwrap();
4368        let config_file = config_dir.path().join("config.json");
4369        let database_file = config_dir.path().join("bliss.db");
4370
4371        let base_config = BaseConfig::new(
4372            Some(config_file.to_owned()),
4373            Some(database_file.to_owned()),
4374            None,
4375        )
4376        .unwrap();
4377        let config = CustomConfig {
4378            base_config,
4379            second_path_to_music_library: "/path/to/somewhere".into(),
4380            ignore_wav_files: true,
4381        };
4382
4383        assert_eq!(config.get_features_version(), FeaturesVersion::LATEST,);
4384
4385        let base_config = BaseConfig::new(
4386            Some(config_file),
4387            Some(database_file),
4388            Some(AnalysisOptions {
4389                features_version: FeaturesVersion::Version1,
4390                ..Default::default()
4391            }),
4392        )
4393        .unwrap();
4394        let mut config = CustomConfig {
4395            base_config,
4396            second_path_to_music_library: "/path/to/somewhere".into(),
4397            ignore_wav_files: true,
4398        };
4399
4400        assert_eq!(config.get_features_version(), FeaturesVersion::Version1);
4401        config
4402            .set_features_version(FeaturesVersion::Version2)
4403            .unwrap();
4404        assert_eq!(config.get_features_version(), FeaturesVersion::Version2);
4405    }
4406
4407    #[test]
4408    fn test_library_create_all_dirs() {
4409        let config_dir = TempDir::new("coucou")
4410            .unwrap()
4411            .path()
4412            .join("path")
4413            .join("to");
4414        assert!(!config_dir.is_dir());
4415        let config_file = config_dir.join("config.json");
4416        let database_file = config_dir.join("bliss.db");
4417        Library::<BaseConfig, DummyDecoder>::new_from_base(
4418            Some(config_file),
4419            Some(database_file),
4420            Some(AnalysisOptions {
4421                number_cores: nzus(1),
4422                ..Default::default()
4423            }),
4424        )
4425        .unwrap();
4426        assert!(config_dir.is_dir());
4427    }
4428
4429    #[test]
4430    #[cfg(feature = "ffmpeg")]
4431    fn test_library_get_failed_songs() {
4432        let (library, _temp_dir, _) = setup_test_library();
4433        let failed_songs = library.get_failed_songs().unwrap();
4434        assert_eq!(
4435            failed_songs,
4436            vec![
4437                ProcessingError {
4438                    song_path: PathBuf::from("./data/not-existing.m4a"),
4439                    error: String::from("error finding the file"),
4440                    features_version: FeaturesVersion::Version1,
4441                },
4442                ProcessingError {
4443                    song_path: PathBuf::from("./data/invalid-file.m4a"),
4444                    error: String::from("error decoding the file"),
4445                    features_version: FeaturesVersion::Version1,
4446                }
4447            ]
4448        );
4449    }
4450
4451    #[test]
4452    #[cfg(feature = "ffmpeg")]
4453    fn test_analyze_store_failed_songs() {
4454        let (mut library, _temp_dir, _) = setup_test_library();
4455        library
4456            .config
4457            .base_config_mut()
4458            .analysis_options
4459            .features_version = FeaturesVersion::Version1;
4460
4461        let paths = vec![
4462            "./data/s16_mono_22_5kHz.flac",
4463            "./data/s16_stereo_22_5kHz.flac",
4464            "non-existing",
4465        ];
4466        library.analyze_paths(paths.to_owned(), false).unwrap();
4467        let failed_songs = library.get_failed_songs().unwrap();
4468        assert!(failed_songs.contains(&ProcessingError {
4469            song_path: PathBuf::from("non-existing"),
4470            error: String::from("error happened while decoding file - while opening format for file 'non-existing': ffmpeg::Error(2: No such file or directory)."),
4471            features_version: FeaturesVersion::Version1,
4472        }));
4473    }
4474}