Skip to main content

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