2022-07-13 21:47:05 +01:00
|
|
|
/// Basic example of how one would combine bliss with an "audio player",
|
2022-09-28 21:41:59 +01:00
|
|
|
/// through [Library], showing how to put extra info in the database for
|
|
|
|
/// each song.
|
2022-07-13 21:47:05 +01:00
|
|
|
///
|
|
|
|
/// For simplicity's sake, this example recursively gets songs from a folder
|
2022-09-15 18:03:56 +01:00
|
|
|
/// to emulate an audio player library, without handling CUE files.
|
2022-07-13 21:47:05 +01:00
|
|
|
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;
|
2022-10-09 11:07:34 +01:00
|
|
|
use std::num::NonZeroUsize;
|
2022-07-13 21:47:05 +01:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
2022-09-28 21:41:59 +01:00
|
|
|
/// A config structure, that will be serialized as a
|
|
|
|
/// JSON file upon Library creation.
|
2022-07-13 21:47:05 +01:00
|
|
|
pub struct Config {
|
|
|
|
#[serde(flatten)]
|
2022-09-28 21:41:59 +01:00
|
|
|
/// The base configuration, containing both the config file
|
|
|
|
/// path, as well as the database path.
|
2022-07-13 21:47:05 +01:00
|
|
|
pub base_config: BaseConfig,
|
2022-09-28 21:41:59 +01:00
|
|
|
/// An extra field, to store the music library path. Any number
|
|
|
|
/// of arbitrary fields (even Serializable structures) can
|
|
|
|
/// of course be added.
|
2022-07-13 21:47:05 +01:00
|
|
|
pub music_library_path: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Config {
|
|
|
|
pub fn new(
|
|
|
|
music_library_path: PathBuf,
|
|
|
|
config_path: Option<PathBuf>,
|
|
|
|
database_path: Option<PathBuf>,
|
2022-10-09 11:07:34 +01:00
|
|
|
number_cores: Option<NonZeroUsize>,
|
2022-07-13 21:47:05 +01:00
|
|
|
) -> Result<Self> {
|
2022-09-28 22:33:14 +01:00
|
|
|
let base_config = BaseConfig::new(config_path, database_path, number_cores)?;
|
2022-07-13 21:47:05 +01:00
|
|
|
Ok(Self {
|
|
|
|
base_config,
|
|
|
|
music_library_path,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-28 21:41:59 +01:00
|
|
|
// The AppConfigTrait must know how to access the base config.
|
2022-07-13 21:47:05 +01:00
|
|
|
impl AppConfigTrait for Config {
|
|
|
|
fn base_config(&self) -> &BaseConfig {
|
|
|
|
&self.base_config
|
|
|
|
}
|
2022-09-15 18:03:56 +01:00
|
|
|
|
|
|
|
fn base_config_mut(&mut self) -> &mut BaseConfig {
|
|
|
|
&mut self.base_config
|
|
|
|
}
|
2022-07-13 21:47:05 +01:00
|
|
|
}
|
|
|
|
|
2022-09-28 21:41:59 +01:00
|
|
|
// 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.
|
2022-07-13 21:47:05 +01:00
|
|
|
trait CustomLibrary {
|
|
|
|
fn song_paths_info(&self) -> Result<Vec<(String, ExtraInfo)>>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CustomLibrary for Library<Config> {
|
2022-09-28 21:41:59 +01:00
|
|
|
/// Get all songs in the player library, along with the extra info
|
|
|
|
/// one would want to store along with each song.
|
2022-07-13 21:47:05 +01:00
|
|
|
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)]
|
2022-09-28 21:41:59 +01:00
|
|
|
// An (somewhat simple) example of what extra metadata one would put, along
|
|
|
|
// with song analysis data.
|
2022-07-13 21:47:05 +01:00
|
|
|
struct ExtraInfo {
|
|
|
|
extension: Option<String>,
|
|
|
|
file_name: Option<String>,
|
|
|
|
mime_type: String,
|
|
|
|
}
|
|
|
|
|
2022-09-28 21:41:59 +01:00
|
|
|
// 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`.
|
2022-07-13 21:47:05 +01:00
|
|
|
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);
|
|
|
|
|
2022-09-28 22:33:14 +01:00
|
|
|
let config = Config::new(folder, config_path, database_path, None)?;
|
2022-07-13 21:47:05 +01:00
|
|
|
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(())
|
|
|
|
}
|