Added enum for fields on Song structs, Field

This commit is contained in:
G2-Games 2023-11-30 02:32:06 -06:00
parent 64a3b67241
commit 6845d34ce7
3 changed files with 137 additions and 65 deletions

View file

@ -7,13 +7,13 @@ description = "A music backend that manages storage, querying, and playback of r
homepage = "https://dangoware.com/dango-music-player" homepage = "https://dangoware.com/dango-music-player"
documentation = "https://docs.rs/dango-core" documentation = "https://docs.rs/dango-core"
readme = "README.md" readme = "README.md"
repository = "https://github.com/DangoWare/dango-music-player" repository = "https://github.com/Dangoware/dango-music-player"
keywords = ["audio", "music"] keywords = ["audio", "music"]
categories = ["multimedia::audio"] categories = ["multimedia::audio"]
[dependencies] [dependencies]
file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] }
lofty = "0.17.0" lofty = "0.17.1"
serde = { version = "1.0.191", features = ["derive"] } serde = { version = "1.0.191", features = ["derive"] }
toml = "0.7.5" toml = "0.7.5"
walkdir = "2.4.0" walkdir = "2.4.0"

View file

@ -1,5 +1,5 @@
// Crate things // Crate things
use super::utils::{normalize, read_library, write_library, find_images}; use super::utils::{find_images, normalize, read_library, write_library};
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
// Various std things // Various std things
@ -8,15 +8,15 @@ use std::error::Error;
use std::ops::ControlFlow::{Break, Continue}; use std::ops::ControlFlow::{Break, Continue};
// Files // Files
use rcue::parser::parse_from_file;
use file_format::{FileFormat, Kind}; use file_format::{FileFormat, Kind};
use walkdir::WalkDir; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt};
use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt, ParseOptions}; use rcue::parser::parse_from_file;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use walkdir::WalkDir;
// Time // Time
use chrono::{serde::ts_seconds_option, DateTime, Utc}; use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
use std::time::Duration; use std::time::Duration;
// Serialization/Compression // Serialization/Compression
@ -42,6 +42,8 @@ impl AlbumArt {
} }
} }
/// A tag for a song
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum Tag { pub enum Tag {
Title, Title,
@ -73,6 +75,42 @@ impl ToString for Tag {
} }
} }
/// A field within a Song struct
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 /// Stores information about a single song
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Song { pub struct Song {
@ -84,43 +122,53 @@ pub struct Song {
pub format: Option<FileFormat>, pub format: Option<FileFormat>,
pub duration: Duration, pub duration: Duration,
pub play_time: Duration, pub play_time: Duration,
#[serde(with = "ts_seconds_option")] #[serde(with = "ts_milliseconds_option")]
pub last_played: Option<DateTime<Utc>>, pub last_played: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")] #[serde(with = "ts_milliseconds_option")]
pub date_added: Option<DateTime<Utc>>, pub date_added: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")] #[serde(with = "ts_milliseconds_option")]
pub date_modified: Option<DateTime<Utc>>, pub date_modified: Option<DateTime<Utc>>,
pub album_art: Vec<AlbumArt>, pub album_art: Vec<AlbumArt>,
pub tags: BTreeMap<Tag, String>, pub tags: BTreeMap<Tag, String>,
} }
impl Song { impl Song {
/** /// Get a tag's value
* Get a tag's value ///
* /// ```
* ``` /// use dango_core::music_storage::music_db::Tag;
* // Assuming an already created song: /// // Assuming an already created song:
* ///
* let tag = this_song.get_tag(Tag::Title); /// let tag = this_song.get_tag(Tag::Title);
* ///
* assert_eq!(tag, "Some Song Title"); /// assert_eq!(tag, "Some Song Title");
* ``` /// ```
**/
pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { pub fn get_tag(&self, target_key: &Tag) -> Option<&String> {
self.tags.get(target_key) self.tags.get(target_key)
} }
pub fn get_field(&self, target_field: &str) -> Option<String> { pub fn get_field(&self, target_field: &str) -> Option<Field> {
match target_field { let lower_target = target_field.to_lowercase();
"location" => Some(self.location.clone().path_string()), match lower_target.as_str() {
"plays" => Some(self.plays.clone().to_string()), "location" => Some(Field::Location(self.location.clone())),
"format" => match self.format { "plays" => Some(Field::Plays(self.plays)),
Some(format) => format.short_name().map(|short| short.to_string()), "skips" => Some(Field::Skips(self.skips)),
None => None, "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 _ => todo!(), // Other field types are not yet supported
} }
} }
pub fn set_tag(&mut self, target_key: Tag, new_value: String) {
self.tags.insert(target_key, new_value);
}
pub fn remove_tag(&mut self, target_key: &Tag) {
self.tags.remove(target_key);
}
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -172,8 +220,10 @@ impl URI {
URI::Remote(_, location) => location, URI::Remote(_, location) => location,
} }
} }
}
pub fn path_string(&self) -> String { impl ToString for URI {
fn to_string(&self) -> String {
let path_str = match self { let path_str = match self {
URI::Local(location) => location.as_path().to_string_lossy(), URI::Local(location) => location.as_path().to_string_lossy(),
URI::Cue { location, .. } => location.as_path().to_string_lossy(), URI::Cue { location, .. } => location.as_path().to_string_lossy(),
@ -302,12 +352,16 @@ impl MusicLibrary {
/// Queries for a [Song] by its [URI], returning a single `Song` /// Queries for a [Song] by its [URI], returning a single `Song`
/// with the `URI` that matches /// with the `URI` that matches
fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> {
let result = self.library.par_iter().enumerate().try_for_each(|(i, track)| { let result = self
if path == &track.location { .library
return std::ops::ControlFlow::Break((track, i)); .par_iter()
} .enumerate()
Continue(()) .try_for_each(|(i, track)| {
}); if path == &track.location {
return std::ops::ControlFlow::Break((track, i));
}
Continue(())
});
match result { match result {
Break(song) => Some(song), Break(song) => Some(song),
@ -435,7 +489,11 @@ impl MusicLibrary {
ItemKey::Comment => Tag::Comment, ItemKey::Comment => Tag::Comment,
ItemKey::AlbumTitle => Tag::Album, ItemKey::AlbumTitle => Tag::Album,
ItemKey::DiscNumber => Tag::Disk, ItemKey::DiscNumber => Tag::Disk,
ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" => continue, ItemKey::Unknown(unknown)
if unknown == "ACOUSTID_FINGERPRINT" || 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)),
}; };
@ -492,7 +550,7 @@ impl MusicLibrary {
Ok(_) => (), Ok(_) => (),
Err(_) => { Err(_) => {
//return Err(error) //return Err(error)
}, }
}; };
Ok(()) Ok(())
@ -516,7 +574,9 @@ impl MusicLibrary {
} }
// Try to remove the original audio file from the db if it exists // 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 } if self.remove_uri(&URI::Local(audio_location.clone())).is_ok() {
tracks_added -= 1
}
let next_track = file.tracks.clone(); let next_track = file.tracks.clone();
let mut next_track = next_track.iter().skip(1); let mut next_track = next_track.iter().skip(1);
@ -538,19 +598,17 @@ impl MusicLibrary {
let duration = match next_track.next() { let duration = match next_track.next() {
Some(future) => match future.indices.get(0) { Some(future) => match future.indices.get(0) {
Some(val) => val.1 - start, Some(val) => val.1 - start,
None => Duration::from_secs(0) None => Duration::from_secs(0),
} },
None => { None => match lofty::read_from_path(audio_location) {
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, Ok(tagged_file) => tagged_file.properties().duration() - start,
Err(_) => match Probe::open(audio_location)?.read() { Err(_) => Duration::from_secs(0),
Ok(tagged_file) => tagged_file.properties().duration() - start, },
},
Err(_) => Duration::from_secs(0),
},
}
}
}; };
let end = start + duration + postgap; let end = start + duration + postgap;
@ -563,11 +621,15 @@ impl MusicLibrary {
// Get some useful tags // Get some useful tags
let mut tags: BTreeMap<Tag, String> = BTreeMap::new(); let mut tags: BTreeMap<Tag, String> = BTreeMap::new();
match album_title { match album_title {
Some(title) => {tags.insert(Tag::Album, title.clone());}, Some(title) => {
tags.insert(Tag::Album, title.clone());
}
None => (), None => (),
} }
match album_artist { match album_artist {
Some(artist) => {tags.insert(Tag::Album, artist.clone());}, Some(artist) => {
tags.insert(Tag::Album, artist.clone());
}
None => (), None => (),
} }
tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string())); tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string()));
@ -625,7 +687,7 @@ impl MusicLibrary {
pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> { pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> {
if self.query_uri(&new_song.location).is_some() { if self.query_uri(&new_song.location).is_some() {
return Err(format!("URI already in database: {:?}", new_song.location).into()) return Err(format!("URI already in database: {:?}", new_song.location).into());
} }
match new_song.location { match new_song.location {
@ -653,11 +715,17 @@ impl MusicLibrary {
} }
/// Scan the song by a location and update its tags /// 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>> { pub fn update_uri(
match self.query_uri(target_uri) { &mut self,
Some(_) => (), 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()), None => return Err("URI not in database!".to_string().into()),
} };
println!("{:?}", target_song.0.location);
for tag in new_tags { for tag in new_tags {
println!("{:?}", tag); println!("{:?}", tag);
@ -673,6 +741,7 @@ impl MusicLibrary {
/// ///
/// Example: /// Example:
/// ``` /// ```
/// use dango_core::music_storage::music_db::Tag;
/// query_tracks( /// query_tracks(
/// &String::from("query"), /// &String::from("query"),
/// &vec![ /// &vec![
@ -702,7 +771,7 @@ impl MusicLibrary {
for tag in target_tags { for tag in target_tags {
let track_result = match tag { let track_result = match tag {
Tag::Field(target) => match track.get_field(target) { Tag::Field(target) => match track.get_field(target) {
Some(value) => value, Some(value) => value.to_string(),
None => continue, None => continue,
}, },
_ => match track.get_tag(tag) { _ => match track.get_tag(tag) {
@ -723,7 +792,9 @@ impl MusicLibrary {
} }
*/ */
if normalize(&track_result.to_string()).contains(&normalize(&query_string.to_owned())) { if normalize(&track_result.to_string())
.contains(&normalize(&query_string.to_owned()))
{
songs.lock().unwrap().push(track); songs.lock().unwrap().push(track);
return; return;
} }
@ -738,7 +809,7 @@ impl MusicLibrary {
for sort_option in sort_by { for sort_option in sort_by {
let tag_a = match sort_option { let tag_a = match sort_option {
Tag::Field(field_selection) => match a.get_field(field_selection) { Tag::Field(field_selection) => match a.get_field(field_selection) {
Some(field_value) => field_value, Some(field_value) => field_value.to_string(),
None => continue, None => continue,
}, },
_ => match a.get_tag(sort_option) { _ => match a.get_tag(sort_option) {
@ -749,7 +820,7 @@ impl MusicLibrary {
let tag_b = match sort_option { let tag_b = match sort_option {
Tag::Field(field_selection) => match b.get_field(field_selection) { Tag::Field(field_selection) => match b.get_field(field_selection) {
Some(field_value) => field_value, Some(field_value) => field_value.to_string(),
None => continue, None => continue,
}, },
_ => match b.get_tag(sort_option) { _ => match b.get_tag(sort_option) {
@ -768,8 +839,8 @@ impl MusicLibrary {
} }
// If all tags are equal, sort by Track number // If all tags are equal, sort by Track number
let path_a = PathBuf::from(a.get_field("location").unwrap()); let path_a = PathBuf::from(a.get_field("location").unwrap().to_string());
let path_b = PathBuf::from(b.get_field("location").unwrap()); let path_b = PathBuf::from(b.get_field("location").unwrap().to_string());
path_a.file_name().cmp(&path_b.file_name()) path_a.file_name().cmp(&path_b.file_name())
}); });
@ -834,8 +905,8 @@ impl MusicLibrary {
num_a.cmp(&num_b) num_a.cmp(&num_b)
} else { } else {
// If parsing doesn't succeed, compare the locations // If parsing doesn't succeed, compare the locations
let path_a = PathBuf::from(a.get_field("location").unwrap()); let path_a = PathBuf::from(a.get_field("location").unwrap().to_string());
let path_b = PathBuf::from(b.get_field("location").unwrap()); let path_b = PathBuf::from(b.get_field("location").unwrap().to_string());
path_a.file_name().cmp(&path_b.file_name()) path_a.file_name().cmp(&path_b.file_name())
} }

View file

@ -14,6 +14,7 @@ pub(super) fn normalize(input_string: &str) -> String {
// Remove non alphanumeric characters // Remove non alphanumeric characters
normalized.retain(|c| c.is_alphanumeric()); normalized.retain(|c| c.is_alphanumeric());
normalized = normalized.to_ascii_lowercase();
normalized normalized
} }