mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-20 06:22:52 -05:00
Compare commits
5 commits
ab529f4c9b
...
9457c5c996
Author | SHA1 | Date | |
---|---|---|---|
|
9457c5c996 | ||
|
b04a166c1b | ||
|
56040bfd28 | ||
|
94e6c25219 | ||
|
f02f5bca41 |
13 changed files with 541 additions and 758 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -12,7 +12,15 @@ keywords = []
|
|||
categories = []
|
||||
|
||||
[dependencies]
|
||||
file-format = { version = "0.23.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] }
|
||||
file-format = { version = "0.23.0", features = [
|
||||
"reader-asf",
|
||||
"reader-ebml",
|
||||
"reader-mp4",
|
||||
"reader-rm",
|
||||
"reader-txt",
|
||||
"reader-xml",
|
||||
"serde",
|
||||
] }
|
||||
lofty = "0.18.2"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
walkdir = "2.4.0"
|
||||
|
@ -38,3 +46,5 @@ deunicode = "1.4.2"
|
|||
opener = { version = "0.7.0", features = ["reveal"] }
|
||||
tempfile = "3.10.1"
|
||||
listenbrainz = "0.7.0"
|
||||
discord-rpc-client = "0.4.0"
|
||||
nestify = "0.3.3"
|
||||
|
|
|
@ -8,13 +8,16 @@ pub mod music_storage {
|
|||
pub mod db_reader;
|
||||
}
|
||||
|
||||
pub mod music_controller{
|
||||
pub mod music_controller {
|
||||
pub mod controller;
|
||||
pub mod connections;
|
||||
pub mod queue;
|
||||
}
|
||||
|
||||
pub mod music_player;
|
||||
pub mod music_player {
|
||||
pub mod gstreamer;
|
||||
pub mod player;
|
||||
}
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod config {
|
||||
pub mod config;
|
||||
|
|
|
@ -1,95 +1,103 @@
|
|||
use std::{
|
||||
sync::{Arc, RwLock},
|
||||
error::Error,
|
||||
};
|
||||
// use std::{
|
||||
// sync::{Arc, RwLock},
|
||||
// error::Error,
|
||||
// };
|
||||
|
||||
use listenbrainz::ListenBrainz;
|
||||
use uuid::Uuid;
|
||||
// use discord_rpc_client::Client;
|
||||
// use listenbrainz::ListenBrainz;
|
||||
// use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
config::config::Config, music_controller::controller::{Controller, QueueCmd, QueueResponse}, music_storage::library::{MusicLibrary, Song, Tag}
|
||||
};
|
||||
// use crate::{
|
||||
// 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 {
|
||||
pub fn listenbrainz_authenticate(&mut self) -> Result<ListenBrainz, Box<dyn Error>> {
|
||||
let config = &self.config.read().unwrap();
|
||||
let mut client = ListenBrainz::new();
|
||||
// impl Controller {
|
||||
// pub fn listenbrainz_authenticate(&mut self) -> Result<ListenBrainz, Box<dyn Error>> {
|
||||
// let config = &self.config.read().unwrap();
|
||||
// let mut client = ListenBrainz::new();
|
||||
|
||||
let lbz_token = match &config.connections.listenbrainz_token {
|
||||
Some(token) => token,
|
||||
None => todo!("No ListenBrainz token in config")
|
||||
};
|
||||
// let lbz_token = match &config.connections.listenbrainz_token {
|
||||
// Some(token) => token,
|
||||
// None => todo!("No ListenBrainz token in config")
|
||||
// };
|
||||
|
||||
if !client.is_authenticated() {
|
||||
client.authenticate(lbz_token)?;
|
||||
}
|
||||
// if !client.is_authenticated() {
|
||||
// client.authenticate(lbz_token)?;
|
||||
// }
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> {
|
||||
let config = &self.config.read().unwrap();
|
||||
// Ok(client)
|
||||
// }
|
||||
// pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> {
|
||||
// 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());
|
||||
// &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(())
|
||||
}
|
||||
// client.listen(artist, track, release)?;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> {
|
||||
let config = &self.config.read().unwrap();
|
||||
// pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box<dyn Error>> {
|
||||
// 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());
|
||||
// &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(())
|
||||
}
|
||||
}
|
||||
// client.listen(artist, track, release)?;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_super {
|
||||
use std::{thread::sleep, time::Duration};
|
||||
// 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()
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
use super::*;
|
||||
use crate::config::config::tests::read_config_lib;
|
||||
// #[cfg(test)]
|
||||
// mod test_super {
|
||||
// use std::{thread::sleep, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn listenbrainz() {
|
||||
let mut c = Controller::start(".\\test-config\\config_test.json").unwrap();
|
||||
// use super::*;
|
||||
// use crate::config::config::tests::read_config_lib;
|
||||
|
||||
let client = c.listenbrainz_authenticate().unwrap();
|
||||
// #[test]
|
||||
// fn listenbrainz() {
|
||||
// let mut c = Controller::start(".\\test-config\\config_test.json").unwrap();
|
||||
|
||||
c.q_new().unwrap();
|
||||
c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap();
|
||||
// let client = c.listenbrainz_authenticate().unwrap();
|
||||
|
||||
let songs = c.lib_get_songs();
|
||||
// c.q_new().unwrap();
|
||||
// c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap();
|
||||
|
||||
c.q_enqueue(0, songs[1].location.to_owned()).unwrap();
|
||||
c.q_play(0).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();
|
||||
}
|
||||
}
|
||||
// sleep(Duration::from_secs(100));
|
||||
// c.lbz_scrobble(client, songs[1].uuid).unwrap();
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -2,89 +2,37 @@
|
|||
//! player. It manages queues, playback, library access, and
|
||||
//! other functions
|
||||
|
||||
use crossbeam_channel;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use listenbrainz::ListenBrainz;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use crossbeam_channel::{Sender, Receiver};
|
||||
use crossbeam_channel;
|
||||
use listenbrainz::ListenBrainz;
|
||||
use std::thread::spawn;
|
||||
|
||||
use std::error::Error;
|
||||
use crossbeam_channel::unbounded;
|
||||
use std::error::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::music_controller::queue::{QueueItem, QueueItemType};
|
||||
use crate::music_controller::queue::QueueItem;
|
||||
use crate::music_player::gstreamer::GStreamer;
|
||||
use crate::music_storage::library::{Tag, URI};
|
||||
use crate::{
|
||||
music_storage::library::{MusicLibrary, Song},
|
||||
config::config::Config,
|
||||
music_controller::queue::Queue,
|
||||
music_storage::library::{MusicLibrary, Song},
|
||||
};
|
||||
|
||||
pub struct Controller {
|
||||
// queues: Vec<Queue>,
|
||||
pub queue: Queue,
|
||||
pub config: Arc<RwLock<Config>>,
|
||||
// library: MusicLibrary,
|
||||
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),
|
||||
pub library: MusicLibrary,
|
||||
player_mail: MailMan<PlayerCmd, PlayerRes>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct MailMan<T: Send, U: Send> {
|
||||
pub tx: Sender<T>,
|
||||
rx: Receiver<U>
|
||||
rx: Receiver<U>,
|
||||
}
|
||||
|
||||
impl<T: Send> MailMan<T, T> {
|
||||
|
@ -98,10 +46,7 @@ impl<T: Send, U: Send> MailMan<T, U> {
|
|||
let (tx, rx) = unbounded::<T>();
|
||||
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>> {
|
||||
|
@ -115,192 +60,61 @@ impl<T: Send, U: Send> MailMan<T, U> {
|
|||
}
|
||||
}
|
||||
|
||||
enum PlayerCmd {
|
||||
Test(URI),
|
||||
}
|
||||
|
||||
enum PlayerRes {
|
||||
Test,
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl Controller {
|
||||
pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>>
|
||||
where std::path::PathBuf: std::convert::From<P>
|
||||
where
|
||||
std::path::PathBuf: std::convert::From<P>,
|
||||
{
|
||||
let config_path = PathBuf::from(config_path);
|
||||
|
||||
let config = Config::read_file(config_path)?;
|
||||
let uuid = config.libraries.get_default()?.uuid;
|
||||
|
||||
let config_ = Arc::new(RwLock::from(config));
|
||||
let mut lib = MusicLibrary::init(config_.clone(), uuid)?;
|
||||
let library = MusicLibrary::init(config_.clone(), uuid)?;
|
||||
|
||||
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();
|
||||
let (player_mail, in_thread) = MailMan::<PlayerCmd, PlayerRes>::double();
|
||||
|
||||
match command {
|
||||
Default => (),
|
||||
Test => {
|
||||
in_thread.send(ControllerResponse::Empty).unwrap();
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
spawn(move || {
|
||||
let mut player = GStreamer::new().unwrap();
|
||||
|
||||
let config = config_.clone();
|
||||
let (out_thread_db, in_thread) = MailMan::double();
|
||||
let db_monitor = spawn(move || {
|
||||
use DatabaseCmd::*;
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
while true {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.queue_mail.push(out_thread_queue);
|
||||
Ok(self.queue_mail.len() - 1)
|
||||
|
||||
Ok(Controller {
|
||||
queue: Queue::new(),
|
||||
config: config_.clone(),
|
||||
library,
|
||||
player_mail,
|
||||
})
|
||||
}
|
||||
|
||||
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_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)
|
||||
}
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
mod test_super {
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use super::Controller;
|
||||
|
@ -309,33 +123,18 @@ mod tests {
|
|||
fn play_test() {
|
||||
let mut a = match Controller::start("test-config/config_test.json".to_string()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => panic!("{e}")
|
||||
Err(e) => panic!("{e}"),
|
||||
};
|
||||
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]
|
||||
fn test_() {
|
||||
let a = match Controller::start("test-config/config_test.json".to_string()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => panic!("{e}")
|
||||
};
|
||||
a.lib_scan_folder("F:/Music/Mp3".to_string());
|
||||
a.lib_save();
|
||||
let c = Controller::start(
|
||||
"F:\\Dangoware\\Dango Music Player\\dmp-core\\test-config\\config_test.json",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
sleep(Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
use uuid::Uuid;
|
||||
use crate::{
|
||||
music_player::{Player, PlayerError},
|
||||
music_storage::library::{Album, MusicLibrary, URI}
|
||||
};
|
||||
use crate::music_storage::library::{MusicLibrary, Song, URI};
|
||||
use std::{
|
||||
error::Error,
|
||||
sync::{Arc, RwLock}
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -15,8 +12,7 @@ pub enum QueueError {
|
|||
#[error("Index out of bounds! Index {0} is over len {1}")]
|
||||
OutOfBounds(usize, usize),
|
||||
#[error("The Queue is empty!")]
|
||||
EmptyQueue
|
||||
|
||||
EmptyQueue,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
|
@ -27,54 +23,6 @@ pub enum QueueState {
|
|||
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
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
|
@ -83,92 +31,102 @@ pub enum PlayerLocation {
|
|||
Library,
|
||||
Playlist(Uuid),
|
||||
File,
|
||||
Custom
|
||||
Custom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct QueueItem<'a> {
|
||||
pub(super) item: QueueItemType<'a>,
|
||||
pub struct QueueItem {
|
||||
pub(super) item: Song,
|
||||
pub(super) state: QueueState,
|
||||
pub(super) source: PlayerLocation,
|
||||
pub(super) by_human: bool
|
||||
pub(super) by_human: bool,
|
||||
}
|
||||
impl QueueItem<'_> {
|
||||
fn new() -> Self {
|
||||
impl From<Song> for QueueItem {
|
||||
fn from(song: Song) -> Self {
|
||||
QueueItem {
|
||||
item: QueueItemType::None,
|
||||
item: song,
|
||||
state: QueueState::NoState,
|
||||
source: PlayerLocation::Library,
|
||||
by_human: false
|
||||
by_human: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Queue<'a> {
|
||||
pub player: Player,
|
||||
pub name: String,
|
||||
pub items: Vec<QueueItem<'a>>,
|
||||
pub played: Vec<QueueItem<'a>>,
|
||||
pub loop_: bool
|
||||
pub struct Queue {
|
||||
pub items: Vec<QueueItem>,
|
||||
pub played: Vec<QueueItem>,
|
||||
pub loop_: bool,
|
||||
pub shuffle: bool,
|
||||
}
|
||||
|
||||
impl<'a> Queue<'a> {
|
||||
impl Queue {
|
||||
fn has_addhere(&self) -> bool {
|
||||
for item in &self.items {
|
||||
if item.state == QueueState::AddHere {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn dbg_items(&self) {
|
||||
dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<QueueItemType>>(), self.items.len());
|
||||
dbg!(
|
||||
self.items
|
||||
.iter()
|
||||
.map(|item| item.item.clone())
|
||||
.collect::<Vec<Song>>(),
|
||||
self.items.len()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, PlayerError> {
|
||||
Ok(
|
||||
Queue {
|
||||
player: Player::new()?,
|
||||
name: String::new(),
|
||||
pub fn new() -> Self {
|
||||
//TODO: Make the queue take settings from config/state if applicable
|
||||
Queue {
|
||||
items: Vec::new(),
|
||||
played: Vec::new(),
|
||||
loop_: false,
|
||||
}
|
||||
)
|
||||
shuffle: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_items(&mut self, tracks: Vec<QueueItem<'a>>) {
|
||||
pub fn set_items(&mut self, tracks: Vec<QueueItem>) {
|
||||
let mut tracks = tracks;
|
||||
self.items.clear();
|
||||
self.items.append(&mut tracks);
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, item: QueueItemType<'a>, source: PlayerLocation, by_human: bool) {
|
||||
pub fn add_item(&mut self, item: Song, source: PlayerLocation, by_human: bool) {
|
||||
let mut i: usize = 0;
|
||||
|
||||
self.items = self.items.iter().enumerate().map(|(j, item_)| {
|
||||
let mut item_ = item_.to_owned();
|
||||
// get the index of the current AddHere item and give it to i
|
||||
if item_.state == QueueState::AddHere {
|
||||
i = j;
|
||||
item_.state = QueueState::NoState;
|
||||
}
|
||||
item_
|
||||
}).collect::<Vec<QueueItem>>();
|
||||
self.items = self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, item_)| {
|
||||
let mut item_ = item_.to_owned();
|
||||
// get the index of the current AddHere item and give it to i
|
||||
if item_.state == QueueState::AddHere {
|
||||
i = j;
|
||||
item_.state = QueueState::NoState;
|
||||
}
|
||||
item_
|
||||
})
|
||||
.collect::<Vec<QueueItem>>();
|
||||
|
||||
self.items.insert(i + if self.items.is_empty() { 0 } else { 1 }, QueueItem {
|
||||
item,
|
||||
state: QueueState::AddHere,
|
||||
source,
|
||||
by_human
|
||||
});
|
||||
self.items.insert(
|
||||
i + if self.items.is_empty() { 0 } else { 1 },
|
||||
QueueItem {
|
||||
item,
|
||||
state: QueueState::AddHere,
|
||||
source,
|
||||
by_human,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_item_next(&mut self, item: QueueItemType<'a>, source: PlayerLocation) {
|
||||
pub fn add_item_next(&mut self, item: Song, source: PlayerLocation) {
|
||||
use QueueState::*;
|
||||
let empty = self.items.is_empty();
|
||||
|
||||
|
@ -176,19 +134,23 @@ impl<'a> Queue<'a> {
|
|||
(if empty { 0 } else { 1 }),
|
||||
QueueItem {
|
||||
item,
|
||||
state: if (self.items.get(1).is_none() || (!self.has_addhere() && self.items.get(1).is_some()) || empty) { AddHere } else { NoState },
|
||||
state: if (self.items.get(1).is_none()
|
||||
|| !self.has_addhere() && self.items.get(1).is_some())
|
||||
|| empty
|
||||
{
|
||||
AddHere
|
||||
} else {
|
||||
NoState
|
||||
},
|
||||
source,
|
||||
by_human: true
|
||||
}
|
||||
by_human: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn add_multi(&mut self, items: Vec<QueueItemType>, source: PlayerLocation, by_human: bool) {
|
||||
|
||||
}
|
||||
pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) {}
|
||||
|
||||
pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> {
|
||||
|
||||
// dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]);
|
||||
|
||||
if remove_index < self.items.len() {
|
||||
|
@ -199,7 +161,7 @@ impl<'a> Queue<'a> {
|
|||
self.items[remove_index].state = QueueState::NoState;
|
||||
self.items.remove(remove_index);
|
||||
Ok(())
|
||||
}else {
|
||||
} else {
|
||||
Err(QueueError::EmptyQueue)
|
||||
}
|
||||
}
|
||||
|
@ -214,11 +176,11 @@ impl<'a> Queue<'a> {
|
|||
|
||||
if !empty && index < self.items.len() {
|
||||
let i = self.items[index].clone();
|
||||
self.items.retain(|item| *item == i );
|
||||
self.items.retain(|item| *item == i);
|
||||
self.items[0].state = AddHere;
|
||||
}else if empty {
|
||||
} else if empty {
|
||||
return Err("Queue is empty!".into());
|
||||
}else {
|
||||
} else {
|
||||
return Err("index out of bounds!".into());
|
||||
}
|
||||
Ok(())
|
||||
|
@ -233,21 +195,19 @@ impl<'a> Queue<'a> {
|
|||
self.played.clear();
|
||||
}
|
||||
|
||||
|
||||
// TODO: uh, fix this?
|
||||
fn move_to(&mut self, index: usize) -> Result<(), QueueError> {
|
||||
use QueueState::*;
|
||||
|
||||
let empty = self.items.is_empty();
|
||||
let nothing_error = Err(QueueError::EmptyQueue);
|
||||
let index = if !empty { index } else { return nothing_error; };
|
||||
|
||||
let index = if !empty {
|
||||
index
|
||||
} else {
|
||||
return Err(QueueError::EmptyQueue);
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
loop {
|
||||
|
@ -259,17 +219,19 @@ impl<'a> Queue<'a> {
|
|||
self.items[1].state = AddHere;
|
||||
}
|
||||
if let Err(e) = self.remove_item(0) {
|
||||
dbg!(&e); self.dbg_items(); return Err(e);
|
||||
dbg!(&e);
|
||||
self.dbg_items();
|
||||
return Err(e);
|
||||
}
|
||||
// dbg!(&to_item.item, &self.items[ind].item);
|
||||
}else if empty {
|
||||
return nothing_error;
|
||||
}else {
|
||||
} else if empty {
|
||||
return Err(QueueError::EmptyQueue);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}else {
|
||||
return Err(QueueError::EmptyQueue.into());
|
||||
} else {
|
||||
return Err(QueueError::EmptyQueue);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -287,132 +249,30 @@ impl<'a> Queue<'a> {
|
|||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self, lib: Arc<RwLock<MusicLibrary>>) -> Result<OutQueue, Box<dyn Error>> {
|
||||
|
||||
|
||||
|
||||
pub fn next(&mut self) -> Result<&QueueItem, Box<dyn Error>> {
|
||||
if self.items.is_empty() {
|
||||
if self.loop_ {
|
||||
return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue
|
||||
}else {
|
||||
} else {
|
||||
return Err(QueueError::EmptyQueue.into());
|
||||
}
|
||||
}
|
||||
// TODO: add an algorithm to detect if the song should be skipped
|
||||
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() {
|
||||
self.items[1].state = QueueState::AddHere;
|
||||
}
|
||||
self.played.push(item);
|
||||
self.items.remove(0);
|
||||
|
||||
Ok(todo!())
|
||||
Ok(&self.items[1])
|
||||
}
|
||||
|
||||
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) {
|
||||
while self.played.len() > 50 {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -15,8 +15,10 @@ use gstreamer::prelude::*;
|
|||
use chrono::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::player::{Player, PlayerError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PlayerCmd {
|
||||
pub enum GstCmd {
|
||||
Play,
|
||||
Pause,
|
||||
Eos,
|
||||
|
@ -24,7 +26,7 @@ pub enum PlayerCmd {
|
|||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum PlayerState {
|
||||
pub enum GstState {
|
||||
Playing,
|
||||
Paused,
|
||||
Ready,
|
||||
|
@ -33,7 +35,7 @@ pub enum PlayerState {
|
|||
VoidPending,
|
||||
}
|
||||
|
||||
impl From<gst::State> for PlayerState {
|
||||
impl From<gst::State> for GstState {
|
||||
fn from(value: gst::State) -> Self {
|
||||
match value {
|
||||
gst::State::VoidPending => Self::VoidPending,
|
||||
|
@ -45,7 +47,7 @@ impl From<gst::State> for PlayerState {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryInto<gst::State> for PlayerState {
|
||||
impl TryInto<gst::State> for GstState {
|
||||
fn try_into(self) -> Result<gst::State, Box<dyn Error>> {
|
||||
match self {
|
||||
Self::VoidPending => Ok(gst::State::VoidPending),
|
||||
|
@ -60,24 +62,6 @@ impl TryInto<gst::State> for PlayerState {
|
|||
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)]
|
||||
enum PlaybackStats {
|
||||
Idle,
|
||||
|
@ -91,10 +75,10 @@ enum PlaybackStats {
|
|||
|
||||
/// An instance of a music player with a GStreamer backend
|
||||
#[derive(Debug)]
|
||||
pub struct Player {
|
||||
pub struct GStreamer {
|
||||
source: Option<URI>,
|
||||
//pub message_tx: Sender<PlayerCmd>,
|
||||
pub message_rx: crossbeam::channel::Receiver<PlayerCmd>,
|
||||
pub message_rx: crossbeam::channel::Receiver<GstCmd>,
|
||||
|
||||
playback_tx: crossbeam::channel::Sender<PlaybackStats>,
|
||||
playbin: Arc<RwLock<Element>>,
|
||||
|
@ -105,7 +89,7 @@ pub struct Player {
|
|||
position: Arc<RwLock<Option<Duration>>>,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
impl GStreamer {
|
||||
pub fn new() -> Result<Self, PlayerError> {
|
||||
// Initialize GStreamer, maybe figure out how to nicely fail here
|
||||
gst::init()?;
|
||||
|
@ -162,14 +146,14 @@ impl Player {
|
|||
// Check if the current playback position is close to the end
|
||||
let finish_point = end - Duration::milliseconds(250);
|
||||
if pos_temp.unwrap() >= end {
|
||||
let _ = playback_tx.try_send(PlayerCmd::Eos);
|
||||
let _ = playback_tx.try_send(GstCmd::Eos);
|
||||
playbin_arc
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_state(gst::State::Ready)
|
||||
.expect("Unable to set the pipeline state");
|
||||
} else if pos_temp.unwrap() >= finish_point {
|
||||
let _ = playback_tx.try_send(PlayerCmd::AboutToFinish);
|
||||
let _ = playback_tx.try_send(GstCmd::AboutToFinish);
|
||||
}
|
||||
|
||||
// This has to be done AFTER the current time in the file
|
||||
|
@ -468,7 +452,7 @@ impl Player {
|
|||
}
|
||||
|
||||
/// Get the current state of the playback
|
||||
pub fn state(&mut self) -> PlayerState {
|
||||
pub fn state(&mut self) -> GstState {
|
||||
self.playbin().unwrap().current_state().into()
|
||||
/*
|
||||
match *self.buffer.read().unwrap() {
|
||||
|
@ -498,7 +482,9 @@ impl Player {
|
|||
}
|
||||
}
|
||||
|
||||
impl Drop for Player {
|
||||
// impl Player for GStreamer {}
|
||||
|
||||
impl Drop for GStreamer {
|
||||
/// Cleans up the `GStreamer` pipeline and the monitoring
|
||||
/// thread when [Player] is dropped.
|
||||
fn drop(&mut self) {
|
0
src/music_player/kira.rs
Normal file
0
src/music_player/kira.rs
Normal file
57
src/music_player/player.rs
Normal file
57
src/music_player/player.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
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>;
|
||||
|
||||
}
|
|
@ -176,14 +176,15 @@ pub struct FoobarPlaylistTrack {
|
|||
impl FoobarPlaylistTrack {
|
||||
fn find_song(&self) -> Song {
|
||||
let location = URI::Local(self.file_name.clone().into());
|
||||
let internal_tags = Vec::new();
|
||||
|
||||
Song {
|
||||
location,
|
||||
location: vec![location],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: 0,
|
||||
skips: 0,
|
||||
favorited: false,
|
||||
// banned: None,
|
||||
banned: None,
|
||||
rating: None,
|
||||
format: None,
|
||||
duration: self.duration,
|
||||
|
@ -193,6 +194,7 @@ impl FoobarPlaylistTrack {
|
|||
date_modified: None,
|
||||
album_art: Vec::new(),
|
||||
tags: BTreeMap::new(),
|
||||
internal_tags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use std::vec::Vec;
|
|||
use chrono::prelude::*;
|
||||
|
||||
use crate::music_storage::db_reader::extern_library::ExternalLibrary;
|
||||
use crate::music_storage::library::{AlbumArt, Service, Song, Tag, URI};
|
||||
use crate::music_storage::library::{AlbumArt, BannedType, Service, Song, Tag, URI};
|
||||
use crate::music_storage::utils;
|
||||
|
||||
use urlencoding::decode;
|
||||
|
@ -166,17 +166,19 @@ impl ExternalLibrary for ITunesLibrary {
|
|||
};
|
||||
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,
|
||||
location: vec![location],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: track.plays,
|
||||
skips: 0,
|
||||
favorited: track.favorited,
|
||||
// banned: if track.banned {
|
||||
// Some(BannedType::All)
|
||||
// }else {
|
||||
// None
|
||||
// },
|
||||
banned: if track.banned {
|
||||
Some(BannedType::All)
|
||||
}else {
|
||||
None
|
||||
},
|
||||
rating: track.rating,
|
||||
format: match FileFormat::from_file(PathBuf::from(&loc)) {
|
||||
Ok(e) => Some(e),
|
||||
|
@ -192,6 +194,7 @@ impl ExternalLibrary for ITunesLibrary {
|
|||
Err(_) => Vec::new(),
|
||||
},
|
||||
tags: tags_,
|
||||
internal_tags,
|
||||
};
|
||||
// dbg!(&ny.tags);
|
||||
bun.push(ny);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use super::playlist::PlaylistFolder;
|
||||
// Crate things
|
||||
use super::utils::{find_images, normalize, read_file, write_file};
|
||||
use crate::config::config::Config;
|
||||
|
@ -8,16 +9,15 @@ use std::error::Error;
|
|||
use std::io::Write;
|
||||
use std::ops::ControlFlow::{Break, Continue};
|
||||
|
||||
|
||||
// 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 uuid::Uuid;
|
||||
use std::fs::{self, File};
|
||||
use tempfile::TempDir;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
// Time
|
||||
|
@ -117,35 +117,58 @@ 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)]
|
||||
#[non_exhaustive]
|
||||
pub enum BannedType {
|
||||
Shuffle,
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[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, Clone, Deserialize, Serialize, PartialEq)]
|
||||
enum SongType {
|
||||
// TODO: add song types
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum SongType {
|
||||
// TODO: add MORE?! song types
|
||||
Main,
|
||||
Instrumental,
|
||||
Remix,
|
||||
Custom(String)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl Default for SongType {
|
||||
fn default() -> Self {
|
||||
SongType::Main
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores information about a single song
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct Song {
|
||||
pub location: URI,
|
||||
pub location: Vec<URI>,
|
||||
pub uuid: Uuid,
|
||||
pub plays: i32,
|
||||
pub skips: i32,
|
||||
pub favorited: bool,
|
||||
// pub banned: Option<BannedType>,
|
||||
pub banned: Option<BannedType>,
|
||||
pub rating: Option<u8>,
|
||||
pub format: Option<FileFormat>,
|
||||
pub duration: Duration,
|
||||
|
@ -158,9 +181,9 @@ pub struct Song {
|
|||
pub date_modified: Option<DateTime<Utc>>,
|
||||
pub album_art: Vec<AlbumArt>,
|
||||
pub tags: BTreeMap<Tag, String>,
|
||||
pub internal_tags: Vec<InternalTag>,
|
||||
}
|
||||
|
||||
|
||||
impl Song {
|
||||
/// Get a tag's value
|
||||
///
|
||||
|
@ -180,7 +203,7 @@ impl Song {
|
|||
pub fn get_field(&self, target_field: &str) -> Option<Field> {
|
||||
let lower_target = target_field.to_lowercase();
|
||||
match lower_target.as_str() {
|
||||
"location" => Some(Field::Location(self.location.clone())),
|
||||
"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)),
|
||||
|
@ -279,13 +302,15 @@ impl Song {
|
|||
// 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: URI::Local(binding),
|
||||
location: vec![URI::Local(binding)],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: 0,
|
||||
skips: 0,
|
||||
favorited: false,
|
||||
// banned: None,
|
||||
banned: None,
|
||||
rating: None,
|
||||
format,
|
||||
duration,
|
||||
|
@ -295,6 +320,7 @@ impl Song {
|
|||
date_modified: Some(chrono::offset::Utc::now()),
|
||||
tags,
|
||||
album_art,
|
||||
internal_tags,
|
||||
};
|
||||
Ok(new_song)
|
||||
}
|
||||
|
@ -314,7 +340,6 @@ impl Song {
|
|||
for file in cue_data.files.iter() {
|
||||
let audio_location = &parent_dir.join(file.file.clone());
|
||||
|
||||
|
||||
if !audio_location.exists() {
|
||||
continue;
|
||||
}
|
||||
|
@ -399,17 +424,17 @@ impl Song {
|
|||
let album_art = find_images(&audio_location.to_path_buf()).unwrap();
|
||||
|
||||
let new_song = Song {
|
||||
location: URI::Cue {
|
||||
location: vec![URI::Cue {
|
||||
location: audio_location.clone(),
|
||||
index: i,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
}],
|
||||
uuid: Uuid::new_v4(),
|
||||
plays: 0,
|
||||
skips: 0,
|
||||
favorited: false,
|
||||
// banned: None,
|
||||
banned: None,
|
||||
rating: None,
|
||||
format,
|
||||
duration,
|
||||
|
@ -419,78 +444,39 @@ impl Song {
|
|||
date_modified: Some(chrono::offset::Utc::now()),
|
||||
tags,
|
||||
album_art,
|
||||
internal_tags: Vec::new(),
|
||||
};
|
||||
tracks.push((new_song, audio_location.clone()));
|
||||
}
|
||||
}
|
||||
Ok(tracks)
|
||||
Ok(tracks)
|
||||
}
|
||||
|
||||
/// Takes the AlbumArt[index] and opens it in the native file viewer
|
||||
/// 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<Vec<&URI>>), Box<dyn Error>> {
|
||||
let mut invalid_uris = Vec::new();
|
||||
let mut valid_uri = None;
|
||||
|
||||
pub fn open_album_art(&self, index: usize, temp_dir: &TempDir) -> Result<(), Box<dyn Error>> {
|
||||
use opener::open;
|
||||
use urlencoding::decode;
|
||||
|
||||
if index >= self.album_art.len() {
|
||||
return Err("Index out of bounds".into());
|
||||
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()),
|
||||
}
|
||||
|
||||
let uri = match &self.album_art[index] {
|
||||
AlbumArt::External(uri) => {
|
||||
PathBuf::from(decode(match uri.as_uri().strip_prefix("file:///") {
|
||||
Some(e) => e,
|
||||
None => return Err("Invalid path?".into())
|
||||
})?.to_owned().to_string())
|
||||
},
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -560,7 +546,7 @@ impl URI {
|
|||
pub fn as_path(&self) -> Result<&PathBuf, Box<dyn Error>> {
|
||||
if let Self::Local(path) = self {
|
||||
Ok(path)
|
||||
}else {
|
||||
} else {
|
||||
Err("This URI is not local!".into())
|
||||
}
|
||||
}
|
||||
|
@ -568,7 +554,7 @@ impl URI {
|
|||
pub fn exists(&self) -> Result<bool, std::io::Error> {
|
||||
match self {
|
||||
URI::Local(loc) => loc.try_exists(),
|
||||
URI::Cue {location, ..} => location.try_exists(),
|
||||
URI::Cue { location, .. } => location.try_exists(),
|
||||
URI::Remote(_, _loc) => todo!(),
|
||||
}
|
||||
}
|
||||
|
@ -652,14 +638,8 @@ pub struct MusicLibrary {
|
|||
pub name: String,
|
||||
pub uuid: Uuid,
|
||||
pub library: Vec<Song>,
|
||||
}
|
||||
|
||||
#[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);
|
||||
pub playlists: PlaylistFolder,
|
||||
pub backup_songs: Vec<Song>, // maybe move this to the config instead?
|
||||
}
|
||||
|
||||
impl MusicLibrary {
|
||||
|
@ -669,6 +649,8 @@ impl MusicLibrary {
|
|||
name,
|
||||
uuid,
|
||||
library: Vec::new(),
|
||||
playlists: PlaylistFolder::new(),
|
||||
backup_songs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -737,8 +719,11 @@ impl MusicLibrary {
|
|||
.par_iter()
|
||||
.enumerate()
|
||||
.try_for_each(|(i, track)| {
|
||||
if path == &track.location {
|
||||
return std::ops::ControlFlow::Break((track, i));
|
||||
for location in &track.location {
|
||||
//TODO: check that this works
|
||||
if path == location {
|
||||
return std::ops::ControlFlow::Break((track, i));
|
||||
}
|
||||
}
|
||||
Continue(())
|
||||
});
|
||||
|
@ -774,7 +759,8 @@ impl MusicLibrary {
|
|||
fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> {
|
||||
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
self.library.par_iter().for_each(|track| {
|
||||
if path == track.location.path() {
|
||||
if path == track.primary_uri().unwrap().0.path() {
|
||||
//TODO: make this also not unwrap
|
||||
result.clone().lock().unwrap().push(track);
|
||||
}
|
||||
});
|
||||
|
@ -786,10 +772,7 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
/// 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 errors = 0;
|
||||
for target_file in WalkDir::new(target_path)
|
||||
|
@ -852,7 +835,6 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let new_song = Song::from_file(target_file)?;
|
||||
match self.add_song(new_song) {
|
||||
Ok(_) => (),
|
||||
|
@ -868,14 +850,13 @@ impl MusicLibrary {
|
|||
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(_) => {},
|
||||
Ok(_) => {}
|
||||
Err(_error) => {
|
||||
//println!("{}", _error);
|
||||
continue;
|
||||
|
@ -886,13 +867,14 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
pub fn add_song(&mut self, new_song: Song) -> Result<(), Box<dyn Error>> {
|
||||
if self.query_uri(&new_song.location).is_some() {
|
||||
return Err(format!("URI already in database: {:?}", new_song.location).into());
|
||||
let location = new_song.primary_uri()?.0;
|
||||
if self.query_uri(location).is_some() {
|
||||
return Err(format!("URI already in database: {:?}", location).into());
|
||||
}
|
||||
|
||||
match new_song.location {
|
||||
URI::Local(_) if self.query_path(new_song.location.path()).is_some() => {
|
||||
return Err(format!("Location exists for {:?}", new_song.location).into())
|
||||
match location {
|
||||
URI::Local(_) if self.query_path(location.path()).is_some() => {
|
||||
return Err(format!("Location exists for {:?}", location).into())
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -915,17 +897,18 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
/// 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<Tag>,
|
||||
) -> 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,
|
||||
None => return Err("URI not in database!".to_string().into()),
|
||||
};
|
||||
|
||||
println!("{:?}", target_song.0.location);
|
||||
println!("{:?}", target_song.location);
|
||||
|
||||
for tag in new_tags {
|
||||
println!("{:?}", tag);
|
||||
|
@ -1143,12 +1126,18 @@ impl MusicLibrary {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{path::Path, thread::sleep, time::{Duration, Instant}};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, RwLock},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::Song;
|
||||
use crate::{config::config::Config, music_storage::library::MusicLibrary};
|
||||
|
||||
use super::Song;
|
||||
|
||||
#[test]
|
||||
fn get_art_test() {
|
||||
|
@ -1158,8 +1147,16 @@ mod test {
|
|||
let now = Instant::now();
|
||||
_ = s.open_album_art(0, 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));
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,52 @@
|
|||
use std::{fs::File, io::Read, path:: PathBuf, sync::{Arc, RwLock}};
|
||||
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 super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
||||
|
||||
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<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 {
|
||||
uuid: Uuid,
|
||||
title: String,
|
||||
|
@ -75,11 +106,16 @@ impl Playlist {
|
|||
// }
|
||||
// None
|
||||
// }
|
||||
pub fn contains_value(&self, tag: &Tag, value: &String, lib: Arc<RwLock<MusicLibrary>>) -> bool {
|
||||
pub fn contains_value(
|
||||
&self,
|
||||
tag: &Tag,
|
||||
value: &String,
|
||||
lib: Arc<RwLock<MusicLibrary>>,
|
||||
) -> bool {
|
||||
let lib = lib.read().unwrap();
|
||||
let items = match lib.query_tracks(value, &vec![tag.to_owned()], &vec![tag.to_owned()]) {
|
||||
Some(e) => e,
|
||||
None => return false
|
||||
None => return false,
|
||||
};
|
||||
|
||||
for item in items {
|
||||
|
@ -102,23 +138,35 @@ impl Playlist {
|
|||
super::utils::read_file(PathBuf::from(path))
|
||||
}
|
||||
|
||||
pub fn to_m3u8(&mut self, lib: Arc<RwLock<MusicLibrary>>, location: &str) -> Result<(), Box<dyn Error>> {
|
||||
pub fn to_m3u8(
|
||||
&mut self,
|
||||
lib: Arc<RwLock<MusicLibrary>>,
|
||||
location: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let lib = lib.read().unwrap();
|
||||
let seg = self.tracks
|
||||
let seg = self
|
||||
.tracks
|
||||
.iter()
|
||||
.filter_map( |uuid| {
|
||||
if let Some((track, _)) = lib.query_uuid(uuid) {
|
||||
if let URI::Local(_) = track.location {
|
||||
Some(MediaSegment {
|
||||
uri: track.location.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 }
|
||||
.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::<Vec<MediaSegment>>();
|
||||
|
||||
let m3u8 = MediaPlaylist {
|
||||
|
@ -142,7 +190,10 @@ impl Playlist {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn from_m3u8(path: &str, lib: Arc<RwLock<MusicLibrary>>) -> Result<Playlist, Box<dyn Error>> {
|
||||
pub fn from_m3u8(
|
||||
path: &str,
|
||||
lib: Arc<RwLock<MusicLibrary>>,
|
||||
) -> Result<Playlist, Box<dyn Error>> {
|
||||
let mut file = match File::open(path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => return Err(e.into()),
|
||||
|
@ -158,7 +209,9 @@ impl Playlist {
|
|||
};
|
||||
|
||||
match playlist {
|
||||
List2::MasterPlaylist(_) => Err("This is a Master Playlist!\nPlase input a Media Playlist".into()),
|
||||
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 {
|
||||
|
@ -167,7 +220,7 @@ impl Playlist {
|
|||
|
||||
let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) {
|
||||
song.uuid
|
||||
}else {
|
||||
} else {
|
||||
let song_ = Song::from_file(&path_)?;
|
||||
let uuid = song_.uuid.to_owned();
|
||||
lib.add_song(song_)?;
|
||||
|
@ -179,21 +232,23 @@ impl Playlist {
|
|||
|
||||
#[cfg(target_family = "windows")]
|
||||
{
|
||||
playlist.title = path.split("\\")
|
||||
.last()
|
||||
.unwrap_or_default()
|
||||
.strip_suffix(".m3u8")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
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.title = path
|
||||
.split("/")
|
||||
.last()
|
||||
.unwrap_or_default()
|
||||
.strip_suffix(".m3u8")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
playlist.set_tracks(uuids);
|
||||
|
@ -202,7 +257,6 @@ impl Playlist {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) {
|
||||
let lib = lib.read().unwrap();
|
||||
let mut songs = vec![];
|
||||
|
@ -211,7 +265,7 @@ impl Playlist {
|
|||
for uuid in &self.tracks {
|
||||
if let Some((track, _)) = lib.query_uuid(uuid) {
|
||||
songs.push(track.to_owned());
|
||||
}else {
|
||||
} else {
|
||||
invalid_uuids.push(uuid);
|
||||
}
|
||||
}
|
||||
|
@ -223,10 +277,12 @@ impl Playlist {
|
|||
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,
|
||||
},
|
||||
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,
|
||||
|
@ -266,7 +322,6 @@ impl Playlist {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
impl Default for Playlist {
|
||||
fn default() -> Self {
|
||||
Playlist {
|
||||
|
@ -276,7 +331,7 @@ impl Default for Playlist {
|
|||
tracks: Vec::default(),
|
||||
sort_order: SortOrder::Manual,
|
||||
play_count: 0,
|
||||
play_time: Duration::from_millis(0),
|
||||
play_time: Duration::from_secs(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -290,17 +345,20 @@ mod test_super {
|
|||
fn list_to_m3u8() {
|
||||
let (_, lib) = read_config_lib();
|
||||
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.to_m3u8(Arc::new(RwLock::from(lib)), ".\\test-config\\playlists\\playlist.m3u8");
|
||||
_ = 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();
|
||||
let playlist =
|
||||
Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap();
|
||||
|
||||
playlist.to_file(".\\test-config\\playlists\\playlist");
|
||||
dbg!(playlist)
|
||||
|
|
|
@ -25,12 +25,12 @@ pub(super) fn normalize(input_string: &str) -> String {
|
|||
|
||||
/// Write any data structure which implements [serde::Serialize]
|
||||
/// out to a [bincode] encoded file compressed using [snap]
|
||||
pub(super) fn write_file<T: serde::Serialize>(
|
||||
pub(super) fn write_file<T: serde::Serialize, U: std::convert::AsRef<Path>+std::convert::AsRef<std::ffi::OsStr>+Clone>(
|
||||
library: T,
|
||||
path: PathBuf,
|
||||
path: U,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Create a temporary name for writing out
|
||||
let mut writer_name = path.clone();
|
||||
let mut writer_name = PathBuf::from(&path);
|
||||
writer_name.set_extension("tmp");
|
||||
|
||||
// Create a new BufWriter on the file and a snap frame encoder
|
||||
|
|
Loading…
Reference in a new issue