mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
Added enum for fields on Song
structs, Field
This commit is contained in:
parent
64a3b67241
commit
6845d34ce7
3 changed files with 137 additions and 65 deletions
|
@ -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"
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue