mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-19 09:42:53 -05:00
first draft of library Rework
This commit is contained in:
parent
7da0b1a1db
commit
f02f5bca41
7 changed files with 149 additions and 57 deletions
|
@ -37,3 +37,4 @@ serde_json = "1.0.111"
|
|||
deunicode = "1.4.2"
|
||||
opener = { version = "0.7.0", features = ["reveal"] }
|
||||
tempfile = "3.10.1"
|
||||
nestify = "0.3.3"
|
||||
|
|
|
@ -314,7 +314,7 @@ mod tests {
|
|||
a.q_set_volume(i, 0.04);
|
||||
// a.new_queue();
|
||||
let songs = a.lib_get_songs();
|
||||
a.q_enqueue(i, songs[2].location.clone());
|
||||
a.q_enqueue(i, songs[2].primary_uri().unwrap().0.clone());
|
||||
// a.enqueue(1, songs[2].location.clone());
|
||||
a.q_play(i).unwrap();
|
||||
// a.play(1).unwrap();
|
||||
|
|
|
@ -57,14 +57,14 @@ impl QueueItemType<'_> {
|
|||
match self {
|
||||
Song(uuid) => {
|
||||
if let Some((song, _)) = lib.query_uuid(uuid) {
|
||||
Some(song.location.clone())
|
||||
Some(song.primary_uri().unwrap().0.clone()) // TODO: error handle these better
|
||||
}else {
|
||||
Option::None
|
||||
}
|
||||
},
|
||||
Album{album, shuffled, current: (disc, index), ..} => {
|
||||
if !shuffled {
|
||||
Some(album.track(*disc as usize, *index as usize).unwrap().location.clone())
|
||||
Some(album.track(*disc as usize, *index as usize).unwrap().primary_uri().unwrap().0.clone())
|
||||
}else {
|
||||
todo!()
|
||||
}
|
||||
|
@ -302,23 +302,22 @@ impl<'a> Queue<'a> {
|
|||
let item = self.items[0].clone();
|
||||
let uri: URI = match &self.items[1].item {
|
||||
QueueItemType::Song(uuid) => {
|
||||
// TODO: Refactor later for multiple URIs
|
||||
match &lib.read().unwrap().query_uuid(uuid) {
|
||||
Some(song) => song.0.location.clone(),
|
||||
Some(song) => song.0.primary_uri()?.0.clone(),
|
||||
None => return Err("Uuid does not exist!".into()),
|
||||
}
|
||||
},
|
||||
QueueItemType::Album { album, current, ..} => {
|
||||
let (disc, track) = (current.0 as usize, current.1 as usize);
|
||||
match album.track(disc, track) {
|
||||
Some(track) => track.location.clone(),
|
||||
Some(track) => track.primary_uri()?.0.clone(),
|
||||
None => return Err(format!("Track in Album {} at disc {} track {} does not exist!", album.title(), disc, track).into())
|
||||
}
|
||||
},
|
||||
QueueItemType::Playlist { current, .. } => {
|
||||
// TODO: Refactor later for multiple URIs
|
||||
match &lib.read().unwrap().query_uuid(current) {
|
||||
Some(song) => song.0.location.clone(),
|
||||
Some(song) => song.0.primary_uri()?.0.clone(),
|
||||
None => return Err("Uuid does not exist!".into()),
|
||||
}
|
||||
},
|
||||
|
|
|
@ -176,14 +176,15 @@ pub struct FoobarPlaylistTrack {
|
|||
impl FoobarPlaylistTrack {
|
||||
fn find_song(&self) -> Song {
|
||||
let location = URI::Local(self.file_name.clone().into());
|
||||
let internal_tags = Vec::new();
|
||||
|
||||
Song {
|
||||
location,
|
||||
location: vec![location],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: 0,
|
||||
skips: 0,
|
||||
favorited: false,
|
||||
// banned: None,
|
||||
banned: None,
|
||||
rating: None,
|
||||
format: None,
|
||||
duration: self.duration,
|
||||
|
@ -193,6 +194,7 @@ impl FoobarPlaylistTrack {
|
|||
date_modified: None,
|
||||
album_art: Vec::new(),
|
||||
tags: BTreeMap::new(),
|
||||
internal_tags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use std::vec::Vec;
|
|||
use chrono::prelude::*;
|
||||
|
||||
use crate::music_storage::db_reader::extern_library::ExternalLibrary;
|
||||
use crate::music_storage::library::{AlbumArt, Service, Song, Tag, URI};
|
||||
use crate::music_storage::library::{AlbumArt, BannedType, Service, Song, Tag, URI};
|
||||
use crate::music_storage::utils;
|
||||
|
||||
use urlencoding::decode;
|
||||
|
@ -166,17 +166,19 @@ impl ExternalLibrary for ITunesLibrary {
|
|||
};
|
||||
let play_time_ = StdDur::from_secs(track.plays as u64 * dur.as_secs());
|
||||
|
||||
let internal_tags = Vec::new(); // TODO: handle internal tags generation
|
||||
|
||||
let ny: Song = Song {
|
||||
location,
|
||||
location: vec![location],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: track.plays,
|
||||
skips: 0,
|
||||
favorited: track.favorited,
|
||||
// banned: if track.banned {
|
||||
// Some(BannedType::All)
|
||||
// }else {
|
||||
// None
|
||||
// },
|
||||
banned: if track.banned {
|
||||
Some(BannedType::All)
|
||||
}else {
|
||||
None
|
||||
},
|
||||
rating: track.rating,
|
||||
format: match FileFormat::from_file(PathBuf::from(&loc)) {
|
||||
Ok(e) => Some(e),
|
||||
|
@ -192,6 +194,7 @@ impl ExternalLibrary for ITunesLibrary {
|
|||
Err(_) => Vec::new(),
|
||||
},
|
||||
tags: tags_,
|
||||
internal_tags,
|
||||
};
|
||||
// dbg!(&ny.tags);
|
||||
bun.push(ny);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use super::playlist::PlaylistFolder;
|
||||
// Crate things
|
||||
use super::utils::{find_images, normalize, read_file, write_file};
|
||||
use crate::config::config::Config;
|
||||
|
@ -117,35 +118,58 @@ impl ToString for Field {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum InternalTag {
|
||||
DoNotTrack(DoNotTrack),
|
||||
SongType(SongType),
|
||||
SongLink(Uuid, SongType),
|
||||
// Volume Adjustment from -100% to 100%
|
||||
VolumeAdjustment(i8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum BannedType {
|
||||
Shuffle,
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum DoNotTrack {
|
||||
// TODO: add services to not track
|
||||
LastFM,
|
||||
LibreFM,
|
||||
MusicBrainz,
|
||||
Discord,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
enum SongType {
|
||||
// TODO: add song types
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum SongType {
|
||||
// TODO: add MORE?! song types
|
||||
Main,
|
||||
Instrumental,
|
||||
Remix,
|
||||
Custom(String)
|
||||
}
|
||||
|
||||
impl Default for SongType {
|
||||
fn default() -> Self {
|
||||
SongType::Main
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores information about a single song
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct Song {
|
||||
pub location: URI,
|
||||
pub location: Vec<URI>,
|
||||
pub uuid: Uuid,
|
||||
pub plays: i32,
|
||||
pub skips: i32,
|
||||
pub favorited: bool,
|
||||
// pub banned: Option<BannedType>,
|
||||
pub banned: Option<BannedType>,
|
||||
pub rating: Option<u8>,
|
||||
pub format: Option<FileFormat>,
|
||||
pub duration: Duration,
|
||||
|
@ -158,6 +182,7 @@ pub struct Song {
|
|||
pub date_modified: Option<DateTime<Utc>>,
|
||||
pub album_art: Vec<AlbumArt>,
|
||||
pub tags: BTreeMap<Tag, String>,
|
||||
pub internal_tags: Vec<InternalTag>
|
||||
}
|
||||
|
||||
|
||||
|
@ -180,7 +205,7 @@ impl 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())),
|
||||
"location" => Some(Field::Location(self.primary_uri().unwrap().0.clone())), //TODO: make this not unwrap()
|
||||
"plays" => Some(Field::Plays(self.plays)),
|
||||
"skips" => Some(Field::Skips(self.skips)),
|
||||
"favorited" => Some(Field::Favorited(self.favorited)),
|
||||
|
@ -279,13 +304,15 @@ impl Song {
|
|||
// TODO: Fix error handling
|
||||
let binding = fs::canonicalize(target_file).unwrap();
|
||||
|
||||
// TODO: Handle creation of internal tag: Song Type and Song Links
|
||||
let internal_tags = { Vec::new() };
|
||||
let new_song = Song {
|
||||
location: URI::Local(binding),
|
||||
location: vec![URI::Local(binding)],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: 0,
|
||||
skips: 0,
|
||||
favorited: false,
|
||||
// banned: None,
|
||||
banned: None,
|
||||
rating: None,
|
||||
format,
|
||||
duration,
|
||||
|
@ -295,6 +322,7 @@ impl Song {
|
|||
date_modified: Some(chrono::offset::Utc::now()),
|
||||
tags,
|
||||
album_art,
|
||||
internal_tags,
|
||||
};
|
||||
Ok(new_song)
|
||||
}
|
||||
|
@ -399,17 +427,17 @@ impl Song {
|
|||
let album_art = find_images(&audio_location.to_path_buf()).unwrap();
|
||||
|
||||
let new_song = Song {
|
||||
location: URI::Cue {
|
||||
location: vec![URI::Cue {
|
||||
location: audio_location.clone(),
|
||||
index: i,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
}],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: 0,
|
||||
skips: 0,
|
||||
favorited: false,
|
||||
// banned: None,
|
||||
banned: None,
|
||||
rating: None,
|
||||
format,
|
||||
duration,
|
||||
|
@ -419,6 +447,7 @@ impl Song {
|
|||
date_modified: Some(chrono::offset::Utc::now()),
|
||||
tags,
|
||||
album_art,
|
||||
internal_tags: Vec::new()
|
||||
};
|
||||
tracks.push((new_song, audio_location.clone()));
|
||||
}
|
||||
|
@ -448,16 +477,17 @@ impl Song {
|
|||
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
|
||||
let tagged_file: lofty::TaggedFile;
|
||||
|
||||
// TODO: add support for other URI types... or don't
|
||||
#[cfg(target_family = "windows")]
|
||||
let uri = urlencoding::decode(
|
||||
match self.location.as_uri().strip_prefix("file:///") {
|
||||
match self.primary_uri()?.0.as_uri().strip_prefix("file:///") {
|
||||
Some(str) => str,
|
||||
None => return Err("invalid path.. again?".into())
|
||||
})?.into_owned();
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
let uri = urlencoding::decode(
|
||||
match self.location.as_uri().strip_prefix("file://") {
|
||||
match self.primary_uri()?.as_uri().strip_prefix("file://") {
|
||||
Some(str) => str,
|
||||
None => return Err("invalid path.. again?".into())
|
||||
})?.into_owned();
|
||||
|
@ -492,6 +522,26 @@ impl Song {
|
|||
dbg!(open(dbg!(uri))?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a reference to the first valid URI in the song, and any invalid URIs that come before it, or errors if there are no valid URIs
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn primary_uri(&self) -> Result<(&URI, Option<Vec<&URI>>), Box<dyn Error>> {
|
||||
let mut invalid_uris = Vec::new();
|
||||
let mut valid_uri = None;
|
||||
|
||||
for uri in &self.location {
|
||||
if uri.exists()? {
|
||||
valid_uri = Some(uri);
|
||||
break;
|
||||
}else {
|
||||
invalid_uris.push(uri);
|
||||
}
|
||||
}
|
||||
match valid_uri {
|
||||
Some(uri) => Ok((uri, if !invalid_uris.is_empty() { Some(invalid_uris) } else { None } )),
|
||||
None => Err("No valid URIs for this song".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
@ -652,14 +702,8 @@ pub struct MusicLibrary {
|
|||
pub name: String,
|
||||
pub uuid: Uuid,
|
||||
pub library: Vec<Song>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_init() {
|
||||
let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap();
|
||||
let target_uuid = config.libraries.libraries[0].uuid;
|
||||
let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap();
|
||||
dbg!(a);
|
||||
pub playlists: PlaylistFolder,
|
||||
pub backup_songs: Vec<Song> // maybe move this to the config instead?
|
||||
}
|
||||
|
||||
impl MusicLibrary {
|
||||
|
@ -669,6 +713,8 @@ impl MusicLibrary {
|
|||
name,
|
||||
uuid,
|
||||
library: Vec::new(),
|
||||
playlists: PlaylistFolder::new(),
|
||||
backup_songs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -736,8 +782,11 @@ impl MusicLibrary {
|
|||
.par_iter()
|
||||
.enumerate()
|
||||
.try_for_each(|(i, track)| {
|
||||
if path == &track.location {
|
||||
return std::ops::ControlFlow::Break((track, i));
|
||||
for location in &track.location {
|
||||
//TODO: check that this works
|
||||
if path == location {
|
||||
return std::ops::ControlFlow::Break((track, i));
|
||||
}
|
||||
}
|
||||
Continue(())
|
||||
});
|
||||
|
@ -773,7 +822,7 @@ impl MusicLibrary {
|
|||
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() {
|
||||
if path == track.primary_uri().unwrap().0.path() { //TODO: make this also not unwrap
|
||||
result.clone().lock().unwrap().push(track);
|
||||
}
|
||||
});
|
||||
|
@ -885,13 +934,14 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
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());
|
||||
let location = new_song.primary_uri()?.0;
|
||||
if self.query_uri(location).is_some() {
|
||||
return Err(format!("URI already in database: {:?}", 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())
|
||||
match location {
|
||||
URI::Local(_) if self.query_path(location.path()).is_some() => {
|
||||
return Err(format!("Location exists for {:?}", location).into())
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -914,17 +964,18 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
/// Scan the song by a location and update its tags
|
||||
// TODO: change this to work with multiple uris
|
||||
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) {
|
||||
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);
|
||||
println!("{:?}", target_song.location);
|
||||
|
||||
for tag in new_tags {
|
||||
println!("{:?}", tag);
|
||||
|
@ -1142,10 +1193,12 @@ impl MusicLibrary {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{path::Path, thread::sleep, time::{Duration, Instant}};
|
||||
use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, thread::sleep, time::{Duration, Instant}};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::{config::config::Config, music_storage::library::MusicLibrary};
|
||||
|
||||
use super::Song;
|
||||
|
||||
|
||||
|
@ -1161,4 +1214,12 @@ mod test {
|
|||
|
||||
sleep(Duration::from_secs(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_init() {
|
||||
let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap();
|
||||
let target_uuid = config.libraries.libraries[0].uuid;
|
||||
let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap();
|
||||
dbg!(a);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,43 @@
|
|||
use std::{fs::File, io::{Read, Error}};
|
||||
use std::{fs::File, io::{Error, Read}, time::Duration};
|
||||
|
||||
use chrono::Duration;
|
||||
// use chrono::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use super::library::{AlbumArt, Song, Tag};
|
||||
|
||||
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
|
||||
use nestify::nest;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub enum SortOrder {
|
||||
Manual,
|
||||
Tag(Tag)
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
nest! {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]*
|
||||
pub struct PlaylistFolder {
|
||||
name: String,
|
||||
items: Vec<
|
||||
pub enum PlaylistFolderItem {
|
||||
Folder(PlaylistFolder),
|
||||
List(Playlist)
|
||||
}
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
impl PlaylistFolder {
|
||||
pub fn new() -> Self {
|
||||
PlaylistFolder {
|
||||
name: String::new(),
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Playlist {
|
||||
uuid: Uuid,
|
||||
title: String,
|
||||
|
@ -28,7 +54,7 @@ impl Playlist {
|
|||
pub fn play_count(&self) -> i32 {
|
||||
self.play_count
|
||||
}
|
||||
pub fn play_time(&self) -> chrono::Duration {
|
||||
pub fn play_time(&self) -> Duration {
|
||||
self.play_time
|
||||
}
|
||||
pub fn set_tracks(&mut self, tracks: Vec<Uuid>) {
|
||||
|
@ -71,7 +97,7 @@ impl Playlist {
|
|||
|track| {
|
||||
|
||||
MediaSegment {
|
||||
uri: track.location.to_string().into(),
|
||||
uri: track.primary_uri().unwrap().0.to_string().into(), // TODO: error handle this better
|
||||
duration: track.duration.as_millis() as f32,
|
||||
title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.into()),
|
||||
..Default::default()
|
||||
|
@ -148,7 +174,7 @@ impl Default for Playlist {
|
|||
tracks: Vec::default(),
|
||||
sort_order: SortOrder::Manual,
|
||||
play_count: 0,
|
||||
play_time: Duration::zero(),
|
||||
play_time: Duration::from_secs(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue