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"
[package.metadata.docs.rs]
features = ["bliss-audio-aubio-rs/rustdoc"]
features = ["bliss-audio-aubio-rs/rustdoc", "library"]
no-default-features = true
[features]

View file

@ -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<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>>;
}
@ -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<()> {
let matches = App::new("library-example")
.version(env!("CARGO_PKG_VERSION"))

View file

@ -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<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
/// 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("*");
@ -71,12 +93,18 @@ impl CustomLibrary for Library<Config> {
}
#[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"))

View file

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

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::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<App: AppConfigTrait> ConfigTrait for App {}
impl AppConfigTrait for BaseConfig {
fn base_config(&self) -> &BaseConfig {
self
@ -215,14 +310,17 @@ pub struct LibrarySong<T: Serialize + DeserializeOwned> {
// TODO add full rescan
// TODO a song_from_path with custom filters
// TODO "smart" playlist
impl<Config: ConfigTrait> Library<Config> {
/// Create a new [Library] object from the given [Config] struct,
// TODO should it really use anyhow errors?
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
/// `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<Self> {
if !config
.base_config()
@ -373,11 +471,12 @@ impl<Config: ConfigTrait> Library<Config> {
/// `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<Config: ConfigTrait> Library<Config> {
/// 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<Config: ConfigTrait> Library<Config> {
/// 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<Config: ConfigTrait> Library<Config> {
/// 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.