Add a proper library / config example

This commit is contained in:
Polochon-street 2022-07-13 22:47:05 +02:00
parent c3f288eb71
commit 661d848331
9 changed files with 2686 additions and 216 deletions

View file

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

File diff suppressed because it is too large Load diff

View file

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

174
examples/library.rs Normal file
View file

@ -0,0 +1,174 @@
/// 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.
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)]
pub struct Config {
#[serde(flatten)]
pub base_config: BaseConfig,
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,
})
}
}
impl AppConfigTrait for Config {
fn base_config(&self) -> &BaseConfig {
&self.base_config
}
}
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>>())
}
}
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(())
}

View file

@ -0,0 +1,194 @@
/// 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.
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)]
pub struct Config {
#[serde(flatten)]
pub base_config: BaseConfig,
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,
})
}
}
impl AppConfigTrait for Config {
fn base_config(&self) -> &BaseConfig {
&self.base_config
}
}
trait CustomLibrary {
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>>;
}
impl CustomLibrary for Library<Config> {
/// Get all songs in the player library
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)]
struct ExtraInfo {
extension: Option<String>,
file_name: Option<String>,
mime_type: String,
// TODO add mime-type so it's more real
}
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(())
}

View file

@ -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.");
}

View file

@ -59,6 +59,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;

1843
src/library.rs Normal file

File diff suppressed because it is too large Load diff

View file

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