#![allow(non_snake_case)] use crate::get_now_timestamp; #[cfg(feature = "napi")] use crate::library::Paths; #[cfg(feature = "napi")] use crate::playlists::{delete_file, remove_from_all_playlists}; use anyhow::{Context, Result, bail}; use linked_hash_map::{Entry, LinkedHashMap}; use nanoid::nanoid; use serde::{Deserialize, Serialize}; use specta::Type; use std::borrow::Cow; use std::collections::HashSet; use std::sync::RwLock; use std::time::Instant; #[derive(Clone, Debug)] pub struct Library { /// DO NOT INSERT DIRECTLY - A corresponding item ID must be generated by .insert_track() tracks: LinkedHashMap, /// LinkedHashMap so sort order isn't random track_item_ids: LinkedHashMap, pub trackLists: TrackLists, /// v1 playtime has two issues: /// - some durations are double counted (or triple, etc.) /// - timestamps aren't updated after pausing pub v1PlayTime: Vec, pub playTime: Vec, pub artists: Option>, pub genres: Option>, } impl Library { pub fn versioned(&'_ self) -> VersionedLibrary<'_> { VersionedLibrary::V2(V2Library { tracks: Cow::Borrowed(&self.tracks), trackLists: Cow::Borrowed(&self.trackLists), v1PlayTime: Cow::Borrowed(&self.v1PlayTime), playTime: Cow::Borrowed(&self.playTime), }) } } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(tag = "version", deny_unknown_fields)] pub enum VersionedLibrary<'a> { #[serde(rename = "1")] V1(Cow<'a, V1Library>), #[serde(rename = "2")] V2(V2Library<'a>), } impl<'a> VersionedLibrary<'a> { pub fn upgrade(self) -> V2Library<'a> { match self { VersionedLibrary::V1(v1) => v1.into_owned().upgrade(), VersionedLibrary::V2(v2) => v2, } } } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(deny_unknown_fields)] pub struct V2Library<'a> { pub tracks: Cow<'a, LinkedHashMap>, pub trackLists: Cow<'a, TrackLists>, /// v1 playtime has two issues: /// - some durations are double counted (or triple, etc.) /// - timestamps aren't updated after pausing pub v1PlayTime: Cow<'a, Vec>, pub playTime: Cow<'a, Vec>, } impl<'a> V2Library<'a> { pub fn init_libary(self) -> Library { let mut library = Library { tracks: LinkedHashMap::new(), track_item_ids: LinkedHashMap::new(), trackLists: self.trackLists.into_owned(), v1PlayTime: self.v1PlayTime.into_owned(), playTime: self.playTime.into_owned(), artists: None, genres: None, }; for (id, track) in self.tracks.into_owned() { library.insert_track(id, track); } library } } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(deny_unknown_fields)] pub struct V1Library { pub tracks: LinkedHashMap, pub trackLists: TrackLists, pub playTime: Vec, } impl V1Library { pub fn upgrade<'a>(self) -> V2Library<'a> { V2Library { tracks: Cow::Owned(LinkedHashMap::new()), trackLists: Cow::Owned(self.trackLists), v1PlayTime: Cow::Owned(self.playTime), playTime: Cow::Owned(Vec::new()), } } } impl Library { pub fn new() -> Self { let mut track_lists = LinkedHashMap::new(); let root = Special { id: SpecialTrackListName::Root.get_id().to_string(), name: SpecialTrackListName::Root, dateCreated: get_now_timestamp(), children: Vec::new(), }; track_lists.insert("root".to_string(), TrackList::Special(root)); Library { tracks: LinkedHashMap::new(), track_item_ids: LinkedHashMap::new(), trackLists: track_lists, v1PlayTime: Vec::new(), playTime: Vec::new(), artists: None, genres: None, } } pub fn get_tracks(&self) -> &LinkedHashMap { &self.tracks } pub fn get_track_item_ids(&self) -> &LinkedHashMap { &self.track_item_ids } pub fn insert_track(&mut self, id: TrackID, track: Track) { match self.tracks.entry(id.clone()) { Entry::Occupied(_) => panic!("Track ID already exists"), Entry::Vacant(entry) => entry.insert(track), }; let mut track_id_map = TRACK_ID_MAP.write().unwrap(); let item_id: ItemId = track_id_map.len() as u32; track_id_map.push(id.clone()); self.track_item_ids.insert(id, item_id); } #[cfg(feature = "napi")] pub fn delete_track_and_file(&mut self, id: &TrackID, paths: &Paths) -> Result<()> { let file_path = { let track = self.get_track(id)?; paths.get_track_file_path(&track.file) }; if !file_path.exists() { bail!("File does not exist: {}", file_path.to_string_lossy()); } remove_from_all_playlists(self, id); self.tracks .remove(id) .expect("Track ID not found when deleting"); self.track_item_ids .remove(id) .expect("Track ID not found when deleting (2)"); delete_file(&file_path)?; Ok(()) } pub fn generate_id(&self) -> String { let alphabet: [char; 32] = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '2', '3', '4', '5', '6', '7', ]; for _ in 0..1000 { let id = nanoid!(7, &alphabet); if !self.get_tracks().contains_key(&id) && !self.trackLists.contains_key(&id) { return id; } } panic!("Error generating ID: Generated IDs already exist") } pub fn new_playlist(&self, name: String, description: Option) -> Playlist { Playlist { id: self.generate_id(), name, description, liked: false, disliked: false, importedFrom: None, originalId: None, dateImported: None, dateCreated: Some(get_now_timestamp()), tracks: Vec::new(), } } pub fn new_folder(&self, name: String, description: Option) -> Folder { Folder { id: self.generate_id(), name, description, liked: false, disliked: false, importedFrom: None, originalId: None, dateImported: None, dateCreated: Some(get_now_timestamp()), children: Vec::new(), } } pub fn get_track(&self, id: &TrackID) -> Result<&Track> { self.get_tracks() .get(id) .with_context(|| format!("Track with ID {id} not found")) } pub fn get_track_mut(&mut self, id: &TrackID) -> Result<&mut Track> { self.tracks .get_mut(id) .with_context(|| format!("Track with ID {id} not found")) } pub fn get_tracklist(&self, id: &str) -> Result<&TrackList> { self.trackLists.get(id).context("Playlist ID not found") } pub fn get_tracklist_mut(&mut self, id: &str) -> Result<&mut TrackList> { self.trackLists.get_mut(id).context("Playlist ID not found") } pub fn get_root_tracklist_mut(&mut self) -> Result<&mut Special> { let tracklist = self.trackLists.get_mut(SpecialTrackListName::Root.get_id()); match tracklist { Some(TrackList::Special(special)) => match special.name { SpecialTrackListName::Root => Ok(special), }, _ => bail!("Root playlist not found"), } } pub fn get_parent_id(&self, id: &str) -> Option { for (parent_id, tracklist) in &self.trackLists { let children = match tracklist { TrackList::Playlist(_) => continue, TrackList::Folder(list) => &list.children, TrackList::Special(list) => &list.children, }; for child_id in children { if child_id == id { return Some(parent_id.to_string()); } } } None } pub fn get_genres(&mut self) -> &Vec { if self.genres.is_none() { let now = Instant::now(); let mut genres = HashSet::new(); for track in self.tracks.values() { if let Some(genre) = &track.genre { genres.insert(genre.clone()); } } let mut genres: Vec = genres.into_iter().collect(); genres.sort(); self.genres = Some(genres); println!("Load genres: {}ms", now.elapsed().as_millis()); } return self.genres.as_ref().unwrap(); } pub fn get_artists(&mut self) -> &Vec { if self.artists.is_none() { let now = Instant::now(); let mut artists = HashSet::new(); for track in self.tracks.values() { if track.artist != "" { artists.insert(track.artist.clone()); } } let mut artists: Vec = artists.into_iter().collect(); artists.sort(); self.artists = Some(artists); println!("Load artists: {}ms", now.elapsed().as_millis()); } return self.artists.as_ref().unwrap(); } } pub type TrackID = String; pub type TrackListID = String; pub type MsSinceUnixEpoch = i64; /// Should be 0-100 pub type PercentInteger = u8; pub type TrackLists = LinkedHashMap; /// (track id, start time, duration) pub type PlayTime = (TrackID, MsSinceUnixEpoch, i64); #[derive(Serialize, Deserialize, Clone, Debug, Type)] #[cfg_attr(feature = "napi", napi(object))] pub struct Track { pub size: i64, pub duration: f64, pub bitrate: f64, pub sampleRate: f64, pub file: String, pub dateModified: MsSinceUnixEpoch, pub dateAdded: MsSinceUnixEpoch, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub importedFrom: Option, /// Imported ID, like iTunes Persistent ID #[serde(default, skip_serializing_if = "Option::is_none")] pub originalId: Option, #[serde(default)] pub artist: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub composer: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortName: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortArtist: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortComposer: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub genre: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub rating: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub year: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub bpm: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub comments: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub grouping: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub liked: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub disliked: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub disabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub compilation: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub albumName: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub albumArtist: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortAlbumName: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortAlbumArtist: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub trackNum: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub trackCount: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub discNum: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub discCount: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateImported: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub playCount: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub plays: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub playsImported: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub skipCount: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub skips: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub skipsImported: Option>, /// -100 to 100 #[serde(default, skip_serializing_if = "Option::is_none")] pub volume: Option, } #[derive(Serialize, Deserialize, Clone, Debug, Type)] #[cfg_attr(feature = "napi", napi(object))] pub struct CountObject { pub count: i64, pub fromDate: MsSinceUnixEpoch, pub toDate: MsSinceUnixEpoch, } #[derive(Serialize, Deserialize, Clone, Debug, Type)] #[serde(tag = "type")] pub enum TrackList { #[serde(rename = "playlist")] Playlist(Playlist), #[serde(rename = "folder")] Folder(Folder), #[serde(rename = "special")] Special(Special), } impl TrackList { pub fn kind(&self) -> &str { match self { TrackList::Playlist(_) => "playlist", TrackList::Folder(_) => "folder", TrackList::Special(_) => "special", } } pub fn id(&self) -> &str { match self { TrackList::Playlist(list) => &list.id, TrackList::Folder(list) => &list.id, TrackList::Special(list) => &list.id, } } } fn is_false(value: &bool) -> bool { *value } // These are used to give each playlist entry an ID. This is for example helpful to keep track of a user's selection. These IDs are unique across the entire library, so that it works for folders folders. pub type ItemId = u32; /// Track IDs indexed by item IDs pub static TRACK_ID_MAP: RwLock> = RwLock::new(Vec::new()); pub fn new_item_ids_from_track_ids(track_ids: &[TrackID]) -> Vec { let mut playlist_track_id_map = TRACK_ID_MAP.write().unwrap(); let playlist_track_ids: Vec = track_ids .iter() .map(|track_id| { let new_index = playlist_track_id_map.len(); playlist_track_id_map.push(track_id.clone()); new_index as u32 }) .collect(); assert_eq!(playlist_track_ids.len(), track_ids.len()); assert!(playlist_track_id_map.len() < u32::MAX as usize); playlist_track_ids } pub fn get_track_ids_from_item_ids(playlist_item_ids: &[ItemId]) -> Vec { let playlist_track_id_map = TRACK_ID_MAP.read().unwrap(); playlist_item_ids .iter() .map(|playlist_item_id| playlist_track_id_map[*playlist_item_id as usize].clone()) .collect() } // pub fn get_track_id_from_item_id(playlist_item_id: ItemId) -> TrackID { // let playlist_track_id_map = TRACK_ID_MAP.read().unwrap(); // playlist_track_id_map[playlist_item_id as usize].clone() // } #[derive(Serialize, Deserialize, Clone, Debug, Type)] #[cfg_attr(feature = "napi", napi(object))] pub struct Playlist { pub id: TrackListID, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default, skip_serializing_if = "is_false")] pub liked: bool, #[serde(default, skip_serializing_if = "is_false")] pub disliked: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub importedFrom: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub originalId: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateImported: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateCreated: Option, #[serde( deserialize_with = "deserialize_playlist_ids", serialize_with = "serialize_playlist_ids" )] #[cfg_attr(feature = "napi", napi(ts_type = "string[]"))] #[specta(type = Vec)] pub tracks: Vec, } impl Playlist { pub fn get_track_ids(&self) -> Vec { get_track_ids_from_item_ids(&self.tracks) } } // Deserialize list of strings into list of numbers fn deserialize_playlist_ids<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { let track_ids: Vec = serde::Deserialize::deserialize(deserializer)?; let playlist_track_ids = new_item_ids_from_track_ids(&track_ids); Ok(playlist_track_ids) } fn serialize_playlist_ids( playlist_track_ids: &[ItemId], serializer: S, ) -> Result where S: serde::Serializer, { let track_ids = get_track_ids_from_item_ids(playlist_track_ids); track_ids.serialize(serializer) } #[derive(Serialize, Deserialize, Clone, Debug, Type)] #[cfg_attr(feature = "napi", napi(object))] pub struct Folder { pub id: TrackListID, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default, skip_serializing_if = "is_false")] pub liked: bool, #[serde(default, skip_serializing_if = "is_false")] pub disliked: bool, /// For example "itunes" #[serde(default, skip_serializing_if = "Option::is_none")] pub importedFrom: Option, /// For example iTunes Persistent ID #[serde(default, skip_serializing_if = "Option::is_none")] pub originalId: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateImported: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateCreated: Option, pub children: Vec, } #[derive(Serialize, Deserialize, Clone, Debug, Type)] #[cfg_attr(feature = "napi", napi(object))] pub struct Special { pub id: TrackListID, pub name: SpecialTrackListName, pub dateCreated: MsSinceUnixEpoch, pub children: Vec, } #[derive(Serialize, Deserialize, Clone, Debug, Type)] #[cfg_attr(feature = "napi-rs", napi)] pub enum SpecialTrackListName { Root, } impl SpecialTrackListName { pub fn get_id(&self) -> &str { match self { SpecialTrackListName::Root => "root", } } pub fn get_name_str(&self) -> &str { match self { SpecialTrackListName::Root => "root", } } }