Final touches

This commit is contained in:
Polochon-street 2022-09-28 22:41:59 +02:00
parent fa3d467536
commit 40f8e399c9
5 changed files with 188 additions and 27 deletions

View file

@ -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]

View file

@ -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"))

View file

@ -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"))

View file

@ -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

View file

@ -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.