Add a proper library / config example
This commit is contained in:
parent
c3f288eb71
commit
661d848331
9 changed files with 2686 additions and 216 deletions
6
.github/workflows/rust.yml
vendored
6
.github/workflows/rust.yml
vendored
|
@ -30,14 +30,16 @@ jobs:
|
|||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Run library tests
|
||||
run: cargo test --verbose --features=library
|
||||
- name: Run example tests
|
||||
run: cargo test --verbose --examples
|
||||
- name: Build benches
|
||||
run: cargo +nightly-2022-02-16 bench --verbose --features=bench --no-run
|
||||
- name: Build examples
|
||||
run: cargo build --examples --verbose --features=serde
|
||||
run: cargo build --examples --verbose --features=serde,library
|
||||
- name: Lint
|
||||
run: cargo clippy --examples --features=serde -- -D warnings
|
||||
run: cargo clippy --examples --features=serde,library -- -D warnings
|
||||
|
||||
build-test-lint-windows:
|
||||
name: Windows - build, test and lint
|
||||
|
|
635
Cargo.lock
generated
635
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
32
Cargo.toml
32
Cargo.toml
|
@ -23,6 +23,12 @@ ffmpeg-static = ["ffmpeg-next/static"]
|
|||
python-bindings = ["bliss-audio-aubio-rs/fftw3"]
|
||||
# Enable the benchmarks with `cargo +nightly bench --features=bench`
|
||||
bench = []
|
||||
library = [
|
||||
"serde", "dep:rusqlite", "dep:dirs", "dep:tempdir",
|
||||
"dep:anyhow", "dep:serde_ini", "dep:serde_json",
|
||||
"dep:indicatif",
|
||||
]
|
||||
serde = ["dep:serde"]
|
||||
|
||||
[dependencies]
|
||||
ripemd160 = "0.9.0"
|
||||
|
@ -44,13 +50,35 @@ thiserror = "1.0.24"
|
|||
bliss-audio-aubio-rs = "0.2.0"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||
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]
|
||||
mime_guess = "2.0.3"
|
||||
glob = "0.3.0"
|
||||
anyhow = "1.0.45"
|
||||
serde_json = "1.0.59"
|
||||
clap = "2.33.3"
|
||||
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
174
examples/library.rs
Normal 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(())
|
||||
}
|
194
examples/library_extra_info.rs
Normal file
194
examples/library_extra_info.rs
Normal 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(())
|
||||
}
|
|
@ -1,26 +1,16 @@
|
|||
#[cfg(feature = "serde")]
|
||||
use anyhow::Result;
|
||||
#[cfg(feature = "serde")]
|
||||
use bliss_audio::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance};
|
||||
#[cfg(feature = "serde")]
|
||||
use bliss_audio::{analyze_paths, Song};
|
||||
#[cfg(feature = "serde")]
|
||||
use clap::{App, Arg};
|
||||
#[cfg(feature = "serde")]
|
||||
use glob::glob;
|
||||
#[cfg(feature = "serde")]
|
||||
use std::env;
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fs;
|
||||
#[cfg(feature = "serde")]
|
||||
use std::io::BufReader;
|
||||
#[cfg(feature = "serde")]
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/* Analyzes a folder recursively, and make a playlist out of the file
|
||||
* provided by the user. */
|
||||
// How to use: ./playlist [-o file.m3u] [-a analysis.json] <folder> <file to start the playlist from>
|
||||
#[cfg(feature = "serde")]
|
||||
fn main() -> Result<()> {
|
||||
let matches = App::new("playlist")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
|
@ -103,8 +93,3 @@ fn main() -> Result<()> {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "serde"))]
|
||||
fn main() {
|
||||
println!("You need the serde feature enabled to run this file.");
|
||||
}
|
||||
|
|
|
@ -59,6 +59,8 @@
|
|||
#![warn(rustdoc::missing_doc_code_examples)]
|
||||
mod chroma;
|
||||
pub mod cue;
|
||||
#[cfg(feature = "library")]
|
||||
pub mod library;
|
||||
mod misc;
|
||||
pub mod playlist;
|
||||
mod song;
|
||||
|
|
1843
src/library.rs
Normal file
1843
src/library.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -62,6 +62,7 @@ pub struct Song {
|
|||
/// Song's album's artist name, read from the metadata
|
||||
pub album_artist: Option<String>,
|
||||
/// Song's tracked number, read from the metadata
|
||||
/// TODO normalize this into an integer
|
||||
pub track_number: Option<String>,
|
||||
/// Song's genre, read from the metadata (`""` if empty)
|
||||
pub genre: Option<String>,
|
||||
|
|
Loading…
Reference in a new issue