diff --git a/Cargo.toml b/Cargo.toml index 2d106a4..3c7d698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,5 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] } serde_json = "1.0.111" deunicode = "1.4.2" opener = { version = "0.7.0", features = ["reveal"] } -tempfile = "3.10.1" \ No newline at end of file +tempfile = "3.10.1" +nestify = "0.3.3" diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index ebcc27f..ca14d12 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -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(); diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 6180e9d..78bbe7c 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -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()), } }, diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs index 815f9a7..4d07e71 100644 --- a/src/music_storage/db_reader/foobar/reader.rs +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -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, } } } diff --git a/src/music_storage/db_reader/itunes/reader.rs b/src/music_storage/db_reader/itunes/reader.rs index 90a48f2..f6032db 100644 --- a/src/music_storage/db_reader/itunes/reader.rs +++ b/src/music_storage/db_reader/itunes/reader.rs @@ -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); diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 1afc991..ab2c231 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -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, pub uuid: Uuid, pub plays: i32, pub skips: i32, pub favorited: bool, - // pub banned: Option, + pub banned: Option, pub rating: Option, pub format: Option, pub duration: Duration, @@ -158,6 +182,7 @@ pub struct Song { pub date_modified: Option>, pub album_art: Vec, pub tags: BTreeMap, + pub internal_tags: Vec } @@ -180,7 +205,7 @@ impl Song { 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())), + "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>), Box> { + 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, -} - -#[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 // 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> { let result: Arc>> = 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> { - 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, ) -> Result<(), Box> { - 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); + } } \ No newline at end of file diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 783a293..eb4fef4 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -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) { @@ -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), } } }