mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-20 04:02:54 -05:00
Added album query function
This commit is contained in:
parent
085927caa1
commit
eeceb27800
3 changed files with 113 additions and 52 deletions
|
@ -39,3 +39,5 @@ log = "0.4"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
cue = "2.0.0"
|
cue = "2.0.0"
|
||||||
jwalk = "0.8.1"
|
jwalk = "0.8.1"
|
||||||
|
base64 = "0.21.5"
|
||||||
|
zip = "0.6.6"
|
||||||
|
|
|
@ -85,6 +85,6 @@ impl MusicController {
|
||||||
sort_by: Vec<Tag>,
|
sort_by: Vec<Tag>,
|
||||||
) -> Option<Vec<&Song>> {
|
) -> Option<Vec<&Song>> {
|
||||||
self.library
|
self.library
|
||||||
.query(query_string, &target_tags, search_location, &sort_by)
|
.query(query_string, &target_tags, &sort_by)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
use file_format::{FileFormat, Kind};
|
use file_format::{FileFormat, Kind};
|
||||||
use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt};
|
use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::{error::Error, io::BufReader};
|
use std::{error::Error, io::BufReader};
|
||||||
|
|
||||||
use chrono::{serde::ts_seconds_option, DateTime, Utc};
|
use chrono::{serde::ts_seconds_option, DateTime, Utc};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
//use walkdir::WalkDir;
|
|
||||||
use cue::cd::CD;
|
use cue::cd::CD;
|
||||||
use jwalk::WalkDir;
|
use jwalk::WalkDir;
|
||||||
|
|
||||||
use bincode::{deserialize_from, serialize_into};
|
use bincode::{deserialize_from, serialize_into};
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
|
@ -33,6 +34,7 @@ pub enum Tag {
|
||||||
Title,
|
Title,
|
||||||
Album,
|
Album,
|
||||||
Artist,
|
Artist,
|
||||||
|
AlbumArtist,
|
||||||
Genre,
|
Genre,
|
||||||
Comment,
|
Comment,
|
||||||
Track,
|
Track,
|
||||||
|
@ -47,6 +49,7 @@ impl ToString for Tag {
|
||||||
Self::Title => "TrackTitle".into(),
|
Self::Title => "TrackTitle".into(),
|
||||||
Self::Album => "AlbumTitle".into(),
|
Self::Album => "AlbumTitle".into(),
|
||||||
Self::Artist => "TrackArtist".into(),
|
Self::Artist => "TrackArtist".into(),
|
||||||
|
Self::AlbumArtist => "AlbumArtist".into(),
|
||||||
Self::Genre => "Genre".into(),
|
Self::Genre => "Genre".into(),
|
||||||
Self::Comment => "Comment".into(),
|
Self::Comment => "Comment".into(),
|
||||||
Self::Track => "TrackNumber".into(),
|
Self::Track => "TrackNumber".into(),
|
||||||
|
@ -103,6 +106,15 @@ impl Song {
|
||||||
match target_field {
|
match target_field {
|
||||||
"location" => Some(self.location.clone().path_string()),
|
"location" => Some(self.location.clone().path_string()),
|
||||||
"plays" => Some(self.plays.clone().to_string()),
|
"plays" => Some(self.plays.clone().to_string()),
|
||||||
|
"format" => {
|
||||||
|
match self.format {
|
||||||
|
Some(format) => match format.short_name() {
|
||||||
|
Some(short) => Some(short.to_string()),
|
||||||
|
None => None
|
||||||
|
},
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
},
|
||||||
_ => None, // Other field types are not yet supported
|
_ => None, // Other field types are not yet supported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +125,7 @@ pub enum URI {
|
||||||
Local(PathBuf),
|
Local(PathBuf),
|
||||||
Cue {
|
Cue {
|
||||||
location: PathBuf,
|
location: PathBuf,
|
||||||
|
index: usize,
|
||||||
start: Duration,
|
start: Duration,
|
||||||
end: Duration,
|
end: Duration,
|
||||||
},
|
},
|
||||||
|
@ -120,6 +133,17 @@ pub enum URI {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl URI {
|
impl URI {
|
||||||
|
pub fn index(&self) -> Result<&usize, Box<dyn Error>> {
|
||||||
|
match self {
|
||||||
|
URI::Local(_) => Err("\"Local\" has no stored index".into()),
|
||||||
|
URI::Remote(_, _) => Err("\"Remote\" has no stored index".into()),
|
||||||
|
URI::Cue {
|
||||||
|
index,
|
||||||
|
..
|
||||||
|
} => Ok(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the start time of a CUEsheet song, or an
|
/// Returns the start time of a CUEsheet song, or an
|
||||||
/// error if the URI is not a Cue variant
|
/// error if the URI is not a Cue variant
|
||||||
pub fn start(&self) -> Result<&Duration, Box<dyn Error>> {
|
pub fn start(&self) -> Result<&Duration, Box<dyn Error>> {
|
||||||
|
@ -127,9 +151,8 @@ impl URI {
|
||||||
URI::Local(_) => Err("\"Local\" has no starting time".into()),
|
URI::Local(_) => Err("\"Local\" has no starting time".into()),
|
||||||
URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
|
URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
|
||||||
URI::Cue {
|
URI::Cue {
|
||||||
location: _,
|
|
||||||
start,
|
start,
|
||||||
end: _,
|
..
|
||||||
} => Ok(start),
|
} => Ok(start),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,9 +164,8 @@ impl URI {
|
||||||
URI::Local(_) => Err("\"Local\" has no starting time".into()),
|
URI::Local(_) => Err("\"Local\" has no starting time".into()),
|
||||||
URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
|
URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
|
||||||
URI::Cue {
|
URI::Cue {
|
||||||
location: _,
|
|
||||||
start: _,
|
|
||||||
end,
|
end,
|
||||||
|
..
|
||||||
} => Ok(end),
|
} => Ok(end),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,8 +176,7 @@ impl URI {
|
||||||
URI::Local(location) => location,
|
URI::Local(location) => location,
|
||||||
URI::Cue {
|
URI::Cue {
|
||||||
location,
|
location,
|
||||||
start: _,
|
..
|
||||||
end: _,
|
|
||||||
} => location,
|
} => location,
|
||||||
URI::Remote(_, location) => location,
|
URI::Remote(_, location) => location,
|
||||||
}
|
}
|
||||||
|
@ -166,8 +187,7 @@ impl URI {
|
||||||
URI::Local(location) => location.as_path().to_string_lossy(),
|
URI::Local(location) => location.as_path().to_string_lossy(),
|
||||||
URI::Cue {
|
URI::Cue {
|
||||||
location,
|
location,
|
||||||
start: _,
|
..
|
||||||
end: _,
|
|
||||||
} => location.as_path().to_string_lossy(),
|
} => location.as_path().to_string_lossy(),
|
||||||
URI::Remote(_, location) => location.as_path().to_string_lossy(),
|
URI::Remote(_, location) => location.as_path().to_string_lossy(),
|
||||||
};
|
};
|
||||||
|
@ -182,30 +202,24 @@ pub enum Service {
|
||||||
Youtube,
|
Youtube,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Rework this entirely
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Playlist {
|
pub struct Album<'a> {
|
||||||
title: String,
|
pub title: &'a String,
|
||||||
cover_art: Box<Path>,
|
pub artist: Option<&'a String>,
|
||||||
|
pub cover: Option<&'a AlbumArt>,
|
||||||
|
pub tracks: Vec<&'a Song>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum MusicObject {
|
|
||||||
Song(Song),
|
|
||||||
Album(Playlist),
|
|
||||||
Playlist(Playlist),
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MusicLibrary {
|
pub struct MusicLibrary {
|
||||||
pub library: Vec<Song>,
|
pub library: Vec<Song>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalize(input_string: &String) -> String {
|
pub fn normalize(input_string: &String) -> String {
|
||||||
unidecode(input_string)
|
unidecode(input_string).chars().filter_map(|c: char| {
|
||||||
.to_ascii_lowercase()
|
let x = c.to_ascii_lowercase();
|
||||||
.replace(|c: char| !c.is_alphanumeric(), "")
|
c.is_alphanumeric().then_some(x)
|
||||||
|
}).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicLibrary {
|
impl MusicLibrary {
|
||||||
|
@ -218,7 +232,7 @@ impl MusicLibrary {
|
||||||
let global_config = &*config.read().unwrap();
|
let global_config = &*config.read().unwrap();
|
||||||
let mut library: Vec<Song> = Vec::new();
|
let mut library: Vec<Song> = Vec::new();
|
||||||
let mut backup_path = global_config.db_path.clone();
|
let mut backup_path = global_config.db_path.clone();
|
||||||
backup_path.set_extension("bkp");
|
backup_path.set_extension("tmp");
|
||||||
|
|
||||||
match global_config.db_path.try_exists() {
|
match global_config.db_path.try_exists() {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
|
@ -253,14 +267,12 @@ impl MusicLibrary {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
// The database exists, so rename it to `.bkp` and
|
// The database exists, so rename it to `.bkp` and
|
||||||
// write the new database file
|
// write the new database file
|
||||||
let mut backup_name = config.db_path.clone();
|
let mut writer_name = config.db_path.clone();
|
||||||
backup_name.set_extension("bkp");
|
writer_name.set_extension("tmp");
|
||||||
fs::rename(config.db_path.as_path(), backup_name.as_path())?;
|
let mut writer = BufWriter::new(fs::File::create(writer_name.to_path_buf())?);
|
||||||
|
|
||||||
// TODO: Make this save properly like in config.rs
|
|
||||||
|
|
||||||
let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?);
|
|
||||||
serialize_into(&mut writer, &self.library)?;
|
serialize_into(&mut writer, &self.library)?;
|
||||||
|
|
||||||
|
fs::rename(writer_name.as_path(), config.db_path.clone().as_path())?;
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
// Create the database if it does not exist
|
// Create the database if it does not exist
|
||||||
|
@ -273,6 +285,7 @@ impl MusicLibrary {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the library size in number of tracks
|
||||||
pub fn size(&self) -> usize {
|
pub fn size(&self) -> usize {
|
||||||
self.library.len()
|
self.library.len()
|
||||||
}
|
}
|
||||||
|
@ -314,7 +327,7 @@ impl MusicLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds all the music files within a specified folder
|
/// Finds all the music files within a specified folder
|
||||||
pub fn find_all_music(
|
pub fn scan_folder(
|
||||||
&mut self,
|
&mut self,
|
||||||
target_path: &str,
|
target_path: &str,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
@ -413,9 +426,12 @@ impl MusicLibrary {
|
||||||
ItemKey::TrackTitle => Tag::Title,
|
ItemKey::TrackTitle => Tag::Title,
|
||||||
ItemKey::TrackNumber => Tag::Track,
|
ItemKey::TrackNumber => Tag::Track,
|
||||||
ItemKey::TrackArtist => Tag::Artist,
|
ItemKey::TrackArtist => Tag::Artist,
|
||||||
|
ItemKey::AlbumArtist => Tag::AlbumArtist,
|
||||||
ItemKey::Genre => Tag::Genre,
|
ItemKey::Genre => Tag::Genre,
|
||||||
ItemKey::Comment => Tag::Comment,
|
ItemKey::Comment => Tag::Comment,
|
||||||
ItemKey::AlbumTitle => Tag::Album,
|
ItemKey::AlbumTitle => Tag::Album,
|
||||||
|
ItemKey::DiscNumber => Tag::Disk,
|
||||||
|
ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" => continue,
|
||||||
ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()),
|
ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()),
|
||||||
custom => Tag::Key(format!("{:?}", custom)),
|
custom => Tag::Key(format!("{:?}", custom)),
|
||||||
};
|
};
|
||||||
|
@ -423,7 +439,7 @@ impl MusicLibrary {
|
||||||
let value = match item.value() {
|
let value = match item.value() {
|
||||||
ItemValue::Text(value) => String::from(value),
|
ItemValue::Text(value) => String::from(value),
|
||||||
ItemValue::Locator(value) => String::from(value),
|
ItemValue::Locator(value) => String::from(value),
|
||||||
ItemValue::Binary(_) => String::from(""),
|
ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)),
|
||||||
};
|
};
|
||||||
|
|
||||||
tags.push((key, value))
|
tags.push((key, value))
|
||||||
|
@ -541,6 +557,7 @@ impl MusicLibrary {
|
||||||
let mut tags: Vec<(Tag, String)> = Vec::new();
|
let mut tags: Vec<(Tag, String)> = Vec::new();
|
||||||
tags.push((Tag::Album, album_title.clone()));
|
tags.push((Tag::Album, album_title.clone()));
|
||||||
tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone()));
|
tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone()));
|
||||||
|
tags.push((Tag::Track, (i + 1).to_string()));
|
||||||
match track.get_cdtext().read(cue::cd_text::PTI::Title) {
|
match track.get_cdtext().read(cue::cd_text::PTI::Title) {
|
||||||
Some(title) => tags.push((Tag::Title, title)),
|
Some(title) => tags.push((Tag::Title, title)),
|
||||||
None => match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) {
|
None => match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) {
|
||||||
|
@ -569,6 +586,7 @@ impl MusicLibrary {
|
||||||
let new_song = Song {
|
let new_song = Song {
|
||||||
location: URI::Cue {
|
location: URI::Cue {
|
||||||
location: audio_location,
|
location: audio_location,
|
||||||
|
index: i,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
@ -641,39 +659,53 @@ impl MusicLibrary {
|
||||||
|
|
||||||
/// Query the database, returning a list of [Song]s
|
/// Query the database, returning a list of [Song]s
|
||||||
///
|
///
|
||||||
/// The order in which the `sort_by` `Vec` is arranged
|
/// The order in which the sort by Vec is arranged
|
||||||
/// determines the output sorting
|
/// determines the output sorting.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```
|
||||||
|
/// query(
|
||||||
|
/// &String::from("query"),
|
||||||
|
/// &vec![
|
||||||
|
/// Tag::Title
|
||||||
|
/// ],
|
||||||
|
/// &vec![
|
||||||
|
/// Tag::Field("location".to_string()),
|
||||||
|
/// Tag::Album,
|
||||||
|
/// Tag::Disk,
|
||||||
|
/// Tag::Track,
|
||||||
|
/// ],
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// This would find all titles containing the sequence
|
||||||
|
/// "query", and would return the results sorted first
|
||||||
|
/// by path, then album, disk number, and finally track number.
|
||||||
pub fn query(
|
pub fn query(
|
||||||
&self,
|
&self,
|
||||||
query_string: &String, // The query itself
|
query_string: &String, // The query itself
|
||||||
target_tags: &Vec<Tag>, // The tags to search
|
target_tags: &Vec<Tag>, // The tags to search
|
||||||
search_location: bool, // Whether to search the location field or not
|
|
||||||
sort_by: &Vec<Tag>, // Tags to sort the resulting data by
|
sort_by: &Vec<Tag>, // Tags to sort the resulting data by
|
||||||
) -> Option<Vec<&Song>> {
|
) -> Option<Vec<&Song>> {
|
||||||
let songs = Arc::new(Mutex::new(Vec::new()));
|
let songs = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
self.library.par_iter().for_each(|track| {
|
self.library.par_iter().for_each(|track| {
|
||||||
for tag in target_tags {
|
for tag in target_tags {
|
||||||
let track_result = match track.get_tag(&tag) {
|
let track_result = match tag {
|
||||||
|
Tag::Field(target) => match track.get_field(&target) {
|
||||||
Some(value) => value,
|
Some(value) => value,
|
||||||
None => continue,
|
None => continue,
|
||||||
|
},
|
||||||
|
_ => match track.get_tag(&tag) {
|
||||||
|
Some(value) => value.to_owned(),
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if normalize(track_result).contains(&normalize(&query_string)) {
|
if normalize(&track_result).contains(&normalize(&query_string)) {
|
||||||
songs.lock().unwrap().push(track);
|
songs.lock().unwrap().push(track);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !search_location {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a URL in the song
|
|
||||||
if normalize(&track.location.path_string()).contains(&normalize(&query_string)) {
|
|
||||||
songs.lock().unwrap().push(track);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!");
|
let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!");
|
||||||
|
@ -732,4 +764,31 @@ impl MusicLibrary {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn albums(&self) -> Result<Vec<Album>, Box<dyn Error>> {
|
||||||
|
let mut albums: Vec<Album> = Vec::new();
|
||||||
|
for result in &self.library {
|
||||||
|
let title = match result.get_tag(&Tag::Album){
|
||||||
|
Some(title) => title,
|
||||||
|
None => continue
|
||||||
|
};
|
||||||
|
|
||||||
|
match albums.binary_search_by_key(&normalize(&title), |album| normalize(&album.title.to_owned())) {
|
||||||
|
Ok(pos) => {
|
||||||
|
albums[pos].tracks.push(result);
|
||||||
|
},
|
||||||
|
Err(pos) => {
|
||||||
|
let new_album = Album {
|
||||||
|
title,
|
||||||
|
artist: result.get_tag(&Tag::AlbumArtist),
|
||||||
|
tracks: vec![result],
|
||||||
|
cover: None,
|
||||||
|
};
|
||||||
|
albums.insert(pos, new_album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(albums)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue