Compare commits

..

No commits in common. "9457c5c9965e44c256ea91f9182d99608dacf883" and "ab529f4c9b82e1a376d93fbccaf86ec5dad0a5d6" have entirely different histories.

13 changed files with 760 additions and 543 deletions

View file

@ -12,15 +12,7 @@ keywords = []
categories = [] categories = []
[dependencies] [dependencies]
file-format = { version = "0.23.0", features = [ file-format = { version = "0.23.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] }
"reader-asf",
"reader-ebml",
"reader-mp4",
"reader-rm",
"reader-txt",
"reader-xml",
"serde",
] }
lofty = "0.18.2" lofty = "0.18.2"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
walkdir = "2.4.0" walkdir = "2.4.0"
@ -46,5 +38,3 @@ deunicode = "1.4.2"
opener = { version = "0.7.0", features = ["reveal"] } opener = { version = "0.7.0", features = ["reveal"] }
tempfile = "3.10.1" tempfile = "3.10.1"
listenbrainz = "0.7.0" listenbrainz = "0.7.0"
discord-rpc-client = "0.4.0"
nestify = "0.3.3"

View file

@ -8,16 +8,13 @@ pub mod music_storage {
pub mod db_reader; pub mod db_reader;
} }
pub mod music_controller { pub mod music_controller{
pub mod controller; pub mod controller;
pub mod connections; pub mod connections;
pub mod queue; pub mod queue;
} }
pub mod music_player { pub mod music_player;
pub mod gstreamer;
pub mod player;
}
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
pub mod config { pub mod config {
pub mod config; pub mod config;

View file

@ -1,103 +1,95 @@
// use std::{ use std::{
// sync::{Arc, RwLock}, sync::{Arc, RwLock},
// error::Error, error::Error,
// }; };
// use discord_rpc_client::Client; use listenbrainz::ListenBrainz;
// use listenbrainz::ListenBrainz; use uuid::Uuid;
// use uuid::Uuid;
// use crate::{ use crate::{
// config::config::Config, music_controller::controller::{Controller, QueueCmd, QueueResponse}, music_storage::library::{MusicLibrary, Song, Tag} config::config::Config, music_controller::controller::{Controller, QueueCmd, QueueResponse}, music_storage::library::{MusicLibrary, Song, Tag}
// }; };
// use super::controller::DatabaseResponse; use super::controller::DatabaseResponse;
// impl Controller { impl Controller {
// pub fn listenbrainz_authenticate(&mut self) -> Result<ListenBrainz, Box<dyn Error>> { pub fn listenbrainz_authenticate(&mut self) -> Result<ListenBrainz, Box<dyn Error>> {
// let config = &self.config.read().unwrap(); let config = &self.config.read().unwrap();
// let mut client = ListenBrainz::new(); let mut client = ListenBrainz::new();
// let lbz_token = match &config.connections.listenbrainz_token { let lbz_token = match &config.connections.listenbrainz_token {
// Some(token) => token, Some(token) => token,
// None => todo!("No ListenBrainz token in config") None => todo!("No ListenBrainz token in config")
// }; };
// if !client.is_authenticated() { if !client.is_authenticated() {
// client.authenticate(lbz_token)?; client.authenticate(lbz_token)?;
// } }
// Ok(client) Ok(client)
// } }
// pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> {
// let config = &self.config.read().unwrap(); let config = &self.config.read().unwrap();
// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid));
// let res = &self.db_mail.recv()?; let res = &self.db_mail.recv()?;
// let song = match res { let song = match res {
// DatabaseResponse::Song(song) => song, DatabaseResponse::Song(song) => song,
// _ => todo!() _ => todo!()
// }; };
// let unknown = &"unknown".to_string(); let unknown = &"unknown".to_string();
// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown);
// let track = song.get_tag(&Tag::Title).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()); let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str());
// client.listen(artist, track, release)?; client.listen(artist, track, release)?;
// Ok(()) Ok(())
// } }
// pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> { pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> {
// let config = &self.config.read().unwrap(); let config = &self.config.read().unwrap();
// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid));
// let res = &self.db_mail.recv()?; let res = &self.db_mail.recv()?;
// let song = match res { let song = match res {
// DatabaseResponse::Song(song) => song, DatabaseResponse::Song(song) => song,
// _ => todo!() _ => todo!()
// }; };
// let unknown = &"unknown".to_string(); let unknown = &"unknown".to_string();
// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown);
// let track = song.get_tag(&Tag::Title).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()); let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str());
// client.listen(artist, track, release)?; client.listen(artist, track, release)?;
// Ok(()) Ok(())
// } }
}
// pub fn discord_song_change(client: &mut Client,song: Song) { #[cfg(test)]
// client.set_activity(|a| { mod test_super {
// a.state(format!("Listening to {}", song.get_tag(&Tag::Title).unwrap())) use std::{thread::sleep, time::Duration};
// .into()
// });
// }
// }
// #[cfg(test)] use super::*;
// mod test_super { use crate::config::config::tests::read_config_lib;
// use std::{thread::sleep, time::Duration};
// use super::*; #[test]
// use crate::config::config::tests::read_config_lib; fn listenbrainz() {
let mut c = Controller::start(".\\test-config\\config_test.json").unwrap();
// #[test] let client = c.listenbrainz_authenticate().unwrap();
// 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();
// c.q_new().unwrap(); let songs = c.lib_get_songs();
// 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();
// c.q_enqueue(0, songs[1].location.to_owned()).unwrap();
// c.q_play(0).unwrap();
// sleep(Duration::from_secs(100)); sleep(Duration::from_secs(100));
// c.lbz_scrobble(client, songs[1].uuid).unwrap(); c.lbz_scrobble(client, songs[1].uuid).unwrap();
// } }
// } }

View file

@ -2,37 +2,89 @@
//! player. It manages queues, playback, library access, and //! player. It manages queues, playback, library access, and
//! other functions //! other functions
use crossbeam_channel;
use crossbeam_channel::{Receiver, Sender};
use listenbrainz::ListenBrainz;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crossbeam_channel::{Sender, Receiver};
use crossbeam_channel;
use listenbrainz::ListenBrainz;
use std::thread::spawn; use std::thread::spawn;
use crossbeam_channel::unbounded;
use std::error::Error; use std::error::Error;
use crossbeam_channel::unbounded;
use uuid::Uuid; use uuid::Uuid;
use crate::music_controller::queue::QueueItem; use crate::music_controller::queue::{QueueItem, QueueItemType};
use crate::music_player::gstreamer::GStreamer;
use crate::music_storage::library::{Tag, URI}; use crate::music_storage::library::{Tag, URI};
use crate::{ use crate::{
music_storage::library::{MusicLibrary, Song},
config::config::Config, config::config::Config,
music_controller::queue::Queue, music_controller::queue::Queue,
music_storage::library::{MusicLibrary, Song},
}; };
pub struct Controller { pub struct Controller {
pub queue: Queue, // queues: Vec<Queue>,
pub config: Arc<RwLock<Config>>, pub config: Arc<RwLock<Config>>,
pub library: MusicLibrary, // library: MusicLibrary,
player_mail: MailMan<PlayerCmd, PlayerRes>, pub(super) controller_mail: MailMan<ControllerCmd, ControllerResponse>,
pub(super) db_mail: MailMan<DatabaseCmd, DatabaseResponse>,
pub(super) queue_mail: Vec<MailMan<QueueCmd, QueueResponse>>,
}
#[derive(Debug)]
pub(super) enum ControllerCmd {
Default,
Test
}
#[derive(Debug)]
pub(super) enum ControllerResponse {
Empty,
QueueMailMan(MailMan<QueueCmd, QueueResponse>),
}
#[derive(Debug)]
pub(super) enum DatabaseCmd {
Default,
Test,
SaveLibrary,
GetSongs,
QueryUuid(Uuid),
QueryUuids(Vec<Uuid>),
ReadFolder(String),
}
#[derive(Debug)]
pub(super) enum DatabaseResponse {
Empty,
Song(Song),
Songs(Vec<Song>),
Library(MusicLibrary),
}
#[derive(Debug)]
pub(super) enum QueueCmd {
Default,
Test,
Play,
Pause,
// SetSongs(Vec<QueueItem<QueueState>>),
// SetLocation(URI),
Enqueue(URI),
SetVolume(f64),
}
#[derive(Debug)]
pub(super) enum QueueResponse {
Default,
Test,
Index(i32),
Uuid(Uuid),
} }
#[derive(Debug)] #[derive(Debug)]
pub(super) struct MailMan<T: Send, U: Send> { pub(super) struct MailMan<T: Send, U: Send> {
pub tx: Sender<T>, pub tx: Sender<T>,
rx: Receiver<U>, rx: Receiver<U>
} }
impl<T: Send> MailMan<T, T> { impl<T: Send> MailMan<T, T> {
@ -46,7 +98,10 @@ impl<T: Send, U: Send> MailMan<T, U> {
let (tx, rx) = unbounded::<T>(); let (tx, rx) = unbounded::<T>();
let (tx1, rx1) = unbounded::<U>(); let (tx1, rx1) = unbounded::<U>();
(MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx }) (
MailMan { tx, rx: rx1 },
MailMan { tx: tx1, rx }
)
} }
pub fn send(&self, mail: T) -> Result<(), Box<dyn Error>> { pub fn send(&self, mail: T) -> Result<(), Box<dyn Error>> {
@ -60,61 +115,192 @@ impl<T: Send, U: Send> MailMan<T, U> {
} }
} }
enum PlayerCmd {
Test(URI),
}
enum PlayerRes {
Test,
}
#[allow(unused_variables)] #[allow(unused_variables)]
impl Controller { impl Controller {
pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>> pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>>
where where std::path::PathBuf: std::convert::From<P>
std::path::PathBuf: std::convert::From<P>,
{ {
let config_path = PathBuf::from(config_path); let config_path = PathBuf::from(config_path);
let config = Config::read_file(config_path)?; let config = Config::read_file(config_path)?;
let uuid = config.libraries.get_default()?.uuid; let uuid = config.libraries.get_default()?.uuid;
let config_ = Arc::new(RwLock::from(config)); let config_ = Arc::new(RwLock::from(config));
let library = MusicLibrary::init(config_.clone(), uuid)?; let mut lib = MusicLibrary::init(config_.clone(), uuid)?;
let (player_mail, in_thread) = MailMan::<PlayerCmd, PlayerRes>::double(); let config = config_.clone();
let (out_thread_controller, in_thread) = MailMan::double();
let monitor_thread = spawn(move || {
use ControllerCmd::*;
loop {
let command = in_thread.recv().unwrap();
spawn(move || { match command {
let mut player = GStreamer::new().unwrap(); Default => (),
Test => {
while true { in_thread.send(ControllerResponse::Empty).unwrap();
match in_thread.recv().unwrap() { },
PlayerCmd::Test(uri) => {
&player.set_volume(0.04);
_ = &player.enqueue_next(&uri).unwrap();
_ = &player.play();
in_thread.send(PlayerRes::Test).unwrap();
}
} }
} }
}); });
Ok(Controller { let config = config_.clone();
queue: Queue::new(), let (out_thread_db, in_thread) = MailMan::double();
config: config_.clone(), let db_monitor = spawn(move || {
library, use DatabaseCmd::*;
player_mail, loop {
}) let command = in_thread.recv().unwrap();
match command {
Default => {},
Test => {
in_thread.send(DatabaseResponse::Empty).unwrap();
},
GetSongs => {
let songs = lib.query_tracks(&String::from(""), &(vec![Tag::Title]), &(vec![Tag::Title])).unwrap().iter().cloned().cloned().collect();
in_thread.send(DatabaseResponse::Songs(songs)).unwrap();
},
SaveLibrary => {
//TODO: make this send lib ref to the function to save instead
lib.save(config.read().unwrap().to_owned()).unwrap();
},
QueryUuid(uuid) => {
match lib.query_uuid(&uuid) {
Some(song) => in_thread.send(DatabaseResponse::Song(song.0.clone())).unwrap(),
None => in_thread.send(DatabaseResponse::Empty).unwrap(),
}
},
QueryUuids(uuids) => {
let mut vec = Vec::new();
for uuid in uuids {
match lib.query_uuid(&uuid) {
Some(song) => vec.push(song.0.clone()),
None => unimplemented!()
}
}
in_thread.send(DatabaseResponse::Songs(vec)).unwrap();
},
ReadFolder(folder) => {
lib.scan_folder(&folder).unwrap();
in_thread.send(DatabaseResponse::Empty).unwrap();
} }
pub fn q_add(&mut self, item: &Uuid, source: super::queue::PlayerLocation, by_human: bool) {
let item = self.library.query_uuid(item).unwrap().0.to_owned();
self.queue.add_item(item, source, by_human)
} }
}
});
Ok(
Controller {
// queues: Vec::new(),
config: config_.clone(),
controller_mail: out_thread_controller,
db_mail: out_thread_db,
queue_mail: Vec::new(),
}
)
}
pub fn lib_get_songs(&self) -> Vec<Song> {
self.db_mail.send(DatabaseCmd::GetSongs);
match self.db_mail.recv().unwrap() {
DatabaseResponse::Songs(songs) => songs,
_ => Vec::new()
}
}
pub fn lib_scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> {
let mail = &self.db_mail;
mail.send(DatabaseCmd::ReadFolder(folder))?;
dbg!(mail.recv()?);
Ok(())
}
pub fn lib_save(&self) -> Result<(), Box<dyn Error>> {
self.db_mail.send(DatabaseCmd::SaveLibrary);
Ok(())
}
pub fn q_new(&mut self) -> Result<usize, Box<dyn Error>> {
let (out_thread_queue, in_thread) = MailMan::<QueueCmd, QueueResponse>::double();
let queues_monitor = spawn(move || {
use QueueCmd::*;
let mut queue = Queue::new().unwrap();
loop {
let command = in_thread.recv().unwrap();
match command {
Default => {},
Test => { in_thread.send(QueueResponse::Test).unwrap() },
Play => {
match queue.player.play() {
Ok(_) => in_thread.send(QueueResponse::Default).unwrap(),
Err(_) => todo!()
};
},
Pause => {
match queue.player.pause() {
Ok(_) => in_thread.send(QueueResponse::Default).unwrap(),
Err(_) => todo!()
}
},
// SetSongs(songs) => {
// queue.set_tracks(songs);
// in_thread.send(QueueResponse::Default).unwrap();
// },
Enqueue(uri) => {
queue.player.enqueue_next(&uri).unwrap();
// in_thread.send(QueueResponse::Default).unwrap();
},
SetVolume(vol) => {
queue.player.set_volume(vol);
}
}
}
});
self.queue_mail.push(out_thread_queue);
Ok(self.queue_mail.len() - 1)
}
pub fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> {
let mail = &self.queue_mail[index];
mail.send(QueueCmd::Play)?;
dbg!(mail.recv()?);
Ok(())
}
pub fn q_pause(&self, index: usize) -> Result<(), Box<dyn Error>> {
let mail = &self.queue_mail[index];
mail.send(QueueCmd::Pause)?;
dbg!(mail.recv()?);
Ok(())
}
pub fn q_set_volume(&self, index: usize, volume: f64) -> Result<(), Box<dyn Error>> {
let mail = &self.queue_mail[index];
mail.send(QueueCmd::SetVolume(volume))?;
Ok(())
}
// fn q_set_songs(&self, index: usize, songs: Vec<QueueItem<QueueState>>) -> Result<(), Box<dyn Error>> {
// let mail = &self.queue_mail[index];
// mail.send(QueueCmd::SetSongs(songs))?;
// dbg!(mail.recv()?);
// Ok(())
// }
pub fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> {
let mail = &self.queue_mail[index];
mail.send(QueueCmd::Enqueue(uri))?;
// dbg!(mail.recv()?);
Ok(())
}
} }
#[cfg(test)] #[cfg(test)]
mod test_super { mod tests {
use std::{thread::sleep, time::Duration}; use std::{thread::sleep, time::Duration};
use super::Controller; use super::Controller;
@ -123,18 +309,33 @@ mod test_super {
fn play_test() { fn play_test() {
let mut a = match Controller::start("test-config/config_test.json".to_string()) { let mut a = match Controller::start("test-config/config_test.json".to_string()) {
Ok(c) => c, Ok(c) => c,
Err(e) => panic!("{e}"), Err(e) => panic!("{e}")
}; };
sleep(Duration::from_millis(500)); sleep(Duration::from_millis(500));
let i = a.q_new().unwrap();
a.q_set_volume(i, 0.04);
// a.new_queue();
let songs = a.lib_get_songs();
a.q_enqueue(i, songs[2].location.clone());
// a.enqueue(1, songs[2].location.clone());
a.q_play(i).unwrap();
// a.play(1).unwrap();
sleep(Duration::from_secs(10));
a.q_pause(i);
sleep(Duration::from_secs(10));
a.q_play(i);
sleep(Duration::from_secs(1000));
} }
#[test] #[test]
fn test_() { fn test_() {
let c = Controller::start( let a = match Controller::start("test-config/config_test.json".to_string()) {
"F:\\Dangoware\\Dango Music Player\\dmp-core\\test-config\\config_test.json", Ok(c) => c,
) Err(e) => panic!("{e}")
.unwrap(); };
a.lib_scan_folder("F:/Music/Mp3".to_string());
sleep(Duration::from_secs(60)); a.lib_save();
} }
} }

View file

@ -1,9 +1,12 @@
use crate::music_storage::library::{MusicLibrary, Song, URI}; use uuid::Uuid;
use crate::{
music_player::{Player, PlayerError},
music_storage::library::{Album, MusicLibrary, URI}
};
use std::{ use std::{
error::Error, error::Error,
sync::{Arc, RwLock}, sync::{Arc, RwLock}
}; };
use uuid::Uuid;
use thiserror::Error; use thiserror::Error;
@ -12,7 +15,8 @@ pub enum QueueError {
#[error("Index out of bounds! Index {0} is over len {1}")] #[error("Index out of bounds! Index {0} is over len {1}")]
OutOfBounds(usize, usize), OutOfBounds(usize, usize),
#[error("The Queue is empty!")] #[error("The Queue is empty!")]
EmptyQueue, EmptyQueue
} }
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
@ -23,6 +27,54 @@ pub enum QueueState {
NoState, NoState,
} }
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum QueueItemType<'a> {
Song(Uuid),
ExternalSong(URI),
Album{
album: Album<'a>,
shuffled: bool,
order: Option<Vec<Uuid>>,
// disc #, track #
current: (i32, i32)
},
Playlist {
uuid: Uuid,
shuffled: bool,
order: Option<Vec<Uuid>>,
current: Uuid
},
None,
Test
}
impl QueueItemType<'_> {
fn get_uri(&self, lib: Arc<RwLock<MusicLibrary>>) -> Option<URI> {
use QueueItemType::*;
let lib = lib.read().unwrap();
match self {
Song(uuid) => {
if let Some((song, _)) = lib.query_uuid(uuid) {
Some(song.location.clone())
}else {
Option::None
}
},
Album{album, shuffled, current: (disc, index), ..} => {
if !shuffled {
Some(album.track(*disc as usize, *index as usize).unwrap().location.clone())
}else {
todo!()
}
},
ExternalSong(uri) => { Some(uri.clone()) },
_ => { Option::None }
}
}
}
// TODO: move this to a different location to be used elsewhere // TODO: move this to a different location to be used elsewhere
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
@ -31,80 +83,74 @@ pub enum PlayerLocation {
Library, Library,
Playlist(Uuid), Playlist(Uuid),
File, File,
Custom, Custom
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
pub struct QueueItem { pub struct QueueItem<'a> {
pub(super) item: Song, pub(super) item: QueueItemType<'a>,
pub(super) state: QueueState, pub(super) state: QueueState,
pub(super) source: PlayerLocation, pub(super) source: PlayerLocation,
pub(super) by_human: bool, pub(super) by_human: bool
} }
impl From<Song> for QueueItem { impl QueueItem<'_> {
fn from(song: Song) -> Self { fn new() -> Self {
QueueItem { QueueItem {
item: song, item: QueueItemType::None,
state: QueueState::NoState, state: QueueState::NoState,
source: PlayerLocation::Library, source: PlayerLocation::Library,
by_human: false, by_human: false
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Queue { pub struct Queue<'a> {
pub items: Vec<QueueItem>, pub player: Player,
pub played: Vec<QueueItem>, pub name: String,
pub loop_: bool, pub items: Vec<QueueItem<'a>>,
pub shuffle: bool, pub played: Vec<QueueItem<'a>>,
pub loop_: bool
} }
impl Queue { impl<'a> Queue<'a> {
fn has_addhere(&self) -> bool { fn has_addhere(&self) -> bool {
for item in &self.items { for item in &self.items {
if item.state == QueueState::AddHere { if item.state == QueueState::AddHere {
return true; return true
} }
} }
false false
} }
fn dbg_items(&self) { fn dbg_items(&self) {
dbg!( dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<QueueItemType>>(), self.items.len());
self.items
.iter()
.map(|item| item.item.clone())
.collect::<Vec<Song>>(),
self.items.len()
);
} }
pub fn new() -> Self { pub fn new() -> Result<Self, PlayerError> {
//TODO: Make the queue take settings from config/state if applicable Ok(
Queue { Queue {
player: Player::new()?,
name: String::new(),
items: Vec::new(), items: Vec::new(),
played: Vec::new(), played: Vec::new(),
loop_: false, loop_: false,
shuffle: false,
} }
)
} }
pub fn set_items(&mut self, tracks: Vec<QueueItem>) { pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) {
let mut tracks = tracks; let mut tracks = tracks;
self.items.clear(); self.items.clear();
self.items.append(&mut tracks); self.items.append(&mut tracks);
} }
pub fn add_item(&mut self, item: Song, source: PlayerLocation, by_human: bool) { pub fn add_item(&mut self, item: QueueItemType<'a>, source: PlayerLocation, by_human: bool) {
let mut i: usize = 0; let mut i: usize = 0;
self.items = self self.items = self.items.iter().enumerate().map(|(j, item_)| {
.items
.iter()
.enumerate()
.map(|(j, item_)| {
let mut item_ = item_.to_owned(); let mut item_ = item_.to_owned();
// get the index of the current AddHere item and give it to i // get the index of the current AddHere item and give it to i
if item_.state == QueueState::AddHere { if item_.state == QueueState::AddHere {
@ -112,21 +158,17 @@ impl Queue {
item_.state = QueueState::NoState; item_.state = QueueState::NoState;
} }
item_ item_
}) }).collect::<Vec<QueueItem>>();
.collect::<Vec<QueueItem>>();
self.items.insert( self.items.insert(i + if self.items.is_empty() { 0 } else { 1 }, QueueItem {
i + if self.items.is_empty() { 0 } else { 1 },
QueueItem {
item, item,
state: QueueState::AddHere, state: QueueState::AddHere,
source, source,
by_human, by_human
}, });
);
} }
pub fn add_item_next(&mut self, item: Song, source: PlayerLocation) { pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: PlayerLocation) {
use QueueState::*; use QueueState::*;
let empty = self.items.is_empty(); let empty = self.items.is_empty();
@ -134,23 +176,19 @@ impl Queue {
(if empty { 0 } else { 1 }), (if empty { 0 } else { 1 }),
QueueItem { QueueItem {
item, item,
state: if (self.items.get(1).is_none() state: if (self.items.get(1).is_none() || (!self.has_addhere() && self.items.get(1).is_some()) || empty) { AddHere } else { NoState },
|| !self.has_addhere() && self.items.get(1).is_some())
|| empty
{
AddHere
} else {
NoState
},
source, source,
by_human: true, by_human: true
}, }
) )
} }
pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) {} pub fn add_multi(&mut self, items: Vec<QueueItemType>, source: PlayerLocation, by_human: bool) {
}
pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> { pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> {
// dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]); // dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]);
if remove_index < self.items.len() { if remove_index < self.items.len() {
@ -161,7 +199,7 @@ impl Queue {
self.items[remove_index].state = QueueState::NoState; self.items[remove_index].state = QueueState::NoState;
self.items.remove(remove_index); self.items.remove(remove_index);
Ok(()) Ok(())
} else { }else {
Err(QueueError::EmptyQueue) Err(QueueError::EmptyQueue)
} }
} }
@ -176,11 +214,11 @@ impl Queue {
if !empty && index < self.items.len() { if !empty && index < self.items.len() {
let i = self.items[index].clone(); let i = self.items[index].clone();
self.items.retain(|item| *item == i); self.items.retain(|item| *item == i );
self.items[0].state = AddHere; self.items[0].state = AddHere;
} else if empty { }else if empty {
return Err("Queue is empty!".into()); return Err("Queue is empty!".into());
} else { }else {
return Err("index out of bounds!".into()); return Err("index out of bounds!".into());
} }
Ok(()) Ok(())
@ -195,19 +233,21 @@ impl Queue {
self.played.clear(); self.played.clear();
} }
// TODO: uh, fix this? // TODO: uh, fix this?
fn move_to(&mut self, index: usize) -> Result<(), QueueError> { fn move_to(&mut self, index: usize) -> Result<(), QueueError> {
use QueueState::*; use QueueState::*;
let empty = self.items.is_empty(); let empty = self.items.is_empty();
let nothing_error = Err(QueueError::EmptyQueue);
let index = if !empty { let index = if !empty { index } else { return nothing_error; };
index
} else {
return Err(QueueError::EmptyQueue);
};
if !empty && index < self.items.len() { if !empty && index < self.items.len() {
let position = self.player.position();
if position.is_some_and(|dur| !dur.is_zero() ) {
self.played.push(self.items[0].clone());
}
let to_item = self.items[index].clone(); let to_item = self.items[index].clone();
loop { loop {
@ -219,19 +259,17 @@ impl Queue {
self.items[1].state = AddHere; self.items[1].state = AddHere;
} }
if let Err(e) = self.remove_item(0) { if let Err(e) = self.remove_item(0) {
dbg!(&e); dbg!(&e); self.dbg_items(); return Err(e);
self.dbg_items();
return Err(e);
} }
// dbg!(&to_item.item, &self.items[ind].item); // dbg!(&to_item.item, &self.items[ind].item);
} else if empty { }else if empty {
return Err(QueueError::EmptyQueue); return nothing_error;
} else { }else {
break; break;
} }
} }
} else { }else {
return Err(QueueError::EmptyQueue); return Err(QueueError::EmptyQueue.into());
} }
Ok(()) Ok(())
} }
@ -249,30 +287,132 @@ impl Queue {
} }
#[allow(clippy::should_implement_trait)] #[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<&QueueItem, Box<dyn Error>> { pub fn next(&mut self, lib: Arc<RwLock<MusicLibrary>>) -> Result<OutQueue, Box<dyn Error>> {
if self.items.is_empty() { if self.items.is_empty() {
if self.loop_ { if self.loop_ {
return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue
} else { }else {
return Err(QueueError::EmptyQueue.into()); return Err(QueueError::EmptyQueue.into());
} }
} }
// TODO: add an algorithm to detect if the song should be skipped // TODO: add an algorithm to detect if the song should be skipped
let item = self.items[0].clone(); let item = self.items[0].clone();
let uri: URI = match &self.items[1].item {
QueueItemType::Song(uuid) => {
// TODO: Refactor later for multiple URIs
match &lib.read().unwrap().query_uuid(uuid) {
Some(song) => song.0.location.clone(),
None => return Err("Uuid does not exist!".into()),
}
},
QueueItemType::Album { album, current, ..} => {
let (disc, track) = (current.0 as usize, current.1 as usize);
match album.track(disc, track) {
Some(track) => track.location.clone(),
None => return Err(format!("Track in Album {} at disc {} track {} does not exist!", album.title(), disc, track).into())
}
},
QueueItemType::Playlist { current, .. } => {
// TODO: Refactor later for multiple URIs
match &lib.read().unwrap().query_uuid(current) {
Some(song) => song.0.location.clone(),
None => return Err("Uuid does not exist!".into()),
}
},
_ => todo!()
};
if !self.player.is_paused() {
self.player.enqueue_next(&uri)?;
self.player.play()?
}
if self.items[0].state == QueueState::AddHere || !self.has_addhere() { if self.items[0].state == QueueState::AddHere || !self.has_addhere() {
self.items[1].state = QueueState::AddHere; self.items[1].state = QueueState::AddHere;
} }
self.played.push(item); self.played.push(item);
self.items.remove(0); self.items.remove(0);
Ok(&self.items[1]) Ok(todo!())
} }
pub fn prev() {} pub fn prev() {}
pub fn enqueue_item(&mut self, item: QueueItem, lib: Arc<RwLock<MusicLibrary>>) -> Result<(), Box<dyn Error>> {
if let Some(uri) = item.item.get_uri(lib) {
self.player.enqueue_next(&uri)?;
}else {
return Err("this item does not exist!".into());
}
Ok(())
}
pub fn check_played(&mut self) { pub fn check_played(&mut self) {
while self.played.len() > 50 { while self.played.len() > 50 {
self.played.remove(0); self.played.remove(0);
} }
} }
} }
pub struct OutQueue {
}
pub enum OutQueueItem {
}
#[test]
fn item_add_test() {
let mut q = Queue::new().unwrap();
for _ in 0..5 {
// dbg!("tick!");
q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true);
// dbg!(&q.items, &q.items.len());
}
for _ in 0..1 {
q.remove_item(0).inspect_err(|e| println!("{e:?}"));
}
for _ in 0..2 {
q.items.push(QueueItem { item: QueueItemType::Test, state: QueueState::NoState, source: PlayerLocation::Library, by_human: false });
}
dbg!(5);
q.add_item_next(QueueItemType::Test, PlayerLocation::Test);
dbg!(6);
dbg!(&q.items, &q.items.len());
}
#[test]
fn test_() {
let mut q = Queue::new().unwrap();
for _ in 0..400 {
q.items.push(QueueItem { item: QueueItemType::Song(Uuid::new_v4()), state: QueueState::NoState, source: PlayerLocation::File, by_human: false });
}
for _ in 0..50000 {
q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true);
}
// q.add_item_next(QueueItemType::Test, PlayerLocation::File);
// dbg!(&q.items, &q.items.len());
}
#[test]
fn move_test() {
let mut q = Queue::new().unwrap();
for _ in 0..5 {
q.add_item(QueueItemType::Song(Uuid::new_v4()), PlayerLocation::Library, true);
}
// q.add_item(QueueItemType::Test, QueueSource::Library, true).unwrap();
dbg!(&q.items, &q.items.len());
q.move_to(3).inspect_err(|e| {dbg!(e);});
dbg!(&q.items, &q.items.len());
// q.dbg_items();
}

View file

@ -15,10 +15,8 @@ use gstreamer::prelude::*;
use chrono::Duration; use chrono::Duration;
use thiserror::Error; use thiserror::Error;
use super::player::{Player, PlayerError};
#[derive(Debug)] #[derive(Debug)]
pub enum GstCmd { pub enum PlayerCmd {
Play, Play,
Pause, Pause,
Eos, Eos,
@ -26,7 +24,7 @@ pub enum GstCmd {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum GstState { pub enum PlayerState {
Playing, Playing,
Paused, Paused,
Ready, Ready,
@ -35,7 +33,7 @@ pub enum GstState {
VoidPending, VoidPending,
} }
impl From<gst::State> for GstState { impl From<gst::State> for PlayerState {
fn from(value: gst::State) -> Self { fn from(value: gst::State) -> Self {
match value { match value {
gst::State::VoidPending => Self::VoidPending, gst::State::VoidPending => Self::VoidPending,
@ -47,7 +45,7 @@ impl From<gst::State> for GstState {
} }
} }
impl TryInto<gst::State> for GstState { impl TryInto<gst::State> for PlayerState {
fn try_into(self) -> Result<gst::State, Box<dyn Error>> { fn try_into(self) -> Result<gst::State, Box<dyn Error>> {
match self { match self {
Self::VoidPending => Ok(gst::State::VoidPending), Self::VoidPending => Ok(gst::State::VoidPending),
@ -62,6 +60,24 @@ impl TryInto<gst::State> for GstState {
type Error = Box<dyn Error>; type Error = Box<dyn Error>;
} }
#[derive(Error, Debug)]
pub enum PlayerError {
#[error("player initialization failed")]
Init(#[from] glib::Error),
#[error("element factory failed to create playbin3")]
Factory(#[from] glib::BoolError),
#[error("could not change playback state")]
StateChange(#[from] gst::StateChangeError),
#[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,
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
enum PlaybackStats { enum PlaybackStats {
Idle, Idle,
@ -75,10 +91,10 @@ enum PlaybackStats {
/// An instance of a music player with a GStreamer backend /// An instance of a music player with a GStreamer backend
#[derive(Debug)] #[derive(Debug)]
pub struct GStreamer { pub struct Player {
source: Option<URI>, source: Option<URI>,
//pub message_tx: Sender<PlayerCmd>, //pub message_tx: Sender<PlayerCmd>,
pub message_rx: crossbeam::channel::Receiver<GstCmd>, pub message_rx: crossbeam::channel::Receiver<PlayerCmd>,
playback_tx: crossbeam::channel::Sender<PlaybackStats>, playback_tx: crossbeam::channel::Sender<PlaybackStats>,
playbin: Arc<RwLock<Element>>, playbin: Arc<RwLock<Element>>,
@ -89,7 +105,7 @@ pub struct GStreamer {
position: Arc<RwLock<Option<Duration>>>, position: Arc<RwLock<Option<Duration>>>,
} }
impl GStreamer { impl Player {
pub fn new() -> Result<Self, PlayerError> { pub fn new() -> Result<Self, PlayerError> {
// Initialize GStreamer, maybe figure out how to nicely fail here // Initialize GStreamer, maybe figure out how to nicely fail here
gst::init()?; gst::init()?;
@ -146,14 +162,14 @@ impl GStreamer {
// Check if the current playback position is close to the end // Check if the current playback position is close to the end
let finish_point = end - Duration::milliseconds(250); let finish_point = end - Duration::milliseconds(250);
if pos_temp.unwrap() >= end { if pos_temp.unwrap() >= end {
let _ = playback_tx.try_send(GstCmd::Eos); let _ = playback_tx.try_send(PlayerCmd::Eos);
playbin_arc playbin_arc
.write() .write()
.unwrap() .unwrap()
.set_state(gst::State::Ready) .set_state(gst::State::Ready)
.expect("Unable to set the pipeline state"); .expect("Unable to set the pipeline state");
} else if pos_temp.unwrap() >= finish_point { } else if pos_temp.unwrap() >= finish_point {
let _ = playback_tx.try_send(GstCmd::AboutToFinish); let _ = playback_tx.try_send(PlayerCmd::AboutToFinish);
} }
// This has to be done AFTER the current time in the file // This has to be done AFTER the current time in the file
@ -452,7 +468,7 @@ impl GStreamer {
} }
/// Get the current state of the playback /// Get the current state of the playback
pub fn state(&mut self) -> GstState { pub fn state(&mut self) -> PlayerState {
self.playbin().unwrap().current_state().into() self.playbin().unwrap().current_state().into()
/* /*
match *self.buffer.read().unwrap() { match *self.buffer.read().unwrap() {
@ -482,9 +498,7 @@ impl GStreamer {
} }
} }
// impl Player for GStreamer {} impl Drop for Player {
impl Drop for GStreamer {
/// Cleans up the `GStreamer` pipeline and the monitoring /// Cleans up the `GStreamer` pipeline and the monitoring
/// thread when [Player] is dropped. /// thread when [Player] is dropped.
fn drop(&mut self) { fn drop(&mut self) {

View file

@ -1,57 +0,0 @@
use chrono::Duration;
use thiserror::Error;
use gstreamer as gst;
use crate::music_storage::library::URI;
#[derive(Error, Debug)]
pub enum PlayerError {
#[error("player initialization failed")]
Init(#[from] glib::Error),
#[error("element factory failed to create playbin3")]
Factory(#[from] glib::BoolError),
#[error("could not change playback state")]
StateChange(#[from] gst::StateChangeError),
#[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,
}
pub trait Player {
fn source(&self) -> &Option<URI>;
fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>;
fn set_volume(&mut self, volume: f64);
fn volume(&mut self) -> f64;
fn ready(&mut self) -> Result<(), PlayerError>;
fn play(&mut self) -> Result<(), PlayerError>;
fn resume(&mut self) -> Result<(), PlayerError>;
fn pause(&mut self) -> Result<(), PlayerError>;
fn stop(&mut self) -> Result<(), PlayerError>;
fn is_paused(&mut self) -> bool;
fn position(&mut self) -> Option<Duration>;
fn duration(&mut self) -> Option<Duration>;
fn raw_duration(&self) -> Option<Duration>;
fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>;
fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>;
}

View file

@ -176,15 +176,14 @@ pub struct FoobarPlaylistTrack {
impl FoobarPlaylistTrack { impl FoobarPlaylistTrack {
fn find_song(&self) -> Song { fn find_song(&self) -> Song {
let location = URI::Local(self.file_name.clone().into()); let location = URI::Local(self.file_name.clone().into());
let internal_tags = Vec::new();
Song { Song {
location: vec![location], location,
uuid: Uuid::new_v4(), uuid: Uuid::new_v4(),
plays: 0, plays: 0,
skips: 0, skips: 0,
favorited: false, favorited: false,
banned: None, // banned: None,
rating: None, rating: None,
format: None, format: None,
duration: self.duration, duration: self.duration,
@ -194,7 +193,6 @@ impl FoobarPlaylistTrack {
date_modified: None, date_modified: None,
album_art: Vec::new(), album_art: Vec::new(),
tags: BTreeMap::new(), tags: BTreeMap::new(),
internal_tags,
} }
} }
} }

View file

@ -13,7 +13,7 @@ use std::vec::Vec;
use chrono::prelude::*; use chrono::prelude::*;
use crate::music_storage::db_reader::extern_library::ExternalLibrary; use crate::music_storage::db_reader::extern_library::ExternalLibrary;
use crate::music_storage::library::{AlbumArt, BannedType, Service, Song, Tag, URI}; use crate::music_storage::library::{AlbumArt, Service, Song, Tag, URI};
use crate::music_storage::utils; use crate::music_storage::utils;
use urlencoding::decode; use urlencoding::decode;
@ -166,19 +166,17 @@ impl ExternalLibrary for ITunesLibrary {
}; };
let play_time_ = StdDur::from_secs(track.plays as u64 * dur.as_secs()); 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 { let ny: Song = Song {
location: vec![location], location,
uuid: Uuid::new_v4(), uuid: Uuid::new_v4(),
plays: track.plays, plays: track.plays,
skips: 0, skips: 0,
favorited: track.favorited, favorited: track.favorited,
banned: if track.banned { // banned: if track.banned {
Some(BannedType::All) // Some(BannedType::All)
}else { // }else {
None // None
}, // },
rating: track.rating, rating: track.rating,
format: match FileFormat::from_file(PathBuf::from(&loc)) { format: match FileFormat::from_file(PathBuf::from(&loc)) {
Ok(e) => Some(e), Ok(e) => Some(e),
@ -194,7 +192,6 @@ impl ExternalLibrary for ITunesLibrary {
Err(_) => Vec::new(), Err(_) => Vec::new(),
}, },
tags: tags_, tags: tags_,
internal_tags,
}; };
// dbg!(&ny.tags); // dbg!(&ny.tags);
bun.push(ny); bun.push(ny);

View file

@ -1,4 +1,3 @@
use super::playlist::PlaylistFolder;
// Crate things // Crate things
use super::utils::{find_images, normalize, read_file, write_file}; use super::utils::{find_images, normalize, read_file, write_file};
use crate::config::config::Config; use crate::config::config::Config;
@ -9,15 +8,16 @@ use std::error::Error;
use std::io::Write; use std::io::Write;
use std::ops::ControlFlow::{Break, Continue}; use std::ops::ControlFlow::{Break, Continue};
// Files // Files
use file_format::{FileFormat, Kind}; use file_format::{FileFormat, Kind};
use glib::filename_to_uri; use glib::filename_to_uri;
use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt};
use rcue::parser::parse_from_file; use rcue::parser::parse_from_file;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
use uuid::Uuid; use uuid::Uuid;
use std::fs::{self, File};
use tempfile::TempDir;
use std::path::{Path, PathBuf};
use walkdir::WalkDir; use walkdir::WalkDir;
// Time // Time
@ -117,58 +117,35 @@ impl ToString for Field {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum InternalTag {
DoNotTrack(DoNotTrack),
SongType(SongType),
SongLink(Uuid, SongType),
// Volume Adjustment from -100% to 100%
VolumeAdjustment(i8),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub enum BannedType { pub enum BannedType {
Shuffle, Shuffle,
All, All,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub enum DoNotTrack { pub enum DoNotTrack {
// TODO: add services to not track // TODO: add services to not track
LastFM,
LibreFM,
MusicBrainz,
Discord,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[non_exhaustive] enum SongType {
pub enum SongType { // TODO: add song types
// TODO: add MORE?! song types
Main, Main,
Instrumental, Instrumental,
Remix, Remix,
Custom(String), Custom(String)
}
impl Default for SongType {
fn default() -> Self {
SongType::Main
}
} }
/// Stores information about a single song /// Stores information about a single song
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Song { pub struct Song {
pub location: Vec<URI>, pub location: URI,
pub uuid: Uuid, pub uuid: Uuid,
pub plays: i32, pub plays: i32,
pub skips: i32, pub skips: i32,
pub favorited: bool, pub favorited: bool,
pub banned: Option<BannedType>, // pub banned: Option<BannedType>,
pub rating: Option<u8>, pub rating: Option<u8>,
pub format: Option<FileFormat>, pub format: Option<FileFormat>,
pub duration: Duration, pub duration: Duration,
@ -181,9 +158,9 @@ pub struct Song {
pub date_modified: Option<DateTime<Utc>>, pub date_modified: Option<DateTime<Utc>>,
pub album_art: Vec<AlbumArt>, pub album_art: Vec<AlbumArt>,
pub tags: BTreeMap<Tag, String>, pub tags: BTreeMap<Tag, String>,
pub internal_tags: Vec<InternalTag>,
} }
impl Song { impl Song {
/// Get a tag's value /// Get a tag's value
/// ///
@ -203,7 +180,7 @@ impl Song {
pub fn get_field(&self, target_field: &str) -> Option<Field> { pub fn get_field(&self, target_field: &str) -> Option<Field> {
let lower_target = target_field.to_lowercase(); let lower_target = target_field.to_lowercase();
match lower_target.as_str() { match lower_target.as_str() {
"location" => Some(Field::Location(self.primary_uri().unwrap().0.clone())), //TODO: make this not unwrap() "location" => Some(Field::Location(self.location.clone())),
"plays" => Some(Field::Plays(self.plays)), "plays" => Some(Field::Plays(self.plays)),
"skips" => Some(Field::Skips(self.skips)), "skips" => Some(Field::Skips(self.skips)),
"favorited" => Some(Field::Favorited(self.favorited)), "favorited" => Some(Field::Favorited(self.favorited)),
@ -302,15 +279,13 @@ impl Song {
// TODO: Fix error handling // TODO: Fix error handling
let binding = fs::canonicalize(target_file).unwrap(); 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 { let new_song = Song {
location: vec![URI::Local(binding)], location: URI::Local(binding),
uuid: Uuid::new_v4(), uuid: Uuid::new_v4(),
plays: 0, plays: 0,
skips: 0, skips: 0,
favorited: false, favorited: false,
banned: None, // banned: None,
rating: None, rating: None,
format, format,
duration, duration,
@ -320,7 +295,6 @@ impl Song {
date_modified: Some(chrono::offset::Utc::now()), date_modified: Some(chrono::offset::Utc::now()),
tags, tags,
album_art, album_art,
internal_tags,
}; };
Ok(new_song) Ok(new_song)
} }
@ -340,6 +314,7 @@ impl Song {
for file in cue_data.files.iter() { for file in cue_data.files.iter() {
let audio_location = &parent_dir.join(file.file.clone()); let audio_location = &parent_dir.join(file.file.clone());
if !audio_location.exists() { if !audio_location.exists() {
continue; continue;
} }
@ -424,17 +399,17 @@ impl Song {
let album_art = find_images(&audio_location.to_path_buf()).unwrap(); let album_art = find_images(&audio_location.to_path_buf()).unwrap();
let new_song = Song { let new_song = Song {
location: vec![URI::Cue { location: URI::Cue {
location: audio_location.clone(), location: audio_location.clone(),
index: i, index: i,
start, start,
end, end,
}], },
uuid: Uuid::new_v4(), uuid: Uuid::new_v4(),
plays: 0, plays: 0,
skips: 0, skips: 0,
favorited: false, favorited: false,
banned: None, // banned: None,
rating: None, rating: None,
format, format,
duration, duration,
@ -444,7 +419,6 @@ impl Song {
date_modified: Some(chrono::offset::Utc::now()), date_modified: Some(chrono::offset::Utc::now()),
tags, tags,
album_art, album_art,
internal_tags: Vec::new(),
}; };
tracks.push((new_song, audio_location.clone())); tracks.push((new_song, audio_location.clone()));
} }
@ -452,32 +426,72 @@ impl Song {
Ok(tracks) 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 /// Takes the AlbumArt[index] and opens it in the native file viewer
#[allow(clippy::type_complexity)]
pub fn primary_uri(&self) -> Result<(&URI, Option<Vec<&URI>>), Box<dyn Error>> {
let mut invalid_uris = Vec::new();
let mut valid_uri = None;
for uri in &self.location { pub fn open_album_art(&self, index: usize, temp_dir: &TempDir) -> Result<(), Box<dyn Error>> {
if uri.exists()? { use opener::open;
valid_uri = Some(uri); use urlencoding::decode;
break;
} else { if index >= self.album_art.len() {
invalid_uris.push(uri); return Err("Index out of bounds".into());
} }
}
match valid_uri { let uri = match &self.album_art[index] {
Some(uri) => Ok(( AlbumArt::External(uri) => {
uri, PathBuf::from(decode(match uri.as_uri().strip_prefix("file:///") {
if !invalid_uris.is_empty() { Some(e) => e,
Some(invalid_uris) None => return Err("Invalid path?".into())
} else { })?.to_owned().to_string())
None },
AlbumArt::Embedded(_) => {
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
let tagged_file: lofty::TaggedFile;
#[cfg(target_family = "windows")]
let uri = urlencoding::decode(
match self.location.as_uri().strip_prefix("file:///") {
Some(str) => str,
None => return Err("invalid path.. again?".into())
})?.into_owned();
#[cfg(target_family = "unix")]
let uri = urlencoding::decode(
match self.location.as_uri().strip_prefix("file://") {
Some(str) => str,
None => return Err("invalid path.. again?".into())
})?.into_owned();
let tag = match Probe::open(uri)?.options(normal_options).read() {
Ok(file) => {
tagged_file = file;
match tagged_file.primary_tag() {
Some(primary_tag) => primary_tag,
None => match tagged_file.first_tag() {
Some(first_tag) => first_tag,
None => blank_tag,
}, },
)),
None => Err("No valid URIs for this song".into()),
} }
} }
Err(_) => blank_tag,
};
let data = tag.pictures()[index].data();
let fmt = FileFormat::from_bytes(data);
let file_path = temp_dir.path().join(format!("{}_{index}.{}", self.uuid, fmt.extension()));
File::create(&file_path)?.write_all(data)?;
file_path
},
};
dbg!(open(dbg!(uri))?);
Ok(())
}
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -546,7 +560,7 @@ impl URI {
pub fn as_path(&self) -> Result<&PathBuf, Box<dyn Error>> { pub fn as_path(&self) -> Result<&PathBuf, Box<dyn Error>> {
if let Self::Local(path) = self { if let Self::Local(path) = self {
Ok(path) Ok(path)
} else { }else {
Err("This URI is not local!".into()) Err("This URI is not local!".into())
} }
} }
@ -554,7 +568,7 @@ impl URI {
pub fn exists(&self) -> Result<bool, std::io::Error> { pub fn exists(&self) -> Result<bool, std::io::Error> {
match self { match self {
URI::Local(loc) => loc.try_exists(), URI::Local(loc) => loc.try_exists(),
URI::Cue { location, .. } => location.try_exists(), URI::Cue {location, ..} => location.try_exists(),
URI::Remote(_, _loc) => todo!(), URI::Remote(_, _loc) => todo!(),
} }
} }
@ -638,8 +652,14 @@ pub struct MusicLibrary {
pub name: String, pub name: String,
pub uuid: Uuid, pub uuid: Uuid,
pub library: Vec<Song>, pub library: Vec<Song>,
pub playlists: PlaylistFolder, }
pub backup_songs: Vec<Song>, // maybe move this to the config instead?
#[test]
fn library_init() {
let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap();
let target_uuid = config.libraries.libraries[0].uuid;
let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap();
dbg!(a);
} }
impl MusicLibrary { impl MusicLibrary {
@ -649,8 +669,6 @@ impl MusicLibrary {
name, name,
uuid, uuid,
library: Vec::new(), library: Vec::new(),
playlists: PlaylistFolder::new(),
backup_songs: Vec::new(),
} }
} }
@ -719,12 +737,9 @@ impl MusicLibrary {
.par_iter() .par_iter()
.enumerate() .enumerate()
.try_for_each(|(i, track)| { .try_for_each(|(i, track)| {
for location in &track.location { if path == &track.location {
//TODO: check that this works
if path == location {
return std::ops::ControlFlow::Break((track, i)); return std::ops::ControlFlow::Break((track, i));
} }
}
Continue(()) Continue(())
}); });
@ -759,8 +774,7 @@ impl MusicLibrary {
fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> { fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> {
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
self.library.par_iter().for_each(|track| { self.library.par_iter().for_each(|track| {
if path == track.primary_uri().unwrap().0.path() { if path == track.location.path() {
//TODO: make this also not unwrap
result.clone().lock().unwrap().push(track); result.clone().lock().unwrap().push(track);
} }
}); });
@ -772,7 +786,10 @@ impl MusicLibrary {
} }
/// Finds all the audio files within a specified folder /// Finds all the audio files within a specified folder
pub fn scan_folder(&mut self, target_path: &str) -> Result<i32, Box<dyn std::error::Error>> { pub fn scan_folder(
&mut self,
target_path: &str,
) -> Result<i32, Box<dyn std::error::Error>> {
let mut total = 0; let mut total = 0;
let mut errors = 0; let mut errors = 0;
for target_file in WalkDir::new(target_path) for target_file in WalkDir::new(target_path)
@ -835,6 +852,7 @@ impl MusicLibrary {
} }
pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> {
let new_song = Song::from_file(target_file)?; let new_song = Song::from_file(target_file)?;
match self.add_song(new_song) { match self.add_song(new_song) {
Ok(_) => (), Ok(_) => (),
@ -850,13 +868,14 @@ impl MusicLibrary {
let tracks = Song::from_cue(cuesheet)?; let tracks = Song::from_cue(cuesheet)?;
let mut tracks_added = tracks.len() as i32; let mut tracks_added = tracks.len() as i32;
for (new_song, location) in tracks { for (new_song, location) in tracks {
// Try to remove the original audio file from the db if it exists // Try to remove the original audio file from the db if it exists
if self.remove_uri(&URI::Local(location.clone())).is_ok() { if self.remove_uri(&URI::Local(location.clone())).is_ok() {
tracks_added -= 1 tracks_added -= 1
} }
match self.add_song(new_song) { match self.add_song(new_song) {
Ok(_) => {} Ok(_) => {},
Err(_error) => { Err(_error) => {
//println!("{}", _error); //println!("{}", _error);
continue; continue;
@ -867,14 +886,13 @@ impl MusicLibrary {
} }
pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> { pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> {
let location = new_song.primary_uri()?.0; if self.query_uri(&new_song.location).is_some() {
if self.query_uri(location).is_some() { return Err(format!("URI already in database: {:?}", new_song.location).into());
return Err(format!("URI already in database: {:?}", location).into());
} }
match location { match new_song.location {
URI::Local(_) if self.query_path(location.path()).is_some() => { URI::Local(_) if self.query_path(new_song.location.path()).is_some() => {
return Err(format!("Location exists for {:?}", location).into()) return Err(format!("Location exists for {:?}", new_song.location).into())
} }
_ => (), _ => (),
} }
@ -897,18 +915,17 @@ impl MusicLibrary {
} }
/// Scan the song by a location and update its tags /// Scan the song by a location and update its tags
// TODO: change this to work with multiple uris
pub fn update_uri( pub fn update_uri(
&mut self, &mut self,
target_uri: &URI, target_uri: &URI,
new_tags: Vec<Tag>, new_tags: Vec<Tag>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let (target_song, _) = match self.query_uri(target_uri) { let target_song = match self.query_uri(target_uri) {
Some(song) => song, Some(song) => song,
None => return Err("URI not in database!".to_string().into()), None => return Err("URI not in database!".to_string().into()),
}; };
println!("{:?}", target_song.location); println!("{:?}", target_song.0.location);
for tag in new_tags { for tag in new_tags {
println!("{:?}", tag); println!("{:?}", tag);
@ -1126,19 +1143,13 @@ impl MusicLibrary {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{ use std::{path::Path, thread::sleep, time::{Duration, Instant}};
path::{Path, PathBuf},
sync::{Arc, RwLock},
thread::sleep,
time::{Duration, Instant},
};
use tempfile::TempDir; use tempfile::TempDir;
use crate::{config::config::Config, music_storage::library::MusicLibrary};
use super::Song; use super::Song;
#[test] #[test]
fn get_art_test() { fn get_art_test() {
let s = Song::from_file(Path::new("")).unwrap(); let s = Song::from_file(Path::new("")).unwrap();
@ -1147,16 +1158,8 @@ mod test {
let now = Instant::now(); let now = Instant::now();
_ = s.open_album_art(0, dir).inspect_err(|e| println!("{e:?}")); _ = s.open_album_art(0, dir).inspect_err(|e| println!("{e:?}"));
_ = s.open_album_art(1, dir).inspect_err(|e| println!("{e:?}")); _ = s.open_album_art(1, dir).inspect_err(|e| println!("{e:?}"));
println!("{}ms", now.elapsed().as_millis()); println!("{}ms", now.elapsed().as_millis() );
sleep(Duration::from_secs(20)); sleep(Duration::from_secs(20));
} }
#[test]
fn library_init() {
let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap();
let target_uuid = config.libraries.libraries[0].uuid;
let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap();
dbg!(a);
}
} }

View file

@ -1,52 +1,21 @@
use std::{fs::File, io::Read, path:: PathBuf, sync::{Arc, RwLock}};
use std::error::Error; use std::error::Error;
use std::{
fs::File,
io::Read,
path::PathBuf,
sync::{Arc, RwLock},
};
use std::time::Duration; use std::time::Duration;
// use chrono::Duration;
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
use nestify::nest;
use rayon::prelude::*; use rayon::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SortOrder { pub enum SortOrder {
Manual, Manual,
Tag(Vec<Tag>), Tag(Vec<Tag>)
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
nest! {
#[derive(Debug, Clone, Deserialize, Serialize)]*
pub struct PlaylistFolder {
name: String,
items: Vec<
pub enum PlaylistFolderItem {
Folder(PlaylistFolder),
List(Playlist)
}
>
}
}
impl PlaylistFolder {
pub fn new() -> Self {
PlaylistFolder {
name: String::new(),
items: Vec::new(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Playlist { pub struct Playlist {
uuid: Uuid, uuid: Uuid,
title: String, title: String,
@ -106,16 +75,11 @@ impl Playlist {
// } // }
// None // None
// } // }
pub fn contains_value( pub fn contains_value(&self, tag: &Tag, value: &String, lib: Arc<RwLock<MusicLibrary>>) -> bool {
&self,
tag: &Tag,
value: &String,
lib: Arc<RwLock<MusicLibrary>>,
) -> bool {
let lib = lib.read().unwrap(); let lib = lib.read().unwrap();
let items = match lib.query_tracks(value, &vec![tag.to_owned()], &vec![tag.to_owned()]) { let items = match lib.query_tracks(value, &vec![tag.to_owned()], &vec![tag.to_owned()]) {
Some(e) => e, Some(e) => e,
None => return false, None => return false
}; };
for item in items { for item in items {
@ -138,35 +102,23 @@ impl Playlist {
super::utils::read_file(PathBuf::from(path)) super::utils::read_file(PathBuf::from(path))
} }
pub fn to_m3u8( pub fn to_m3u8(&mut self, lib: Arc<RwLock<MusicLibrary>>, location: &str) -> Result<(), Box<dyn Error>> {
&mut self,
lib: Arc<RwLock<MusicLibrary>>,
location: &str,
) -> Result<(), Box<dyn Error>> {
let lib = lib.read().unwrap(); let lib = lib.read().unwrap();
let seg = self let seg = self.tracks
.tracks
.iter() .iter()
.filter_map(|uuid| { .filter_map( |uuid| {
// TODO: The Unwraps need to be handled here
if let Some((track, _)) = lib.query_uuid(uuid) { if let Some((track, _)) = lib.query_uuid(uuid) {
if let URI::Local(_) = track.primary_uri().unwrap().0 { if let URI::Local(_) = track.location {
Some(MediaSegment { Some(MediaSegment {
uri: track.primary_uri().unwrap().0.to_string(), uri: track.location.to_string(),
duration: track.duration.as_millis() as f32, duration: track.duration.as_millis() as f32,
title: track title: track.tags.get_key_value(&Tag::Title).map(|tag| tag.1.into()),
.tags
.get_key_value(&Tag::Title)
.map(|tag| tag.1.into()),
..Default::default() ..Default::default()
}) })
} else { }else { None }
None }else { None }
} }
} else { )
None
}
})
.collect::<Vec<MediaSegment>>(); .collect::<Vec<MediaSegment>>();
let m3u8 = MediaPlaylist { let m3u8 = MediaPlaylist {
@ -190,10 +142,7 @@ impl Playlist {
Ok(()) Ok(())
} }
pub fn from_m3u8( pub fn from_m3u8(path: &str, lib: Arc<RwLock<MusicLibrary>>) -> Result<Playlist, Box<dyn Error>> {
path: &str,
lib: Arc<RwLock<MusicLibrary>>,
) -> Result<Playlist, Box<dyn Error>> {
let mut file = match File::open(path) { let mut file = match File::open(path) {
Ok(file) => file, Ok(file) => file,
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
@ -209,9 +158,7 @@ impl Playlist {
}; };
match playlist { match playlist {
List2::MasterPlaylist(_) => { List2::MasterPlaylist(_) => Err("This is a Master Playlist!\nPlase input a Media Playlist".into()),
Err("This is a Master Playlist!\nPlase input a Media Playlist".into())
}
List2::MediaPlaylist(playlist_) => { List2::MediaPlaylist(playlist_) => {
let mut uuids = Vec::new(); let mut uuids = Vec::new();
for seg in playlist_.segments { for seg in playlist_.segments {
@ -220,7 +167,7 @@ impl Playlist {
let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) { let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) {
song.uuid song.uuid
} else { }else {
let song_ = Song::from_file(&path_)?; let song_ = Song::from_file(&path_)?;
let uuid = song_.uuid.to_owned(); let uuid = song_.uuid.to_owned();
lib.add_song(song_)?; lib.add_song(song_)?;
@ -232,8 +179,7 @@ impl Playlist {
#[cfg(target_family = "windows")] #[cfg(target_family = "windows")]
{ {
playlist.title = path playlist.title = path.split("\\")
.split("\\")
.last() .last()
.unwrap_or_default() .unwrap_or_default()
.strip_suffix(".m3u8") .strip_suffix(".m3u8")
@ -242,8 +188,7 @@ impl Playlist {
} }
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
{ {
playlist.title = path playlist.title = path.split("/")
.split("/")
.last() .last()
.unwrap_or_default() .unwrap_or_default()
.strip_suffix(".m3u8") .strip_suffix(".m3u8")
@ -257,6 +202,7 @@ impl Playlist {
} }
} }
pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) { pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) {
let lib = lib.read().unwrap(); let lib = lib.read().unwrap();
let mut songs = vec![]; let mut songs = vec![];
@ -265,7 +211,7 @@ impl Playlist {
for uuid in &self.tracks { for uuid in &self.tracks {
if let Some((track, _)) = lib.query_uuid(uuid) { if let Some((track, _)) = lib.query_uuid(uuid) {
songs.push(track.to_owned()); songs.push(track.to_owned());
} else { }else {
invalid_uuids.push(uuid); invalid_uuids.push(uuid);
} }
} }
@ -277,12 +223,10 @@ impl Playlist {
for (i, sort_option) in sort_by.iter().enumerate() { for (i, sort_option) in sort_by.iter().enumerate() {
dbg!(&i); dbg!(&i);
let tag_a = match sort_option { let tag_a = match sort_option {
Tag::Field(field_selection) => { Tag::Field(field_selection) => match a.get_field(field_selection.as_str()) {
match a.get_field(field_selection.as_str()) {
Some(field_value) => field_value.to_string(), Some(field_value) => field_value.to_string(),
None => continue, None => continue,
} },
}
_ => match a.get_tag(sort_option) { _ => match a.get_tag(sort_option) {
Some(tag_value) => tag_value.to_owned(), Some(tag_value) => tag_value.to_owned(),
None => continue, None => continue,
@ -322,6 +266,7 @@ impl Playlist {
} }
} }
impl Default for Playlist { impl Default for Playlist {
fn default() -> Self { fn default() -> Self {
Playlist { Playlist {
@ -331,7 +276,7 @@ impl Default for Playlist {
tracks: Vec::default(), tracks: Vec::default(),
sort_order: SortOrder::Manual, sort_order: SortOrder::Manual,
play_count: 0, play_count: 0,
play_time: Duration::from_secs(0), play_time: Duration::from_millis(0),
} }
} }
} }
@ -345,20 +290,17 @@ mod test_super {
fn list_to_m3u8() { fn list_to_m3u8() {
let (_, lib) = read_config_lib(); let (_, lib) = read_config_lib();
let mut playlist = Playlist::new(); let mut playlist = Playlist::new();
let tracks = lib.library.iter().map(|track| track.uuid).collect(); let tracks = lib.library.iter().map(|track| track.uuid ).collect();
playlist.set_tracks(tracks); playlist.set_tracks(tracks);
_ = playlist.to_m3u8( _ = playlist.to_m3u8(Arc::new(RwLock::from(lib)), ".\\test-config\\playlists\\playlist.m3u8");
Arc::new(RwLock::from(lib)),
".\\test-config\\playlists\\playlist.m3u8",
);
} }
fn m3u8_to_list() -> Playlist { fn m3u8_to_list() -> Playlist {
let (_, lib) = read_config_lib(); let (_, lib) = read_config_lib();
let arc = Arc::new(RwLock::from(lib)); let arc = Arc::new(RwLock::from(lib));
let playlist = let playlist = Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap();
Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap();
playlist.to_file(".\\test-config\\playlists\\playlist"); playlist.to_file(".\\test-config\\playlists\\playlist");
dbg!(playlist) dbg!(playlist)

View file

@ -25,12 +25,12 @@ pub(super) fn normalize(input_string: &str) -> String {
/// Write any data structure which implements [serde::Serialize] /// Write any data structure which implements [serde::Serialize]
/// out to a [bincode] encoded file compressed using [snap] /// out to a [bincode] encoded file compressed using [snap]
pub(super) fn write_file<T: serde::Serialize, U: std::convert::AsRef<Path>+std::convert::AsRef<std::ffi::OsStr>+Clone>( pub(super) fn write_file<T: serde::Serialize>(
library: T, library: T,
path: U, path: PathBuf,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// Create a temporary name for writing out // Create a temporary name for writing out
let mut writer_name = PathBuf::from(&path); let mut writer_name = path.clone();
writer_name.set_extension("tmp"); writer_name.set_extension("tmp");
// Create a new BufWriter on the file and a snap frame encoder // Create a new BufWriter on the file and a snap frame encoder