diff --git a/Cargo.toml b/Cargo.toml index 90ec0d7..192d833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,13 @@ description = "A music backend that manages storage, querying, and playback of r homepage = "https://dangoware.com/dango-music-player" documentation = "https://docs.rs/dango-core" readme = "README.md" -repository = "https://github.com/DangoWare/dango-music-player" +repository = "https://github.com/Dangoware/dango-music-player" keywords = ["audio", "music"] categories = ["multimedia::audio"] [dependencies] 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"] } toml = "0.7.5" walkdir = "2.4.0" diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 2bc3bd9..17275ae 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,5 +1,5 @@ // 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; // Various std things @@ -8,15 +8,15 @@ use std::error::Error; use std::ops::ControlFlow::{Break, Continue}; // Files -use rcue::parser::parse_from_file; use file_format::{FileFormat, Kind}; -use walkdir::WalkDir; -use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt, ParseOptions}; +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_seconds_option, DateTime, Utc}; +use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; use std::time::Duration; // Serialization/Compression @@ -42,6 +42,8 @@ impl AlbumArt { } } +/// A tag for a song +#[non_exhaustive] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] pub enum Tag { 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), + DateAdded(DateTime), + DateModified(DateTime), +} + +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)] pub struct Song { @@ -84,43 +122,53 @@ pub struct Song { pub format: Option, pub duration: Duration, pub play_time: Duration, - #[serde(with = "ts_seconds_option")] + #[serde(with = "ts_milliseconds_option")] pub last_played: Option>, - #[serde(with = "ts_seconds_option")] + #[serde(with = "ts_milliseconds_option")] pub date_added: Option>, - #[serde(with = "ts_seconds_option")] + #[serde(with = "ts_milliseconds_option")] pub date_modified: Option>, pub album_art: Vec, pub tags: BTreeMap, } impl Song { - /** - * Get a tag's value - * - * ``` - * // Assuming an already created song: - * - * let tag = this_song.get_tag(Tag::Title); - * - * assert_eq!(tag, "Some Song Title"); - * ``` - **/ + /// 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) } - pub fn get_field(&self, target_field: &str) -> Option { - match target_field { - "location" => Some(self.location.clone().path_string()), - "plays" => Some(self.plays.clone().to_string()), - "format" => match self.format { - Some(format) => format.short_name().map(|short| short.to_string()), - None => None, - }, + pub fn get_field(&self, target_field: &str) -> Option { + 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 } } + + 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)] @@ -172,8 +220,10 @@ impl URI { URI::Remote(_, location) => location, } } +} - pub fn path_string(&self) -> 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(), @@ -302,12 +352,16 @@ impl MusicLibrary { /// Queries for a [Song] by its [URI], returning a single `Song` /// with the `URI` that matches 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(()) - }); + 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), @@ -435,7 +489,11 @@ impl MusicLibrary { 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) + if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" => + { + continue + } ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), custom => Tag::Key(format!("{:?}", custom)), }; @@ -492,7 +550,7 @@ impl MusicLibrary { Ok(_) => (), Err(_) => { //return Err(error) - }, + } }; Ok(()) @@ -516,7 +574,9 @@ impl MusicLibrary { } // 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 mut next_track = next_track.iter().skip(1); @@ -538,19 +598,17 @@ impl MusicLibrary { 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) { + 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(_) => match Probe::open(audio_location)?.read() { - Ok(tagged_file) => tagged_file.properties().duration() - start, - - Err(_) => Duration::from_secs(0), - }, - } - } + Err(_) => Duration::from_secs(0), + }, + }, }; let end = start + duration + postgap; @@ -563,11 +621,15 @@ impl MusicLibrary { // Get some useful tags let mut tags: BTreeMap = BTreeMap::new(); match album_title { - Some(title) => {tags.insert(Tag::Album, title.clone());}, + Some(title) => { + tags.insert(Tag::Album, title.clone()); + } None => (), } match album_artist { - Some(artist) => {tags.insert(Tag::Album, artist.clone());}, + Some(artist) => { + tags.insert(Tag::Album, artist.clone()); + } None => (), } 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> { 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 { @@ -653,11 +715,17 @@ impl MusicLibrary { } /// Scan the song by a location and update its tags - pub fn update_uri(&mut self, target_uri: &URI, new_tags: Vec) -> Result<(), Box> { - match self.query_uri(target_uri) { - Some(_) => (), + pub fn update_uri( + &mut self, + target_uri: &URI, + new_tags: Vec, + ) -> Result<(), Box> { + 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); @@ -673,6 +741,7 @@ impl MusicLibrary { /// /// Example: /// ``` + /// use dango_core::music_storage::music_db::Tag; /// query_tracks( /// &String::from("query"), /// &vec![ @@ -702,7 +771,7 @@ impl MusicLibrary { for tag in target_tags { let track_result = match tag { Tag::Field(target) => match track.get_field(target) { - Some(value) => value, + Some(value) => value.to_string(), None => continue, }, _ => 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); return; } @@ -738,7 +809,7 @@ impl MusicLibrary { 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, + Some(field_value) => field_value.to_string(), None => continue, }, _ => match a.get_tag(sort_option) { @@ -749,7 +820,7 @@ impl MusicLibrary { let tag_b = match sort_option { Tag::Field(field_selection) => match b.get_field(field_selection) { - Some(field_value) => field_value, + Some(field_value) => field_value.to_string(), None => continue, }, _ => match b.get_tag(sort_option) { @@ -768,8 +839,8 @@ impl MusicLibrary { } // If all tags are equal, sort by Track number - let path_a = PathBuf::from(a.get_field("location").unwrap()); - let path_b = PathBuf::from(b.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().to_string()); path_a.file_name().cmp(&path_b.file_name()) }); @@ -834,8 +905,8 @@ impl MusicLibrary { num_a.cmp(&num_b) } else { // If parsing doesn't succeed, compare the locations - let path_a = PathBuf::from(a.get_field("location").unwrap()); - let path_b = PathBuf::from(b.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().to_string()); path_a.file_name().cmp(&path_b.file_name()) } diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 0687db4..5c4865a 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -14,6 +14,7 @@ pub(super) fn normalize(input_string: &str) -> String { // Remove non alphanumeric characters normalized.retain(|c| c.is_alphanumeric()); + normalized = normalized.to_ascii_lowercase(); normalized }