Final touches
This commit is contained in:
parent
fa3d467536
commit
40f8e399c9
5 changed files with 188 additions and 27 deletions
|
@ -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]
|
||||||
|
|
|
@ -12,9 +12,16 @@ use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
// A config structure, that will be serialized as a
|
||||||
|
// JSON file upon Library creation.
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
// The base configuration, containing both the config file
|
||||||
|
// path, as well as the database path.
|
||||||
pub base_config: BaseConfig,
|
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,
|
pub music_library_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +39,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The AppConfigTrait must know how to access the base config.
|
||||||
impl AppConfigTrait for Config {
|
impl AppConfigTrait for Config {
|
||||||
fn base_config(&self) -> &BaseConfig {
|
fn base_config(&self) -> &BaseConfig {
|
||||||
&self.base_config
|
&self.base_config
|
||||||
|
@ -42,6 +50,18 @@ impl AppConfigTrait for 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 {
|
trait CustomLibrary {
|
||||||
fn song_paths(&self) -> Result<Vec<String>>;
|
fn song_paths(&self) -> Result<Vec<String>>;
|
||||||
}
|
}
|
||||||
|
@ -63,6 +83,10 @@ impl CustomLibrary for Library<Config> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<()> {
|
fn main() -> Result<()> {
|
||||||
let matches = App::new("library-example")
|
let matches = App::new("library-example")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/// Basic example of how one would combine bliss with an "audio player",
|
/// Basic example of how one would combine bliss with an "audio player",
|
||||||
/// through [Library].
|
/// 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
|
/// For simplicity's sake, this example recursively gets songs from a folder
|
||||||
/// to emulate an audio player library, without handling CUE files.
|
/// to emulate an audio player library, without handling CUE files.
|
||||||
|
@ -12,9 +13,16 @@ use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
/// A config structure, that will be serialized as a
|
||||||
|
/// JSON file upon Library creation.
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
/// The base configuration, containing both the config file
|
||||||
|
/// path, as well as the database path.
|
||||||
pub base_config: BaseConfig,
|
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,
|
pub music_library_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +40,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The AppConfigTrait must know how to access the base config.
|
||||||
impl AppConfigTrait for Config {
|
impl AppConfigTrait for Config {
|
||||||
fn base_config(&self) -> &BaseConfig {
|
fn base_config(&self) -> &BaseConfig {
|
||||||
&self.base_config
|
&self.base_config
|
||||||
|
@ -42,12 +51,25 @@ impl AppConfigTrait for 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 {
|
trait CustomLibrary {
|
||||||
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>>;
|
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomLibrary for Library<Config> {
|
impl CustomLibrary for Library<Config> {
|
||||||
/// Get all songs in the player library
|
/// 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)>> {
|
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>> {
|
||||||
let music_path = &self.config.music_library_path;
|
let music_path = &self.config.music_library_path;
|
||||||
let pattern = Path::new(&music_path).join("**").join("*");
|
let pattern = Path::new(&music_path).join("**").join("*");
|
||||||
|
@ -71,12 +93,18 @@ impl CustomLibrary for Library<Config> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)]
|
#[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 {
|
struct ExtraInfo {
|
||||||
extension: Option<String>,
|
extension: Option<String>,
|
||||||
file_name: Option<String>,
|
file_name: Option<String>,
|
||||||
mime_type: 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<()> {
|
fn main() -> Result<()> {
|
||||||
let matches = App::new("library-example")
|
let matches = App::new("library-example")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
149
src/library.rs
149
src/library.rs
|
@ -1,4 +1,111 @@
|
||||||
//! Module containing utilities to manage a SQLite library of [Song]s.
|
//! Module containing utilities to properly manage a library of [Song]s,
|
||||||
|
//! for people wanting to e.g. implement a bliss plugin for an existing
|
||||||
|
//! audio player. A good resource to look at for inspiration is
|
||||||
|
//! [blissify](https://github.com/Polochon-street/blissify-rs)'s source code.
|
||||||
|
//!
|
||||||
|
//! Useful to have direct and easy access to functions that analyze
|
||||||
|
//! and store analysis of songs in a SQLite database, as well as retrieve it,
|
||||||
|
//! and make playlists directly from analyzed songs. All functions are as
|
||||||
|
//! thoroughly tested as possible, so you don't have to do it yourself,
|
||||||
|
//! including for instance bliss features version handling, etc.
|
||||||
|
//!
|
||||||
|
//! It works in three parts:
|
||||||
|
//! * The first part is the configuration part, which allows you to
|
||||||
|
//! specify extra information that your plugin might need that will
|
||||||
|
//! be automatically stored / retrieved when you instanciate a
|
||||||
|
//! [Library] (the core of your plugin).
|
||||||
|
//!
|
||||||
|
//! To do so implies specifying a configuration struct, that will implement
|
||||||
|
//! [AppConfigTrait], i.e. implement `Serialize`, `Deserialize`, and a
|
||||||
|
//! function to retrieve the [BaseConfig] (which is just a structure
|
||||||
|
//! holding the path to the configuration file and the path to the database).
|
||||||
|
//!
|
||||||
|
//! The most straightforward way to do so is to have something like this (
|
||||||
|
//! in this example, we assume that `path_to_extra_information` is something
|
||||||
|
//! you would want stored in your configuration file, path to a second music
|
||||||
|
//! folder for instance:
|
||||||
|
//! ```
|
||||||
|
//! use anyhow::Result;
|
||||||
|
//! use serde::{Deserialize, Serialize};
|
||||||
|
//! use std::path::PathBuf;
|
||||||
|
//! use bliss_audio::BlissError;
|
||||||
|
//! use bliss_audio::library::{AppConfigTrait, BaseConfig};
|
||||||
|
//!
|
||||||
|
//! #[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
//! pub struct Config {
|
||||||
|
//! #[serde(flatten)]
|
||||||
|
//! pub base_config: BaseConfig,
|
||||||
|
//! pub music_library_path: PathBuf,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! impl AppConfigTrait for Config {
|
||||||
|
//! fn base_config(&self) -> &BaseConfig {
|
||||||
|
//! &self.base_config
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! fn base_config_mut(&mut self) -> &mut BaseConfig {
|
||||||
|
//! &mut self.base_config
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! impl Config {
|
||||||
|
//! pub fn new(
|
||||||
|
//! music_library_path: PathBuf,
|
||||||
|
//! config_path: Option<PathBuf>,
|
||||||
|
//! database_path: Option<PathBuf>,
|
||||||
|
//! ) -> Result<Self> {
|
||||||
|
//! // Note that by passing `(None, None)` here, the paths will
|
||||||
|
//! // be inferred automatically using user data dirs.
|
||||||
|
//! let base_config = BaseConfig::new(config_path, database_path)?;
|
||||||
|
//! Ok(Self {
|
||||||
|
//! base_config,
|
||||||
|
//! music_library_path,
|
||||||
|
//! })
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//! * The second part is the actual [Library] structure, that makes the
|
||||||
|
//! bulk of the plug-in. To initialize a library once with a given config,
|
||||||
|
//! you can do (here with a base configuration):
|
||||||
|
//! ```no_run
|
||||||
|
//! use anyhow::{Error, Result};
|
||||||
|
//! use bliss_audio::library::{BaseConfig, Library};
|
||||||
|
//! use std::path::PathBuf;
|
||||||
|
//!
|
||||||
|
//! let config_path = Some(PathBuf::from("path/to/config/config.json"));
|
||||||
|
//! let database_path = Some(PathBuf::from("path/to/config/bliss.db"));
|
||||||
|
//! let config = BaseConfig::new(config_path, database_path)?;
|
||||||
|
//! let library: Library<BaseConfig> = Library::new(config)?;
|
||||||
|
//! # Ok::<(), Error>(())
|
||||||
|
//! ```
|
||||||
|
//! Once this is done, you can simply load the library by doing
|
||||||
|
//! `Library::from_config_path(config_path);`
|
||||||
|
//! * The third part is using the [Library] itself: it provides you with
|
||||||
|
//! utilies such as [Library::analyze_paths], which analyzes all songs
|
||||||
|
//! in given paths and stores it in the databases, as well as
|
||||||
|
//! [Library::playlist_from], which allows you to generate a playlist
|
||||||
|
//! from any given analyzed song.
|
||||||
|
//!
|
||||||
|
//! The [Library] structure also comes with a [LibrarySong] song struct,
|
||||||
|
//! which represents a song stored in the database.
|
||||||
|
//!
|
||||||
|
//! It is made of a `bliss_song` field, containing the analyzed bliss
|
||||||
|
//! song (with the normal metatada such as the artist, etc), and an
|
||||||
|
//! `extra_info` field, which can be any user-defined serialized struct.
|
||||||
|
//! For most use cases, it would just be the unit type `()` (which is no
|
||||||
|
//! extra info), that would be used like
|
||||||
|
//! `library.playlist_from<()>(song, path, playlist_length)`,
|
||||||
|
//! but functions such as [Library::analyze_paths_extra_info] and
|
||||||
|
//! [Library::analyze_paths_convert_extra_info] let you customize what
|
||||||
|
//! information you store for each song.
|
||||||
|
//!
|
||||||
|
//! The files in
|
||||||
|
//! [examples/library.rs](https://github.com/Polochon-street/bliss-rs/blob/master/examples/library.rs)
|
||||||
|
//! and
|
||||||
|
//! [examples/libray_extra_info.rs](https://github.com/Polochon-street/bliss-rs/blob/master/examples/library_extra_info.rs)
|
||||||
|
//! should provide the user with enough information to start with. For a more
|
||||||
|
//! "real-life" example, the
|
||||||
|
//! [blissify](https://github.com/Polochon-street/blissify-rs)'s code is using
|
||||||
|
//! [Library] to implement bliss for a MPD player.
|
||||||
use crate::analyze_paths;
|
use crate::analyze_paths;
|
||||||
use crate::cue::CueInfo;
|
use crate::cue::CueInfo;
|
||||||
use crate::playlist::closest_album_to_group_by_key;
|
use crate::playlist::closest_album_to_group_by_key;
|
||||||
|
@ -92,15 +199,6 @@ pub trait AppConfigTrait: Serialize + Sized + DeserializeOwned {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actual configuration trait that will be used.
|
|
||||||
pub trait ConfigTrait: AppConfigTrait {
|
|
||||||
/// Do some specific configuration things.
|
|
||||||
fn do_config_things(&self) {
|
|
||||||
let config = self.base_config();
|
|
||||||
config.do_config_things()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
||||||
/// The minimum configuration an application needs to work with
|
/// The minimum configuration an application needs to work with
|
||||||
/// a [Library].
|
/// a [Library].
|
||||||
|
@ -153,11 +251,8 @@ impl BaseConfig {
|
||||||
features_version: FEATURES_VERSION,
|
features_version: FEATURES_VERSION,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_config_things(&self) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<App: AppConfigTrait> ConfigTrait for App {}
|
|
||||||
impl AppConfigTrait for BaseConfig {
|
impl AppConfigTrait for BaseConfig {
|
||||||
fn base_config(&self) -> &BaseConfig {
|
fn base_config(&self) -> &BaseConfig {
|
||||||
self
|
self
|
||||||
|
@ -215,14 +310,17 @@ pub struct LibrarySong<T: Serialize + DeserializeOwned> {
|
||||||
// TODO add full rescan
|
// TODO add full rescan
|
||||||
// TODO a song_from_path with custom filters
|
// TODO a song_from_path with custom filters
|
||||||
// TODO "smart" playlist
|
// TODO "smart" playlist
|
||||||
impl<Config: ConfigTrait> Library<Config> {
|
// TODO should it really use anyhow errors?
|
||||||
/// Create a new [Library] object from the given [Config] struct,
|
impl<Config: AppConfigTrait> Library<Config> {
|
||||||
|
/// Create a new [Library] object from the given Config struct that
|
||||||
|
/// implements the [AppConfigTrait].
|
||||||
/// writing the configuration to the file given in
|
/// writing the configuration to the file given in
|
||||||
/// `config.config_path`.
|
/// `config.config_path`.
|
||||||
///
|
///
|
||||||
/// This function should only be called once, when a user wishes to
|
/// This function should only be called once, when a user wishes to
|
||||||
/// create a completely new "library".
|
/// create a completely new "library".
|
||||||
/// Otherwise, load an existing library file using [Library::from_config].
|
/// Otherwise, load an existing library file using
|
||||||
|
/// [Library::from_config_path].
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
pub fn new(config: Config) -> Result<Self> {
|
||||||
if !config
|
if !config
|
||||||
.base_config()
|
.base_config()
|
||||||
|
@ -373,11 +471,12 @@ impl<Config: ConfigTrait> Library<Config> {
|
||||||
/// `true`.
|
/// `true`.
|
||||||
///
|
///
|
||||||
/// You can use ready to use distance metrics such as
|
/// You can use ready to use distance metrics such as
|
||||||
/// [playlist::euclidean_distance], and ready to use sorting functions like
|
/// [euclidean_distance], and ready to use sorting functions like
|
||||||
/// [playlist::closest_to_first_song_by_key].
|
/// [closest_to_first_song_by_key].
|
||||||
///
|
///
|
||||||
/// In most cases, you just want to use [playlist_from]. Use this if you want
|
/// In most cases, you just want to use [Library::playlist_from].
|
||||||
/// to experiment with different distance metrics / sorting functions.
|
/// Use `playlist_from_custom` if you want to experiment with different
|
||||||
|
/// distance metrics / sorting functions.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// `library.playlist_from_song_custom(song_path, 20, euclidean_distance,
|
/// `library.playlist_from_song_custom(song_path, 20, euclidean_distance,
|
||||||
|
@ -484,7 +583,9 @@ impl<Config: ConfigTrait> Library<Config> {
|
||||||
/// that can't directly be serializable,
|
/// that can't directly be serializable,
|
||||||
/// or that need input from the analyzed Song to be processed. If you
|
/// or that need input from the analyzed Song to be processed. If you
|
||||||
/// just want to analyze and store songs along with some directly
|
/// just want to analyze and store songs along with some directly
|
||||||
/// serializable values, consider using [update_library_extra_info].
|
/// serializable values, consider using [Library::update_library_extra_info],
|
||||||
|
/// or [Library::update_library] if you just want the analyzed songs
|
||||||
|
/// stored as is.
|
||||||
///
|
///
|
||||||
/// `paths_extra_info` is a tuple made out of song paths, along
|
/// `paths_extra_info` is a tuple made out of song paths, along
|
||||||
/// with any extra info you want to store for each song.
|
/// with any extra info you want to store for each song.
|
||||||
|
@ -493,7 +594,7 @@ impl<Config: ConfigTrait> Library<Config> {
|
||||||
/// CUE track names: passing `vec![file.cue]` will add
|
/// CUE track names: passing `vec![file.cue]` will add
|
||||||
/// individual tracks with the `cue_info` field set in the database.
|
/// individual tracks with the `cue_info` field set in the database.
|
||||||
///
|
///
|
||||||
/// `convert_extra_info` is a function that you should specify
|
/// `convert_extra_info` is a function that you should specify how
|
||||||
/// to convert that extra info to something serializable.
|
/// to convert that extra info to something serializable.
|
||||||
// TODO have a `delete` option
|
// TODO have a `delete` option
|
||||||
pub fn update_library_convert_extra_info<
|
pub fn update_library_convert_extra_info<
|
||||||
|
@ -590,8 +691,8 @@ impl<Config: ConfigTrait> Library<Config> {
|
||||||
/// or that need input from the analyzed Song to be processed.
|
/// or that need input from the analyzed Song to be processed.
|
||||||
/// If you just want to analyze and store songs, along with some
|
/// If you just want to analyze and store songs, along with some
|
||||||
/// directly serializable metadata values, consider using
|
/// directly serializable metadata values, consider using
|
||||||
/// [analyze_paths_extra_info], or [analyze_paths] for the simpler
|
/// [Library::analyze_paths_extra_info], or [Library::analyze_paths] for
|
||||||
/// use cases.
|
/// the simpler use cases.
|
||||||
///
|
///
|
||||||
/// Updates the value of `features_version` in the config, using bliss'
|
/// Updates the value of `features_version` in the config, using bliss'
|
||||||
/// latest version.
|
/// latest version.
|
||||||
|
|
Loading…
Reference in a new issue