dmp-core/src/music_storage/library.rs
2024-01-12 23:47:00 -05:00

968 lines
32 KiB
Rust

use super::music_collection::MusicCollection;
// Crate things
use super::utils::{find_images, normalize, read_library, write_library};
use crate::music_controller::config::Config;
// Various std things
use std::collections::BTreeMap;
use std::error::Error;
use std::ops::ControlFlow::{Break, Continue};
// Files
use file_format::{FileFormat, Kind};
use glib::filename_to_uri;
use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt};
use rcue::parser::parse_from_file;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
// Time
use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
use std::time::Duration;
// Serialization/Compression
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
// Fun parallel stuff
use rayon::prelude::*;
use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum AlbumArt {
Embedded(usize),
External(URI),
}
impl AlbumArt {
pub fn uri(&self) -> Option<&URI> {
match self {
Self::Embedded(_) => None,
Self::External(uri) => Some(uri),
}
}
}
/// A tag for a song
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum Tag {
Title,
Album,
Artist,
AlbumArtist,
Genre,
Comment,
Track,
Disk,
Key(String),
Field(String),
}
impl ToString for Tag {
fn to_string(&self) -> String {
match self {
Self::Title => "TrackTitle".into(),
Self::Album => "AlbumTitle".into(),
Self::Artist => "TrackArtist".into(),
Self::AlbumArtist => "AlbumArtist".into(),
Self::Genre => "Genre".into(),
Self::Comment => "Comment".into(),
Self::Track => "TrackNumber".into(),
Self::Disk => "DiscNumber".into(),
Self::Key(key) => key.into(),
Self::Field(f) => f.into(),
}
}
}
/// A field within a Song struct
#[derive(Debug)]
pub enum Field {
Location(URI),
Plays(i32),
Skips(i32),
Favorited(bool),
Rating(u8),
Format(FileFormat),
Duration(Duration),
PlayTime(Duration),
LastPlayed(DateTime<Utc>),
DateAdded(DateTime<Utc>),
DateModified(DateTime<Utc>),
}
impl ToString for Field {
fn to_string(&self) -> String {
match self {
Self::Location(location) => location.to_string(),
Self::Plays(plays) => plays.to_string(),
Self::Skips(skips) => skips.to_string(),
Self::Favorited(fav) => fav.to_string(),
Self::Rating(rating) => rating.to_string(),
Self::Format(format) => match format.short_name() {
Some(name) => name.to_string(),
None => format.to_string(),
},
Self::Duration(duration) => duration.as_millis().to_string(),
Self::PlayTime(time) => time.as_millis().to_string(),
Self::LastPlayed(last) => last.to_rfc2822(),
Self::DateAdded(added) => added.to_rfc2822(),
Self::DateModified(modified) => modified.to_rfc2822(),
}
}
}
/// Stores information about a single song
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Song {
pub location: URI,
pub plays: i32,
pub skips: i32,
pub favorited: bool,
pub rating: Option<u8>,
pub format: Option<FileFormat>,
pub duration: Duration,
pub play_time: Duration,
#[serde(with = "ts_milliseconds_option")]
pub last_played: Option<DateTime<Utc>>,
#[serde(with = "ts_milliseconds_option")]
pub date_added: Option<DateTime<Utc>>,
#[serde(with = "ts_milliseconds_option")]
pub date_modified: Option<DateTime<Utc>>,
pub album_art: Vec<AlbumArt>,
pub tags: BTreeMap<Tag, String>,
}
impl Song {
/// Get a tag's value
///
/// ```
/// use dango_core::music_storage::music_db::Tag;
/// // Assuming an already created song:
///
/// let tag = this_song.get_tag(Tag::Title);
///
/// assert_eq!(tag, "Some Song Title");
/// ```
pub fn get_tag(&self, target_key: &Tag) -> Option<&String> {
self.tags.get(target_key)
}
/// Gets an internal field from a song
pub fn get_field(&self, target_field: &str) -> Option<Field> {
let lower_target = target_field.to_lowercase();
match lower_target.as_str() {
"location" => Some(Field::Location(self.location.clone())),
"plays" => Some(Field::Plays(self.plays)),
"skips" => Some(Field::Skips(self.skips)),
"favorited" => Some(Field::Favorited(self.favorited)),
"rating" => self.rating.map(Field::Rating),
"duration" => Some(Field::Duration(self.duration)),
"play_time" => Some(Field::PlayTime(self.play_time)),
"format" => self.format.map(Field::Format),
_ => todo!(), // Other field types are not yet supported
}
}
/// Sets the value of a tag in the song
pub fn set_tag(&mut self, target_key: Tag, new_value: String) {
self.tags.insert(target_key, new_value);
}
/// Deletes a tag from the song
pub fn remove_tag(&mut self, target_key: &Tag) {
self.tags.remove(target_key);
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum URI {
Local(PathBuf),
Cue {
location: PathBuf,
index: usize,
start: Duration,
end: Duration,
},
Remote(Service, String),
}
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
/// error if the URI is not a Cue variant
pub fn start(&self) -> Result<&Duration, Box<dyn Error>> {
match self {
URI::Local(_) => Err("\"Local\" has no starting time".into()),
URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
URI::Cue { start, .. } => Ok(start),
}
}
/// Returns the end time of a CUEsheet song, or an
/// error if the URI is not a Cue variant
pub fn end(&self) -> Result<&Duration, Box<dyn Error>> {
match self {
URI::Local(_) => Err("\"Local\" has no starting time".into()),
URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
URI::Cue { end, .. } => Ok(end),
}
}
/// Returns the location as a PathBuf
pub fn path(&self) -> PathBuf {
match self {
URI::Local(location) => location.clone(),
URI::Cue { location, .. } => location.clone(),
URI::Remote(_, location) => PathBuf::from(location),
}
}
pub fn as_uri(&self) -> String {
let path_str = match self {
URI::Local(location) => filename_to_uri(location, None)
.expect("couldn't convert path to URI")
.to_string(),
URI::Cue { location, .. } => filename_to_uri(location, None)
.expect("couldn't convert path to URI")
.to_string(),
URI::Remote(_, location) => location.clone(),
};
path_str.to_string()
}
}
impl ToString for URI {
fn to_string(&self) -> String {
let path_str = match self {
URI::Local(location) => location.as_path().to_string_lossy(),
URI::Cue { location, .. } => location.as_path().to_string_lossy(),
URI::Remote(_, location) => location.into(),
};
path_str.to_string()
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Service {
InternetRadio,
Spotify,
Youtube,
None,
}
#[derive(Clone, Debug)]
pub struct Album<'a> {
title: &'a String,
artist: Option<&'a String>,
cover: Option<&'a AlbumArt>,
discs: BTreeMap<usize, Vec<&'a Song>>,
}
#[allow(clippy::len_without_is_empty)]
impl Album<'_> {
/// Returns the Album Artist, if they exist
pub fn artist(&self) -> Option<&String> {
self.artist
}
pub fn discs(&self) -> &BTreeMap<usize, Vec<&Song>> {
&self.discs
}
/// Returns the specified track at `index` from the album, returning
/// an error if the track index is out of range
pub fn track(&self, disc: usize, index: usize) -> Option<&Song> {
Some(self.discs.get(&disc)?[index])
}
/// Returns the number of songs in the album
pub fn len(&self) -> usize {
let mut total = 0;
for disc in &self.discs {
total += disc.1.len();
}
total
}
}
impl MusicCollection for Album<'_> {
//returns the Album title
fn title(&self) -> &String {
self.title
}
/// Returns the album cover as an AlbumArt struct, if it exists
fn cover(&self) -> Option<&AlbumArt> {
self.cover
}
fn tracks(&self) -> Vec<&Song> {
let mut songs = Vec::new();
for disc in &self.discs {
songs.append(&mut disc.1.clone())
}
songs
}
}
const BLOCKED_EXTENSIONS: [&str; 4] = ["vob", "log", "txt", "sf2"];
#[derive(Debug)]
pub struct MusicLibrary {
pub library: Vec<Song>,
}
impl MusicLibrary {
/// Initialize the database
///
/// If the database file already exists, return the [MusicLibrary], otherwise create
/// the database first. This needs to be run before anything else to retrieve
/// the [MusicLibrary] Vec
pub fn init(config: Arc<RwLock<Config>>) -> Result<Self, Box<dyn Error>> {
let global_config = &*config.read().unwrap();
let mut library: Vec<Song> = Vec::new();
let mut backup_path = global_config.db_path.clone();
backup_path.set_extension("bkp");
match global_config.db_path.exists() {
true => {
library = read_library(*global_config.db_path.clone())?;
}
false => {
// Create the database if it does not exist
// possibly from the backup file
if backup_path.exists() {
library = read_library(*backup_path.clone())?;
write_library(&library, global_config.db_path.to_path_buf(), false)?;
} else {
write_library(&library, global_config.db_path.to_path_buf(), false)?;
}
}
};
Ok(Self { library })
}
/// Serializes the database out to the file specified in the config
pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> {
match config.db_path.try_exists() {
Ok(exists) => {
write_library(&self.library, config.db_path.to_path_buf(), exists)?;
}
Err(error) => return Err(error.into()),
}
Ok(())
}
/// Returns the library size in number of tracks
pub fn size(&self) -> usize {
self.library.len()
}
/// Queries for a [Song] by its [URI], returning a single `Song`
/// with the `URI` that matches
pub fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> {
let result = self
.library
.par_iter()
.enumerate()
.try_for_each(|(i, track)| {
if path == &track.location {
return std::ops::ControlFlow::Break((track, i));
}
Continue(())
});
match result {
Break(song) => Some(song),
Continue(_) => None,
}
}
/// Queries for a [Song] by its [PathBuf], returning a `Vec<Song>`
/// with matching `PathBuf`s
fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> {
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
self.library.par_iter().for_each(|track| {
if path == track.location.path() {
result.clone().lock().unwrap().push(track);
}
});
if result.lock().unwrap().len() > 0 {
Some(Arc::try_unwrap(result).unwrap().into_inner().unwrap())
} else {
None
}
}
/// Finds all the audio files within a specified folder
pub fn scan_folder(
&mut self,
target_path: &str,
config: &Config,
) -> Result<i32, Box<dyn std::error::Error>> {
let mut total = 0;
let mut errors = 0;
for target_file in WalkDir::new(target_path)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let path = target_file.path();
// Ensure the target is a file and not a directory,
// if it isn't a file, skip this loop
if !path.is_file() {
continue;
}
/* TODO: figure out how to increase the speed of this maybe
// Check if the file path is already in the db
if self.query_uri(&URI::Local(path.to_path_buf())).is_some() {
continue;
}
// Save periodically while scanning
i += 1;
if i % 500 == 0 {
self.save(config).unwrap();
}
*/
let format = FileFormat::from_file(path)?;
let extension = match path.extension() {
Some(ext) => ext.to_string_lossy().to_ascii_lowercase(),
None => String::new(),
};
// If it's a normal file, add it to the database
// if it's a cuesheet, do a bunch of fancy stuff
if (format.kind() == Kind::Audio || format.kind() == Kind::Video)
&& !BLOCKED_EXTENSIONS.contains(&extension.as_str())
{
match self.add_file(target_file.path()) {
Ok(_) => total += 1,
Err(_error) => {
errors += 1;
println!("{}, {:?}: {}", format, target_file.file_name(), _error)
} // TODO: Handle more of these errors
};
} else if extension == "cue" {
total += match self.add_cuesheet(target_file.path()) {
Ok(added) => added,
Err(error) => {
errors += 1;
println!("{}", error);
0
}
}
}
}
// Save the database after scanning finishes
self.save(config).unwrap();
println!("ERRORS: {}", errors);
Ok(total)
}
pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> {
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
let tagged_file: lofty::TaggedFile;
let mut duration = Duration::from_secs(0);
let tag = match Probe::open(target_file)?.options(normal_options).read() {
Ok(file) => {
tagged_file = file;
duration = tagged_file.properties().duration();
// Ensure the tags exist, if not, insert blank data
match tagged_file.primary_tag() {
Some(primary_tag) => primary_tag,
None => match tagged_file.first_tag() {
Some(first_tag) => first_tag,
None => blank_tag,
},
}
}
Err(_) => blank_tag,
};
let mut tags: BTreeMap<Tag, String> = BTreeMap::new();
for item in tag.items() {
let key = match item.key() {
ItemKey::TrackTitle => Tag::Title,
ItemKey::TrackNumber => Tag::Track,
ItemKey::TrackArtist => Tag::Artist,
ItemKey::AlbumArtist => Tag::AlbumArtist,
ItemKey::Genre => Tag::Genre,
ItemKey::Comment => Tag::Comment,
ItemKey::AlbumTitle => Tag::Album,
ItemKey::DiscNumber => Tag::Disk,
ItemKey::Unknown(unknown)
if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" =>
{
continue
}
ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()),
custom => Tag::Key(format!("{:?}", custom)),
};
let value = match item.value() {
ItemValue::Text(value) => value.clone(),
ItemValue::Locator(value) => value.clone(),
ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)),
};
tags.insert(key, value);
}
// Get all the album artwork information from the file
let mut album_art: Vec<AlbumArt> = Vec::new();
for (i, _art) in tag.pictures().iter().enumerate() {
let new_art = AlbumArt::Embedded(i);
album_art.push(new_art)
}
// Find images around the music file that can be used
let mut found_images = find_images(target_file).unwrap();
album_art.append(&mut found_images);
// Get the format as a string
let format: Option<FileFormat> = match FileFormat::from_file(target_file) {
Ok(fmt) => Some(fmt),
Err(_) => None,
};
// TODO: Fix error handling
let binding = fs::canonicalize(target_file).unwrap();
let new_song = Song {
location: URI::Local(binding),
plays: 0,
skips: 0,
favorited: false,
rating: None,
format,
duration,
play_time: Duration::from_secs(0),
last_played: None,
date_added: Some(chrono::offset::Utc::now()),
date_modified: Some(chrono::offset::Utc::now()),
tags,
album_art,
};
match self.add_song(new_song) {
Ok(_) => (),
Err(_) => {
//return Err(error)
}
};
Ok(())
}
pub fn add_cuesheet(&mut self, cuesheet: &Path) -> Result<i32, Box<dyn Error>> {
let mut tracks_added = 0;
let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap();
// Get album level information
let album_title = &cue_data.title;
let album_artist = &cue_data.performer;
let parent_dir = cuesheet.parent().expect("The file has no parent path??");
for file in cue_data.files.iter() {
let audio_location = &parent_dir.join(file.file.clone());
if !audio_location.exists() {
continue;
}
// Try to remove the original audio file from the db if it exists
if self.remove_uri(&URI::Local(audio_location.clone())).is_ok() {
tracks_added -= 1
}
let next_track = file.tracks.clone();
let mut next_track = next_track.iter().skip(1);
for (i, track) in file.tracks.iter().enumerate() {
// Get the track timing information
let pregap = match track.pregap {
Some(pregap) => pregap,
None => Duration::from_secs(0),
};
let postgap = match track.postgap {
Some(postgap) => postgap,
None => Duration::from_secs(0),
};
let mut start;
if track.indices.len() > 1 {
start = track.indices[1].1;
} else {
start = track.indices[0].1;
}
if !start.is_zero() {
start -= pregap;
}
let duration = match next_track.next() {
Some(future) => match future.indices.get(0) {
Some(val) => val.1 - start,
None => Duration::from_secs(0),
},
None => match lofty::read_from_path(audio_location) {
Ok(tagged_file) => tagged_file.properties().duration() - start,
Err(_) => match Probe::open(audio_location)?.read() {
Ok(tagged_file) => tagged_file.properties().duration() - start,
Err(_) => Duration::from_secs(0),
},
},
};
let end = start + duration + postgap;
// Get the format as a string
let format: Option<FileFormat> = match FileFormat::from_file(audio_location) {
Ok(fmt) => Some(fmt),
Err(_) => None,
};
// Get some useful tags
let mut tags: BTreeMap<Tag, String> = BTreeMap::new();
match album_title {
Some(title) => {
tags.insert(Tag::Album, title.clone());
}
None => (),
}
match album_artist {
Some(artist) => {
tags.insert(Tag::Artist, artist.clone());
}
None => (),
}
tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string()));
match track.title.clone() {
Some(title) => tags.insert(Tag::Title, title),
None => match track.isrc.clone() {
Some(title) => tags.insert(Tag::Title, title),
None => {
let namestr = format!("{} - {}", i, file.file.clone());
tags.insert(Tag::Title, namestr)
}
},
};
match track.performer.clone() {
Some(artist) => tags.insert(Tag::Artist, artist),
None => None,
};
// Find images around the music file that can be used
let album_art = find_images(&audio_location.to_path_buf()).unwrap();
let new_song = Song {
location: URI::Cue {
location: audio_location.clone(),
index: i,
start,
end,
},
plays: 0,
skips: 0,
favorited: false,
rating: None,
format,
duration,
play_time: Duration::from_secs(0),
last_played: None,
date_added: Some(chrono::offset::Utc::now()),
date_modified: Some(chrono::offset::Utc::now()),
tags,
album_art,
};
match self.add_song(new_song) {
Ok(_) => tracks_added += 1,
Err(_error) => {
//println!("{}", _error);
continue;
}
};
}
}
Ok(tracks_added)
}
pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> {
if self.query_uri(&new_song.location).is_some() {
return Err(format!("URI already in database: {:?}", new_song.location).into());
}
match new_song.location {
URI::Local(_) if self.query_path(new_song.location.path()).is_some() => {
return Err(format!("Location exists for {:?}", new_song.location).into())
}
_ => (),
}
self.library.push(new_song);
Ok(())
}
/// Removes a song indexed by URI, returning the position removed
pub fn remove_uri(&mut self, target_uri: &URI) -> Result<usize, Box<dyn Error>> {
let location = match self.query_uri(target_uri) {
Some(value) => value.1,
None => return Err("URI not in database".into()),
};
self.library.remove(location);
Ok(location)
}
/// Scan the song by a location and update its tags
pub fn update_uri(
&mut self,
target_uri: &URI,
new_tags: Vec<Tag>,
) -> Result<(), Box<dyn std::error::Error>> {
let target_song = match self.query_uri(target_uri) {
Some(song) => song,
None => return Err("URI not in database!".to_string().into()),
};
println!("{:?}", target_song.0.location);
for tag in new_tags {
println!("{:?}", tag);
}
todo!()
}
/// Query the database, returning a list of [Song]s
///
/// The order in which the `sort by` Vec is arranged
/// determines the output sorting.
///
/// Example:
/// ```
/// use dango_core::music_storage::music_db::Tag;
/// query_tracks(
/// &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_tracks(
&self,
query_string: &String, // The query itself
target_tags: &Vec<Tag>, // The tags to search
sort_by: &Vec<Tag>, // Tags to sort the resulting data by
) -> Option<Vec<&Song>> {
let songs = Arc::new(Mutex::new(Vec::new()));
//let matcher = SkimMatcherV2::default();
self.library.par_iter().for_each(|track| {
for tag in target_tags {
let track_result = match tag {
Tag::Field(target) => match track.get_field(target) {
Some(value) => value.to_string(),
None => continue,
},
_ => match track.get_tag(tag) {
Some(value) => value.clone(),
None => continue,
},
};
/*
let match_level = match matcher.fuzzy_match(&normalize(&track_result), &normalize(query_string)) {
Some(conf) => conf,
None => continue
};
if match_level > 100 {
songs.lock().unwrap().push(track);
return;
}
*/
if normalize(&track_result.to_string())
.contains(&normalize(&query_string.to_owned()))
{
songs.lock().unwrap().push(track);
return;
}
}
});
let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!");
let mut new_songs = lock.into_inner().expect("Mutex cannot be locked!");
// Sort the returned list of songs
new_songs.par_sort_by(|a, b| {
for sort_option in sort_by {
let tag_a = match sort_option {
Tag::Field(field_selection) => match a.get_field(field_selection) {
Some(field_value) => field_value.to_string(),
None => continue,
},
_ => match a.get_tag(sort_option) {
Some(tag_value) => tag_value.to_owned(),
None => continue,
},
};
let tag_b = match sort_option {
Tag::Field(field_selection) => match b.get_field(field_selection) {
Some(field_value) => field_value.to_string(),
None => continue,
},
_ => match b.get_tag(sort_option) {
Some(tag_value) => tag_value.to_owned(),
None => continue,
},
};
if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::<i32>(), tag_b.parse::<i32>()) {
// If parsing succeeds, compare as numbers
return num_a.cmp(&num_b);
} else {
// If parsing fails, compare as strings
return tag_a.cmp(&tag_b);
}
}
// If all tags are equal, sort by Track number
let path_a = PathBuf::from(a.get_field("location").unwrap().to_string());
let path_b = PathBuf::from(b.get_field("location").unwrap().to_string());
path_a.file_name().cmp(&path_b.file_name())
});
if !new_songs.is_empty() {
Some(new_songs)
} else {
None
}
}
/// Generates all albums from the track list
pub fn albums(&self) -> BTreeMap<String, Album> {
let mut albums: BTreeMap<String, Album> = BTreeMap::new();
for result in &self.library {
let title = match result.get_tag(&Tag::Album) {
Some(title) => title,
None => continue,
};
let norm_title = normalize(title);
let disc_num = result
.get_tag(&Tag::Disk)
.unwrap_or(&"".to_string())
.parse::<usize>()
.unwrap_or(1);
match albums.get_mut(&norm_title) {
// If the album is in the list, add the track to the appropriate disc in it
Some(album) => match album.discs.get_mut(&disc_num) {
Some(disc) => disc.push(result),
None => {
album.discs.insert(disc_num, vec![result]);
}
},
// If the album is not in the list, make a new one and add it
None => {
let album_art = result.album_art.get(0);
let new_album = Album {
title,
artist: result.get_tag(&Tag::AlbumArtist),
discs: BTreeMap::from([(disc_num, vec![result])]),
cover: album_art,
};
albums.insert(norm_title, new_album);
}
}
}
// Sort the tracks in each disk in each album
let blank = String::from("");
albums.par_iter_mut().for_each(|album| {
for disc in &mut album.1.discs {
disc.1.par_sort_by(|a, b| {
let a_track = a.get_tag(&Tag::Track).unwrap_or(&blank);
let b_track = b.get_tag(&Tag::Track).unwrap_or(&blank);
if let (Ok(num_a), Ok(num_b)) = (a_track.parse::<i32>(), b_track.parse::<i32>())
{
// If parsing the track numbers succeeds, compare as numbers
num_a.cmp(&num_b)
} else {
// If parsing doesn't succeed, compare the locations
let path_a = PathBuf::from(a.get_field("location").unwrap().to_string());
let path_b = PathBuf::from(b.get_field("location").unwrap().to_string());
path_a.file_name().cmp(&path_b.file_name())
}
});
}
});
// Return the albums!
albums
}
/// Queries a list of albums by title
pub fn query_albums(
&self,
query_string: &str, // The query itself
) -> Result<Vec<Album>, Box<dyn Error>> {
let all_albums = self.albums();
let normalized_query = normalize(query_string);
let albums: Vec<Album> = all_albums
.par_iter()
.filter_map(|album| {
if normalize(album.0).contains(&normalized_query) {
Some(album.1.clone())
} else {
None
}
})
.collect();
Ok(albums)
}
}