Added album query function

This commit is contained in:
G2-Games 2023-11-03 08:39:19 -05:00
parent 085927caa1
commit eeceb27800
3 changed files with 113 additions and 52 deletions

View file

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

View file

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

View file

@ -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 {
Some(value) => value, Tag::Field(target) => match track.get_field(&target) {
None => continue, Some(value) => value,
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)
}
} }