From 40f8e399c9137a933d494eb61f0b7537ff0ee2a2 Mon Sep 17 00:00:00 2001 From: Polochon-street Date: Wed, 28 Sep 2022 22:41:59 +0200 Subject: [PATCH] Final touches --- Cargo.toml | 2 +- examples/library.rs | 24 ++++++ examples/library_extra_info.rs | 32 ++++++- src/lib.rs | 8 ++ src/library.rs | 149 +++++++++++++++++++++++++++------ 5 files changed, 188 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 19194e9..7707af9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["audio", "analysis", "MIR", "playlist", "similarity"] readme = "README.md" [package.metadata.docs.rs] -features = ["bliss-audio-aubio-rs/rustdoc"] +features = ["bliss-audio-aubio-rs/rustdoc", "library"] no-default-features = true [features] diff --git a/examples/library.rs b/examples/library.rs index 4759d05..c7ea861 100644 --- a/examples/library.rs +++ b/examples/library.rs @@ -12,9 +12,16 @@ 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, } @@ -32,6 +39,7 @@ impl Config { } } +// The AppConfigTrait must know how to access the base config. impl AppConfigTrait for Config { fn base_config(&self) -> &BaseConfig { &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, +// 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>; } @@ -63,6 +83,10 @@ impl CustomLibrary for Library { } } +// 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")) diff --git a/examples/library_extra_info.rs b/examples/library_extra_info.rs index 6f27f3a..239f942 100644 --- a/examples/library_extra_info.rs +++ b/examples/library_extra_info.rs @@ -1,5 +1,6 @@ /// 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 /// to emulate an audio player library, without handling CUE files. @@ -12,9 +13,16 @@ 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, } @@ -32,6 +40,7 @@ impl Config { } } +// The AppConfigTrait must know how to access the base config. impl AppConfigTrait for Config { fn base_config(&self) -> &BaseConfig { &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, +// 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>; } impl CustomLibrary for Library { - /// 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> { let music_path = &self.config.music_library_path; let pattern = Path::new(&music_path).join("**").join("*"); @@ -71,12 +93,18 @@ impl CustomLibrary for Library { } #[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, file_name: Option, 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")) diff --git a/src/lib.rs b/src/lib.rs index 760e17a..19a51d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,14 @@ //! is as easy as computing distances between that song and the rest, and ordering //! 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 //! //! ### Analyze & compute the distance between two songs diff --git a/src/library.rs b/src/library.rs index 521ff03..8e07fc2 100644 --- a/src/library.rs +++ b/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, +//! database_path: Option, +//! ) -> Result { +//! // 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 = 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::cue::CueInfo; 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)] /// The minimum configuration an application needs to work with /// a [Library]. @@ -153,11 +251,8 @@ impl BaseConfig { features_version: FEATURES_VERSION, }) } - - fn do_config_things(&self) {} } -impl ConfigTrait for App {} impl AppConfigTrait for BaseConfig { fn base_config(&self) -> &BaseConfig { self @@ -215,14 +310,17 @@ pub struct LibrarySong { // TODO add full rescan // TODO a song_from_path with custom filters // TODO "smart" playlist -impl Library { - /// Create a new [Library] object from the given [Config] struct, +// TODO should it really use anyhow errors? +impl Library { + /// Create a new [Library] object from the given Config struct that + /// implements the [AppConfigTrait]. /// writing the configuration to the file given in /// `config.config_path`. /// /// This function should only be called once, when a user wishes to /// 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 { if !config .base_config() @@ -373,11 +471,12 @@ impl Library { /// `true`. /// /// You can use ready to use distance metrics such as - /// [playlist::euclidean_distance], and ready to use sorting functions like - /// [playlist::closest_to_first_song_by_key]. + /// [euclidean_distance], and ready to use sorting functions like + /// [closest_to_first_song_by_key]. /// - /// In most cases, you just want to use [playlist_from]. Use this if you want - /// to experiment with different distance metrics / sorting functions. + /// In most cases, you just want to use [Library::playlist_from]. + /// Use `playlist_from_custom` if you want to experiment with different + /// distance metrics / sorting functions. /// /// Example: /// `library.playlist_from_song_custom(song_path, 20, euclidean_distance, @@ -484,7 +583,9 @@ impl Library { /// that can't directly be serializable, /// or that need input from the analyzed Song to be processed. If you /// 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 /// with any extra info you want to store for each song. @@ -493,7 +594,7 @@ impl Library { /// CUE track names: passing `vec![file.cue]` will add /// 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. // TODO have a `delete` option pub fn update_library_convert_extra_info< @@ -590,8 +691,8 @@ impl Library { /// or that need input from the analyzed Song to be processed. /// If you just want to analyze and store songs, along with some /// directly serializable metadata values, consider using - /// [analyze_paths_extra_info], or [analyze_paths] for the simpler - /// use cases. + /// [Library::analyze_paths_extra_info], or [Library::analyze_paths] for + /// the simpler use cases. /// /// Updates the value of `features_version` in the config, using bliss' /// latest version.