Merge pull request #41 from Polochon-street/add-actually-useful-library-struct
Add a proper library / config example
This commit is contained in:
commit
bf428a62af
11 changed files with 4218 additions and 244 deletions
6
.github/workflows/rust.yml
vendored
6
.github/workflows/rust.yml
vendored
|
@ -30,14 +30,16 @@ jobs:
|
||||||
run: cargo build --verbose
|
run: cargo build --verbose
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose
|
run: cargo test --verbose
|
||||||
|
- name: Run library tests
|
||||||
|
run: cargo test --verbose --features=library
|
||||||
- name: Run example tests
|
- name: Run example tests
|
||||||
run: cargo test --verbose --examples
|
run: cargo test --verbose --examples
|
||||||
- name: Build benches
|
- name: Build benches
|
||||||
run: cargo +nightly-2022-02-16 bench --verbose --features=bench --no-run
|
run: cargo +nightly-2022-02-16 bench --verbose --features=bench --no-run
|
||||||
- name: Build examples
|
- name: Build examples
|
||||||
run: cargo build --examples --verbose --features=serde
|
run: cargo build --examples --verbose --features=serde,library
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: cargo clippy --examples --features=serde -- -D warnings
|
run: cargo clippy --examples --features=serde,library -- -D warnings
|
||||||
|
|
||||||
build-test-lint-windows:
|
build-test-lint-windows:
|
||||||
name: Windows - build, test and lint
|
name: Windows - build, test and lint
|
||||||
|
|
635
Cargo.lock
generated
635
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
34
Cargo.toml
34
Cargo.toml
|
@ -11,7 +11,7 @@ keywords = ["audio", "analysis", "MIR", "playlist", "similarity"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["bliss-audio-aubio-rs/rustdoc"]
|
features = ["bliss-audio-aubio-rs/rustdoc", "library"]
|
||||||
no-default-features = true
|
no-default-features = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -23,6 +23,12 @@ ffmpeg-static = ["ffmpeg-next/static"]
|
||||||
python-bindings = ["bliss-audio-aubio-rs/fftw3"]
|
python-bindings = ["bliss-audio-aubio-rs/fftw3"]
|
||||||
# Enable the benchmarks with `cargo +nightly bench --features=bench`
|
# Enable the benchmarks with `cargo +nightly bench --features=bench`
|
||||||
bench = []
|
bench = []
|
||||||
|
library = [
|
||||||
|
"serde", "dep:rusqlite", "dep:dirs", "dep:tempdir",
|
||||||
|
"dep:anyhow", "dep:serde_ini", "dep:serde_json",
|
||||||
|
"dep:indicatif",
|
||||||
|
]
|
||||||
|
serde = ["dep:serde"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ripemd160 = "0.9.0"
|
ripemd160 = "0.9.0"
|
||||||
|
@ -44,13 +50,35 @@ thiserror = "1.0.24"
|
||||||
bliss-audio-aubio-rs = "0.2.0"
|
bliss-audio-aubio-rs = "0.2.0"
|
||||||
strum = "0.21"
|
strum = "0.21"
|
||||||
strum_macros = "0.21"
|
strum_macros = "0.21"
|
||||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
|
||||||
rcue = "0.1.1"
|
rcue = "0.1.1"
|
||||||
|
|
||||||
|
# Deps for the library feature
|
||||||
|
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0.59", optional = true }
|
||||||
|
serde_ini = { version = "0.2.0", optional = true }
|
||||||
|
tempdir = { version = "0.3.7", optional = true }
|
||||||
|
rusqlite = { version = "0.27.0", optional = true }
|
||||||
|
dirs = { version = "4.0.0", optional = true }
|
||||||
|
anyhow = { version = "1.0.58", optional = true }
|
||||||
|
indicatif = { version = "0.17.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mime_guess = "2.0.3"
|
mime_guess = "2.0.3"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
anyhow = "1.0.45"
|
anyhow = "1.0.45"
|
||||||
serde_json = "1.0.59"
|
|
||||||
clap = "2.33.3"
|
clap = "2.33.3"
|
||||||
pretty_assertions = "1.2.1"
|
pretty_assertions = "1.2.1"
|
||||||
|
serde_json = "1.0.59"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "library"
|
||||||
|
required-features = ["library"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "library_extra_info"
|
||||||
|
required-features = ["library"]
|
||||||
|
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "playlist"
|
||||||
|
required-features = ["serde"]
|
||||||
|
|
202
examples/library.rs
Normal file
202
examples/library.rs
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
/// Basic example of how one would combine bliss with an "audio player",
|
||||||
|
/// through [Library].
|
||||||
|
///
|
||||||
|
/// For simplicity's sake, this example recursively gets songs from a folder
|
||||||
|
/// to emulate an audio player library, without handling CUE files.
|
||||||
|
use anyhow::Result;
|
||||||
|
use bliss_audio::library::{AppConfigTrait, BaseConfig, Library};
|
||||||
|
use clap::{App, Arg, SubCommand};
|
||||||
|
use glob::glob;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
// A config structure, that will be serialized as a
|
||||||
|
// JSON file upon Library creation.
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(flatten)]
|
||||||
|
// The base configuration, containing both the config file
|
||||||
|
// path, as well as the database path.
|
||||||
|
pub base_config: BaseConfig,
|
||||||
|
// An extra field, to store the music library path. Any number
|
||||||
|
// of arbitrary fields (even Serializable structures) can
|
||||||
|
// of course be added.
|
||||||
|
pub music_library_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new(
|
||||||
|
music_library_path: PathBuf,
|
||||||
|
config_path: Option<PathBuf>,
|
||||||
|
database_path: Option<PathBuf>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let base_config = BaseConfig::new(config_path, database_path)?;
|
||||||
|
Ok(Self {
|
||||||
|
base_config,
|
||||||
|
music_library_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The AppConfigTrait must know how to access the base config.
|
||||||
|
impl AppConfigTrait for Config {
|
||||||
|
fn base_config(&self) -> &BaseConfig {
|
||||||
|
&self.base_config
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_config_mut(&mut self) -> &mut BaseConfig {
|
||||||
|
&mut self.base_config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A trait allowing to implement methods for the Library,
|
||||||
|
// useful if you don't need to store extra information in fields.
|
||||||
|
// Otherwise, doing
|
||||||
|
// ```
|
||||||
|
// struct CustomLibrary {
|
||||||
|
// library: Library<Config>,
|
||||||
|
// extra_field: ...,
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
// and implementing functions for that struct would be the way to go.
|
||||||
|
// That's what the [reference](https://github.com/Polochon-street/blissify-rs)
|
||||||
|
// implementation does.
|
||||||
|
trait CustomLibrary {
|
||||||
|
fn song_paths(&self) -> Result<Vec<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomLibrary for Library<Config> {
|
||||||
|
/// Get all songs in the player library
|
||||||
|
fn song_paths(&self) -> Result<Vec<String>> {
|
||||||
|
let music_path = &self.config.music_library_path;
|
||||||
|
let pattern = Path::new(&music_path).join("**").join("*");
|
||||||
|
|
||||||
|
Ok(glob(&pattern.to_string_lossy())?
|
||||||
|
.map(|e| fs::canonicalize(e.unwrap()).unwrap())
|
||||||
|
.filter(|e| match mime_guess::from_path(e).first() {
|
||||||
|
Some(m) => m.type_() == "audio",
|
||||||
|
None => false,
|
||||||
|
})
|
||||||
|
.map(|x| x.to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<String>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A simple example of what a CLI-app would look.
|
||||||
|
//
|
||||||
|
// Note that `Library::new` is used only on init, and subsequent
|
||||||
|
// commands use `Library::from_path`.
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let matches = App::new("library-example")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.author("Polochon_street")
|
||||||
|
.about("Example binary implementing bliss for an audio player.")
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("init")
|
||||||
|
.about(
|
||||||
|
"Initialize a Library, both storing the config and analyzing folders
|
||||||
|
containing songs.",
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("FOLDER")
|
||||||
|
.help("A folder containing the music library to analyze.")
|
||||||
|
.required(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("database-path")
|
||||||
|
.short("d")
|
||||||
|
.long("database-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to store the database file containing
|
||||||
|
the songs' analysis. Defaults to XDG_DATA_HOME/bliss-rs/bliss.db.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("config-path")
|
||||||
|
.short("c")
|
||||||
|
.long("config-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to store the config file containing
|
||||||
|
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("update")
|
||||||
|
.about(
|
||||||
|
"Update a Library's songs, trying to analyze failed songs,
|
||||||
|
as well as songs not in the library.",
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("config-path")
|
||||||
|
.short("c")
|
||||||
|
.long("config-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to load the config file containing
|
||||||
|
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("playlist")
|
||||||
|
.about(
|
||||||
|
"Make a playlist, starting with the song at SONG_PATH, returning
|
||||||
|
the songs' paths.",
|
||||||
|
)
|
||||||
|
.arg(Arg::with_name("SONG_PATH").takes_value(true))
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("config-path")
|
||||||
|
.short("c")
|
||||||
|
.long("config-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to load the config file containing
|
||||||
|
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("playlist-length")
|
||||||
|
.short("l")
|
||||||
|
.long("playlist-length")
|
||||||
|
.help("Optional playlist length. Defaults to 20.")
|
||||||
|
.takes_value(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
if let Some(sub_m) = matches.subcommand_matches("init") {
|
||||||
|
let folder = PathBuf::from(sub_m.value_of("FOLDER").unwrap());
|
||||||
|
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
|
||||||
|
let database_path = sub_m.value_of("database-path").map(PathBuf::from);
|
||||||
|
|
||||||
|
let config = Config::new(folder, config_path, database_path)?;
|
||||||
|
let mut library = Library::new(config)?;
|
||||||
|
|
||||||
|
library.analyze_paths(library.song_paths()?, true)?;
|
||||||
|
} else if let Some(sub_m) = matches.subcommand_matches("update") {
|
||||||
|
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
|
||||||
|
let mut library: Library<Config> = Library::from_config_path(config_path)?;
|
||||||
|
library.update_library(library.song_paths()?, true)?;
|
||||||
|
} else if let Some(sub_m) = matches.subcommand_matches("playlist") {
|
||||||
|
let song_path = sub_m.value_of("SONG_PATH").unwrap();
|
||||||
|
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
|
||||||
|
let playlist_length = sub_m
|
||||||
|
.value_of("playlist-length")
|
||||||
|
.unwrap_or("20")
|
||||||
|
.parse::<usize>()?;
|
||||||
|
let library: Library<Config> = Library::from_config_path(config_path)?;
|
||||||
|
let songs = library.playlist_from::<()>(song_path, playlist_length)?;
|
||||||
|
let song_paths = songs
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.bliss_song.path.to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
for song in song_paths {
|
||||||
|
println!("{:?}", song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
225
examples/library_extra_info.rs
Normal file
225
examples/library_extra_info.rs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
/// Basic example of how one would combine bliss with an "audio player",
|
||||||
|
/// through [Library], showing how to put extra info in the database for
|
||||||
|
/// each song.
|
||||||
|
///
|
||||||
|
/// For simplicity's sake, this example recursively gets songs from a folder
|
||||||
|
/// to emulate an audio player library, without handling CUE files.
|
||||||
|
use anyhow::Result;
|
||||||
|
use bliss_audio::library::{AppConfigTrait, BaseConfig, Library};
|
||||||
|
use clap::{App, Arg, SubCommand};
|
||||||
|
use glob::glob;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
/// A config structure, that will be serialized as a
|
||||||
|
/// JSON file upon Library creation.
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(flatten)]
|
||||||
|
/// The base configuration, containing both the config file
|
||||||
|
/// path, as well as the database path.
|
||||||
|
pub base_config: BaseConfig,
|
||||||
|
/// An extra field, to store the music library path. Any number
|
||||||
|
/// of arbitrary fields (even Serializable structures) can
|
||||||
|
/// of course be added.
|
||||||
|
pub music_library_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new(
|
||||||
|
music_library_path: PathBuf,
|
||||||
|
config_path: Option<PathBuf>,
|
||||||
|
database_path: Option<PathBuf>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let base_config = BaseConfig::new(config_path, database_path)?;
|
||||||
|
Ok(Self {
|
||||||
|
base_config,
|
||||||
|
music_library_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The AppConfigTrait must know how to access the base config.
|
||||||
|
impl AppConfigTrait for Config {
|
||||||
|
fn base_config(&self) -> &BaseConfig {
|
||||||
|
&self.base_config
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_config_mut(&mut self) -> &mut BaseConfig {
|
||||||
|
&mut self.base_config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A trait allowing to implement methods for the Library,
|
||||||
|
// useful if you don't need to store extra information in fields.
|
||||||
|
// Otherwise, doing
|
||||||
|
// ```
|
||||||
|
// struct CustomLibrary {
|
||||||
|
// library: Library<Config>,
|
||||||
|
// extra_field: ...,
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
// and implementing functions for that struct would be the way to go.
|
||||||
|
// That's what the [reference](https://github.com/Polochon-street/blissify-rs)
|
||||||
|
// implementation does.
|
||||||
|
trait CustomLibrary {
|
||||||
|
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomLibrary for Library<Config> {
|
||||||
|
/// Get all songs in the player library, along with the extra info
|
||||||
|
/// one would want to store along with each song.
|
||||||
|
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>> {
|
||||||
|
let music_path = &self.config.music_library_path;
|
||||||
|
let pattern = Path::new(&music_path).join("**").join("*");
|
||||||
|
|
||||||
|
Ok(glob(&pattern.to_string_lossy())?
|
||||||
|
.map(|e| fs::canonicalize(e.unwrap()).unwrap())
|
||||||
|
.filter_map(|e| {
|
||||||
|
mime_guess::from_path(&e).first().map(|m| {
|
||||||
|
(
|
||||||
|
e.to_string_lossy().to_string(),
|
||||||
|
ExtraInfo {
|
||||||
|
extension: e.extension().map(|e| e.to_string_lossy().to_string()),
|
||||||
|
file_name: e.file_name().map(|e| e.to_string_lossy().to_string()),
|
||||||
|
mime_type: format!("{}/{}", m.type_(), m.subtype()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<(String, ExtraInfo)>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)]
|
||||||
|
// An (somewhat simple) example of what extra metadata one would put, along
|
||||||
|
// with song analysis data.
|
||||||
|
struct ExtraInfo {
|
||||||
|
extension: Option<String>,
|
||||||
|
file_name: Option<String>,
|
||||||
|
mime_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A simple example of what a CLI-app would look.
|
||||||
|
//
|
||||||
|
// Note that `Library::new` is used only on init, and subsequent
|
||||||
|
// commands use `Library::from_path`.
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let matches = App::new("library-example")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.author("Polochon_street")
|
||||||
|
.about("Example binary implementing bliss for an audio player.")
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("init")
|
||||||
|
.about(
|
||||||
|
"Initialize a Library, both storing the config and analyzing folders
|
||||||
|
containing songs.",
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("FOLDER")
|
||||||
|
.help("A folder containing the music library to analyze.")
|
||||||
|
.required(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("database-path")
|
||||||
|
.short("d")
|
||||||
|
.long("database-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to store the database file containing
|
||||||
|
the songs' analysis. Defaults to XDG_DATA_HOME/bliss-rs/bliss.db.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("config-path")
|
||||||
|
.short("c")
|
||||||
|
.long("config-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to store the config file containing
|
||||||
|
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("update")
|
||||||
|
.about(
|
||||||
|
"Update a Library's songs, trying to analyze failed songs,
|
||||||
|
as well as songs not in the library.",
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("config-path")
|
||||||
|
.short("c")
|
||||||
|
.long("config-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to load the config file containing
|
||||||
|
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("playlist")
|
||||||
|
.about(
|
||||||
|
"Make a playlist, starting with the song at SONG_PATH, returning
|
||||||
|
the songs' paths.",
|
||||||
|
)
|
||||||
|
.arg(Arg::with_name("SONG_PATH").takes_value(true))
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("config-path")
|
||||||
|
.short("c")
|
||||||
|
.long("config-path")
|
||||||
|
.help(
|
||||||
|
"Optional path where to load the config file containing
|
||||||
|
the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("playlist-length")
|
||||||
|
.short("l")
|
||||||
|
.long("playlist-length")
|
||||||
|
.help("Optional playlist length. Defaults to 20.")
|
||||||
|
.takes_value(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
if let Some(sub_m) = matches.subcommand_matches("init") {
|
||||||
|
let folder = PathBuf::from(sub_m.value_of("FOLDER").unwrap());
|
||||||
|
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
|
||||||
|
let database_path = sub_m.value_of("database-path").map(PathBuf::from);
|
||||||
|
|
||||||
|
let config = Config::new(folder, config_path, database_path)?;
|
||||||
|
let mut library = Library::new(config)?;
|
||||||
|
|
||||||
|
library.analyze_paths_extra_info(library.song_paths_info()?, true)?;
|
||||||
|
} else if let Some(sub_m) = matches.subcommand_matches("update") {
|
||||||
|
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
|
||||||
|
let mut library: Library<Config> = Library::from_config_path(config_path)?;
|
||||||
|
library.update_library_extra_info(library.song_paths_info()?, true)?;
|
||||||
|
} else if let Some(sub_m) = matches.subcommand_matches("playlist") {
|
||||||
|
let song_path = sub_m.value_of("SONG_PATH").unwrap();
|
||||||
|
let config_path = sub_m.value_of("config-path").map(PathBuf::from);
|
||||||
|
let playlist_length = sub_m
|
||||||
|
.value_of("playlist-length")
|
||||||
|
.unwrap_or("20")
|
||||||
|
.parse::<usize>()?;
|
||||||
|
let library: Library<Config> = Library::from_config_path(config_path)?;
|
||||||
|
let songs = library.playlist_from::<ExtraInfo>(song_path, playlist_length)?;
|
||||||
|
let playlist = songs
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
(
|
||||||
|
s.bliss_song.path.to_string_lossy().to_string(),
|
||||||
|
s.extra_info.mime_type,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<(String, String)>>();
|
||||||
|
for (path, mime_type) in playlist {
|
||||||
|
println!("{} <{}>", path, mime_type,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,26 +1,16 @@
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use bliss_audio::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance};
|
use bliss_audio::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance};
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use bliss_audio::{analyze_paths, Song};
|
use bliss_audio::{analyze_paths, Song};
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg};
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use std::env;
|
use std::env;
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/* Analyzes a folder recursively, and make a playlist out of the file
|
/* Analyzes a folder recursively, and make a playlist out of the file
|
||||||
* provided by the user. */
|
* provided by the user. */
|
||||||
// How to use: ./playlist [-o file.m3u] [-a analysis.json] <folder> <file to start the playlist from>
|
// How to use: ./playlist [-o file.m3u] [-a analysis.json] <folder> <file to start the playlist from>
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let matches = App::new("playlist")
|
let matches = App::new("playlist")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
@ -103,8 +93,3 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "serde"))]
|
|
||||||
fn main() {
|
|
||||||
println!("You need the serde feature enabled to run this file.");
|
|
||||||
}
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ impl BlissCueFile {
|
||||||
let song = Song {
|
let song = Song {
|
||||||
path: PathBuf::from(format!(
|
path: PathBuf::from(format!(
|
||||||
"{}/CUE_TRACK{:03}",
|
"{}/CUE_TRACK{:03}",
|
||||||
self.audio_file_path.to_string_lossy(),
|
self.cue_path.to_string_lossy(),
|
||||||
index,
|
index,
|
||||||
)),
|
)),
|
||||||
album: self.album.to_owned(),
|
album: self.album.to_owned(),
|
||||||
|
@ -210,7 +210,7 @@ mod tests {
|
||||||
let songs = BlissCue::songs_from_path("data/testcue.cue").unwrap();
|
let songs = BlissCue::songs_from_path("data/testcue.cue").unwrap();
|
||||||
let expected = vec![
|
let expected = vec![
|
||||||
Ok(Song {
|
Ok(Song {
|
||||||
path: Path::new("data/testcue.flac/CUE_TRACK001").to_path_buf(),
|
path: Path::new("data/testcue.cue/CUE_TRACK001").to_path_buf(),
|
||||||
analysis: Analysis {
|
analysis: Analysis {
|
||||||
internal_analysis: [
|
internal_analysis: [
|
||||||
0.38463724,
|
0.38463724,
|
||||||
|
@ -250,7 +250,7 @@ mod tests {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
Ok(Song {
|
Ok(Song {
|
||||||
path: Path::new("data/testcue.flac/CUE_TRACK002").to_path_buf(),
|
path: Path::new("data/testcue.cue/CUE_TRACK002").to_path_buf(),
|
||||||
analysis: Analysis {
|
analysis: Analysis {
|
||||||
internal_analysis: [
|
internal_analysis: [
|
||||||
0.18622077,
|
0.18622077,
|
||||||
|
@ -290,7 +290,7 @@ mod tests {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
Ok(Song {
|
Ok(Song {
|
||||||
path: Path::new("data/testcue.flac/CUE_TRACK003").to_path_buf(),
|
path: Path::new("data/testcue.cue/CUE_TRACK003").to_path_buf(),
|
||||||
analysis: Analysis {
|
analysis: Analysis {
|
||||||
internal_analysis: [
|
internal_analysis: [
|
||||||
0.0024261475,
|
0.0024261475,
|
||||||
|
|
38
src/lib.rs
38
src/lib.rs
|
@ -15,6 +15,14 @@
|
||||||
//! is as easy as computing distances between that song and the rest, and ordering
|
//! is as easy as computing distances between that song and the rest, and ordering
|
||||||
//! the songs by distance, ascending.
|
//! the songs by distance, ascending.
|
||||||
//!
|
//!
|
||||||
|
//! If you want to implement a bliss plugin for an already existing audio
|
||||||
|
//! player, the [Library] struct is a collection of goodies that should prove
|
||||||
|
//! useful (it contains utilities to store analyzed songs in a self-contained
|
||||||
|
//! database file, to make playlists directly from the database, etc).
|
||||||
|
//! [blissify](https://github.com/Polochon-street/blissify-rs/) for both
|
||||||
|
//! an example of how the [Library] struct works, and a real-life demo of bliss
|
||||||
|
//! implemented for [MPD](https://www.musicpd.org/).
|
||||||
|
//!
|
||||||
//! # Examples
|
//! # Examples
|
||||||
//!
|
//!
|
||||||
//! ### Analyze & compute the distance between two songs
|
//! ### Analyze & compute the distance between two songs
|
||||||
|
@ -59,6 +67,8 @@
|
||||||
#![warn(rustdoc::missing_doc_code_examples)]
|
#![warn(rustdoc::missing_doc_code_examples)]
|
||||||
mod chroma;
|
mod chroma;
|
||||||
pub mod cue;
|
pub mod cue;
|
||||||
|
#[cfg(feature = "library")]
|
||||||
|
pub mod library;
|
||||||
mod misc;
|
mod misc;
|
||||||
pub mod playlist;
|
pub mod playlist;
|
||||||
mod song;
|
mod song;
|
||||||
|
@ -262,11 +272,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_analyze_paths() {
|
fn test_analyze_paths() {
|
||||||
let paths = vec![
|
let paths = vec![
|
||||||
PathBuf::from("./data/s16_mono_22_5kHz.flac"),
|
"./data/s16_mono_22_5kHz.flac",
|
||||||
PathBuf::from("./data/testcue.cue"),
|
"./data/testcue.cue",
|
||||||
PathBuf::from("./data/white_noise.flac"),
|
"./data/white_noise.flac",
|
||||||
PathBuf::from("definitely-not-existing.foo"),
|
"definitely-not-existing.foo",
|
||||||
PathBuf::from("not-existing.foo"),
|
"not-existing.foo",
|
||||||
];
|
];
|
||||||
let mut results = analyze_paths(&paths)
|
let mut results = analyze_paths(&paths)
|
||||||
.map(|x| match &x.1 {
|
.map(|x| match &x.1 {
|
||||||
|
@ -304,21 +314,9 @@ mod tests {
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
(true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None),
|
(true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None),
|
||||||
(
|
(true, PathBuf::from("./data/testcue.cue/CUE_TRACK001"), None),
|
||||||
true,
|
(true, PathBuf::from("./data/testcue.cue/CUE_TRACK002"), None),
|
||||||
PathBuf::from("./data/testcue.flac/CUE_TRACK001"),
|
(true, PathBuf::from("./data/testcue.cue/CUE_TRACK003"), None),
|
||||||
None,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
true,
|
|
||||||
PathBuf::from("./data/testcue.flac/CUE_TRACK002"),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
true,
|
|
||||||
PathBuf::from("./data/testcue.flac/CUE_TRACK003"),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
(true, PathBuf::from("./data/white_noise.flac"), None),
|
(true, PathBuf::from("./data/white_noise.flac"), None),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
2850
src/library.rs
Normal file
2850
src/library.rs
Normal file
File diff suppressed because it is too large
Load diff
448
src/playlist.rs
448
src/playlist.rs
|
@ -7,6 +7,8 @@
|
||||||
//! They will yield different styles of playlists, so don't hesitate to
|
//! They will yield different styles of playlists, so don't hesitate to
|
||||||
//! experiment with them if the default (euclidean distance for now) doesn't
|
//! experiment with them if the default (euclidean distance for now) doesn't
|
||||||
//! suit you.
|
//! suit you.
|
||||||
|
// TODO on the `by_key` functions: maybe Fn(&T) -> &Song is enough? Compared
|
||||||
|
// to -> Song
|
||||||
use crate::{BlissError, BlissResult, Song, NUMBER_FEATURES};
|
use crate::{BlissError, BlissResult, Song, NUMBER_FEATURES};
|
||||||
use ndarray::{Array, Array1, Array2, Axis};
|
use ndarray::{Array, Array1, Array2, Axis};
|
||||||
use ndarray_stats::QuantileExt;
|
use ndarray_stats::QuantileExt;
|
||||||
|
@ -47,6 +49,23 @@ pub fn closest_to_first_song(
|
||||||
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(song, &distance)));
|
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(song, &distance)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sort `songs` in place by putting songs close to `first_song` first
|
||||||
|
/// using the `distance` metric.
|
||||||
|
///
|
||||||
|
/// Sort songs with a key extraction function, useful for when you have a
|
||||||
|
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||||||
|
pub fn closest_to_first_song_by_key<F, T>(
|
||||||
|
first_song: &T,
|
||||||
|
#[allow(clippy::ptr_arg)] songs: &mut Vec<T>,
|
||||||
|
distance: impl DistanceMetric,
|
||||||
|
key_fn: F,
|
||||||
|
) where
|
||||||
|
F: Fn(&T) -> Song,
|
||||||
|
{
|
||||||
|
let first_song = key_fn(first_song);
|
||||||
|
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&key_fn(song), &distance)));
|
||||||
|
}
|
||||||
|
|
||||||
/// Sort `songs` in place using the `distance` metric and ordering by
|
/// Sort `songs` in place using the `distance` metric and ordering by
|
||||||
/// the smallest distance between each song.
|
/// the smallest distance between each song.
|
||||||
///
|
///
|
||||||
|
@ -71,6 +90,43 @@ pub fn song_to_song(first_song: &Song, songs: &mut Vec<Song>, distance: impl Dis
|
||||||
*songs = new_songs;
|
*songs = new_songs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sort `songs` in place using the `distance` metric and ordering by
|
||||||
|
/// the smallest distance between each song.
|
||||||
|
///
|
||||||
|
/// If the generated playlist is `[song1, song2, song3, song4]`, it means
|
||||||
|
/// song2 is closest to song1, song3 is closest to song2, and song4 is closest
|
||||||
|
/// to song3.
|
||||||
|
///
|
||||||
|
/// Note that this has a tendency to go from one style to the other very fast,
|
||||||
|
/// and it can be slow on big libraries.
|
||||||
|
///
|
||||||
|
/// Sort songs with a key extraction function, useful for when you have a
|
||||||
|
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||||||
|
// TODO: maybe Clone is not needed?
|
||||||
|
pub fn song_to_song_by_key<F, T: std::cmp::PartialEq + Clone>(
|
||||||
|
first_song: &T,
|
||||||
|
songs: &mut Vec<T>,
|
||||||
|
distance: impl DistanceMetric,
|
||||||
|
key_fn: F,
|
||||||
|
) where
|
||||||
|
F: Fn(&T) -> Song,
|
||||||
|
{
|
||||||
|
let mut new_songs: Vec<T> = Vec::with_capacity(songs.len());
|
||||||
|
let mut bliss_song = key_fn(&first_song.to_owned());
|
||||||
|
|
||||||
|
while !songs.is_empty() {
|
||||||
|
let distances: Array1<f32> = Array::from_shape_fn(songs.len(), |i| {
|
||||||
|
bliss_song.custom_distance(&key_fn(&songs[i]), &distance)
|
||||||
|
});
|
||||||
|
let idx = distances.argmin().unwrap();
|
||||||
|
let song = songs[idx].to_owned();
|
||||||
|
bliss_song = key_fn(&songs[idx]).to_owned();
|
||||||
|
new_songs.push(song.to_owned());
|
||||||
|
songs.retain(|s| s != &song);
|
||||||
|
}
|
||||||
|
*songs = new_songs;
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove duplicate songs from a playlist, in place.
|
/// Remove duplicate songs from a playlist, in place.
|
||||||
///
|
///
|
||||||
/// Two songs are considered duplicates if they either have the same,
|
/// Two songs are considered duplicates if they either have the same,
|
||||||
|
@ -86,6 +142,29 @@ pub fn dedup_playlist(songs: &mut Vec<Song>, distance_threshold: Option<f32>) {
|
||||||
dedup_playlist_custom_distance(songs, distance_threshold, euclidean_distance);
|
dedup_playlist_custom_distance(songs, distance_threshold, euclidean_distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove duplicate songs from a playlist, in place.
|
||||||
|
///
|
||||||
|
/// Two songs are considered duplicates if they either have the same,
|
||||||
|
/// non-empty title and artist name, or if they are close enough in terms
|
||||||
|
/// of distance.
|
||||||
|
///
|
||||||
|
/// Dedup songs with a key extraction function, useful for when you have a
|
||||||
|
/// structure like `CustomSong { bliss_song: Song, something_else: bool }` you
|
||||||
|
/// want to deduplicate.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `songs`: The playlist to remove duplicates from.
|
||||||
|
/// * `distance_threshold`: The distance threshold under which two songs are
|
||||||
|
/// considered identical. If `None`, a default value of 0.05 will be used.
|
||||||
|
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
|
||||||
|
pub fn dedup_playlist_by_key<T, F>(songs: &mut Vec<T>, distance_threshold: Option<f32>, key_fn: F)
|
||||||
|
where
|
||||||
|
F: Fn(&T) -> Song,
|
||||||
|
{
|
||||||
|
dedup_playlist_custom_distance_by_key(songs, distance_threshold, euclidean_distance, key_fn);
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove duplicate songs from a playlist, in place, using a custom distance
|
/// Remove duplicate songs from a playlist, in place, using a custom distance
|
||||||
/// metric.
|
/// metric.
|
||||||
///
|
///
|
||||||
|
@ -115,6 +194,45 @@ pub fn dedup_playlist_custom_distance(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove duplicate songs from a playlist, in place, using a custom distance
|
||||||
|
/// metric.
|
||||||
|
///
|
||||||
|
/// Two songs are considered duplicates if they either have the same,
|
||||||
|
/// non-empty title and artist name, or if they are close enough in terms
|
||||||
|
/// of distance.
|
||||||
|
///
|
||||||
|
/// Dedup songs with a key extraction function, useful for when you have a
|
||||||
|
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||||||
|
/// you want to deduplicate.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `songs`: The playlist to remove duplicates from.
|
||||||
|
/// * `distance_threshold`: The distance threshold under which two songs are
|
||||||
|
/// considered identical. If `None`, a default value of 0.05 will be used.
|
||||||
|
/// * `distance`: A custom distance metric.
|
||||||
|
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
|
||||||
|
pub fn dedup_playlist_custom_distance_by_key<F, T>(
|
||||||
|
songs: &mut Vec<T>,
|
||||||
|
distance_threshold: Option<f32>,
|
||||||
|
distance: impl DistanceMetric,
|
||||||
|
key_fn: F,
|
||||||
|
) where
|
||||||
|
F: Fn(&T) -> Song,
|
||||||
|
{
|
||||||
|
songs.dedup_by(|s1, s2| {
|
||||||
|
let s1 = key_fn(s1);
|
||||||
|
let s2 = key_fn(s2);
|
||||||
|
n32(s1.custom_distance(&s2, &distance)) < distance_threshold.unwrap_or(0.05)
|
||||||
|
|| (s1.title.is_some()
|
||||||
|
&& s2.title.is_some()
|
||||||
|
&& s1.artist.is_some()
|
||||||
|
&& s2.artist.is_some()
|
||||||
|
&& s1.title == s2.title
|
||||||
|
&& s1.artist == s2.artist)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Return a list of albums in a `pool` of songs that are similar to
|
/// Return a list of albums in a `pool` of songs that are similar to
|
||||||
/// songs in `group`, discarding songs that don't belong to an album.
|
/// songs in `group`, discarding songs that don't belong to an album.
|
||||||
/// It basically makes an "album" playlist from the `pool` of songs.
|
/// It basically makes an "album" playlist from the `pool` of songs.
|
||||||
|
@ -203,6 +321,114 @@ pub fn closest_album_to_group(group: Vec<Song>, pool: Vec<Song>) -> BlissResult<
|
||||||
Ok(playlist)
|
Ok(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a list of albums in a `pool` of songs that are similar to
|
||||||
|
/// songs in `group`, discarding songs that don't belong to an album.
|
||||||
|
/// It basically makes an "album" playlist from the `pool` of songs.
|
||||||
|
///
|
||||||
|
/// `group` should be ordered by track number.
|
||||||
|
///
|
||||||
|
/// Songs from `group` would usually just be songs from an album, but not
|
||||||
|
/// necessarily - they are discarded from `pool` no matter what.
|
||||||
|
///
|
||||||
|
/// Order songs with a key extraction function, useful for when you have a
|
||||||
|
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||||||
|
/// you want to order.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `group` - A small group of songs, e.g. an album.
|
||||||
|
/// * `pool` - A pool of songs to find similar songs in, e.g. a user's song
|
||||||
|
/// library.
|
||||||
|
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of T, including `group` at the beginning, that you
|
||||||
|
/// most likely want to plug in your audio player by using something like
|
||||||
|
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
|
||||||
|
// TODO: maybe Clone is not needed?
|
||||||
|
pub fn closest_album_to_group_by_key<T: PartialEq + Clone, F>(
|
||||||
|
group: Vec<T>,
|
||||||
|
pool: Vec<T>,
|
||||||
|
key_fn: F,
|
||||||
|
) -> BlissResult<Vec<T>>
|
||||||
|
where
|
||||||
|
F: Fn(&T) -> Song,
|
||||||
|
{
|
||||||
|
let mut albums_analysis: HashMap<String, Array2<f32>> = HashMap::new();
|
||||||
|
let mut albums = Vec::new();
|
||||||
|
|
||||||
|
// Remove songs from the group from the pool.
|
||||||
|
let pool = pool
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| !group.contains(s))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for song in &pool {
|
||||||
|
let song = key_fn(song);
|
||||||
|
if let Some(album) = song.album {
|
||||||
|
if let Some(analysis) = albums_analysis.get_mut(&album as &str) {
|
||||||
|
analysis
|
||||||
|
.push_row(song.analysis.as_arr1().view())
|
||||||
|
.map_err(|e| {
|
||||||
|
BlissError::ProviderError(format!("while computing distances: {}", e))
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
let mut array = Array::zeros((1, song.analysis.as_arr1().len()));
|
||||||
|
array.assign(&song.analysis.as_arr1());
|
||||||
|
albums_analysis.insert(album.to_owned(), array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut group_analysis = Array::zeros((group.len(), NUMBER_FEATURES));
|
||||||
|
for (song, mut column) in group.iter().zip(group_analysis.axis_iter_mut(Axis(0))) {
|
||||||
|
let song = key_fn(song);
|
||||||
|
column.assign(&song.analysis.as_arr1());
|
||||||
|
}
|
||||||
|
let first_analysis = group_analysis
|
||||||
|
.mean_axis(Axis(0))
|
||||||
|
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
|
||||||
|
for (album, analysis) in albums_analysis.iter() {
|
||||||
|
let mean_analysis = analysis
|
||||||
|
.mean_axis(Axis(0))
|
||||||
|
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
|
||||||
|
let album = album.to_owned();
|
||||||
|
albums.push((album, mean_analysis.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
albums.sort_by_key(|(_, analysis)| n32(euclidean_distance(&first_analysis, analysis)));
|
||||||
|
let mut playlist = group;
|
||||||
|
for (album, _) in albums {
|
||||||
|
let mut al = pool
|
||||||
|
.iter()
|
||||||
|
.filter(|s| {
|
||||||
|
let s = key_fn(s);
|
||||||
|
s.album.is_some() && s.album.as_ref().unwrap() == &album.to_string()
|
||||||
|
})
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.collect::<Vec<T>>();
|
||||||
|
al.sort_by(|s1, s2| {
|
||||||
|
let s1 = key_fn(s1);
|
||||||
|
let s2 = key_fn(s2);
|
||||||
|
let track_number1 = s1
|
||||||
|
.track_number
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| String::from(""));
|
||||||
|
let track_number2 = s2
|
||||||
|
.track_number
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| String::from(""));
|
||||||
|
if let Ok(x) = track_number1.parse::<i32>() {
|
||||||
|
if let Ok(y) = track_number2.parse::<i32>() {
|
||||||
|
return x.cmp(&y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s1.track_number.cmp(&s2.track_number)
|
||||||
|
});
|
||||||
|
playlist.extend_from_slice(&al);
|
||||||
|
}
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -210,6 +436,12 @@ mod test {
|
||||||
use ndarray::arr1;
|
use ndarray::arr1;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
struct CustomSong {
|
||||||
|
something: bool,
|
||||||
|
bliss_song: Song,
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dedup_playlist_custom_distance() {
|
fn test_dedup_playlist_custom_distance() {
|
||||||
let first_song = Song {
|
let first_song = Song {
|
||||||
|
@ -316,6 +548,91 @@ mod test {
|
||||||
fourth_song.to_owned(),
|
fourth_song.to_owned(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let first_song = CustomSong {
|
||||||
|
bliss_song: first_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let second_song = CustomSong {
|
||||||
|
bliss_song: second_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let first_song_dupe = CustomSong {
|
||||||
|
bliss_song: first_song_dupe,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let third_song = CustomSong {
|
||||||
|
bliss_song: third_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let fourth_song = CustomSong {
|
||||||
|
bliss_song: fourth_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fifth_song = CustomSong {
|
||||||
|
bliss_song: fifth_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut playlist = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
dedup_playlist_custom_distance_by_key(&mut playlist, None, euclidean_distance, |s| {
|
||||||
|
s.bliss_song.to_owned()
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
playlist,
|
||||||
|
vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let mut playlist = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
dedup_playlist_custom_distance_by_key(&mut playlist, Some(20.), cosine_distance, |s| {
|
||||||
|
s.bliss_song.to_owned()
|
||||||
|
});
|
||||||
|
assert_eq!(playlist, vec![first_song.to_owned()]);
|
||||||
|
let mut playlist = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
dedup_playlist_by_key(&mut playlist, Some(20.), |s| s.bliss_song.to_owned());
|
||||||
|
assert_eq!(playlist, vec![first_song.to_owned()]);
|
||||||
|
let mut playlist = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
dedup_playlist_by_key(&mut playlist, None, |s| s.bliss_song.to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
playlist,
|
||||||
|
vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -358,20 +675,64 @@ mod test {
|
||||||
};
|
};
|
||||||
let mut songs = vec![
|
let mut songs = vec![
|
||||||
first_song.to_owned(),
|
first_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
first_song_dupe.to_owned(),
|
first_song_dupe.to_owned(),
|
||||||
second_song.to_owned(),
|
second_song.to_owned(),
|
||||||
third_song.to_owned(),
|
|
||||||
fourth_song.to_owned(),
|
fourth_song.to_owned(),
|
||||||
];
|
];
|
||||||
song_to_song(&first_song, &mut songs, euclidean_distance);
|
song_to_song(&first_song, &mut songs, euclidean_distance);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
songs,
|
songs,
|
||||||
vec![
|
vec![
|
||||||
first_song,
|
first_song.to_owned(),
|
||||||
first_song_dupe.to_owned(),
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_song = CustomSong {
|
||||||
|
bliss_song: first_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let second_song = CustomSong {
|
||||||
|
bliss_song: second_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let first_song_dupe = CustomSong {
|
||||||
|
bliss_song: first_song_dupe,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let third_song = CustomSong {
|
||||||
|
bliss_song: third_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let fourth_song = CustomSong {
|
||||||
|
bliss_song: fourth_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut songs: Vec<CustomSong> = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
];
|
||||||
|
|
||||||
|
song_to_song_by_key(&first_song, &mut songs, euclidean_distance, |s| {
|
||||||
|
s.bliss_song.to_owned()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
songs,
|
||||||
|
vec![
|
||||||
|
first_song,
|
||||||
|
first_song_dupe,
|
||||||
second_song,
|
second_song,
|
||||||
third_song,
|
third_song,
|
||||||
fourth_song
|
fourth_song,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -431,6 +792,46 @@ mod test {
|
||||||
fifth_song.to_owned(),
|
fifth_song.to_owned(),
|
||||||
];
|
];
|
||||||
closest_to_first_song(&first_song, &mut songs, euclidean_distance);
|
closest_to_first_song(&first_song, &mut songs, euclidean_distance);
|
||||||
|
|
||||||
|
let first_song = CustomSong {
|
||||||
|
bliss_song: first_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let second_song = CustomSong {
|
||||||
|
bliss_song: second_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let first_song_dupe = CustomSong {
|
||||||
|
bliss_song: first_song_dupe,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let third_song = CustomSong {
|
||||||
|
bliss_song: third_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let fourth_song = CustomSong {
|
||||||
|
bliss_song: fourth_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fifth_song = CustomSong {
|
||||||
|
bliss_song: fifth_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut songs: Vec<CustomSong> = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
|
||||||
|
closest_to_first_song_by_key(&first_song, &mut songs, euclidean_distance, |s| {
|
||||||
|
s.bliss_song.to_owned()
|
||||||
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
songs,
|
songs,
|
||||||
vec![
|
vec![
|
||||||
|
@ -538,5 +939,46 @@ mod test {
|
||||||
],
|
],
|
||||||
closest_album_to_group(group, pool.to_owned()).unwrap(),
|
closest_album_to_group(group, pool.to_owned()).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let first_song = CustomSong {
|
||||||
|
bliss_song: first_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let second_song = CustomSong {
|
||||||
|
bliss_song: second_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let third_song = CustomSong {
|
||||||
|
bliss_song: third_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
let fourth_song = CustomSong {
|
||||||
|
bliss_song: fourth_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fifth_song = CustomSong {
|
||||||
|
bliss_song: fifth_song,
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
let group = vec![first_song.to_owned(), third_song.to_owned()];
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
second_song.to_owned()
|
||||||
|
],
|
||||||
|
closest_album_to_group_by_key(group, pool.to_owned(), |s| s.bliss_song.to_owned())
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ pub struct Song {
|
||||||
/// Song's album's artist name, read from the metadata
|
/// Song's album's artist name, read from the metadata
|
||||||
pub album_artist: Option<String>,
|
pub album_artist: Option<String>,
|
||||||
/// Song's tracked number, read from the metadata
|
/// Song's tracked number, read from the metadata
|
||||||
|
/// TODO normalize this into an integer
|
||||||
pub track_number: Option<String>,
|
pub track_number: Option<String>,
|
||||||
/// Song's genre, read from the metadata (`""` if empty)
|
/// Song's genre, read from the metadata (`""` if empty)
|
||||||
pub genre: Option<String>,
|
pub genre: Option<String>,
|
||||||
|
|
Loading…
Reference in a new issue