> {
+// let config = &self.config.read().unwrap();
+
+// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid));
+// let res = &self.db_mail.recv()?;
+// let song = match res {
+// DatabaseResponse::Song(song) => song,
+// _ => todo!()
+// };
+// let unknown = &"unknown".to_string();
+// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown);
+// let track = song.get_tag(&Tag::Title).unwrap_or(unknown);
+// let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str());
+
+// client.listen(artist, track, release)?;
+// Ok(())
+// }
+
+// pub fn discord_song_change(client: &mut Client,song: Song) {
+// client.set_activity(|a| {
+// a.state(format!("Listening to {}", song.get_tag(&Tag::Title).unwrap()))
+// .into()
+// });
+// }
+// }
+
+// #[cfg(test)]
+// mod test_super {
+// use std::{thread::sleep, time::Duration};
+
+// use super::*;
+// use crate::config::config::tests::read_config_lib;
+
+// #[test]
+// fn listenbrainz() {
+// let mut c = Controller::start(".\\test-config\\config_test.json").unwrap();
+
+// let client = c.listenbrainz_authenticate().unwrap();
+
+// c.q_new().unwrap();
+// c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap();
+
+// let songs = c.lib_get_songs();
+
+// c.q_enqueue(0, songs[1].location.to_owned()).unwrap();
+// c.q_play(0).unwrap();
+
+// sleep(Duration::from_secs(100));
+// c.lbz_scrobble(client, songs[1].uuid).unwrap();
+// }
+// }
diff --git a/dmp-core/src/music_controller/controller.rs b/dmp-core/src/music_controller/controller.rs
new file mode 100644
index 0000000..950d7d5
--- /dev/null
+++ b/dmp-core/src/music_controller/controller.rs
@@ -0,0 +1,499 @@
+//! The [Controller] is the input and output for the entire
+//! player. It manages queues, playback, library access, and
+//! other functions
+#![allow(while_true)]
+
+use kushi::{Queue, QueueItemType};
+use kushi::{QueueError, QueueItem};
+use std::error::Error;
+use std::marker::PhantomData;
+use std::sync::{Arc, RwLock};
+use thiserror::Error;
+use uuid::Uuid;
+
+use crate::config::ConfigError;
+use crate::music_player::player::{Player, PlayerError};
+use crate::music_storage::library::Song;
+use crate::{config::Config, music_storage::library::MusicLibrary};
+
+use super::queue::{QueueAlbum, QueueSong};
+
+pub struct Controller<'a, P>(&'a PhantomData);
+
+#[derive(Error, Debug)]
+pub enum ControllerError {
+ #[error("{0:?}")]
+ QueueError(#[from] QueueError),
+ #[error("{0:?}")]
+ PlayerError(#[from] PlayerError),
+ #[error("{0:?}")]
+ ConfigError(#[from] ConfigError),
+}
+
+// TODO: move this to a different location to be used elsewhere
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[non_exhaustive]
+pub enum PlayerLocation {
+ Test,
+ Library,
+ Playlist(Uuid),
+ File,
+ Custom,
+}
+
+#[derive(Debug, Clone)]
+pub struct MailMan {
+ tx: async_channel::Sender,
+ rx: async_channel::Receiver,
+}
+
+impl MailMan {
+ pub fn double() -> (MailMan, MailMan) {
+ let (tx, rx) = async_channel::unbounded::();
+ let (tx1, rx1) = async_channel::unbounded::();
+
+ (MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx })
+ }
+
+ pub async fn send(&self, mail: Tx) -> Result<(), async_channel::SendError> {
+ self.tx.send(mail).await
+ }
+
+ pub async fn recv(&self) -> Result {
+ self.rx.recv().await
+ }
+}
+
+#[derive(Debug, PartialEq, PartialOrd, Clone)]
+pub enum PlayerCommand {
+ NextSong,
+ PrevSong,
+ Pause,
+ Play,
+ Enqueue(usize),
+ SetVolume(f64),
+}
+
+#[derive(Debug, PartialEq, PartialOrd, Clone)]
+pub enum PlayerResponse {
+ Empty,
+}
+
+pub enum LibraryCommand {
+ Song(Uuid),
+ AllSongs,
+ GetLibrary,
+}
+
+pub enum LibraryResponse {
+ Song(Song),
+ AllSongs(Vec),
+ Library(MusicLibrary),
+}
+
+enum InnerLibraryCommand {
+ Song(Uuid),
+ AllSongs,
+}
+
+enum InnerLibraryResponse<'a> {
+ Song(&'a Song),
+ AllSongs(&'a Vec),
+}
+
+pub enum QueueCommand {
+ Append(QueueItem),
+ Next,
+ Prev,
+ GetIndex(usize),
+ NowPlaying,
+}
+
+pub enum QueueResponse {
+ Ok,
+ Item(QueueItem),
+}
+
+
+pub struct ControllerInput {
+ player_mail: (
+ MailMan,
+ MailMan,
+ ),
+ lib_mail: MailMan,
+ library: MusicLibrary,
+ config: Arc>,
+}
+
+pub struct ControllerHandle {
+ pub lib_mail: MailMan,
+ pub player_mail: MailMan,
+}
+
+impl ControllerHandle {
+ pub fn new(library: MusicLibrary, config: Arc>) -> (Self, ControllerInput) {
+ let lib_mail = MailMan::double();
+ let player_mail = MailMan::double();
+
+ (
+ ControllerHandle {
+ lib_mail: lib_mail.0,
+ player_mail: player_mail.0.clone()
+ },
+ ControllerInput {
+ player_mail,
+ lib_mail: lib_mail.1,
+ library,
+ config
+ }
+ )
+ }
+}
+
+#[allow(unused_variables)]
+impl<'c, P: Player + Send + Sync> Controller<'c, P> {
+ pub async fn start(
+ ControllerInput {
+ player_mail,
+ lib_mail,
+ mut library,
+ config
+ }: ControllerInput
+ ) -> Result<(), Box>
+ where
+ P: Player,
+ {
+ //TODO: make a separate event loop for sccessing library that clones borrowed values from inner library loop?
+ let mut queue: Queue = Queue {
+ items: Vec::new(),
+ played: Vec::new(),
+ loop_: false,
+ shuffle: None,
+ };
+
+ for song in &library.library {
+ queue.add_item(
+ QueueSong {
+ song: song.clone(),
+ location: PlayerLocation::Test,
+ },
+ true,
+ );
+ }
+ let inner_lib_mail = MailMan::double();
+ let queue = queue;
+
+ std::thread::scope(|scope| {
+ let queue_mail = MailMan::double();
+ let a = scope.spawn(|| {
+ futures::executor::block_on(async {
+ moro::async_scope!(|scope| {
+ println!("async scope created");
+ let player = Arc::new(RwLock::new(P::new().unwrap()));
+
+ let _player = player.clone();
+ scope
+ .spawn(async move {
+ Controller::::player_command_loop(
+ _player,
+ player_mail.1,
+ queue_mail.0,
+ )
+ .await
+ .unwrap();
+ });
+ scope
+ .spawn(async move {
+ Controller::
::player_event_loop(player, player_mail.0)
+ .await
+ .unwrap();
+ });
+ scope
+ .spawn(async {
+ Controller::
::inner_library_loop(inner_lib_mail.1, &mut library).await
+ .unwrap()
+ });
+ scope
+ .spawn(async {
+ Controller::
::outer_library_loop(lib_mail, inner_lib_mail.0)
+ .await
+ .unwrap();
+ });
+ })
+ .await;
+ })
+ });
+
+ let b = scope.spawn(|| {
+ futures::executor::block_on(async {
+ Controller::
::queue_loop(queue, queue_mail.1).await;
+ })
+ });
+ a.join().unwrap();
+ b.join().unwrap();
+ });
+
+ Ok(())
+ }
+
+ async fn player_command_loop(
+ player: Arc>,
+ player_mail: MailMan,
+ queue_mail: MailMan,
+ ) -> Result<(), ()> {
+ {
+ player.write().unwrap().set_volume(0.05);
+ }
+ while true {
+ let _mail = player_mail.recv().await;
+ if let Ok(mail) = _mail {
+ match mail {
+ PlayerCommand::Play => {
+ player.write().unwrap().play().unwrap();
+ player_mail.send(PlayerResponse::Empty).await.unwrap();
+ }
+ PlayerCommand::Pause => {
+ player.write().unwrap().pause().unwrap();
+ player_mail.send(PlayerResponse::Empty).await.unwrap();
+ }
+ PlayerCommand::SetVolume(volume) => {
+ player.write().unwrap().set_volume(volume);
+ println!("volume set to {volume}");
+ player_mail.send(PlayerResponse::Empty).await.unwrap();
+ }
+ PlayerCommand::NextSong => {
+ queue_mail.send(QueueCommand::Next).await.unwrap();
+
+ if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() {
+ let uri = match &item.item {
+ QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
+ _ => unimplemented!(),
+ };
+ player.write().unwrap().enqueue_next(uri).unwrap();
+ player_mail.send(PlayerResponse::Empty).await.unwrap();
+ }
+ }
+ PlayerCommand::PrevSong => {
+ queue_mail.send(QueueCommand::Prev).await.unwrap();
+
+ if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() {
+ let uri = match &item.item {
+ QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
+ _ => unimplemented!(),
+ };
+ player.write().unwrap().enqueue_next(uri).unwrap();
+ player_mail.send(PlayerResponse::Empty).await.unwrap();
+ }
+ }
+ PlayerCommand::Enqueue(index) => {
+ queue_mail
+ .send(QueueCommand::GetIndex(index))
+ .await
+ .unwrap();
+ if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() {
+ match item.item {
+ QueueItemType::Single(song) => {
+ player
+ .write()
+ .unwrap()
+ .enqueue_next(song.song.primary_uri().unwrap().0)
+ .unwrap();
+ }
+ _ => unimplemented!(),
+ }
+ player_mail.send(PlayerResponse::Empty).await.unwrap();
+ }
+ }
+ }
+ } else {
+ return Err(());
+ }
+ }
+ Ok(())
+ }
+
+ async fn outer_library_loop(
+ lib_mail: MailMan,
+ inner_lib_mail: MailMan>,
+ ) -> Result<(), ()> {
+ println!("outer lib loop");
+ while true {
+ match lib_mail.recv().await.unwrap() {
+ LibraryCommand::Song(uuid) => {
+ println!("got song commandf");
+ inner_lib_mail
+ .send(InnerLibraryCommand::Song(uuid))
+ .await
+ .unwrap();
+ let x = inner_lib_mail.recv().await.unwrap();
+ }
+ LibraryCommand::AllSongs => {
+ println!("got command");
+ inner_lib_mail
+ .send(InnerLibraryCommand::AllSongs)
+ .await
+ .unwrap();
+ println!("sent");
+ let x = inner_lib_mail.recv().await.unwrap();
+ println!("recieved");
+ if let InnerLibraryResponse::AllSongs(songs) = x {
+ lib_mail.send(LibraryResponse::AllSongs(songs.clone())).await.unwrap();
+ } else {
+ unreachable!()
+ }
+ },
+ _ => { todo!() }
+ }
+ }
+ Ok(())
+ }
+
+ async fn inner_library_loop(
+ lib_mail: MailMan, InnerLibraryCommand>,
+ library: &'c mut MusicLibrary,
+ ) -> Result<(), ()> {
+ while true {
+ match lib_mail.recv().await.unwrap() {
+ InnerLibraryCommand::Song(uuid) => {
+ let song: &'c Song = library.query_uuid(&uuid).unwrap().0;
+ lib_mail
+ .send(InnerLibraryResponse::Song(song))
+ .await
+ .unwrap();
+ }
+ InnerLibraryCommand::AllSongs => {
+ let songs: &'c Vec = &library.library;
+ lib_mail.send(InnerLibraryResponse::AllSongs(songs))
+ .await
+ .unwrap();
+ }
+ }
+ }
+ Ok(())
+ }
+
+ async fn player_event_loop(
+ player: Arc>,
+ player_mail: MailMan,
+ ) -> Result<(), ()> {
+ // just pretend this does something
+ Ok(())
+ }
+
+ async fn queue_loop(
+ mut queue: Queue,
+ queue_mail: MailMan,
+ ) {
+ while true {
+ match queue_mail.recv().await.unwrap() {
+ QueueCommand::Append(item) => match item.item {
+ QueueItemType::Single(song) => queue.add_item(song, true),
+ _ => unimplemented!(),
+ },
+ QueueCommand::Next => {
+ let next = queue.next().unwrap();
+ queue_mail
+ .send(QueueResponse::Item(next.clone()))
+ .await
+ .unwrap();
+ }
+ QueueCommand::Prev => {
+ let next = queue.prev().unwrap();
+ queue_mail
+ .send(QueueResponse::Item(next.clone()))
+ .await
+ .unwrap();
+ }
+ QueueCommand::GetIndex(index) => {
+ let item = queue.items[index].clone();
+ queue_mail.send(QueueResponse::Item(item)).await.unwrap();
+ }
+ QueueCommand::NowPlaying => {
+ let item = queue.current().unwrap();
+ queue_mail
+ .send(QueueResponse::Item(item.clone()))
+ .await
+ .unwrap();
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_super {
+ use std::{
+ path::PathBuf,
+ sync::{Arc, RwLock},
+ thread::spawn,
+ };
+
+ use crate::{
+ config::{tests::new_config_lib, Config},
+ music_controller::controller::{
+ LibraryCommand, LibraryResponse, MailMan, PlayerCommand, PlayerResponse, ControllerHandle
+ },
+ music_player::gstreamer::GStreamer,
+ music_storage::library::MusicLibrary,
+ };
+
+ use super::Controller;
+
+ #[tokio::test]
+ async fn construct_controller() {
+ // use if you don't have a config setup and add music to the music folder
+ new_config_lib();
+
+ let config = Config::read_file(PathBuf::from(std::env!("CONFIG-PATH"))).unwrap();
+ let mut library = {
+ MusicLibrary::init(
+ config.libraries.get_default().unwrap().path.clone(),
+ config.libraries.get_default().unwrap().uuid,
+ )
+ .unwrap()
+ };
+
+ let (handle, input) = ControllerHandle::new(library, Arc::new(RwLock::new(config)));
+
+ let b = spawn(move || {
+ futures::executor::block_on(async {
+ handle.player_mail
+ .send(PlayerCommand::SetVolume(0.01))
+ .await
+ .unwrap();
+ loop {
+ let buf: String = text_io::read!();
+ dbg!(&buf);
+ handle.player_mail
+ .send(match buf.to_lowercase().as_str() {
+ "next" => PlayerCommand::NextSong,
+ "prev" => PlayerCommand::PrevSong,
+ "pause" => PlayerCommand::Pause,
+ "play" => PlayerCommand::Play,
+ x if x.parse::().is_ok() => {
+ PlayerCommand::Enqueue(x.parse::().unwrap())
+ }
+ _ => continue,
+ })
+ .await
+ .unwrap();
+ println!("sent it");
+ println!("{:?}", handle.player_mail.recv().await.unwrap())
+ }
+ })
+ });
+
+ let a = spawn(move || {
+ futures::executor::block_on(async {
+
+
+ Controller::::start(input)
+ .await
+ .unwrap();
+ });
+ });
+
+ b.join().unwrap();
+ a.join().unwrap();
+ }
+}
diff --git a/dmp-core/src/music_controller/queue.rs b/dmp-core/src/music_controller/queue.rs
new file mode 100644
index 0000000..27f8075
--- /dev/null
+++ b/dmp-core/src/music_controller/queue.rs
@@ -0,0 +1,25 @@
+use std::vec::IntoIter;
+
+use crate::music_storage::library::{Album, AlbumTrack, Song};
+
+use super::controller::PlayerLocation;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct QueueSong {
+ pub song: Song,
+ pub location: PlayerLocation,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct QueueAlbum {
+ pub album: Album,
+ pub location: PlayerLocation,
+}
+
+impl IntoIterator for QueueAlbum {
+ type Item = AlbumTrack;
+ type IntoIter = IntoIter;
+ fn into_iter(self) -> Self::IntoIter {
+ self.album.into_iter()
+ }
+}
diff --git a/dmp-core/src/music_player/gstreamer.rs b/dmp-core/src/music_player/gstreamer.rs
new file mode 100644
index 0000000..7733d85
--- /dev/null
+++ b/dmp-core/src/music_player/gstreamer.rs
@@ -0,0 +1,521 @@
+// Crate things
+use crate::music_storage::library::URI;
+use crossbeam_channel::{unbounded, Receiver, Sender};
+use std::error::Error;
+use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
+
+// GStreamer things
+use glib::FlagsClass;
+use gst::{ClockTime, Element};
+use gstreamer as gst;
+use gstreamer::prelude::*;
+
+// Extra things
+use chrono::Duration;
+
+use super::player::{Player, PlayerCommand, PlayerError, PlayerState};
+
+impl From for PlayerState {
+ fn from(value: gst::State) -> Self {
+ match value {
+ gst::State::VoidPending => Self::VoidPending,
+ gst::State::Playing => Self::Playing,
+ gst::State::Paused => Self::Paused,
+ gst::State::Ready => Self::Ready,
+ gst::State::Null => Self::Null,
+ }
+ }
+}
+
+impl TryInto for PlayerState {
+ fn try_into(self) -> Result> {
+ match self {
+ Self::VoidPending => Ok(gst::State::VoidPending),
+ Self::Playing => Ok(gst::State::Playing),
+ Self::Paused => Ok(gst::State::Paused),
+ Self::Ready => Ok(gst::State::Ready),
+ Self::Null => Ok(gst::State::Null),
+ state => Err(format!("Invalid gst::State: {:?}", state).into()),
+ }
+ }
+
+ type Error = Box;
+}
+
+#[derive(Debug, PartialEq, Eq)]
+enum PlaybackInfo {
+ Idle,
+ Switching,
+ Playing {
+ start: Duration,
+ end: Duration,
+ },
+
+ /// When this is sent, the thread will die! Use it when the [Player] is
+ /// done playing
+ Finished,
+}
+
+/// An instance of a music player with a GStreamer backend
+#[derive(Debug)]
+pub struct GStreamer {
+ source: Option,
+
+ message_rx: crossbeam::channel::Receiver,
+ playback_tx: crossbeam::channel::Sender,
+
+ playbin: Arc>,
+ volume: f64,
+ start: Option,
+ end: Option,
+ paused: Arc>,
+ position: Arc>>,
+}
+
+impl From for PlayerError {
+ fn from(value: gst::StateChangeError) -> Self {
+ PlayerError::StateChange(value.to_string())
+ }
+}
+
+impl From for PlayerError {
+ fn from(value: glib::BoolError) -> Self {
+ PlayerError::General(value.to_string())
+ }
+}
+
+impl GStreamer {
+ /// Set the playback URI
+ fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> {
+ if !source.exists().is_ok_and(|x| x) {
+ // If the source doesn't exist, gstreamer will crash!
+ return Err(PlayerError::NotFound);
+ }
+
+ // Make sure the playback tracker knows the stuff is stopped
+ println!("Beginning switch");
+ self.playback_tx.send(PlaybackInfo::Switching).unwrap();
+
+ let uri = self.playbin.read().unwrap().property_value("current-uri");
+ self.source = Some(source.clone());
+ match source {
+ URI::Cue { start, end, .. } => {
+ self.playbin
+ .write()
+ .unwrap()
+ .set_property("uri", source.as_uri());
+
+ // Set the start and end positions of the CUE file
+ self.start = Some(Duration::from_std(*start).unwrap());
+ self.end = Some(Duration::from_std(*end).unwrap());
+
+ // Send the updated position to the tracker
+ self.playback_tx
+ .send(PlaybackInfo::Playing {
+ start: self.start.unwrap(),
+ end: self.end.unwrap(),
+ })
+ .unwrap();
+
+ // Wait for it to be ready, and then move to the proper position
+ self.play().unwrap();
+ let now = std::time::Instant::now();
+ while now.elapsed() < std::time::Duration::from_millis(20) {
+ if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() {
+ return Ok(());
+ }
+ std::thread::sleep(std::time::Duration::from_millis(1));
+ }
+ //panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)");
+ return Err(PlayerError::StateChange(
+ "Could not seek to beginning of CUE track".into(),
+ ));
+ }
+ _ => {
+ self.playbin
+ .write()
+ .unwrap()
+ .set_property("uri", source.as_uri());
+
+ if self.state() != PlayerState::Playing {
+ self.play().unwrap();
+ }
+
+ while self.raw_duration().is_none() {
+ std::thread::sleep(std::time::Duration::from_millis(10));
+ }
+
+ self.start = Some(Duration::seconds(0));
+ self.end = self.raw_duration();
+
+ // Send the updated position to the tracker
+ self.playback_tx
+ .send(PlaybackInfo::Playing {
+ start: self.start.unwrap(),
+ end: self.end.unwrap(),
+ })
+ .unwrap();
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Gets a mutable reference to the playbin element
+ fn playbin_mut(
+ &mut self,
+ ) -> Result, std::sync::PoisonError>>
+ {
+ let element = match self.playbin.write() {
+ Ok(element) => element,
+ Err(err) => return Err(err),
+ };
+ Ok(element)
+ }
+
+ /// Gets a read-only reference to the playbin element
+ fn playbin(
+ &self,
+ ) -> Result, std::sync::PoisonError>>
+ {
+ let element = match self.playbin.read() {
+ Ok(element) => element,
+ Err(err) => return Err(err),
+ };
+ Ok(element)
+ }
+
+ /// Set volume of the internal playbin player, can be
+ /// used to bypass the main volume control for seeking
+ fn set_gstreamer_volume(&mut self, volume: f64) {
+ self.playbin_mut().unwrap().set_property("volume", volume)
+ }
+
+ fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> {
+ self.playbin_mut().unwrap().set_state(state)?;
+
+ Ok(())
+ }
+
+ fn raw_duration(&self) -> Option {
+ self.playbin()
+ .unwrap()
+ .query_duration::()
+ .map(|pos| Duration::nanoseconds(pos.nseconds() as i64))
+ }
+
+ /// Get the current state of the playback
+ fn state(&mut self) -> PlayerState {
+ self.playbin().unwrap().current_state().into()
+ /*
+ match *self.buffer.read().unwrap() {
+ None => self.playbin().unwrap().current_state().into(),
+ Some(value) => PlayerState::Buffering(value),
+ }
+ */
+ }
+
+ fn property(&self, property: &str) -> glib::Value {
+ self.playbin().unwrap().property_value(property)
+ }
+
+ fn ready(&mut self) -> Result<(), PlayerError> {
+ self.set_state(gst::State::Ready)?;
+ Ok(())
+ }
+}
+
+impl Player for GStreamer {
+ fn new() -> Result {
+ // Initialize GStreamer, maybe figure out how to nicely fail here
+ if let Err(err) = gst::init() {
+ return Err(PlayerError::Init(err.to_string()));
+ };
+ let ctx = glib::MainContext::default();
+ let _guard = ctx.acquire();
+ let mainloop = glib::MainLoop::new(Some(&ctx), false);
+
+ let playbin_arc = Arc::new(RwLock::new(
+ match gst::ElementFactory::make("playbin3").build() {
+ Ok(playbin) => playbin,
+ Err(error) => return Err(PlayerError::Init(error.to_string())),
+ },
+ ));
+
+ let playbin = playbin_arc.clone();
+
+ let flags = playbin.read().unwrap().property_value("flags");
+ let flags_class = FlagsClass::with_type(flags.type_()).unwrap();
+
+ // Set up the Playbin flags to only play audio
+ let flags = flags_class
+ .builder_with_value(flags)
+ .ok_or(PlayerError::Build)?
+ .set_by_nick("audio")
+ .set_by_nick("download")
+ .unset_by_nick("video")
+ .unset_by_nick("text")
+ .build()
+ .ok_or(PlayerError::Build)?;
+
+ playbin
+ .write()
+ .unwrap()
+ .set_property_from_value("flags", &flags);
+ //playbin.write().unwrap().set_property("instant-uri", true);
+
+ let position = Arc::new(RwLock::new(None));
+
+ // Set up the thread to monitor the position
+ let (playback_tx, playback_rx) = unbounded();
+ let (status_tx, status_rx) = unbounded::();
+ let position_update = Arc::clone(&position);
+
+ std::thread::spawn(|| {
+ playback_monitor(playbin_arc, status_rx, playback_tx, position_update)
+ });
+
+ // Set up the thread to monitor bus messages
+ let playbin_bus_ctrl = Arc::clone(&playbin);
+ let paused = Arc::new(RwLock::new(false));
+ let bus_paused = Arc::clone(&paused);
+ let bus_watch = playbin
+ .read()
+ .unwrap()
+ .bus()
+ .expect("Failed to get GStreamer message bus")
+ .add_watch(move |_bus, msg| {
+ match msg.view() {
+ gst::MessageView::Eos(_) => println!("End of stream"),
+ gst::MessageView::StreamStart(_) => println!("Stream start"),
+ gst::MessageView::Error(err) => {
+ println!("Error recieved: {}", err);
+ return glib::ControlFlow::Break;
+ }
+ gst::MessageView::Buffering(buffering) => {
+ if *bus_paused.read().unwrap() == true {
+ return glib::ControlFlow::Continue;
+ }
+
+ // If the player is not paused, pause it
+ let percent = buffering.percent();
+ if percent < 100 {
+ playbin_bus_ctrl
+ .write()
+ .unwrap()
+ .set_state(gst::State::Paused)
+ .unwrap();
+ } else if percent >= 100 {
+ println!("Finished buffering");
+ playbin_bus_ctrl
+ .write()
+ .unwrap()
+ .set_state(gst::State::Playing)
+ .unwrap();
+ }
+ }
+ _ => (),
+ }
+ glib::ControlFlow::Continue
+ })
+ .expect("Failed to connect to GStreamer message bus");
+
+ // Set up a thread to watch the messages
+ std::thread::spawn(move || {
+ let _watch = bus_watch;
+ mainloop.run()
+ });
+
+ let source = None;
+ Ok(Self {
+ source,
+ playbin,
+ message_rx: playback_rx,
+ playback_tx: status_tx,
+ volume: 1.0,
+ start: None,
+ end: None,
+ paused,
+ position,
+ })
+ }
+
+ fn source(&self) -> &Option {
+ &self.source
+ }
+
+ fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> {
+ println!("enqueuing in fn");
+ self.set_source(next_track)
+ }
+
+ fn set_volume(&mut self, volume: f64) {
+ self.volume = volume.clamp(0.0, 1.0);
+ self.set_gstreamer_volume(self.volume);
+ }
+
+ fn volume(&self) -> f64 {
+ self.volume
+ }
+
+ fn play(&mut self) -> Result<(), PlayerError> {
+ if self.state() == PlayerState::Playing {
+ return Ok(());
+ }
+ *self.paused.write().unwrap() = false;
+ self.set_state(gst::State::Playing)?;
+ Ok(())
+ }
+
+ fn pause(&mut self) -> Result<(), PlayerError> {
+ if self.state() == PlayerState::Paused || *self.paused.read().unwrap() {
+ return Ok(());
+ }
+ *self.paused.write().unwrap() = true;
+ self.set_state(gst::State::Paused)?;
+ Ok(())
+ }
+
+ fn is_paused(&self) -> bool {
+ self.playbin().unwrap().current_state() == gst::State::Paused
+ }
+
+ fn position(&self) -> Option {
+ *self.position.read().unwrap()
+ }
+
+ fn duration(&self) -> Option {
+ if self.end.is_some() && self.start.is_some() {
+ Some(self.end.unwrap() - self.start.unwrap())
+ } else {
+ self.raw_duration()
+ }
+ }
+
+ fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError> {
+ let time_pos = match *self.position.read().unwrap() {
+ Some(pos) => pos,
+ None => return Err(PlayerError::Seek("No position".into())),
+ };
+ let seek_pos = time_pos + seek_amount;
+
+ self.seek_to(seek_pos)?;
+ Ok(())
+ }
+
+ fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError> {
+ let start = if self.start.is_none() {
+ return Err(PlayerError::Seek("No START time".into()));
+ } else {
+ self.start.unwrap()
+ };
+
+ let end = if self.end.is_none() {
+ return Err(PlayerError::Seek("No END time".into()));
+ } else {
+ self.end.unwrap()
+ };
+
+ let adjusted_target = target_pos + start;
+ let clamped_target = adjusted_target.clamp(start, end);
+
+ let seek_pos_clock =
+ ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64);
+
+ self.set_gstreamer_volume(0.0);
+ self.playbin_mut()
+ .unwrap()
+ .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?;
+ self.set_gstreamer_volume(self.volume);
+ Ok(())
+ }
+
+ fn stop(&mut self) -> Result<(), PlayerError> {
+ self.pause()?;
+ self.ready()?;
+
+ // Send the updated position to the tracker
+ self.playback_tx.send(PlaybackInfo::Idle).unwrap();
+
+ // Set all positions to none
+ *self.position.write().unwrap() = None;
+ self.start = None;
+ self.end = None;
+ Ok(())
+ }
+
+ fn message_channel(&self) -> &crossbeam::channel::Receiver {
+ &self.message_rx
+ }
+}
+
+impl Drop for GStreamer {
+ /// Cleans up the `GStreamer` pipeline and the monitoring
+ /// thread when [Player] is dropped.
+ fn drop(&mut self) {
+ self.playbin_mut()
+ .unwrap()
+ .set_state(gst::State::Null)
+ .expect("Unable to set the pipeline to the `Null` state");
+ let _ = self.playback_tx.send(PlaybackInfo::Finished);
+ }
+}
+
+fn playback_monitor(
+ playbin: Arc>,
+ status_rx: Receiver,
+ playback_tx: Sender,
+ position: Arc>>,
+) {
+ let mut stats = PlaybackInfo::Idle;
+ let mut pos_temp;
+ let mut sent_atf = false;
+ loop {
+ // Check for new messages to decide how to proceed
+ if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(50)) {
+ stats = result
+ }
+
+ pos_temp = playbin
+ .read()
+ .unwrap()
+ .query_position::()
+ .map(|pos| Duration::nanoseconds(pos.nseconds() as i64));
+
+ match stats {
+ PlaybackInfo::Playing { start, end } if pos_temp.is_some() => {
+ // Check if the current playback position is close to the end
+ let finish_point = end - Duration::milliseconds(2000);
+ if pos_temp.unwrap().num_microseconds() >= end.num_microseconds() {
+ println!("MONITOR: End of stream");
+ let _ = playback_tx.try_send(PlayerCommand::EndOfStream);
+ playbin
+ .write()
+ .unwrap()
+ .set_state(gst::State::Ready)
+ .expect("Unable to set the pipeline state");
+ sent_atf = false
+ } else if pos_temp.unwrap().num_microseconds() >= finish_point.num_microseconds()
+ && !sent_atf
+ {
+ println!("MONITOR: About to finish");
+ let _ = playback_tx.try_send(PlayerCommand::AboutToFinish);
+ sent_atf = true;
+ }
+
+ // This has to be done AFTER the current time in the file
+ // is calculated, or everything else is wrong
+ pos_temp = Some(pos_temp.unwrap() - start)
+ }
+ PlaybackInfo::Finished => {
+ println!("MONITOR: Shutting down");
+ *position.write().unwrap() = None;
+ break;
+ }
+ PlaybackInfo::Idle | PlaybackInfo::Switching => sent_atf = false,
+ _ => (),
+ }
+
+ *position.write().unwrap() = pos_temp;
+ }
+}
diff --git a/dmp-core/src/music_player/kira.rs b/dmp-core/src/music_player/kira.rs
new file mode 100644
index 0000000..e69de29
diff --git a/dmp-core/src/music_player/player.rs b/dmp-core/src/music_player/player.rs
new file mode 100644
index 0000000..4b74498
--- /dev/null
+++ b/dmp-core/src/music_player/player.rs
@@ -0,0 +1,99 @@
+use chrono::Duration;
+use thiserror::Error;
+
+use crate::music_storage::library::URI;
+
+#[derive(Error, Debug)]
+pub enum PlayerError {
+ #[error("player initialization failed: {0}")]
+ Init(String),
+ #[error("could not change playback state")]
+ StateChange(String),
+ #[error("seeking failed: {0}")]
+ Seek(String),
+ #[error("the file or source is not found")]
+ NotFound,
+ #[error("failed to build gstreamer item")]
+ Build,
+ #[error("poison error")]
+ Poison,
+ #[error("general player error")]
+ General(String),
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum PlayerState {
+ Playing,
+ Paused,
+ Ready,
+ Buffering(u8),
+ Null,
+ VoidPending,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum PlayerCommand {
+ Play,
+ Pause,
+ EndOfStream,
+ AboutToFinish,
+}
+
+pub trait Player {
+ /// Create a new player.
+ fn new() -> Result
+ where
+ Self: Sized;
+
+ /// Get the currently playing [URI] from the player.
+ fn source(&self) -> &Option;
+
+ /// Insert a new [`URI`] to be played. This method should be called at the
+ /// beginning to start playback of something, and once the [`PlayerCommand`]
+ /// indicates the track is about to finish to enqueue gaplessly.
+ ///
+ /// For backends which do not support gapless playback, `AboutToFinish`
+ /// will not be called, and the next [`URI`] should be enqueued once `Eos`
+ /// occurs.
+ fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>;
+
+ /// Set the playback volume, accepts a float from `0` to `1`.
+ ///
+ /// Values outside the range of `0` to `1` will be capped.
+ fn set_volume(&mut self, volume: f64);
+
+ /// Returns the current volume level, a float from `0` to `1`.
+ fn volume(&self) -> f64;
+
+ /// If the player is paused or stopped, starts playback.
+ fn play(&mut self) -> Result<(), PlayerError>;
+
+ /// If the player is playing, pause playback.
+ fn pause(&mut self) -> Result<(), PlayerError>;
+
+ /// Stop the playback entirely, removing the current [`URI`] from the player.
+ fn stop(&mut self) -> Result<(), PlayerError>;
+
+ /// Convenience function to check if playback is paused.
+ fn is_paused(&self) -> bool;
+
+ /// Get the current playback position of the player.
+ fn position(&self) -> Option;
+
+ /// Get the duration of the currently playing track.
+ fn duration(&self) -> Option;
+
+ /// Seek relative to the current position.
+ ///
+ /// The position is capped at the duration of the song, and zero.
+ fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>;
+
+ /// Seek absolutely within the song.
+ ///
+ /// The position is capped at the duration of the song, and zero.
+ fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>;
+
+ /// Return a reference to the player message channel, which can be cloned
+ /// in order to monitor messages from the player.
+ fn message_channel(&self) -> &crossbeam::channel::Receiver;
+}
diff --git a/dmp-core/src/music_storage/db_reader/common.rs b/dmp-core/src/music_storage/db_reader/common.rs
new file mode 100644
index 0000000..368d86e
--- /dev/null
+++ b/dmp-core/src/music_storage/db_reader/common.rs
@@ -0,0 +1,49 @@
+use chrono::{DateTime, TimeZone, Utc};
+
+pub fn get_bytes(iterator: &mut std::vec::IntoIter) -> [u8; S] {
+ let mut bytes = [0; S];
+
+ for byte in bytes.iter_mut().take(S) {
+ *byte = iterator.next().unwrap();
+ }
+
+ 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());
+ }
+
+ 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 &= 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()
+}
diff --git a/dmp-core/src/music_storage/db_reader/extern_library.rs b/dmp-core/src/music_storage/db_reader/extern_library.rs
new file mode 100644
index 0000000..4312c4a
--- /dev/null
+++ b/dmp-core/src/music_storage/db_reader/extern_library.rs
@@ -0,0 +1,11 @@
+use std::path::Path;
+
+use crate::music_storage::library::Song;
+
+pub trait ExternalLibrary {
+ fn from_file(file: &Path) -> Self;
+ fn write(&self) {
+ unimplemented!();
+ }
+ fn to_songs(&self) -> Vec;
+}
diff --git a/dmp-core/src/music_storage/db_reader/foobar/reader.rs b/dmp-core/src/music_storage/db_reader/foobar/reader.rs
new file mode 100644
index 0000000..4d07e71
--- /dev/null
+++ b/dmp-core/src/music_storage/db_reader/foobar/reader.rs
@@ -0,0 +1,200 @@
+use std::collections::BTreeMap;
+use std::{fs::File, io::Read, path::Path, time::Duration};
+
+use uuid::Uuid;
+
+use super::utils::meta_offset;
+use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec};
+use crate::music_storage::db_reader::extern_library::ExternalLibrary;
+use crate::music_storage::library::{Song, URI};
+
+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 {
+ metadata: Vec,
+ songs: Vec,
+}
+
+impl ExternalLibrary for FoobarPlaylist {
+ /// Reads the entire MusicBee library and returns relevant values
+ /// as a `Vec` of `Song`s
+ fn from_file(file: &Path) -> Self {
+ let mut f = File::open(file).unwrap();
+ let mut buffer = Vec::new();
+ let mut retrieved_songs: Vec = Vec::new();
+
+ // Read the whole file
+ f.read_to_end(&mut buffer).unwrap();
+
+ let mut buf_iter = buffer.into_iter();
+
+ // Parse the header
+ let magic = get_bytes::<16>(&mut buf_iter);
+ if magic != MAGIC {
+ panic!("Magic bytes mismatch!");
+ }
+
+ let meta_size = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize;
+ let 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 = meta_offset(metadata, 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));
+
+ // TODO: Figure out how to make this work properly
+ let file_time = i64::from_le_bytes(get_bytes(&mut buf_iter));
+
+ let duration = Duration::from_nanos(u64::from_le_bytes(get_bytes(&mut buf_iter)) / 100);
+
+ let rpg_album = f32::from_le_bytes(get_bytes(&mut buf_iter));
+
+ let rpg_track = f32::from_le_bytes(get_bytes(&mut buf_iter));
+
+ let rpk_album = f32::from_le_bytes(get_bytes(&mut buf_iter));
+
+ let rpk_track = f32::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 = meta_offset(
+ metadata,
+ 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 = meta_offset(
+ metadata,
+ 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 = meta_offset(
+ metadata,
+ i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize,
+ );
+ let value = meta_offset(
+ metadata,
+ 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);
+ }
+
+ Self {
+ songs: retrieved_songs,
+ metadata: metadata.clone(),
+ }
+ }
+
+ fn to_songs(&self) -> Vec {
+ self.songs.iter().map(|song| song.find_song()).collect()
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct FoobarPlaylistTrack {
+ flags: i32,
+ file_name: String,
+ subsong_index: i32,
+ file_size: i64,
+ file_time: i64,
+ duration: Duration,
+ rpg_album: f32,
+ rpg_track: f32,
+ rpk_album: f32,
+ rpk_track: f32,
+ entries: Vec<(String, String)>,
+}
+
+impl FoobarPlaylistTrack {
+ fn find_song(&self) -> Song {
+ let location = URI::Local(self.file_name.clone().into());
+ let internal_tags = Vec::new();
+
+ Song {
+ location: vec![location],
+ uuid: Uuid::new_v4(),
+ plays: 0,
+ skips: 0,
+ favorited: false,
+ banned: None,
+ rating: None,
+ format: None,
+ duration: self.duration,
+ play_time: Duration::from_secs(0),
+ last_played: None,
+ date_added: None,
+ date_modified: None,
+ album_art: Vec::new(),
+ tags: BTreeMap::new(),
+ internal_tags,
+ }
+ }
+}
diff --git a/dmp-core/src/music_storage/db_reader/foobar/utils.rs b/dmp-core/src/music_storage/db_reader/foobar/utils.rs
new file mode 100644
index 0000000..278aa1a
--- /dev/null
+++ b/dmp-core/src/music_storage/db_reader/foobar/utils.rs
@@ -0,0 +1,15 @@
+pub fn meta_offset(metadata: &[u8], offset: usize) -> String {
+ let mut result_vec = Vec::new();
+
+ let mut i = offset;
+ loop {
+ if metadata[i] == 0x00 {
+ break;
+ }
+
+ result_vec.push(metadata[i]);
+ i += 1;
+ }
+
+ String::from_utf8_lossy(&result_vec).into()
+}
diff --git a/dmp-core/src/music_storage/db_reader/itunes/reader.rs b/dmp-core/src/music_storage/db_reader/itunes/reader.rs
new file mode 100644
index 0000000..05075f2
--- /dev/null
+++ b/dmp-core/src/music_storage/db_reader/itunes/reader.rs
@@ -0,0 +1,381 @@
+use file_format::FileFormat;
+use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt};
+use quick_xml::events::Event;
+use quick_xml::reader::Reader;
+use std::collections::{BTreeMap, HashMap};
+use std::fs::File;
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+use std::time::Duration as StdDur;
+use std::vec::Vec;
+use uuid::Uuid;
+
+use chrono::prelude::*;
+
+use crate::music_storage::db_reader::extern_library::ExternalLibrary;
+use crate::music_storage::library::{AlbumArt, BannedType, Service, Song, Tag, URI};
+use crate::music_storage::utils;
+
+use urlencoding::decode;
+
+#[derive(Debug, Default, Clone)]
+pub struct ITunesLibrary {
+ tracks: Vec,
+}
+impl ITunesLibrary {
+ fn new() -> Self {
+ Default::default()
+ }
+ pub fn tracks(self) -> Vec {
+ self.tracks
+ }
+}
+impl ExternalLibrary for ITunesLibrary {
+ fn from_file(file: &Path) -> 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 iTunesSong
+ 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(ITunesSong::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?!")
+ }
+ }
+ }
+ }
+ Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
+ Ok(Event::Eof) => break,
+ _ => (),
+ }
+ buf.clear();
+ }
+ let elasped = now.elapsed();
+ println!(
+ "\n\niTunesReader grabbed {} songs in {:#?} seconds\nIDs Skipped: {}",
+ count3,
+ elasped.as_secs(),
+ count4
+ );
+ let mut lib = ITunesLibrary::new();
+ lib.tracks.append(converted_songs.as_mut());
+ lib
+ }
+ fn to_songs(&self) -> Vec {
+ let mut count = 0;
+ let mut bun: Vec = Vec::new();
+ for track in &self.tracks {
+ //grab "other" tags
+ let mut tags_: BTreeMap = BTreeMap::new();
+ for (key, val) in &track.tags {
+ tags_.insert(to_tag(key.clone()), val.clone());
+ }
+ //make the path readable
+ let loc_ = if track.location.contains("file://localhost/") {
+ decode(track.location.strip_prefix("file://localhost/").unwrap())
+ .unwrap()
+ .into_owned()
+ } else {
+ decode(track.location.as_str()).unwrap().into_owned()
+ };
+ let loc = loc_.as_str();
+ if File::open(loc).is_err() && !loc.contains("http") {
+ count += 1;
+ dbg!(loc);
+ continue;
+ }
+
+ let location: URI = if track.location.contains("file://localhost/") {
+ URI::Local(PathBuf::from(
+ decode(track.location.strip_prefix("file://localhost/").unwrap())
+ .unwrap()
+ .into_owned()
+ .as_str(),
+ ))
+ } else {
+ URI::Remote(Service::None, decode(&track.location).unwrap().into_owned())
+ };
+ let dur = match get_duration(Path::new(&loc)) {
+ Ok(e) => e,
+ Err(e) => {
+ dbg!(e);
+ StdDur::from_secs(0)
+ }
+ };
+ 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: vec![location],
+ uuid: Uuid::new_v4(),
+ plays: track.plays,
+ skips: 0,
+ favorited: track.favorited,
+ banned: if track.banned {
+ Some(BannedType::All)
+ } else {
+ None
+ },
+ rating: track.rating,
+ format: match FileFormat::from_file(PathBuf::from(&loc)) {
+ Ok(e) => Some(e),
+ Err(_) => None,
+ },
+ duration: dur,
+ play_time: play_time_,
+ last_played: track.last_played,
+ date_added: track.date_added,
+ date_modified: track.date_modified,
+ album_art: match get_art(Path::new(&loc)) {
+ Ok(e) => e,
+ Err(_) => Vec::new(),
+ },
+ tags: tags_,
+ internal_tags,
+ };
+ // dbg!(&ny.tags);
+ bun.push(ny);
+ }
+ println!("skipped: {}", count);
+ bun
+ }
+}
+fn to_tag(string: String) -> Tag {
+ match string.to_lowercase().as_str() {
+ "name" => Tag::Title,
+ "album" => Tag::Album,
+ "artist" => Tag::Artist,
+ "album artist" => Tag::AlbumArtist,
+ "genre" => Tag::Genre,
+ "comment" => Tag::Comment,
+ "track number" => Tag::Track,
+ "disc number" => Tag::Disk,
+ _ => Tag::Key(string),
+ }
+}
+fn get_duration(file: &Path) -> Result {
+ let dur = match Probe::open(file)?.read() {
+ Ok(tagged_file) => tagged_file.properties().duration(),
+
+ Err(_) => StdDur::from_secs(0),
+ };
+ Ok(dur)
+}
+fn get_art(file: &Path) -> Result, LoftyError> {
+ let mut album_art: Vec = Vec::new();
+
+ let blank_tag = &lofty::Tag::new(TagType::Id3v2);
+ let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
+ let tagged_file: lofty::TaggedFile;
+
+ let tag = match Probe::open(file)?.options(normal_options).read() {
+ Ok(e) => {
+ tagged_file = e;
+ match tagged_file.primary_tag() {
+ Some(primary_tag) => primary_tag,
+
+ None => match tagged_file.first_tag() {
+ Some(first_tag) => first_tag,
+ None => blank_tag,
+ },
+ }
+ }
+ Err(_) => blank_tag,
+ };
+ let mut img = match utils::find_images(file) {
+ Ok(e) => e,
+ Err(_) => Vec::new(),
+ };
+ if !img.is_empty() {
+ album_art.append(img.as_mut());
+ }
+
+ for (i, _art) in tag.pictures().iter().enumerate() {
+ let new_art = AlbumArt::Embedded(i);
+
+ album_art.push(new_art)
+ }
+
+ Ok(album_art)
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct ITunesSong {
+ 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 ITunesSong {
+ pub fn new() -> ITunesSong {
+ Default::default()
+ }
+
+ fn from_hashmap(map: &mut HashMap) -> Result {
+ let mut song = ITunesSong::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)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{
+ path::{Path, PathBuf},
+ sync::{Arc, RwLock},
+ };
+
+ use crate::{
+ config::{Config, ConfigLibrary},
+ music_storage::{db_reader::extern_library::ExternalLibrary, library::MusicLibrary},
+ };
+
+ use super::ITunesLibrary;
+
+ #[test]
+ fn itunes_lib_test() {
+ let mut config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap();
+ let config_lib = ConfigLibrary::new(
+ PathBuf::from("test-config/library2"),
+ String::from("library2"),
+ None,
+ );
+ config.libraries.libraries.push(config_lib.clone());
+
+ let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs();
+
+ let mut library = MusicLibrary::init(
+ config.libraries.get_default().unwrap().path.clone(),
+ config_lib.uuid,
+ )
+ .unwrap();
+
+ songs
+ .iter()
+ .for_each(|song| library.add_song(song.to_owned()).unwrap());
+
+ config.write_file().unwrap();
+ library
+ .save(config.libraries.get_default().unwrap().path.clone())
+ .unwrap();
+ }
+}
diff --git a/dmp-core/src/music_storage/db_reader/mod.rs b/dmp-core/src/music_storage/db_reader/mod.rs
new file mode 100644
index 0000000..be1841f
--- /dev/null
+++ b/dmp-core/src/music_storage/db_reader/mod.rs
@@ -0,0 +1,13 @@
+pub mod foobar {
+ pub mod reader;
+ pub mod utils;
+}
+pub mod musicbee {
+ pub mod reader;
+ pub mod utils;
+}
+pub mod itunes {
+ pub mod reader;
+}
+pub mod common;
+pub mod extern_library;
diff --git a/dmp-core/src/music_storage/db_reader/musicbee/reader.rs b/dmp-core/src/music_storage/db_reader/musicbee/reader.rs
new file mode 100644
index 0000000..9811862
--- /dev/null
+++ b/dmp-core/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 >>= 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/dmp-core/src/music_storage/db_reader/musicbee/utils.rs b/dmp-core/src/music_storage/db_reader/musicbee/utils.rs
new file mode 100644
index 0000000..65d6333
--- /dev/null
+++ b/dmp-core/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/dmp-core/src/music_storage/library.rs b/dmp-core/src/music_storage/library.rs
new file mode 100644
index 0000000..fc24915
--- /dev/null
+++ b/dmp-core/src/music_storage/library.rs
@@ -0,0 +1,1243 @@
+use super::playlist::PlaylistFolder;
+// Crate things
+use super::utils::{find_images, normalize, read_file, write_file};
+use crate::config::Config;
+
+use std::cmp::Ordering;
+// Various std things
+use std::collections::{BTreeMap, HashMap};
+use std::error::Error;
+use std::ops::ControlFlow::{Break, Continue};
+use std::vec::IntoIter;
+
+// Files
+use file_format::{FileFormat, Kind};
+use glib::filename_to_uri;
+
+use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt};
+use rcue::parser::parse_from_file;
+use std::fs;
+use std::path::{Path, PathBuf};
+use uuid::Uuid;
+use walkdir::WalkDir;
+
+// Time
+use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
+use std::time::Duration;
+
+// Serialization/Compression
+use base64::{engine::general_purpose, Engine as _};
+use serde::{Deserialize, Serialize};
+
+// Fun parallel stuff
+use rayon::prelude::*;
+use std::sync::{Arc, Mutex, RwLock};
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
+pub enum AlbumArt {
+ Embedded(usize),
+ External(URI),
+}
+
+impl AlbumArt {
+ pub fn uri(&self) -> Option<&URI> {
+ match self {
+ Self::Embedded(_) => None,
+ Self::External(uri) => Some(uri),
+ }
+ }
+}
+
+/// A tag for a song
+#[non_exhaustive]
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Tag {
+ Title,
+ Album,
+ Artist,
+ AlbumArtist,
+ Genre,
+ Comment,
+ Track,
+ Disk,
+ Key(String),
+ Field(String),
+}
+
+impl ToString for Tag {
+ fn to_string(&self) -> String {
+ match self {
+ Self::Title => "TrackTitle".into(),
+ Self::Album => "AlbumTitle".into(),
+ Self::Artist => "TrackArtist".into(),
+ Self::AlbumArtist => "AlbumArtist".into(),
+ Self::Genre => "Genre".into(),
+ Self::Comment => "Comment".into(),
+ Self::Track => "TrackNumber".into(),
+ Self::Disk => "DiscNumber".into(),
+ Self::Key(key) => key.into(),
+ Self::Field(f) => f.into(),
+ }
+ }
+}
+
+/// A field within a Song struct
+#[derive(Debug)]
+pub enum Field {
+ Location(URI),
+ Plays(i32),
+ Skips(i32),
+ Favorited(bool),
+ Rating(u8),
+ Format(FileFormat),
+ Duration(Duration),
+ PlayTime(Duration),
+ LastPlayed(DateTime),
+ DateAdded(DateTime),
+ DateModified(DateTime),
+}
+
+impl ToString for Field {
+ fn to_string(&self) -> String {
+ match self {
+ Self::Location(location) => location.to_string(),
+ Self::Plays(plays) => plays.to_string(),
+ Self::Skips(skips) => skips.to_string(),
+ Self::Favorited(fav) => fav.to_string(),
+ Self::Rating(rating) => rating.to_string(),
+ Self::Format(format) => match format.short_name() {
+ Some(name) => name.to_string(),
+ None => format.to_string(),
+ },
+ Self::Duration(duration) => duration.as_millis().to_string(),
+ Self::PlayTime(time) => time.as_millis().to_string(),
+ Self::LastPlayed(last) => last.to_rfc2822(),
+ Self::DateAdded(added) => added.to_rfc2822(),
+ Self::DateModified(modified) => modified.to_rfc2822(),
+ }
+ }
+}
+
+#[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, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum DoNotTrack {
+ // TODO: add services to not track
+ LastFM,
+ LibreFM,
+ MusicBrainz,
+ Discord,
+}
+
+#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum SongType {
+ // TODO: add MORE?! song types
+ #[default]
+ Main,
+ Instrumental,
+ Remix,
+ Custom(String),
+}
+
+/// Stores information about a single song
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
+pub struct Song {
+ pub location: Vec,
+ pub uuid: Uuid,
+ pub plays: i32,
+ pub skips: i32,
+ pub favorited: bool,
+ pub banned: Option,
+ pub rating: Option,
+ pub format: Option,
+ pub duration: Duration,
+ pub play_time: Duration,
+ #[serde(with = "ts_milliseconds_option")]
+ pub last_played: Option>,
+ #[serde(with = "ts_milliseconds_option")]
+ pub date_added: Option>,
+ #[serde(with = "ts_milliseconds_option")]
+ pub date_modified: Option>,
+ pub album_art: Vec,
+ pub tags: BTreeMap,
+ pub internal_tags: Vec,
+}
+
+impl Song {
+ /// Get a tag's value
+ ///
+ /// ```
+ /// use dango_core::music_storage::music_db::Tag;
+ /// // Assuming an already created song:
+ ///
+ /// let tag = this_song.get_tag(Tag::Title);
+ ///
+ /// assert_eq!(tag, "Some Song Title");
+ /// ```
+ pub fn get_tag(&self, target_key: &Tag) -> Option<&String> {
+ self.tags.get(target_key)
+ }
+
+ /// Gets an internal field from a 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.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)),
+ "rating" => self.rating.map(Field::Rating),
+ "duration" => Some(Field::Duration(self.duration)),
+ "play_time" => Some(Field::PlayTime(self.play_time)),
+ "format" => self.format.map(Field::Format),
+ _ => todo!(), // Other field types are not yet supported
+ }
+ }
+
+ /// Sets the value of a tag in the song
+ pub fn set_tag(&mut self, target_key: Tag, new_value: String) {
+ self.tags.insert(target_key, new_value);
+ }
+
+ /// Deletes a tag from the song
+ pub fn remove_tag(&mut self, target_key: &Tag) {
+ self.tags.remove(target_key);
+ }
+
+ /// Creates a `Song` from a music file
+ pub fn from_file>(target_file: &P) -> Result> {
+ let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
+
+ let blank_tag = &lofty::Tag::new(TagType::Id3v2);
+ let tagged_file: lofty::TaggedFile;
+ let mut duration = Duration::from_secs(0);
+ let tag = match Probe::open(target_file)?.options(normal_options).read() {
+ Ok(file) => {
+ tagged_file = file;
+
+ duration = tagged_file.properties().duration();
+
+ // Ensure the tags exist, if not, insert blank data
+ match tagged_file.primary_tag() {
+ Some(primary_tag) => primary_tag,
+
+ None => match tagged_file.first_tag() {
+ Some(first_tag) => first_tag,
+ None => blank_tag,
+ },
+ }
+ }
+
+ Err(_) => blank_tag,
+ };
+
+ let mut tags: BTreeMap = BTreeMap::new();
+ for item in tag.items() {
+ let key = match item.key() {
+ ItemKey::TrackTitle => Tag::Title,
+ ItemKey::TrackNumber => Tag::Track,
+ ItemKey::TrackArtist => Tag::Artist,
+ ItemKey::AlbumArtist => Tag::AlbumArtist,
+ ItemKey::Genre => Tag::Genre,
+ ItemKey::Comment => Tag::Comment,
+ ItemKey::AlbumTitle => Tag::Album,
+ ItemKey::DiscNumber => Tag::Disk,
+ ItemKey::Unknown(unknown)
+ if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" =>
+ {
+ continue
+ }
+ ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()),
+ custom => Tag::Key(format!("{:?}", custom)),
+ };
+
+ let value = match item.value() {
+ ItemValue::Text(value) => value.clone(),
+ ItemValue::Locator(value) => value.clone(),
+ ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)),
+ };
+
+ tags.insert(key, value);
+ }
+
+ // Get all the album artwork information from the file
+ let mut album_art: Vec = Vec::new();
+ for (i, _art) in tag.pictures().iter().enumerate() {
+ let new_art = AlbumArt::Embedded(i);
+
+ album_art.push(new_art)
+ }
+
+ // Find images around the music file that can be used
+ let found_images = find_images(target_file.as_ref()).unwrap();
+ album_art.extend_from_slice(&found_images);
+
+ // Get the format as a string
+ let format: Option = match FileFormat::from_file(target_file) {
+ Ok(fmt) => Some(fmt),
+ Err(_) => None,
+ };
+
+ // 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: vec![URI::Local(binding)],
+ uuid: Uuid::new_v4(),
+ plays: 0,
+ skips: 0,
+ favorited: false,
+ banned: None,
+ rating: None,
+ format,
+ duration,
+ play_time: Duration::from_secs(0),
+ last_played: None,
+ date_added: Some(chrono::offset::Utc::now()),
+ date_modified: Some(chrono::offset::Utc::now()),
+ tags,
+ album_art,
+ internal_tags,
+ };
+ Ok(new_song)
+ }
+
+ /// creates a `Vec` from a cue file
+ pub fn from_cue(cuesheet: &Path) -> Result, Box> {
+ let mut tracks = Vec::new();
+
+ let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap();
+
+ // Get album level information
+ let album_title = &cue_data.title;
+ let album_artist = &cue_data.performer;
+
+ let parent_dir = cuesheet.parent().expect("The file has no parent path??");
+ for file in cue_data.files.iter() {
+ let audio_location = &parent_dir.join(file.file.clone());
+
+ if !audio_location.exists() {
+ continue;
+ }
+
+ let next_track = file.tracks.clone();
+ let mut next_track = next_track.iter().skip(1);
+ for (i, track) in file.tracks.iter().enumerate() {
+ // Get the track timing information
+ let pregap = match track.pregap {
+ Some(pregap) => pregap,
+ None => Duration::from_secs(0),
+ };
+ let postgap = match track.postgap {
+ Some(postgap) => postgap,
+ None => Duration::from_secs(0),
+ };
+
+ let mut start;
+ if track.indices.len() > 1 {
+ start = track.indices[1].1;
+ } else {
+ start = track.indices[0].1;
+ }
+ if !start.is_zero() {
+ start -= pregap;
+ }
+
+ let duration = match next_track.next() {
+ Some(future) => match future.indices.first() {
+ Some(val) => val.1 - start,
+ None => Duration::from_secs(0),
+ },
+ None => match lofty::read_from_path(audio_location) {
+ Ok(tagged_file) => tagged_file.properties().duration() - start,
+
+ Err(_) => match Probe::open(audio_location)?.read() {
+ Ok(tagged_file) => tagged_file.properties().duration() - start,
+
+ Err(_) => Duration::from_secs(0),
+ },
+ },
+ };
+ let end = start + duration + postgap;
+
+ // Get the format as a string
+ let format: Option = match FileFormat::from_file(audio_location) {
+ Ok(fmt) => Some(fmt),
+ Err(_) => None,
+ };
+
+ // Get some useful tags
+ let mut tags: BTreeMap = BTreeMap::new();
+ match album_title {
+ Some(title) => {
+ tags.insert(Tag::Album, title.clone());
+ }
+ None => (),
+ }
+ match album_artist {
+ Some(artist) => {
+ tags.insert(Tag::Artist, artist.clone());
+ }
+ None => (),
+ }
+ tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string()));
+ match track.title.clone() {
+ Some(title) => tags.insert(Tag::Title, title),
+ None => match track.isrc.clone() {
+ Some(title) => tags.insert(Tag::Title, title),
+ None => {
+ let namestr = format!("{} - {}", i, file.file.clone());
+ tags.insert(Tag::Title, namestr)
+ }
+ },
+ };
+ match track.performer.clone() {
+ Some(artist) => tags.insert(Tag::Artist, artist),
+ None => None,
+ };
+
+ // Find images around the music file that can be used
+ let album_art = find_images(&audio_location.to_path_buf()).unwrap();
+
+ let new_song = Song {
+ location: vec![URI::Cue {
+ location: audio_location.clone(),
+ index: i,
+ start,
+ end,
+ }],
+ uuid: Uuid::new_v4(),
+ plays: 0,
+ skips: 0,
+ favorited: false,
+ banned: None,
+ rating: None,
+ format,
+ duration,
+ play_time: Duration::from_secs(0),
+ last_played: None,
+ date_added: Some(chrono::offset::Utc::now()),
+ date_modified: Some(chrono::offset::Utc::now()),
+ tags,
+ album_art,
+ internal_tags: Vec::new(),
+ };
+ tracks.push((new_song, audio_location.clone()));
+ }
+ }
+ Ok(tracks)
+ }
+
+ /// 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)]
+pub enum URI {
+ Local(PathBuf),
+ Cue {
+ location: PathBuf,
+ index: usize,
+ start: Duration,
+ end: Duration,
+ },
+ Remote(Service, String),
+}
+
+impl URI {
+ pub fn index(&self) -> Result<&usize, Box> {
+ match self {
+ URI::Local(_) => Err("\"Local\" has no stored index".into()),
+ URI::Remote(_, _) => Err("\"Remote\" has no stored index".into()),
+ URI::Cue { index, .. } => Ok(index),
+ }
+ }
+
+ /// Returns the start time of a CUEsheet song, or an
+ /// error if the URI is not a Cue variant
+ pub fn start(&self) -> Result<&Duration, Box> {
+ match self {
+ URI::Local(_) => Err("\"Local\" has no starting time".into()),
+ URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
+ URI::Cue { start, .. } => Ok(start),
+ }
+ }
+
+ /// Returns the end time of a CUEsheet song, or an
+ /// error if the URI is not a Cue variant
+ pub fn end(&self) -> Result<&Duration, Box> {
+ match self {
+ URI::Local(_) => Err("\"Local\" has no starting time".into()),
+ URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()),
+ URI::Cue { end, .. } => Ok(end),
+ }
+ }
+
+ /// Returns the location as a PathBuf
+ pub fn path(&self) -> PathBuf {
+ match self {
+ URI::Local(location) => location.clone(),
+ URI::Cue { location, .. } => location.clone(),
+ URI::Remote(_, location) => PathBuf::from(location),
+ }
+ }
+
+ pub fn as_uri(&self) -> String {
+ let path_str = match self {
+ URI::Local(location) => filename_to_uri(location, None)
+ .expect("couldn't convert path to URI")
+ .to_string(),
+ URI::Cue { location, .. } => filename_to_uri(location, None)
+ .expect("couldn't convert path to URI")
+ .to_string(),
+ URI::Remote(_, location) => location.clone(),
+ };
+ path_str.to_string()
+ }
+
+ pub fn as_path(&self) -> Result<&PathBuf, Box> {
+ if let Self::Local(path) = self {
+ Ok(path)
+ } else {
+ Err("This URI is not local!".into())
+ }
+ }
+
+ pub fn exists(&self) -> Result {
+ match self {
+ URI::Local(loc) => loc.try_exists(),
+ URI::Cue { location, .. } => location.try_exists(),
+ URI::Remote(_, _loc) => Ok(true), // TODO: Investigate a way to do this?
+ }
+ }
+}
+
+impl ToString for URI {
+ fn to_string(&self) -> String {
+ let path_str = match self {
+ URI::Local(location) => location.as_path().to_string_lossy(),
+ URI::Cue { location, .. } => location.as_path().to_string_lossy(),
+ URI::Remote(_, location) => location.into(),
+ };
+ path_str.to_string()
+ }
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub enum Service {
+ InternetRadio,
+ Spotify,
+ Youtube,
+ None,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Album {
+ title: String,
+ artist: Option,
+ cover: Option,
+ discs: BTreeMap>,
+}
+
+#[allow(clippy::len_without_is_empty)]
+impl Album {
+ //returns the Album title
+ pub fn title(&self) -> &String {
+ &self.title
+ }
+
+ /// Returns the album cover as an AlbumArt struct, if it exists
+ fn cover(&self) -> &Option {
+ &self.cover
+ }
+
+ /// Returns the Album Artist, if they exist
+ pub fn artist(&self) -> &Option {
+ &self.artist
+ }
+
+ pub fn discs(&self) -> &BTreeMap> {
+ &self.discs
+ }
+ /// Returns the specified track at `index` from the album, returning
+ /// an error if the track index is out of range
+ pub fn track(&self, disc: u16, index: usize) -> Option<&(u16, Uuid)> {
+ self.discs.get(&disc)?.get(index)
+ }
+
+ fn tracks(&self) -> Vec<(u16, Uuid)> {
+ let mut songs = Vec::new();
+ for disc in self.discs.values() {
+ songs.extend_from_slice(&disc)
+ }
+ songs
+ }
+
+ /// Returns the number of songs in the album
+ pub fn len(&self) -> usize {
+ let mut total = 0;
+ for disc in self.discs.values() {
+ total += disc.len();
+ }
+ total
+ }
+}
+
+impl IntoIterator for Album {
+ type Item = AlbumTrack;
+ type IntoIter = IntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ let mut vec = vec![];
+
+ for (disc, mut tracks) in self.discs {
+ tracks.par_sort_by(|a, b| a.0.cmp(&b.0));
+
+ let mut tracks = tracks
+ .into_iter()
+ .map(|(track, uuid)| AlbumTrack { disc, track, uuid })
+ .collect::>();
+
+ vec.append(&mut tracks);
+ }
+ vec.into_iter()
+ }
+}
+
+pub struct AlbumTrack {
+ disc: u16,
+ track: u16,
+ uuid: Uuid,
+}
+
+impl AlbumTrack {
+ pub fn disc(&self) -> &u16 {
+ &self.disc
+ }
+
+ pub fn track(&self) -> &u16 {
+ &self.track
+ }
+
+ pub fn uuid(&self) -> &Uuid {
+ &self.uuid
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct MusicLibrary {
+ pub name: String,
+ pub uuid: Uuid,
+ pub library: Vec,
+ pub playlists: PlaylistFolder,
+ pub backup_songs: Vec, // maybe move this to the config instead?
+}
+
+impl MusicLibrary {
+ const BLOCKED_EXTENSIONS: &'static [&'static str] = &["vob", "log", "txt", "sf2"];
+
+ /// Create a new library from a name and [Uuid]
+ fn new(name: String, uuid: Uuid) -> Self {
+ MusicLibrary {
+ name,
+ uuid,
+ library: Vec::new(),
+ playlists: PlaylistFolder::default(),
+ backup_songs: Vec::new(),
+ }
+ }
+
+ /// Initialize the database
+ ///
+ /// If the database file already exists, return the [MusicLibrary], otherwise create
+ /// the database first. This needs to be run before anything else to retrieve
+ /// the [MusicLibrary] Vec
+ pub fn init(path: PathBuf, uuid: Uuid) -> Result> {
+ let library: MusicLibrary = match path.exists() {
+ true => read_file(path)?,
+ false => {
+ // If the library does not exist, re-create it
+ let lib = MusicLibrary::new(String::new(), uuid);
+ write_file(&lib, path)?;
+ lib
+ }
+ };
+ Ok(library)
+ }
+
+ //#[cfg(debug_assertions)] // We probably wouldn't want to use this for real, but maybe it would have some utility?
+ pub fn from_path>(path: &P) -> Result> {
+ let path: PathBuf = path.as_ref().to_path_buf();
+ let library: MusicLibrary = match path.exists() {
+ true => read_file(path)?,
+ false => {
+ let lib = MusicLibrary::new(String::new(), Uuid::new_v4());
+ write_file(&lib, path)?;
+ lib
+ }
+ };
+ Ok(library)
+ }
+
+ /// Serializes the database out to the file specified in the config
+ pub fn save_path>(&self, path: &P) -> Result<(), Box> {
+ let path = path.as_ref();
+ match path.try_exists() {
+ Ok(_) => write_file(self, path)?,
+ Err(error) => return Err(error.into()),
+ }
+
+ Ok(())
+ }
+
+ /// Serializes the database out to the file specified in the config
+ pub fn save(&self, path: PathBuf) -> Result<(), Box> {
+ match path.try_exists() {
+ Ok(_) => write_file(self, path)?,
+ Err(error) => return Err(error.into()),
+ }
+
+ Ok(())
+ }
+
+ /// Returns the library size in number of tracks
+ pub fn len_tracks(&self) -> usize {
+ self.library.len()
+ }
+
+ /// Returns the library size in number of albums
+ pub fn len_albums(&self) -> usize {
+ self.albums().len()
+ }
+
+ /// Queries for a [Song] by its [URI], returning a single `Song`
+ /// with the `URI` that matches along with its position in the library
+ #[inline(always)]
+ pub fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> {
+ let result = self
+ .library
+ .par_iter()
+ .enumerate()
+ .try_for_each(|(i, track)| {
+ for location in &track.location {
+ //TODO: check that this works
+ if path == location {
+ return Break((track, i));
+ }
+ }
+ Continue(())
+ });
+
+ match result {
+ Break(song) => Some(song),
+ Continue(_) => None,
+ }
+ }
+
+ /// Queries for a [Song] by its [Uuid], returning a single `Song`
+ /// with the `Uuid` that matches along with its position in the library
+ pub fn query_uuid(&self, uuid: &Uuid) -> Option<(&Song, usize)> {
+ let result = self
+ .library
+ .par_iter()
+ .enumerate()
+ .try_for_each(|(i, track)| {
+ if uuid == &track.uuid {
+ return std::ops::ControlFlow::Break((track, i));
+ }
+ Continue(())
+ });
+
+ match result {
+ Break(song) => Some(song),
+ Continue(_) => None,
+ }
+ }
+
+ /// Queries for a [Song] by its [PathBuf], returning a `Vec<&Song>`
+ /// with matching `PathBuf`s
+ 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.primary_uri().unwrap().0.path() {
+ //TODO: make this also not unwrap
+ Arc::clone(&result).lock().unwrap().push(track);
+ }
+ });
+
+ if result.lock().unwrap().len() > 0 {
+ Some(Arc::try_unwrap(result).unwrap().into_inner().unwrap())
+ } else {
+ None
+ }
+ }
+
+ /// Finds all the audio files within a specified folder
+ pub fn scan_folder>(
+ &mut self,
+ target_path: &P,
+ ) -> Result> {
+ let mut total = 0;
+ let mut errors = 0;
+ for target_file in WalkDir::new(target_path)
+ .follow_links(true)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ {
+ let path = target_file.path();
+
+ // Ensure the target is a file and not a directory,
+ // if it isn't a file, skip this loop
+ if !path.is_file() {
+ continue;
+ }
+
+ // Check if the file path is already in the db
+ if self.query_uri(&URI::Local(path.to_path_buf())).is_some() {
+ continue;
+ }
+
+ let format = FileFormat::from_file(path)?;
+ let extension = match path.extension() {
+ Some(ext) => ext.to_string_lossy().to_ascii_lowercase(),
+ None => String::new(),
+ };
+
+ // If it's a normal file, add it to the database
+ // if it's a cuesheet, do a bunch of fancy stuff
+ if (format.kind() == Kind::Audio || format.kind() == Kind::Video)
+ && !Self::BLOCKED_EXTENSIONS.contains(&extension.as_str())
+ {
+ match self.add_file(target_file.path()) {
+ Ok(_) => total += 1,
+ Err(_error) => {
+ errors += 1;
+ println!("{:?}: {}", target_file.file_name(), _error)
+ } // TODO: Handle more of these errors
+ };
+ } else if extension == "cue" {
+ total += match self.add_cuesheet(target_file.path()) {
+ Ok(added) => added,
+ Err(_error) => {
+ errors += 1;
+ println!("{:?}: {}", target_file.file_name(), _error);
+ 0
+ }
+ }
+ }
+ }
+
+ println!("Total scanning errors: {}", errors);
+
+ Ok(total)
+ }
+
+ pub fn remove_missing(&mut self) {
+ let target_removals = Arc::new(Mutex::new(Vec::new()));
+ self.library.par_iter().for_each(|t| {
+ for location in &t.location {
+ if !location.exists().unwrap() {
+ Arc::clone(&target_removals)
+ .lock()
+ .unwrap()
+ .push(location.clone());
+ }
+ }
+ });
+
+ let target_removals = Arc::try_unwrap(target_removals)
+ .unwrap()
+ .into_inner()
+ .unwrap();
+ for location in target_removals {
+ self.remove_uri(&location).unwrap();
+ }
+ }
+
+ pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box> {
+ let new_song = Song::from_file(target_file)?;
+ match self.add_song(new_song) {
+ Ok(_) => (),
+ Err(_) => {
+ //return Err(error)
+ }
+ };
+
+ Ok(())
+ }
+
+ pub fn add_cuesheet(&mut self, cuesheet: &Path) -> Result> {
+ let tracks = Song::from_cue(cuesheet)?;
+ let mut tracks_added = tracks.len() as i32;
+
+ for (new_song, location) in tracks {
+ // Try to remove the original audio file from the db if it exists
+ if self.remove_uri(&URI::Local(location.clone())).is_ok() {
+ tracks_added -= 1
+ }
+ match self.add_song(new_song) {
+ Ok(_) => {}
+ Err(_error) => {
+ //println!("{}", _error);
+ continue;
+ }
+ };
+ }
+ Ok(tracks_added)
+ }
+
+ pub fn add_song(&mut self, new_song: Song) -> Result<(), Box> {
+ let location = new_song.primary_uri()?.0;
+ if self.query_uri(location).is_some() {
+ return Err(format!("URI already in database: {:?}", location).into());
+ }
+
+ match location {
+ URI::Local(_) if self.query_path(location.path()).is_some() => {
+ return Err(format!("Location exists for {:?}", location).into())
+ }
+ _ => (),
+ }
+
+ self.library.push(new_song);
+
+ Ok(())
+ }
+
+ /// Removes a song indexed by URI, returning the position removed
+ pub fn remove_uri(&mut self, target_uri: &URI) -> Result> {
+ let location = match self.query_uri(target_uri) {
+ Some(value) => value.1,
+ None => return Err("URI not in database".into()),
+ };
+
+ self.library.remove(location);
+
+ Ok(location)
+ }
+
+ /// 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) {
+ Some(song) => song,
+ None => return Err("URI not in database!".to_string().into()),
+ };
+
+ println!("{:?}", target_song.location);
+
+ for tag in new_tags {
+ println!("{:?}", tag);
+ }
+
+ todo!()
+ }
+
+ /// Query the database, returning a list of [Song]s
+ ///
+ /// The order in which the `sort by` Vec is arranged
+ /// determines the output sorting.
+ ///
+ /// Example:
+ /// ```
+ /// use dango_core::music_storage::music_db::Tag;
+ /// query_tracks(
+ /// &String::from("query"),
+ /// &vec![
+ /// Tag::Title
+ /// ],
+ /// &vec![
+ /// Tag::Field("location".to_string()),
+ /// Tag::Album,
+ /// Tag::Disk,
+ /// Tag::Track,
+ /// ],
+ /// )
+ /// ```
+ /// This would find all titles containing the sequence
+ /// "query", and would return the results sorted first
+ /// by path, then album, disk number, and finally track number.
+ pub fn query_tracks(
+ &self,
+ query_string: &String, // The query itself
+ target_tags: &Vec, // The tags to search
+ sort_by: &Vec, // Tags to sort the resulting data by
+ ) -> Option> {
+ let songs = Arc::new(Mutex::new(Vec::new()));
+ //let matcher = SkimMatcherV2::default();
+
+ self.library.par_iter().for_each(|track| {
+ for tag in target_tags {
+ let track_result = match tag {
+ Tag::Field(target) => match track.get_field(target) {
+ Some(value) => value.to_string(),
+ None => continue,
+ },
+ _ => match track.get_tag(tag) {
+ Some(value) => value.clone(),
+ None => continue,
+ },
+ };
+
+ /*
+ let match_level = match matcher.fuzzy_match(&normalize(&track_result), &normalize(query_string)) {
+ Some(conf) => conf,
+ None => continue
+ };
+
+ if match_level > 100 {
+ songs.lock().unwrap().push(track);
+ return;
+ }
+ */
+
+ if normalize(&track_result.to_string())
+ .contains(&normalize(&query_string.to_owned()))
+ {
+ songs.lock().unwrap().push(track);
+ return;
+ }
+ }
+ });
+
+ let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!");
+ let mut new_songs = lock.into_inner().expect("Mutex cannot be locked!");
+
+ // Sort the returned list of songs
+ new_songs.par_sort_by(|a, b| {
+ for sort_option in sort_by {
+ let tag_a = match sort_option {
+ Tag::Field(field_selection) => match a.get_field(field_selection) {
+ Some(field_value) => field_value.to_string(),
+ None => continue,
+ },
+ _ => match a.get_tag(sort_option) {
+ Some(tag_value) => tag_value.to_owned(),
+ None => continue,
+ },
+ };
+
+ let tag_b = match sort_option {
+ Tag::Field(field_selection) => match b.get_field(field_selection) {
+ Some(field_value) => field_value.to_string(),
+ None => continue,
+ },
+ _ => match b.get_tag(sort_option) {
+ Some(tag_value) => tag_value.to_owned(),
+ None => continue,
+ },
+ };
+
+ if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) {
+ // If parsing succeeds, compare as numbers
+ return num_a.cmp(&num_b);
+ } else {
+ // If parsing fails, compare as strings
+ return tag_a.cmp(&tag_b);
+ }
+ }
+
+ // If all tags are equal, sort by Track number
+ let path_a = PathBuf::from(a.get_field("location").unwrap().to_string());
+ let path_b = PathBuf::from(b.get_field("location").unwrap().to_string());
+
+ path_a.file_name().cmp(&path_b.file_name())
+ });
+
+ if !new_songs.is_empty() {
+ Some(new_songs)
+ } else {
+ None
+ }
+ }
+
+ /// Generates all albums from the track list
+ pub fn albums(&self) -> BTreeMap {
+ let mut paths = BTreeMap::new();
+
+ let mut albums: BTreeMap = BTreeMap::new();
+ for song in &self.library {
+ let album_title = match song.get_tag(&Tag::Album) {
+ Some(title) => title.clone(),
+ None => continue,
+ };
+ //let norm_title = normalize(&album_title);
+
+ let disc_num = song
+ .get_tag(&Tag::Disk)
+ .unwrap_or(&"".to_string())
+ .parse::()
+ .unwrap_or(1);
+
+ match albums.get_mut(&album_title) {
+ // If the album is in the list, add the track to the appropriate disc within the album
+ Some(album) => match album.discs.get_mut(&disc_num) {
+ Some(disc) => disc.push((
+ song.get_tag(&Tag::Track)
+ .unwrap_or(&String::new())
+ .parse::()
+ .unwrap_or_default(),
+ song.uuid,
+ )),
+ None => {
+ album.discs.insert(
+ disc_num,
+ vec![(
+ song.get_tag(&Tag::Track)
+ .unwrap_or(&String::new())
+ .parse::()
+ .unwrap_or_default(),
+ song.uuid,
+ )],
+ );
+ }
+ },
+ // If the album is not in the list, make it new one and add it
+ None => {
+ let album_art = song.album_art.first();
+ let new_album = Album {
+ title: album_title.clone(),
+ artist: song.get_tag(&Tag::AlbumArtist).cloned(),
+ discs: BTreeMap::from([(
+ disc_num,
+ vec![(
+ song.get_tag(&Tag::Track)
+ .unwrap_or(&String::new())
+ .parse::()
+ .unwrap_or_default(),
+ song.uuid,
+ )],
+ )]),
+ cover: album_art.cloned(),
+ };
+ albums.insert(album_title, new_album);
+ }
+ }
+ paths.insert(song.uuid, song.primary_uri().unwrap());
+ }
+
+ // Sort the tracks in each disk in each album
+ albums.par_iter_mut().for_each(|album| {
+ for disc in &mut album.1.discs {
+ disc.1.sort_by(|a, b| {
+ let num_a = a.0;
+ let num_b = b.0;
+
+ if (num_a, num_b) != (0, 0) {
+ // If parsing the track numbers succeeds, compare as numbers
+ num_a.cmp(&num_b)
+ } else {
+ // If parsing doesn't succeed, compare the locations
+ let a = match paths.get_key_value(&a.1) {
+ Some((_, (uri, _))) => uri,
+ None => return Ordering::Equal,
+ };
+ let b = match paths.get_key_value(&b.1) {
+ Some((_, (uri, _))) => uri,
+ None => return Ordering::Equal,
+ };
+
+ a.as_uri().cmp(&b.as_uri())
+ }
+ });
+ }
+ });
+
+ // Return the albums!
+ albums
+ }
+
+ /// Queries a list of albums by title
+ pub fn query_albums(
+ &self,
+ query_string: &str, // The query itself
+ ) -> Result, Box> {
+ let all_albums = self.albums();
+
+ let normalized_query = normalize(query_string);
+ let albums: Vec = all_albums
+ .par_iter()
+ .filter_map(|album| {
+ if normalize(album.0).contains(&normalized_query) {
+ Some(album.1.clone())
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ Ok(albums)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::{
+ path::PathBuf,
+ sync::{Arc, RwLock},
+ };
+
+ use crate::{
+ config::{tests::new_config_lib, Config},
+ music_storage::library::MusicLibrary,
+ };
+
+ #[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(
+ config.libraries.get_default().unwrap().path.clone(),
+ target_uuid,
+ )
+ .unwrap();
+ dbg!(a);
+ }
+}
diff --git a/dmp-core/src/music_storage/music_collection.rs b/dmp-core/src/music_storage/music_collection.rs
new file mode 100644
index 0000000..964c79a
--- /dev/null
+++ b/dmp-core/src/music_storage/music_collection.rs
@@ -0,0 +1,7 @@
+use crate::music_storage::library::{AlbumArt, Song};
+
+pub trait MusicCollection {
+ fn title(&self) -> &String;
+ fn cover(&self) -> Option<&AlbumArt>;
+ fn tracks(&self) -> Vec;
+}
diff --git a/dmp-core/src/music_storage/playlist.rs b/dmp-core/src/music_storage/playlist.rs
new file mode 100644
index 0000000..c4c0d8c
--- /dev/null
+++ b/dmp-core/src/music_storage/playlist.rs
@@ -0,0 +1,353 @@
+use std::error::Error;
+use std::{
+ fs::File,
+ io::Read,
+ path::PathBuf,
+ sync::{Arc, RwLock},
+};
+
+use std::time::Duration;
+
+// use chrono::Duration;
+use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
+use nestify::nest;
+
+use rayon::prelude::*;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum SortOrder {
+ Manual,
+ Tag(Vec),
+}
+
+nest! {
+ #[derive(Debug, Clone, Deserialize, Serialize)]*
+ #[derive(Default)]
+ pub struct PlaylistFolder {
+ name: String,
+ items: Vec<
+ pub enum PlaylistFolderItem {
+ Folder(PlaylistFolder),
+ List(Playlist)
+ }
+ >
+ }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct Playlist {
+ uuid: Uuid,
+ title: String,
+ cover: Option,
+ tracks: Vec,
+ sort_order: SortOrder,
+ play_count: i32,
+ play_time: Duration,
+}
+impl Playlist {
+ pub fn new() -> Self {
+ Default::default()
+ }
+ pub fn play_count(&self) -> i32 {
+ self.play_count
+ }
+ pub fn play_time(&self) -> Duration {
+ self.play_time
+ }
+
+ pub fn title(&self) -> &String {
+ &self.title
+ }
+
+ pub fn cover(&self) -> Option<&AlbumArt> {
+ match &self.cover {
+ Some(e) => Some(e),
+ None => None,
+ }
+ }
+
+ pub fn tracks(&self) -> Vec {
+ self.tracks.to_owned()
+ }
+
+ pub fn set_tracks(&mut self, tracks: Vec) {
+ self.tracks = tracks;
+ }
+
+ pub fn add_track(&mut self, track: Uuid) {
+ self.tracks.push(track);
+ }
+
+ pub fn remove_track(&mut self, index: i32) {
+ let index = index as usize;
+ if (self.tracks.len() - 1) >= index {
+ self.tracks.remove(index);
+ }
+ }
+ pub fn get_index(&self, uuid: Uuid) -> Option {
+ let mut i = 0;
+ if self.contains(uuid) {
+ for track in &self.tracks {
+ i += 1;
+ if &uuid == track {
+ dbg!("Index gotted! ", i);
+ return Some(i);
+ }
+ }
+ }
+ None
+ }
+ pub fn contains(&self, uuid: Uuid) -> bool {
+ self.get_index(uuid).is_some()
+ }
+
+ pub fn to_file(&self, path: &str) -> Result<(), Box> {
+ super::utils::write_file(self, PathBuf::from(path))?;
+ Ok(())
+ }
+
+ pub fn from_file(path: &str) -> Result> {
+ super::utils::read_file(PathBuf::from(path))
+ }
+
+ pub fn to_m3u8(
+ &mut self,
+ lib: Arc>,
+ location: &str,
+ ) -> Result<(), Box> {
+ let lib = lib.read().unwrap();
+ let seg = self
+ .tracks
+ .iter()
+ .filter_map(|uuid| {
+ // TODO: The Unwraps need to be handled here
+ if let Some((track, _)) = lib.query_uuid(uuid) {
+ if let URI::Local(_) = track.primary_uri().unwrap().0 {
+ Some(MediaSegment {
+ uri: track.primary_uri().unwrap().0.to_string(),
+ duration: track.duration.as_millis() as f32,
+ title: track
+ .tags
+ .get_key_value(&Tag::Title)
+ .map(|tag| tag.1.into()),
+ ..Default::default()
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ .collect::>();
+
+ let m3u8 = MediaPlaylist {
+ version: Some(6),
+ target_duration: 3.0,
+ media_sequence: 338559,
+ discontinuity_sequence: 1234,
+ end_list: true,
+ playlist_type: Some(MediaPlaylistType::Vod),
+ segments: seg.clone(),
+ ..Default::default()
+ };
+
+ let mut file = std::fs::OpenOptions::new()
+ .read(true)
+ .create(true)
+ .truncate(true)
+ .write(true)
+ .open(location)?;
+ m3u8.write_to(&mut file)?;
+ Ok(())
+ }
+
+ pub fn from_m3u8(
+ path: &str,
+ lib: Arc>,
+ ) -> Result> {
+ let mut file = match File::open(path) {
+ Ok(file) => file,
+ Err(e) => return Err(e.into()),
+ };
+ let mut bytes = Vec::new();
+ file.read_to_end(&mut bytes).unwrap();
+
+ let parsed = m3u8_rs::parse_playlist(&bytes);
+
+ let playlist = match parsed {
+ Result::Ok((_, playlist)) => playlist,
+ Result::Err(e) => panic!("Parsing error: \n{}", e),
+ };
+
+ match playlist {
+ List2::MasterPlaylist(_) => {
+ Err("This is a Master Playlist!\nPlase input a Media Playlist".into())
+ }
+ List2::MediaPlaylist(playlist_) => {
+ let mut uuids = Vec::new();
+ for seg in playlist_.segments {
+ let path_ = PathBuf::from(seg.uri.to_owned());
+ let mut lib = lib.write().unwrap();
+
+ let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) {
+ song.uuid
+ } else {
+ let song_ = Song::from_file(&path_)?;
+ let uuid = song_.uuid.to_owned();
+ lib.add_song(song_)?;
+ uuid
+ };
+ uuids.push(uuid);
+ }
+ let mut playlist = Playlist::new();
+
+ #[cfg(target_family = "windows")]
+ {
+ playlist.title = path
+ .split('\\')
+ .last()
+ .unwrap_or_default()
+ .strip_suffix(".m3u8")
+ .unwrap_or_default()
+ .to_string();
+ }
+ #[cfg(target_family = "unix")]
+ {
+ playlist.title = path
+ .split("/")
+ .last()
+ .unwrap_or_default()
+ .strip_suffix(".m3u8")
+ .unwrap_or_default()
+ .to_string();
+ }
+
+ playlist.set_tracks(uuids);
+ Ok(playlist)
+ }
+ }
+ }
+
+ pub fn out_tracks(&self, lib: Arc>) -> (Vec, Vec<&Uuid>) {
+ let lib = lib.read().unwrap();
+ let mut songs = vec![];
+ let mut invalid_uuids = vec![];
+
+ for uuid in &self.tracks {
+ if let Some((track, _)) = lib.query_uuid(uuid) {
+ songs.push(track.to_owned());
+ } else {
+ invalid_uuids.push(uuid);
+ }
+ }
+
+ if let SortOrder::Tag(sort_by) = &self.sort_order {
+ println!("sorting by: {:?}", sort_by);
+
+ songs.par_sort_by(|a, b| {
+ for (i, sort_option) in sort_by.iter().enumerate() {
+ dbg!(&i);
+ let tag_a = match sort_option {
+ Tag::Field(field_selection) => {
+ match a.get_field(field_selection.as_str()) {
+ Some(field_value) => field_value.to_string(),
+ None => continue,
+ }
+ }
+ _ => match a.get_tag(sort_option) {
+ Some(tag_value) => tag_value.to_owned(),
+ None => continue,
+ },
+ };
+
+ let tag_b = match sort_option {
+ Tag::Field(field_selection) => match b.get_field(field_selection) {
+ Some(field_value) => field_value.to_string(),
+ None => continue,
+ },
+ _ => match b.get_tag(sort_option) {
+ Some(tag_value) => tag_value.to_owned(),
+ None => continue,
+ },
+ };
+ dbg!(&i);
+
+ if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) {
+ // If parsing succeeds, compare as numbers
+ return dbg!(num_a.cmp(&num_b));
+ } else {
+ // If parsing fails, compare as strings
+ return dbg!(tag_a.cmp(&tag_b));
+ }
+ }
+
+ // If all tags are equal, sort by Track number
+ let path_a = PathBuf::from(a.get_field("location").unwrap().to_string());
+ let path_b = PathBuf::from(b.get_field("location").unwrap().to_string());
+
+ path_a.file_name().cmp(&path_b.file_name())
+ })
+ }
+
+ (songs, invalid_uuids)
+ }
+}
+
+impl Default for Playlist {
+ fn default() -> Self {
+ Playlist {
+ uuid: Uuid::new_v4(),
+ title: String::default(),
+ cover: None,
+ tracks: Vec::default(),
+ sort_order: SortOrder::Manual,
+ play_count: 0,
+ play_time: Duration::from_secs(0),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_super {
+ use super::*;
+ use crate::config::tests::read_config_lib;
+
+ #[test]
+ fn list_to_m3u8() {
+ let (_, lib) = read_config_lib();
+ let mut playlist = Playlist::new();
+ let tracks = lib.library.iter().map(|track| track.uuid).collect();
+ playlist.set_tracks(tracks);
+
+ _ = playlist.to_m3u8(
+ Arc::new(RwLock::from(lib)),
+ ".\\test-config\\playlists\\playlist.m3u8",
+ );
+ }
+
+ fn m3u8_to_list() -> Playlist {
+ let (_, lib) = read_config_lib();
+ let arc = Arc::new(RwLock::from(lib));
+ let playlist =
+ Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap();
+
+ _ = playlist.to_file(".\\test-config\\playlists\\playlist");
+ dbg!(playlist)
+ }
+
+ #[test]
+ fn out_queue_sort() {
+ let (_, lib) = read_config_lib();
+ let mut list = m3u8_to_list();
+ list.sort_order = SortOrder::Tag(vec![Tag::Album]);
+
+ let songs = &list.out_tracks(Arc::new(RwLock::from(lib)));
+
+ dbg!(songs);
+ }
+}
diff --git a/dmp-core/src/music_storage/utils.rs b/dmp-core/src/music_storage/utils.rs
new file mode 100644
index 0000000..991d3d7
--- /dev/null
+++ b/dmp-core/src/music_storage/utils.rs
@@ -0,0 +1,97 @@
+use ciborium::{from_reader, into_writer};
+use deunicode::deunicode_with_tofu;
+use file_format::{FileFormat, Kind};
+use std::error::Error;
+use std::fs::{self, File};
+use std::io::{BufReader, BufWriter};
+use std::path::{Path, PathBuf};
+use walkdir::WalkDir;
+
+use super::library::{AlbumArt, URI};
+
+#[cfg(target_family = "windows")]
+use std::os::windows::fs::MetadataExt;
+
+pub(super) fn normalize(input_string: &str) -> String {
+ // Normalize the string to latin characters... this needs a lot of work
+ let mut normalized = deunicode_with_tofu(input_string, " ");
+
+ // Remove non alphanumeric characters
+ normalized.retain(|c| c.is_alphanumeric());
+ normalized = normalized.to_ascii_lowercase();
+
+ normalized
+}
+
+/// Write any data structure which implements [serde::Serialize]
+/// out to a [cbor] encoded file compressed using [ciborium]
+pub(super) fn write_file<
+ T: serde::Serialize,
+ U: std::convert::AsRef + std::convert::AsRef + Clone,
+>(
+ library: T,
+ path: U,
+) -> Result<(), Box> {
+ // Create a temporary name for writing out
+ let mut writer_name = PathBuf::from(&path);
+ writer_name.set_extension("tmp");
+
+ // Create a new BufWriter on the file and a snap frame encoder
+ let writer = BufWriter::new(File::create(&writer_name)?);
+ //let mut e = snap::write::FrameEncoder::new(writer);
+
+ // Write out the data
+ into_writer(&library, writer)?;
+ fs::rename(writer_name, &path)?;
+
+ Ok(())
+}
+
+/// Read a file serialized out with [write_file] and turn it into
+/// the desired structure
+pub(super) fn read_file serde::Deserialize<'de>>(
+ path: PathBuf,
+) -> Result> {
+ // Create a new snap reader over the file
+ let file_reader = BufReader::new(File::open(path)?);
+ //let mut d = snap::read::FrameDecoder::new(file_reader);
+
+ // Decode the library from the serialized data into the vec
+ let library: T = from_reader(file_reader)?;
+
+ Ok(library)
+}
+
+pub fn find_images(song_path: &Path) -> Result, Box> {
+ let mut images: Vec = Vec::new();
+
+ let song_dir = song_path.parent().ok_or("")?;
+ for target_file in WalkDir::new(song_dir)
+ .follow_links(true)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.depth() < 3)
+ // Don't recurse very deep
+ {
+ let path = target_file.path();
+ if !path.is_file() || !path.exists() {
+ continue;
+ }
+
+ let format = FileFormat::from_file(path)?.kind();
+ if format != Kind::Image {
+ continue;
+ }
+
+ #[cfg(target_family = "windows")]
+ if (4 & path.metadata().unwrap().file_attributes()) == 4 {
+ continue;
+ }
+
+ let image_uri = URI::Local(path.to_path_buf().canonicalize()?);
+
+ images.push(AlbumArt::External(image_uri));
+ }
+
+ Ok(images)
+}
\ No newline at end of file
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index b642688..ee5ea94 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -6,7 +6,8 @@
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
- "beforeBuildCommand": "npm run build"
+ "beforeBuildCommand": "npm run build",
+ "frontendDist": "../dist"
},
"app": {
"windows": [