From f02f5bca41c75dd022e25c11b3d60c477ad66c3f Mon Sep 17 00:00:00 2001
From: MrDulfin <mrdulfin@mrdulfin.com>
Date: Wed, 3 Apr 2024 01:00:52 -0400
Subject: [PATCH] first draft of library Rework

---
 Cargo.toml                                   |   3 +-
 src/music_controller/controller.rs           |   2 +-
 src/music_controller/queue.rs                |  11 +-
 src/music_storage/db_reader/foobar/reader.rs |   6 +-
 src/music_storage/db_reader/itunes/reader.rs |  17 ++-
 src/music_storage/library.rs                 | 127 ++++++++++++++-----
 src/music_storage/playlist.rs                |  40 +++++-
 7 files changed, 149 insertions(+), 57 deletions(-)

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<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);
+    }
 }
\ 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<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),
         }
     }
 }