Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/kasper9n/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
Something went wrong. Try again.
17 kB · 546 lines
Rust
at sqlite
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547#![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<TrackID, Track>, /// LinkedHashMap so sort order isn't random track_item_ids: LinkedHashMap<TrackID, ItemId>, 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<PlayTime>, pub playTime: Vec<PlayTime>, pub artists: Option<Vec<String>>, pub genres: Option<Vec<String>>,}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<TrackID, Track>>, 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<PlayTime>>, pub playTime: Cow<'a, Vec<PlayTime>>,}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<TrackID, Track>, pub trackLists: TrackLists, pub playTime: Vec<PlayTime>,}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<TrackID, Track> { &self.tracks } pub fn get_track_item_ids(&self) -> &LinkedHashMap<TrackID, ItemId> { &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<String>) -> 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<String>) -> 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<String> { 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<String> { 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<String> = 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<String> { 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<String> = 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-100pub type PercentInteger = u8;pub type TrackLists = LinkedHashMap<TrackListID, TrackList>;
/// (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<String>, /// Imported ID, like iTunes Persistent ID #[serde(default, skip_serializing_if = "Option::is_none")] pub originalId: Option<String>, #[serde(default)] pub artist: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub composer: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortName: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortArtist: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortComposer: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub genre: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub rating: Option<PercentInteger>, #[serde(default, skip_serializing_if = "Option::is_none")] pub year: Option<i64>, #[serde(default, skip_serializing_if = "Option::is_none")] pub bpm: Option<f64>, #[serde(default, skip_serializing_if = "Option::is_none")] pub comments: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub grouping: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub liked: Option<bool>, #[serde(default, skip_serializing_if = "Option::is_none")] pub disliked: Option<bool>, #[serde(default, skip_serializing_if = "Option::is_none")] pub disabled: Option<bool>, #[serde(default, skip_serializing_if = "Option::is_none")] pub compilation: Option<bool>, #[serde(default, skip_serializing_if = "Option::is_none")] pub albumName: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub albumArtist: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortAlbumName: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub sortAlbumArtist: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub trackNum: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub trackCount: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub discNum: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub discCount: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateImported: Option<MsSinceUnixEpoch>, #[serde(default, skip_serializing_if = "Option::is_none")] pub playCount: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub plays: Option<Vec<MsSinceUnixEpoch>>, #[serde(default, skip_serializing_if = "Option::is_none")] pub playsImported: Option<Vec<CountObject>>, #[serde(default, skip_serializing_if = "Option::is_none")] pub skipCount: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub skips: Option<Vec<MsSinceUnixEpoch>>, #[serde(default, skip_serializing_if = "Option::is_none")] pub skipsImported: Option<Vec<CountObject>>, /// -100 to 100 #[serde(default, skip_serializing_if = "Option::is_none")] pub volume: Option<i8>,}
#[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 IDspub static TRACK_ID_MAP: RwLock<Vec<String>> = RwLock::new(Vec::new());
pub fn new_item_ids_from_track_ids(track_ids: &[TrackID]) -> Vec<ItemId> { let mut playlist_track_id_map = TRACK_ID_MAP.write().unwrap(); let playlist_track_ids: Vec<u32> = 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<TrackID> { 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<String>, #[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<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub originalId: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateImported: Option<MsSinceUnixEpoch>, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateCreated: Option<MsSinceUnixEpoch>, #[serde( deserialize_with = "deserialize_playlist_ids", serialize_with = "serialize_playlist_ids" )] #[cfg_attr(feature = "napi", napi(ts_type = "string[]"))] #[specta(type = Vec<String>)] pub tracks: Vec<ItemId>,}impl Playlist { pub fn get_track_ids(&self) -> Vec<TrackID> { get_track_ids_from_item_ids(&self.tracks) }}
// Deserialize list of strings into list of numbersfn deserialize_playlist_ids<'de, D>(deserializer: D) -> Result<Vec<ItemId>, D::Error>where D: serde::Deserializer<'de>,{ let track_ids: Vec<String> = serde::Deserialize::deserialize(deserializer)?; let playlist_track_ids = new_item_ids_from_track_ids(&track_ids); Ok(playlist_track_ids)}
fn serialize_playlist_ids<S>( playlist_track_ids: &[ItemId], serializer: S,) -> Result<S::Ok, S::Error>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<String>, #[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<String>, /// For example iTunes Persistent ID #[serde(default, skip_serializing_if = "Option::is_none")] pub originalId: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateImported: Option<MsSinceUnixEpoch>, #[serde(default, skip_serializing_if = "Option::is_none")] pub dateCreated: Option<MsSinceUnixEpoch>, pub children: Vec<TrackListID>,}
#[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<TrackListID>,}
#[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", } }}