diff --git a/Cargo.toml b/Cargo.toml index a7eb36f..76b0ba9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,5 @@ gstreamer = "0.21.2" glib = "0.18.3" crossbeam-channel = "0.5.8" crossbeam = "0.8.2" +quick-xml = "0.31.0" +leb128 = "0.2.5" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 31538fa..32a8453 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,20 @@ pub mod music_storage { pub mod playlist; mod utils; pub mod music_collection; + pub mod db_reader { + pub mod foobar { + pub mod reader; + } + pub mod musicbee { + pub mod utils; + pub mod reader; + } + pub mod xml { + pub mod reader; + } + pub mod common; + pub mod extern_library; + } } pub mod music_controller { diff --git a/src/music_storage/db_reader/common.rs b/src/music_storage/db_reader/common.rs new file mode 100644 index 0000000..80e0910 --- /dev/null +++ b/src/music_storage/db_reader/common.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use chrono::{DateTime, TimeZone, Utc}; + +pub fn get_bytes(iterator: &mut std::vec::IntoIter) -> [u8; S] { + let mut bytes = [0; S]; + + for i in 0..S { + bytes[i] = iterator.next().unwrap(); + } + + return bytes; +} + +pub fn get_bytes_vec(iterator: &mut std::vec::IntoIter, number: usize) -> Vec { + let mut bytes = Vec::new(); + + for _ in 0..number { + bytes.push(iterator.next().unwrap()); + } + + return bytes; +} + +/// Converts the windows DateTime into Chrono DateTime +pub fn get_datetime(iterator: &mut std::vec::IntoIter, topbyte: bool) -> DateTime { + let mut datetime_i64 = i64::from_le_bytes(get_bytes(iterator)); + + if topbyte { + // Zero the topmost byte + datetime_i64 = datetime_i64 & 0x00FFFFFFFFFFFFFFF; + } + + if datetime_i64 <= 0 { + return Utc.timestamp_opt(0, 0).unwrap(); + } + + let unix_time_ticks = datetime_i64 - 621355968000000000; + + let unix_time_seconds = unix_time_ticks / 10000000; + + let unix_time_nanos = match (unix_time_ticks as f64 / 10000000.0) - unix_time_seconds as f64 + > 0.0 + { + true => ((unix_time_ticks as f64 / 10000000.0) - unix_time_seconds as f64) * 1000000000.0, + false => 0.0, + }; + + Utc.timestamp_opt(unix_time_seconds, unix_time_nanos as u32) + .unwrap() +} \ No newline at end of file diff --git a/src/music_storage/db_reader/extern_library.rs b/src/music_storage/db_reader/extern_library.rs new file mode 100644 index 0000000..ffe0711 --- /dev/null +++ b/src/music_storage/db_reader/extern_library.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + + +pub trait ExternalLibrary { + fn from_file(&mut self, file: &PathBuf) -> Self; + fn write(&self) { + unimplemented!(); + } +} \ No newline at end of file diff --git a/src/music_storage/db_reader/foobar/reader.rs b/src/music_storage/db_reader/foobar/reader.rs new file mode 100644 index 0000000..017d7fc --- /dev/null +++ b/src/music_storage/db_reader/foobar/reader.rs @@ -0,0 +1,176 @@ +use chrono::{DateTime, Utc}; +use std::{fs::File, io::Read, path::PathBuf, time::Duration}; + +use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec, get_datetime}; + +const MAGIC: [u8; 16] = [ + 0xE1, 0xA0, 0x9C, 0x91, 0xF8, 0x3C, 0x77, 0x42, 0x85, 0x2C, 0x3B, 0xCC, 0x14, 0x01, 0xD3, 0xF2, +]; + +#[derive(Debug)] +pub struct FoobarPlaylist { + path: PathBuf, + metadata: Vec, +} + +#[derive(Debug, Default)] +pub struct FoobarPlaylistTrack { + flags: i32, + file_name: String, + subsong_index: i32, + file_size: i64, + file_time: DateTime, + duration: Duration, + rpg_album: u32, + rpg_track: u32, + rpk_album: u32, + rpk_track: u32, + entries: Vec<(String, String)>, +} + +impl FoobarPlaylist { + pub fn new(path: &String) -> Self { + FoobarPlaylist { + path: PathBuf::from(path), + metadata: Vec::new(), + } + } + + fn get_meta_offset(&self, offset: usize) -> String { + let mut result_vec = Vec::new(); + + let mut i = offset; + loop { + if self.metadata[i] == 0x00 { + break; + } + + result_vec.push(self.metadata[i]); + i += 1; + } + + String::from_utf8_lossy(&result_vec).into() + } + + /// Reads the entire MusicBee library and returns relevant values + /// as a `Vec` of `Song`s + pub fn read(&mut self) -> Result, Box> { + let mut f = File::open(&self.path).unwrap(); + let mut buffer = Vec::new(); + let mut retrieved_songs: Vec = Vec::new(); + + // Read the whole file + f.read_to_end(&mut buffer)?; + + let mut buf_iter = buffer.into_iter(); + + // Parse the header + let magic = get_bytes::<16>(&mut buf_iter); + if magic != MAGIC { + return Err("Magic bytes mismatch!".into()); + } + + let meta_size = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; + self.metadata = get_bytes_vec(&mut buf_iter, meta_size); + let track_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Read all the track fields + for _ in 0..track_count { + let flags = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + let has_metadata = (0x01 & flags) != 0; + let has_padding = (0x04 & flags) != 0; + + let file_name_offset = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; + let file_name = self.get_meta_offset(file_name_offset); + + let subsong_index = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + if !has_metadata { + let track = FoobarPlaylistTrack { + file_name, + subsong_index, + ..Default::default() + }; + retrieved_songs.push(track); + continue; + } + + let file_size = i64::from_le_bytes(get_bytes(&mut buf_iter)); + + let file_time = get_datetime(&mut buf_iter, false); + + let duration = Duration::from_nanos(u64::from_le_bytes(get_bytes(&mut buf_iter)) / 100); + + let rpg_album = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + let rpg_track = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + let rpk_album = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + let rpk_track = u32::from_le_bytes(get_bytes(&mut buf_iter)); + + get_bytes::<4>(&mut buf_iter); + + let mut entries = Vec::new(); + let primary_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + let secondary_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + let _secondary_offset = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get primary keys + for _ in 0..primary_count { + println!("{}", i32::from_le_bytes(get_bytes(&mut buf_iter))); + + let key = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + + entries.push((key, String::new())); + } + + // Consume unknown 32 bit value + println!("unk"); + get_bytes::<4>(&mut buf_iter); + + // Get primary values + for i in 0..primary_count { + println!("primkey {i}"); + + let value = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + + entries[i as usize].1 = value; + } + + // Get secondary Keys + for _ in 0..secondary_count { + let key = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + let value = + self.get_meta_offset(i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize); + entries.push((key, value)); + } + + if has_padding { + get_bytes::<64>(&mut buf_iter); + } + + let track = FoobarPlaylistTrack { + flags, + file_name, + subsong_index, + file_size, + file_time, + duration, + rpg_album, + rpg_track, + rpk_album, + rpk_track, + entries, + }; + + retrieved_songs.push(track); + } + + Ok(retrieved_songs) + } +} diff --git a/src/music_storage/db_reader/musicbee/reader.rs b/src/music_storage/db_reader/musicbee/reader.rs new file mode 100644 index 0000000..0a5cac6 --- /dev/null +++ b/src/music_storage/db_reader/musicbee/reader.rs @@ -0,0 +1,220 @@ +use super::utils::get_string; +use crate::music_storage::db_reader::common::{get_bytes, get_datetime}; +use chrono::{DateTime, Utc}; +use std::fs::File; +use std::io::prelude::*; +use std::time::Duration; + +pub struct MusicBeeDatabase { + path: String, +} + +impl MusicBeeDatabase { + pub fn new(path: String) -> MusicBeeDatabase { + MusicBeeDatabase { path } + } + + /// Reads the entire MusicBee library and returns relevant values + /// as a `Vec` of `Song`s + pub fn read(&self) -> Result, Box> { + let mut f = File::open(&self.path).unwrap(); + let mut buffer = Vec::new(); + let mut retrieved_songs: Vec = Vec::new(); + + // Read the whole file + f.read_to_end(&mut buffer)?; + + let mut buf_iter = buffer.into_iter(); + + // Get the song count from the first 4 bytes + // and then right shift it by 8 for some reason + let mut database_song_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); + database_song_count = database_song_count >> 8; + + let mut song_count = 0; + loop { + // If the file designation is 1, then the end of the database + // has been reached + let file_designation = match buf_iter.next() { + Some(1) => break, + Some(value) => value, + None => break, + }; + + song_count += 1; + + // Get the file status. Unknown what this means + let status = buf_iter.next().unwrap(); + + buf_iter.next(); // Read in a byte to throw it away + + // Get the play count + let play_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the time the song was last played, stored as a signed 64 bit number of microseconds + let last_played = get_datetime(buf_iter.by_ref(), true); + + // Get the number of times the song was skipped + let skip_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the path to the song + let path = get_string(buf_iter.by_ref()); + + // Get the file size + let file_size = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the sample rate + let sample_rate = i32::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the channel count + let channel_count = buf_iter.next().unwrap(); + + // Get the bitrate type (CBR, VBR, etc.) + let bitrate_type = buf_iter.next().unwrap(); + + // Get the actual bitrate + let bitrate = i16::from_le_bytes(get_bytes(&mut buf_iter)); + + // Get the track length in milliseconds + let track_length = + Duration::from_millis(i32::from_le_bytes(get_bytes(&mut buf_iter)) as u64); + + // Get the date added and modified in the same format + let date_added = get_datetime(buf_iter.by_ref(), true); + let date_modified = get_datetime(buf_iter.by_ref(), true); + + // Gets artwork information + // + // Artworks are stored as chunks describing the type + // (embedded, file), and some other information. + let mut artwork: Vec = vec![]; + loop { + let artwork_type = buf_iter.next().unwrap(); + if artwork_type > 253 { + break; + } + + let unknown_string = get_string(buf_iter.by_ref()); + let storage_mode = buf_iter.next().unwrap(); + let storage_path = get_string(buf_iter.by_ref()); + + artwork.push(MusicBeeAlbumArt { + artwork_type, + unknown_string, + storage_mode, + storage_path, + }); + } + + buf_iter.next(); // Read in a byte to throw it away + + // Gets all the tags on the song in the database + let mut tags: Vec = vec![]; + loop { + // If the tag code is 0, the end of the block has been reached, so break. + // + // If the tag code is 255, it pertains to some CUE file values that are not known + // throw away these values + let tag_code = match buf_iter.next() { + Some(0) => break, + Some(255) => { + let repeats = u16::from_le_bytes(get_bytes(&mut buf_iter)); + for _ in 0..(repeats * 13) - 2 { + buf_iter.next().unwrap(); + } + + 255 + } + Some(value) => value, + None => panic!(), + }; + + // Get the string value of the tag + let tag_value = get_string(buf_iter.by_ref()); + tags.push(MusicBeeTag { + tag_code, + tag_value, + }); + } + + // Construct the finished song and add it to the vec + let constructed_song = MusicBeeSong { + file_designation, + status, + play_count, + last_played, + skip_count, + path, + file_size, + sample_rate, + channel_count, + bitrate_type, + bitrate, + track_length, + date_added, + date_modified, + artwork, + tags, + }; + + retrieved_songs.push(constructed_song); + } + + println!("The database claims you have: {database_song_count} songs\nThe retrieved number is: {song_count} songs"); + + match database_song_count == song_count { + true => Ok(retrieved_songs), + false => Err("Song counts do not match!".into()), + } + } +} + +#[derive(Debug)] +pub struct MusicBeeTag { + tag_code: u8, + tag_value: String, +} + +#[derive(Debug)] +pub struct MusicBeeAlbumArt { + artwork_type: u8, + unknown_string: String, + storage_mode: u8, + storage_path: String, +} + +#[derive(Debug)] +pub struct MusicBeeSong { + file_designation: u8, + status: u8, + play_count: u16, + pub last_played: DateTime, + skip_count: u16, + path: String, + file_size: i32, + sample_rate: i32, + channel_count: u8, + bitrate_type: u8, + bitrate: i16, + track_length: Duration, + date_added: DateTime, + date_modified: DateTime, + + /* Album art stuff */ + artwork: Vec, + + /* All tags */ + tags: Vec, +} + +impl MusicBeeSong { + pub fn get_tag_code(self, code: u8) -> Option { + for tag in &self.tags { + if tag.tag_code == code { + return Some(tag.tag_value.clone()); + } + } + + None + } +} diff --git a/src/music_storage/db_reader/musicbee/utils.rs b/src/music_storage/db_reader/musicbee/utils.rs new file mode 100644 index 0000000..65d6333 --- /dev/null +++ b/src/music_storage/db_reader/musicbee/utils.rs @@ -0,0 +1,29 @@ +use leb128; + +/// Gets a string from the MusicBee database format +/// +/// The length of the string is defined by an LEB128 encoded value at the beginning, followed by the string of that length +pub fn get_string(iterator: &mut std::vec::IntoIter) -> String { + let mut string_length = iterator.next().unwrap() as usize; + if string_length == 0 { + return String::new(); + } + + // Decode the LEB128 value + let mut leb_bytes: Vec = vec![]; + loop { + leb_bytes.push(string_length as u8); + + if string_length >> 7 != 1 { + break; + } + string_length = iterator.next().unwrap() as usize; + } + string_length = leb128::read::unsigned(&mut leb_bytes.as_slice()).unwrap() as usize; + + let mut string_bytes = vec![]; + for _ in 0..string_length { + string_bytes.push(iterator.next().unwrap()); + } + String::from_utf8(string_bytes).unwrap() +} diff --git a/src/music_storage/db_reader/xml/reader.rs b/src/music_storage/db_reader/xml/reader.rs new file mode 100644 index 0000000..de2a057 --- /dev/null +++ b/src/music_storage/db_reader/xml/reader.rs @@ -0,0 +1,222 @@ +use quick_xml::events::Event; +use quick_xml::reader::Reader; + +use std::collections::{BTreeMap, HashMap}; +use std::io::Error; +use std::path::PathBuf; +use std::str::FromStr; +use std::vec::Vec; + +use chrono::prelude::*; + +use crate::music_storage::db_reader::extern_library::ExternalLibrary; + +#[derive(Debug, Default, Clone)] +pub struct XmlLibrary { + tracks: Vec +} +impl XmlLibrary { + fn new() -> Self { + Default::default() + } +} +impl ExternalLibrary for XmlLibrary { + fn from_file(&mut self, file: &PathBuf) -> Self { + let mut reader = Reader::from_file(file).unwrap(); + reader.trim_text(true); + //count every event, for fun ig? + let mut count = 0; + //count for skipping useless beginning key + let mut count2 = 0; + //number of grabbed songs + let mut count3 = 0; + //number of IDs skipped + let mut count4 = 0; + + let mut buf = Vec::new(); + let mut skip = false; + + let mut converted_songs: Vec = Vec::new(); + + + let mut song_tags: HashMap = HashMap::new(); + let mut key: String = String::new(); + let mut tagvalue: String = String::new(); + let mut key_selected = false; + + use std::time::Instant; + let now = Instant::now(); + + loop { + //push tag to song_tags map + if !key.is_empty() && !tagvalue.is_empty() { + song_tags.insert(key.clone(), tagvalue.clone()); + key.clear(); + tagvalue.clear(); + key_selected = false; + + //end the song to start a new one, and turn turn current song map into XMLSong + if song_tags.contains_key(&"Location".to_string()) { + count3 += 1; + //check for skipped IDs + if &count3.to_string() + != song_tags.get_key_value(&"Track ID".to_string()).unwrap().1 + { + count3 += 1; + count4 += 1; + } + converted_songs.push(XMLSong::from_hashmap(&mut song_tags).unwrap()); + song_tags.clear(); + skip = true; + } + } + match reader.read_event_into(&mut buf) { + Ok(Event::Start(_)) => { + count += 1; + count2 += 1; + } + Ok(Event::Text(e)) => { + if count < 17 && count != 10 { + continue; + }else if skip { + skip = false; + continue; + } + + let text = e.unescape().unwrap().to_string(); + + if text == count2.to_string() && !key_selected { + continue; + } + + //Add the key/value depenidng on if the key is selected or not ⛩️sorry buzz + + match key_selected { + true => tagvalue.push_str(&text), + false => { + key.push_str(&text); + if !key.is_empty() { + key_selected = true + } else { + panic!("Key not selected?!") + } + } + _ => panic!("WHAT DID YOU JUST DO?!🐰🐰🐰🐰"), + } + } + Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), + Ok(Event::Eof) => break, + _ => (), + } + buf.clear(); + } + let elasped = now.elapsed(); + println!("\n\nXMLReader\n=========================================\n\nDone!\n{} songs grabbed in {:#?}\nIDs Skipped: {}", count3, elasped, count4); + // dbg!(folder); + self.tracks.append(converted_songs.as_mut()); + self.clone() + } +} +#[derive(Debug, Clone, Default)] +pub struct XMLSong { + pub id: i32, + pub plays: i32, + pub favorited: bool, + pub banned: bool, + pub rating: Option, + pub format: Option, + pub song_type: Option, + pub last_played: Option>, + pub date_added: Option>, + pub date_modified: Option>, + pub tags: BTreeMap, + pub location: String, +} + +impl XMLSong { + pub fn new() -> XMLSong { + Default::default() + } + + + fn from_hashmap(map: &mut HashMap) -> Result { + let mut song = XMLSong::new(); + //get the path with the first bit chopped off + let path_: String = map.get_key_value("Location").unwrap().1.clone(); + let track_type: String = map.get_key_value("Track Type").unwrap().1.clone(); + let path: String = match track_type.as_str() { + "File" => { + if path_.contains("file://localhost/") { + path_.strip_prefix("file://localhost/").unwrap(); + } + path_ + } + "URL" => path_, + _ => path_, + }; + + for (key, value) in map { + match key.as_str() { + "Track ID" => song.id = value.parse().unwrap(), + "Location" => song.location = path.to_string(), + "Play Count" => song.plays = value.parse().unwrap(), + "Love" => { + //check if the track is (L)Loved or (B)Banned + match value.as_str() { + "L" => song.favorited = true, + "B" => song.banned = false, + _ => continue, + } + } + "Rating" => song.rating = Some(value.parse().unwrap()), + "Kind" => song.format = Some(value.to_string()), + "Play Date UTC" => { + song.last_played = Some(DateTime::::from_str(value).unwrap()) + } + "Date Added" => song.date_added = Some(DateTime::::from_str(value).unwrap()), + "Date Modified" => { + song.date_modified = Some(DateTime::::from_str(value).unwrap()) + } + "Track Type" => song.song_type = Some(value.to_string()), + _ => { + song.tags.insert(key.to_string(), value.to_string()); + } + } + } + // println!("{:.2?}", song); + Ok(song) + } +} + + +pub fn get_folder(file: &PathBuf) -> String { + let mut reader = Reader::from_file(file).unwrap(); + reader.trim_text(true); + //count every event, for fun ig? + let mut count = 0; + let mut buf = Vec::new(); + let mut folder = String::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(_)) => { + count += 1; + } + Ok(Event::Text(e)) => { + if count == 10 { + folder = String::from( + e.unescape() + .unwrap() + .to_string() + .strip_prefix("file://localhost/") + .unwrap(), + ); + return folder; + } + } + Err(_e) => { + panic!("oh no! something happened in the public function `get_reader_from_xml()!`") + } + _ => (), + } + } +} diff --git a/src/music_storage/library.rs b/src/music_storage/library.rs index 4fdcabd..d264bdf 100644 --- a/src/music_storage/library.rs +++ b/src/music_storage/library.rs @@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize}; use rayon::prelude::*; use std::sync::{Arc, Mutex, RwLock}; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub enum AlbumArt { Embedded(usize), External(URI), @@ -115,7 +115,7 @@ impl ToString for Field { } /// Stores information about a single song -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct Song { pub location: URI, pub plays: i32, @@ -275,7 +275,6 @@ impl Album<'_> { self.artist } - pub fn discs(&self) -> &BTreeMap> { &self.discs diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index 2ad4742..710e6fd 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -1,15 +1,68 @@ +use chrono::Duration; use walkdir::Error; use crate::music_controller::config::Config; use std::{path::Path, default, thread::AccessError}; -use super::{library::{AlbumArt, Song}, music_collection::MusicCollection}; +use super::{library::{AlbumArt, Song, self, Tag}, music_collection::MusicCollection}; -#[derive(Debug, Default)] +#[derive(Debug, Clone)] pub struct Playlist<'a> { title: String, cover: Option<&'a AlbumArt>, tracks: Vec<&'a Song>, + play_count: i32, + play_time: Duration, +} +impl<'a> Playlist<'a> { + pub fn new() -> Self { + Default::default() + } + pub fn play_count(&self) -> i32 { + self.play_count + } + pub fn play_time(&self) -> chrono::Duration { + self.play_time + } + pub fn set_tracks(&mut self, songs: Vec<&'a Song>) -> Result<(), Error> { + self.tracks = songs; + Ok(()) + } + pub fn add_track(&mut self, song: &'a Song) -> Result<(), Error> { + self.tracks.push(song); + Ok(()) + } + pub fn remove_track(&mut self, index: i32) -> Result<(), Error> { + let bun: usize = index as usize; + let mut name = String::new(); + if self.tracks.len() >= bun { + name = String::from(self.tracks[bun].tags.get_key_value(&Tag::Title).unwrap().1); + self.tracks.remove(bun); + } + dbg!(name); + Ok(()) + } + pub fn get_index(&self, song_name: &str) -> Option { + let mut index = 0; + if self.contains(&Tag::Title, song_name) { + for track in &self.tracks { + index += 1; + if song_name == track.tags.get_key_value(&Tag::Title).unwrap().1 { + dbg!("Index gotted! ",index); + return Some(index); + } + } + } + None + } + pub fn contains(&self, tag: &Tag, title: &str) -> bool { + for track in &self.tracks { + if title == track.tags.get_key_value(tag).unwrap().1 { + return true; + } + } + false + } } impl MusicCollection for Playlist<'_> { fn title(&self) -> &String { @@ -25,3 +78,14 @@ impl MusicCollection for Playlist<'_> { self.tracks.clone() } } +impl Default for Playlist<'_> { + fn default() -> Self { + Playlist { + title: String::default(), + cover: None, + tracks: Vec::default(), + play_count: -1, + play_time: Duration::zero(), + } + } +} \ No newline at end of file