mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-19 13:22:54 -05:00
Merge remote-tracking branch 'origin/controller'
This commit is contained in:
commit
a2cb8bd0b5
16 changed files with 1234 additions and 241 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -13,3 +13,5 @@ music_database*
|
||||||
*.m3u
|
*.m3u
|
||||||
*.m3u8
|
*.m3u8
|
||||||
*.json
|
*.json
|
||||||
|
*.zip
|
||||||
|
*.xml
|
||||||
|
|
16
Cargo.toml
16
Cargo.toml
|
@ -12,7 +12,15 @@ keywords = []
|
||||||
categories = []
|
categories = []
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
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"
|
||||||
|
@ -32,7 +40,11 @@ leb128 = "0.2.5"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
m3u8-rs = "5.0.5"
|
m3u8-rs = "5.0.5"
|
||||||
thiserror = "1.0.56"
|
thiserror = "1.0.56"
|
||||||
font = "0.27.0"
|
|
||||||
uuid = { version = "1.6.1", features = ["v4", "serde"] }
|
uuid = { version = "1.6.1", features = ["v4", "serde"] }
|
||||||
serde_json = "1.0.111"
|
serde_json = "1.0.111"
|
||||||
deunicode = "1.4.2"
|
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"
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
fs::{File, OpenOptions, self},
|
fs::{File, OpenOptions, self},
|
||||||
io::{Error, Write, Read}, sync::{Arc, RwLock},
|
io::{Error, Write, Read},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::to_string_pretty;
|
use serde_json::to_string_pretty;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::music_storage::library::{MusicLibrary, self};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ConfigLibrary {
|
pub struct ConfigLibrary {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -71,7 +69,9 @@ impl ConfigLibraries {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> {
|
pub fn get_library(&self, uuid: &Uuid) -> Result<ConfigLibrary, ConfigError> {
|
||||||
|
|
||||||
for library in &self.libraries {
|
for library in &self.libraries {
|
||||||
|
// dbg!(&library.uuid, &uuid);
|
||||||
if &library.uuid == uuid {
|
if &library.uuid == uuid {
|
||||||
return Ok(library.to_owned())
|
return Ok(library.to_owned())
|
||||||
}
|
}
|
||||||
|
@ -90,11 +90,18 @@ impl ConfigLibraries {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ConfigConnections {
|
||||||
|
pub listenbrainz_token: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub backup_folder: Option<PathBuf>,
|
pub backup_folder: Option<PathBuf>,
|
||||||
pub libraries: ConfigLibraries,
|
pub libraries: ConfigLibraries,
|
||||||
pub volume: f32,
|
pub volume: f32,
|
||||||
|
pub connections: ConfigConnections,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -124,12 +131,36 @@ impl Config {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_backup(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match &self.backup_folder {
|
||||||
|
Some(path) => {
|
||||||
|
let mut writer = path.clone();
|
||||||
|
writer.set_extension("tmp");
|
||||||
|
let mut file = OpenOptions::new().create(true).truncate(true).read(true).write(true).open(&writer)?;
|
||||||
|
let config = to_string_pretty(self)?;
|
||||||
|
// dbg!(&config);
|
||||||
|
|
||||||
|
file.write_all(config.as_bytes())?;
|
||||||
|
fs::rename(writer, self.path.as_path())?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
None => Err(ConfigError::NoBackupLibrary.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_file(path: PathBuf) -> Result<Self, Error> {
|
pub fn read_file(path: PathBuf) -> Result<Self, Error> {
|
||||||
let mut file: File = File::open(path)?;
|
let mut file: File = File::open(path)?;
|
||||||
let mut bun: String = String::new();
|
let mut bun: String = String::new();
|
||||||
_ = file.read_to_string(&mut bun);
|
_ = file.read_to_string(&mut bun);
|
||||||
let ny: Config = serde_json::from_str::<Config>(&bun)?;
|
let config: Config = serde_json::from_str::<Config>(&bun)?;
|
||||||
Ok(ny)
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_library(&mut self, lib: ConfigLibrary) {
|
||||||
|
if self.libraries.libraries.is_empty() {
|
||||||
|
self.libraries.default_library = lib.uuid;
|
||||||
|
}
|
||||||
|
self.libraries.libraries.push(lib);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,50 +173,56 @@ pub enum ConfigError {
|
||||||
//TODO: do something about playlists
|
//TODO: do something about playlists
|
||||||
#[error("Please provide a better m3u8 Playlist")]
|
#[error("Please provide a better m3u8 Playlist")]
|
||||||
BadPlaylist,
|
BadPlaylist,
|
||||||
|
#[error("No backup Config folder present")]
|
||||||
|
NoBackupLibrary,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn config_test() {
|
pub mod tests {
|
||||||
let lib_a = ConfigLibrary::new(PathBuf::from("test-config/library1"), String::from("library1"), None);
|
use std::{path::PathBuf, sync::{Arc, RwLock}};
|
||||||
let lib_b = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None);
|
use crate::music_storage::library::MusicLibrary;
|
||||||
let lib_c = ConfigLibrary::new(PathBuf::from("test-config/library3"), String::from("library3"), None);
|
use super::{Config, ConfigLibraries, ConfigLibrary};
|
||||||
let config = Config {
|
|
||||||
|
pub fn new_config_lib() -> (Config, MusicLibrary) {
|
||||||
|
let lib = ConfigLibrary::new(PathBuf::from("test-config/library"), String::from("library"), None);
|
||||||
|
let mut config = Config {
|
||||||
path: PathBuf::from("test-config/config_test.json"),
|
path: PathBuf::from("test-config/config_test.json"),
|
||||||
libraries: ConfigLibraries {
|
|
||||||
libraries: vec![
|
|
||||||
lib_a.clone(),
|
|
||||||
lib_b.clone(),
|
|
||||||
lib_c.clone(),
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
config.write_file();
|
|
||||||
let arc = Arc::new(RwLock::from(config));
|
|
||||||
MusicLibrary::init(arc.clone(), lib_a.uuid.clone()).unwrap();
|
|
||||||
MusicLibrary::init(arc.clone(), lib_b.uuid.clone()).unwrap();
|
|
||||||
MusicLibrary::init(arc.clone(), lib_c.uuid.clone()).unwrap();
|
|
||||||
|
|
||||||
}
|
config.push_library(lib);
|
||||||
|
config.write_file().unwrap();
|
||||||
|
|
||||||
#[test]
|
let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), dbg!(config.libraries.default_library)).unwrap();
|
||||||
fn test2() {
|
|
||||||
let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap();
|
|
||||||
let uuid = config.libraries.get_default().unwrap().uuid.clone();
|
|
||||||
let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap();
|
|
||||||
lib.scan_folder("test-config/music/").unwrap();
|
lib.scan_folder("test-config/music/").unwrap();
|
||||||
lib.save(config.clone()).unwrap();
|
lib.save(config.clone()).unwrap();
|
||||||
dbg!(&lib);
|
|
||||||
dbg!(&config);
|
(config, lib)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_config_lib() -> (Config, MusicLibrary) {
|
||||||
|
let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap();
|
||||||
|
|
||||||
|
// dbg!(&config);
|
||||||
|
|
||||||
|
let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config.libraries.get_default().unwrap().uuid).unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
lib.scan_folder("test-config/music/").unwrap();
|
||||||
|
|
||||||
|
lib.save(config.clone()).unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
(config, lib)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test3() {
|
fn test3() {
|
||||||
let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap();
|
let (config, lib) = read_config_lib();
|
||||||
let uuid = config.libraries.get_default().unwrap().uuid;
|
|
||||||
let mut lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap();
|
|
||||||
|
|
||||||
dbg!(lib);
|
_ = config.write_file();
|
||||||
|
|
||||||
|
dbg!(config);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
use std::{marker::PhantomData, fs::File, path::PathBuf};
|
|
||||||
|
|
||||||
use font::Font;
|
|
||||||
|
|
||||||
pub enum Setting {
|
pub enum Setting {
|
||||||
String {
|
String {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
|
@ -11,9 +11,13 @@ pub mod music_storage {
|
||||||
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 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;
|
||||||
|
|
|
@ -1,8 +1,103 @@
|
||||||
use std::{env, thread, time};
|
// use std::{
|
||||||
|
// sync::{Arc, RwLock},
|
||||||
|
// error::Error,
|
||||||
|
// };
|
||||||
|
|
||||||
use super::controller::Controller;
|
// 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 super::controller::DatabaseResponse;
|
||||||
|
|
||||||
|
|
||||||
impl Controller {
|
|
||||||
//more stuff goes here
|
// 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")
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid));
|
||||||
|
// let res = &self.db_mail.recv()?;
|
||||||
|
// let song = match res {
|
||||||
|
// DatabaseResponse::Song(song) => song,
|
||||||
|
// _ => todo!()
|
||||||
|
// };
|
||||||
|
// let unknown = &"unknown".to_string();
|
||||||
|
// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown);
|
||||||
|
// let track = song.get_tag(&Tag::Title).unwrap_or(unknown);
|
||||||
|
// let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str());
|
||||||
|
|
||||||
|
// client.listen(artist, track, release)?;
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn 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());
|
||||||
|
|
||||||
|
// client.listen(artist, track, release)?;
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn discord_song_change(client: &mut Client,song: Song) {
|
||||||
|
// client.set_activity(|a| {
|
||||||
|
// a.state(format!("Listening to {}", song.get_tag(&Tag::Title).unwrap()))
|
||||||
|
// .into()
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod test_super {
|
||||||
|
// use std::{thread::sleep, time::Duration};
|
||||||
|
|
||||||
|
// use super::*;
|
||||||
|
// use crate::config::config::tests::read_config_lib;
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn listenbrainz() {
|
||||||
|
// let mut c = Controller::start(".\\test-config\\config_test.json").unwrap();
|
||||||
|
|
||||||
|
// let client = c.listenbrainz_authenticate().unwrap();
|
||||||
|
|
||||||
|
// c.q_new().unwrap();
|
||||||
|
// c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap();
|
||||||
|
|
||||||
|
// let songs = c.lib_get_songs();
|
||||||
|
|
||||||
|
// c.q_enqueue(0, songs[1].location.to_owned()).unwrap();
|
||||||
|
// c.q_play(0).unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
// sleep(Duration::from_secs(100));
|
||||||
|
// c.lbz_scrobble(client, songs[1].uuid).unwrap();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -2,25 +2,139 @@
|
||||||
//! 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::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread::spawn;
|
||||||
|
|
||||||
|
use crossbeam_channel::unbounded;
|
||||||
|
use std::error::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::music_controller::queue::QueueItem;
|
||||||
|
use crate::music_player::gstreamer::GStreamer;
|
||||||
|
use crate::music_storage::library::{Tag, URI};
|
||||||
use crate::{
|
use crate::{
|
||||||
music_player::Player,
|
config::config::Config,
|
||||||
music_storage::library::Song,
|
music_controller::queue::Queue,
|
||||||
config::config::Config
|
music_storage::library::{MusicLibrary, Song},
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Queue {
|
|
||||||
player: Player,
|
|
||||||
name: String,
|
|
||||||
songs: Vec<Song>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
queues: Vec<Queue>,
|
pub queue: Queue,
|
||||||
config: Arc<RwLock<Config>>,
|
pub config: Arc<RwLock<Config>>,
|
||||||
|
pub library: MusicLibrary,
|
||||||
|
player_mail: MailMan<PlayerCmd, PlayerRes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Controller {
|
#[derive(Debug)]
|
||||||
// more stuff to come
|
pub(super) struct MailMan<T: Send, U: Send> {
|
||||||
|
pub tx: Sender<T>,
|
||||||
|
rx: Receiver<U>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send> MailMan<T, T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = unbounded::<T>();
|
||||||
|
MailMan { tx, rx }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Send, U: Send> MailMan<T, U> {
|
||||||
|
pub fn double() -> (MailMan<T, U>, MailMan<U, T>) {
|
||||||
|
let (tx, rx) = unbounded::<T>();
|
||||||
|
let (tx1, rx1) = unbounded::<U>();
|
||||||
|
|
||||||
|
(MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, mail: T) -> Result<(), Box<dyn Error>> {
|
||||||
|
self.tx.send(mail).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv(&self) -> Result<U, Box<dyn Error>> {
|
||||||
|
let u = self.rx.recv()?;
|
||||||
|
Ok(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>,
|
||||||
|
{
|
||||||
|
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 library = MusicLibrary::init(config_.clone(), uuid)?;
|
||||||
|
|
||||||
|
let (player_mail, in_thread) = MailMan::<PlayerCmd, PlayerRes>::double();
|
||||||
|
|
||||||
|
spawn(move || {
|
||||||
|
let mut player = GStreamer::new().unwrap();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Controller {
|
||||||
|
queue: Queue::new(),
|
||||||
|
config: config_.clone(),
|
||||||
|
library,
|
||||||
|
player_mail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_super {
|
||||||
|
use std::{thread::sleep, time::Duration};
|
||||||
|
|
||||||
|
use super::Controller;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn play_test() {
|
||||||
|
let mut a = match Controller::start("test-config/config_test.json".to_string()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => panic!("{e}"),
|
||||||
|
};
|
||||||
|
sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_() {
|
||||||
|
let c = Controller::start(
|
||||||
|
"F:\\Dangoware\\Dango Music Player\\dmp-core\\test-config\\config_test.json",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(60));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
278
src/music_controller/queue.rs
Normal file
278
src/music_controller/queue.rs
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
use crate::music_storage::library::{MusicLibrary, Song, URI};
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum QueueError {
|
||||||
|
#[error("Index out of bounds! Index {0} is over len {1}")]
|
||||||
|
OutOfBounds(usize, usize),
|
||||||
|
#[error("The Queue is empty!")]
|
||||||
|
EmptyQueue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum QueueState {
|
||||||
|
Played,
|
||||||
|
First,
|
||||||
|
AddHere,
|
||||||
|
NoState,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move this to a different location to be used elsewhere
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum PlayerLocation {
|
||||||
|
Test,
|
||||||
|
Library,
|
||||||
|
Playlist(Uuid),
|
||||||
|
File,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct QueueItem {
|
||||||
|
pub(super) item: Song,
|
||||||
|
pub(super) state: QueueState,
|
||||||
|
pub(super) source: PlayerLocation,
|
||||||
|
pub(super) by_human: bool,
|
||||||
|
}
|
||||||
|
impl From<Song> for QueueItem {
|
||||||
|
fn from(song: Song) -> Self {
|
||||||
|
QueueItem {
|
||||||
|
item: song,
|
||||||
|
state: QueueState::NoState,
|
||||||
|
source: PlayerLocation::Library,
|
||||||
|
by_human: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Queue {
|
||||||
|
pub items: Vec<QueueItem>,
|
||||||
|
pub played: Vec<QueueItem>,
|
||||||
|
pub loop_: bool,
|
||||||
|
pub shuffle: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Queue {
|
||||||
|
fn has_addhere(&self) -> bool {
|
||||||
|
for item in &self.items {
|
||||||
|
if item.state == QueueState::AddHere {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dbg_items(&self) {
|
||||||
|
dbg!(
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.item.clone())
|
||||||
|
.collect::<Vec<Song>>(),
|
||||||
|
self.items.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
let mut tracks = tracks;
|
||||||
|
self.items.clear();
|
||||||
|
self.items.append(&mut tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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: Song, source: PlayerLocation) {
|
||||||
|
use QueueState::*;
|
||||||
|
let empty = self.items.is_empty();
|
||||||
|
|
||||||
|
self.items.insert(
|
||||||
|
(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
|
||||||
|
},
|
||||||
|
source,
|
||||||
|
by_human: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
// update the state of the next item to replace the item being removed
|
||||||
|
if self.items.get(remove_index + 1).is_some() {
|
||||||
|
self.items[remove_index + 1].state = self.items[remove_index].state;
|
||||||
|
}
|
||||||
|
self.items[remove_index].state = QueueState::NoState;
|
||||||
|
self.items.remove(remove_index);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(QueueError::EmptyQueue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.items.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_except(&mut self, index: usize) -> Result<(), Box<dyn Error>> {
|
||||||
|
use QueueState::*;
|
||||||
|
let empty = self.items.is_empty();
|
||||||
|
|
||||||
|
if !empty && index < self.items.len() {
|
||||||
|
let i = self.items[index].clone();
|
||||||
|
self.items.retain(|item| *item == i);
|
||||||
|
self.items[0].state = AddHere;
|
||||||
|
} else if empty {
|
||||||
|
return Err("Queue is empty!".into());
|
||||||
|
} else {
|
||||||
|
return Err("index out of bounds!".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_played(&mut self) {
|
||||||
|
self.played.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_all(&mut self) {
|
||||||
|
self.items.clear();
|
||||||
|
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 index = if !empty {
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
return Err(QueueError::EmptyQueue);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !empty && index < self.items.len() {
|
||||||
|
let to_item = self.items[index].clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let empty = !self.items.is_empty();
|
||||||
|
let item = self.items[0].item.to_owned();
|
||||||
|
|
||||||
|
if item != to_item.item && !empty {
|
||||||
|
if self.items[0].state == AddHere && self.items.get(1).is_some() {
|
||||||
|
self.items[1].state = AddHere;
|
||||||
|
}
|
||||||
|
if let Err(e) = self.remove_item(0) {
|
||||||
|
dbg!(&e);
|
||||||
|
self.dbg_items();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
// dbg!(&to_item.item, &self.items[ind].item);
|
||||||
|
} else if empty {
|
||||||
|
return Err(QueueError::EmptyQueue);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(QueueError::EmptyQueue);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn swap(&mut self, a: usize, b: usize) {
|
||||||
|
self.items.swap(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_item(&mut self, a: usize, b: usize) {
|
||||||
|
let item = self.items[a].to_owned();
|
||||||
|
if a != b {
|
||||||
|
self.items.remove(a);
|
||||||
|
}
|
||||||
|
self.items.insert(b, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
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 {
|
||||||
|
return Err(QueueError::EmptyQueue.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: add an algorithm to detect if the song should be skipped
|
||||||
|
let item = self.items[0].clone();
|
||||||
|
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(&self.items[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev() {}
|
||||||
|
|
||||||
|
pub fn check_played(&mut self) {
|
||||||
|
while self.played.len() > 50 {
|
||||||
|
self.played.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,8 +15,10 @@ 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 PlayerCmd {
|
pub enum GstCmd {
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Eos,
|
Eos,
|
||||||
|
@ -24,7 +26,7 @@ pub enum PlayerCmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum PlayerState {
|
pub enum GstState {
|
||||||
Playing,
|
Playing,
|
||||||
Paused,
|
Paused,
|
||||||
Ready,
|
Ready,
|
||||||
|
@ -33,7 +35,7 @@ pub enum PlayerState {
|
||||||
VoidPending,
|
VoidPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<gst::State> for PlayerState {
|
impl From<gst::State> for GstState {
|
||||||
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,
|
||||||
|
@ -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>> {
|
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),
|
||||||
|
@ -60,22 +62,6 @@ impl TryInto<gst::State> for PlayerState {
|
||||||
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("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,
|
||||||
|
@ -88,10 +74,11 @@ enum PlaybackStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An instance of a music player with a GStreamer backend
|
/// An instance of a music player with a GStreamer backend
|
||||||
pub struct Player {
|
#[derive(Debug)]
|
||||||
|
pub struct GStreamer {
|
||||||
source: Option<URI>,
|
source: Option<URI>,
|
||||||
//pub message_tx: Sender<PlayerCmd>,
|
//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>,
|
playback_tx: crossbeam::channel::Sender<PlaybackStats>,
|
||||||
playbin: Arc<RwLock<Element>>,
|
playbin: Arc<RwLock<Element>>,
|
||||||
|
@ -102,7 +89,7 @@ pub struct Player {
|
||||||
position: Arc<RwLock<Option<Duration>>>,
|
position: Arc<RwLock<Option<Duration>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl GStreamer {
|
||||||
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()?;
|
||||||
|
@ -159,14 +146,14 @@ impl Player {
|
||||||
// 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(PlayerCmd::Eos);
|
let _ = playback_tx.try_send(GstCmd::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(PlayerCmd::AboutToFinish);
|
let _ = playback_tx.try_send(GstCmd::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
|
||||||
|
@ -177,7 +164,7 @@ impl Player {
|
||||||
*position_update.write().unwrap() = None;
|
*position_update.write().unwrap() = None;
|
||||||
break
|
break
|
||||||
},
|
},
|
||||||
PlaybackStats::Idle | PlaybackStats::Switching => println!("waiting!"),
|
PlaybackStats::Idle | PlaybackStats::Switching => {},
|
||||||
_ => ()
|
_ => ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,12 +244,17 @@ impl Player {
|
||||||
&self.source
|
&self.source
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enqueue_next(&mut self, next_track: &URI) {
|
pub fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> {
|
||||||
self.set_source(next_track);
|
self.set_source(next_track)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the playback URI
|
/// Set the playback URI
|
||||||
fn set_source(&mut self, source: &URI) {
|
fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> {
|
||||||
|
if !source.exists().is_ok_and(|x| x) {
|
||||||
|
// If the source doesn't exist, gstreamer will crash!
|
||||||
|
return Err(PlayerError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the playback tracker knows the stuff is stopped
|
// Make sure the playback tracker knows the stuff is stopped
|
||||||
self.playback_tx.send(PlaybackStats::Switching).unwrap();
|
self.playback_tx.send(PlaybackStats::Switching).unwrap();
|
||||||
|
|
||||||
|
@ -290,7 +282,7 @@ impl Player {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
while now.elapsed() < std::time::Duration::from_millis(20) {
|
while now.elapsed() < std::time::Duration::from_millis(20) {
|
||||||
if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() {
|
if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() {
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
|
@ -321,6 +313,8 @@ impl Player {
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a mutable reference to the playbin element
|
/// Gets a mutable reference to the playbin element
|
||||||
|
@ -458,7 +452,7 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current state of the playback
|
/// 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()
|
self.playbin().unwrap().current_state().into()
|
||||||
/*
|
/*
|
||||||
match *self.buffer.read().unwrap() {
|
match *self.buffer.read().unwrap() {
|
||||||
|
@ -488,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
|
/// 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) {
|
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>;
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::{fs::File, io::Read, path::Path, time::Duration};
|
use std::{fs::File, io::Read, path::Path, time::Duration};
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::utils::meta_offset;
|
use super::utils::meta_offset;
|
||||||
use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec};
|
use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec};
|
||||||
use crate::music_storage::db_reader::extern_library::ExternalLibrary;
|
use crate::music_storage::db_reader::extern_library::ExternalLibrary;
|
||||||
|
@ -174,12 +176,15 @@ 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,
|
location: vec![location],
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
plays: 0,
|
plays: 0,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: false,
|
favorited: false,
|
||||||
|
banned: None,
|
||||||
rating: None,
|
rating: None,
|
||||||
format: None,
|
format: None,
|
||||||
duration: self.duration,
|
duration: self.duration,
|
||||||
|
@ -189,6 +194,7 @@ impl FoobarPlaylistTrack {
|
||||||
date_modified: None,
|
date_modified: None,
|
||||||
album_art: Vec::new(),
|
album_art: Vec::new(),
|
||||||
tags: BTreeMap::new(),
|
tags: BTreeMap::new(),
|
||||||
|
internal_tags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use file_format::FileFormat;
|
||||||
use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt};
|
use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt};
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
use quick_xml::reader::Reader;
|
use quick_xml::reader::Reader;
|
||||||
|
use uuid::Uuid;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -12,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, Service, Song, Tag, URI};
|
use crate::music_storage::library::{AlbumArt, BannedType, Service, Song, Tag, URI};
|
||||||
use crate::music_storage::utils;
|
use crate::music_storage::utils;
|
||||||
|
|
||||||
use urlencoding::decode;
|
use urlencoding::decode;
|
||||||
|
@ -146,7 +147,7 @@ impl ExternalLibrary for ITunesLibrary {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sug: URI = if track.location.contains("file://localhost/") {
|
let location: URI = if track.location.contains("file://localhost/") {
|
||||||
URI::Local(PathBuf::from(
|
URI::Local(PathBuf::from(
|
||||||
decode(track.location.strip_prefix("file://localhost/").unwrap())
|
decode(track.location.strip_prefix("file://localhost/").unwrap())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -165,11 +166,19 @@ 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: sug,
|
location: vec![location],
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
plays: track.plays,
|
plays: track.plays,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: track.favorited,
|
favorited: track.favorited,
|
||||||
|
banned: if track.banned {
|
||||||
|
Some(BannedType::All)
|
||||||
|
}else {
|
||||||
|
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),
|
||||||
|
@ -185,6 +194,7 @@ 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);
|
||||||
|
@ -321,3 +331,28 @@ impl ITunesSong {
|
||||||
Ok(song)
|
Ok(song)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}};
|
||||||
|
|
||||||
|
use crate::{config::config::{Config, ConfigLibrary}, music_storage::{db_reader::extern_library::ExternalLibrary, library::MusicLibrary}};
|
||||||
|
|
||||||
|
use super::ITunesLibrary;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn itunes_lib_test() {
|
||||||
|
let mut config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap();
|
||||||
|
let config_lib = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None);
|
||||||
|
config.libraries.libraries.push(config_lib.clone());
|
||||||
|
|
||||||
|
let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs();
|
||||||
|
|
||||||
|
let mut library = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), config_lib.uuid).unwrap();
|
||||||
|
|
||||||
|
songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap());
|
||||||
|
|
||||||
|
config.write_file().unwrap();
|
||||||
|
library.save(config).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
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;
|
||||||
|
@ -5,17 +6,18 @@ use crate::config::config::Config;
|
||||||
// Various std things
|
// Various std things
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::io::Write;
|
||||||
use std::ops::ControlFlow::{Break, Continue};
|
use std::ops::ControlFlow::{Break, Continue};
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
// 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 uuid::Uuid;
|
use std::fs::{self, File};
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use uuid::Uuid;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
// Time
|
// Time
|
||||||
|
@ -115,13 +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, Eq, PartialOrd, Ord)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum DoNotTrack {
|
||||||
|
// TODO: add services to not track
|
||||||
|
LastFM,
|
||||||
|
LibreFM,
|
||||||
|
MusicBrainz,
|
||||||
|
Discord,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum SongType {
|
||||||
|
// TODO: add MORE?! song types
|
||||||
|
Main,
|
||||||
|
Instrumental,
|
||||||
|
Remix,
|
||||||
|
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: URI,
|
pub location: Vec<URI>,
|
||||||
|
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 rating: Option<u8>,
|
pub rating: Option<u8>,
|
||||||
pub format: Option<FileFormat>,
|
pub format: Option<FileFormat>,
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
|
@ -134,6 +181,7 @@ 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 {
|
||||||
|
@ -155,7 +203,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.location.clone())),
|
"location" => Some(Field::Location(self.primary_uri().unwrap().0.clone())), //TODO: make this not unwrap()
|
||||||
"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)),
|
||||||
|
@ -254,11 +302,15 @@ 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: URI::Local(binding),
|
location: vec![URI::Local(binding)],
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
plays: 0,
|
plays: 0,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: false,
|
favorited: false,
|
||||||
|
banned: None,
|
||||||
rating: None,
|
rating: None,
|
||||||
format,
|
format,
|
||||||
duration,
|
duration,
|
||||||
|
@ -268,6 +320,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -287,7 +340,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -372,15 +424,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: URI::Cue {
|
location: vec![URI::Cue {
|
||||||
location: audio_location.clone(),
|
location: audio_location.clone(),
|
||||||
index: i,
|
index: i,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
},
|
}],
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
plays: 0,
|
plays: 0,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: false,
|
favorited: false,
|
||||||
|
banned: None,
|
||||||
rating: None,
|
rating: None,
|
||||||
format,
|
format,
|
||||||
duration,
|
duration,
|
||||||
|
@ -390,11 +444,39 @@ 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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
|
#[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 {
|
||||||
|
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()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,6 +543,14 @@ impl URI {
|
||||||
path_str.to_string()
|
path_str.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_path(&self) -> Result<&PathBuf, Box<dyn Error>> {
|
||||||
|
if let Self::Local(path) = self {
|
||||||
|
Ok(path)
|
||||||
|
} else {
|
||||||
|
Err("This URI is not local!".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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(),
|
||||||
|
@ -489,7 +579,7 @@ pub enum Service {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Album<'a> {
|
pub struct Album<'a> {
|
||||||
title: &'a String,
|
title: &'a String,
|
||||||
artist: Option<&'a String>,
|
artist: Option<&'a String>,
|
||||||
|
@ -500,7 +590,7 @@ pub struct Album<'a> {
|
||||||
#[allow(clippy::len_without_is_empty)]
|
#[allow(clippy::len_without_is_empty)]
|
||||||
impl Album<'_> {
|
impl Album<'_> {
|
||||||
//returns the Album title
|
//returns the Album title
|
||||||
fn title(&self) -> &String {
|
pub fn title(&self) -> &String {
|
||||||
self.title
|
self.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -548,14 +638,8 @@ 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("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 {
|
||||||
|
@ -565,6 +649,8 @@ impl MusicLibrary {
|
||||||
name,
|
name,
|
||||||
uuid,
|
uuid,
|
||||||
library: Vec::new(),
|
library: Vec::new(),
|
||||||
|
playlists: PlaylistFolder::new(),
|
||||||
|
backup_songs: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,13 +661,15 @@ impl MusicLibrary {
|
||||||
/// the [MusicLibrary] Vec
|
/// the [MusicLibrary] Vec
|
||||||
pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> {
|
pub fn init(config: Arc<RwLock<Config>>, uuid: Uuid) -> Result<Self, Box<dyn Error>> {
|
||||||
let global_config = &*config.read().unwrap();
|
let global_config = &*config.read().unwrap();
|
||||||
|
let path = global_config.libraries.get_library(&uuid)?.path;
|
||||||
|
|
||||||
let library: MusicLibrary = match global_config.libraries.get_library(&uuid)?.path.exists() {
|
let library: MusicLibrary = match path.exists() {
|
||||||
true => read_file(global_config.libraries.get_library(&uuid)?.path)?,
|
true => read_file(path)?,
|
||||||
false => {
|
false => {
|
||||||
// If the library does not exist, re-create it
|
// If the library does not exist, re-create it
|
||||||
let lib = MusicLibrary::new(String::new(), uuid);
|
let lib = MusicLibrary::new(String::new(), uuid);
|
||||||
write_file(&lib, global_config.libraries.get_library(&uuid)?.path)?;
|
|
||||||
|
write_file(&lib, path)?;
|
||||||
lib
|
lib
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -631,7 +719,30 @@ impl MusicLibrary {
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.try_for_each(|(i, track)| {
|
.try_for_each(|(i, track)| {
|
||||||
if path == &track.location {
|
for location in &track.location {
|
||||||
|
//TODO: check that this works
|
||||||
|
if path == location {
|
||||||
|
return std::ops::ControlFlow::Break((track, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Continue(())
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Break(song) => Some(song),
|
||||||
|
Continue(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries for a [Song] by its [Uuid], returning a single `Song`
|
||||||
|
/// with the `Uuid` that matches along with its position in the library
|
||||||
|
pub fn query_uuid(&self, uuid: &Uuid) -> Option<(&Song, usize)> {
|
||||||
|
let result = self
|
||||||
|
.library
|
||||||
|
.par_iter()
|
||||||
|
.enumerate()
|
||||||
|
.try_for_each(|(i, track)| {
|
||||||
|
if uuid == &track.uuid {
|
||||||
return std::ops::ControlFlow::Break((track, i));
|
return std::ops::ControlFlow::Break((track, i));
|
||||||
}
|
}
|
||||||
Continue(())
|
Continue(())
|
||||||
|
@ -648,7 +759,8 @@ 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.location.path() {
|
if path == track.primary_uri().unwrap().0.path() {
|
||||||
|
//TODO: make this also not unwrap
|
||||||
result.clone().lock().unwrap().push(track);
|
result.clone().lock().unwrap().push(track);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -660,10 +772,7 @@ impl MusicLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds all the audio files within a specified folder
|
/// Finds all the audio files within a specified folder
|
||||||
pub fn scan_folder(
|
pub fn scan_folder(&mut self, target_path: &str) -> Result<i32, Box<dyn std::error::Error>> {
|
||||||
&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)
|
||||||
|
@ -726,7 +835,6 @@ 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(_) => (),
|
||||||
|
@ -742,14 +850,13 @@ 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;
|
||||||
|
@ -760,13 +867,14 @@ 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>> {
|
||||||
if self.query_uri(&new_song.location).is_some() {
|
let location = new_song.primary_uri()?.0;
|
||||||
return Err(format!("URI already in database: {:?}", new_song.location).into());
|
if self.query_uri(location).is_some() {
|
||||||
|
return Err(format!("URI already in database: {:?}", location).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
match new_song.location {
|
match location {
|
||||||
URI::Local(_) if self.query_path(new_song.location.path()).is_some() => {
|
URI::Local(_) if self.query_path(location.path()).is_some() => {
|
||||||
return Err(format!("Location exists for {:?}", new_song.location).into())
|
return Err(format!("Location exists for {:?}", location).into())
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -789,17 +897,18 @@ 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.0.location);
|
println!("{:?}", target_song.location);
|
||||||
|
|
||||||
for tag in new_tags {
|
for tag in new_tags {
|
||||||
println!("{:?}", tag);
|
println!("{:?}", tag);
|
||||||
|
@ -1014,3 +1123,40 @@ impl MusicLibrary {
|
||||||
Ok(albums)
|
Ok(albums)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
thread::sleep,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use crate::{config::config::Config, music_storage::library::MusicLibrary};
|
||||||
|
|
||||||
|
use super::Song;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_art_test() {
|
||||||
|
let s = Song::from_file(Path::new("")).unwrap();
|
||||||
|
let dir = &TempDir::new().unwrap();
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
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,59 +1,97 @@
|
||||||
use std::{fs::File, path::{Path, PathBuf}, io::{Read, Error}};
|
use std::error::Error;
|
||||||
|
use std::{
|
||||||
use bincode::config;
|
fs::File,
|
||||||
use chrono::Duration;
|
io::Read,
|
||||||
use uuid::Uuid;
|
path::PathBuf,
|
||||||
// use walkdir::Error;
|
sync::{Arc, RwLock},
|
||||||
|
|
||||||
use crate::music_controller::controller::Controller;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
library::{AlbumArt, Song, Tag},
|
|
||||||
music_collection::MusicCollection, db_reader::{
|
|
||||||
itunes::reader::ITunesLibrary,
|
|
||||||
extern_library::ExternalLibrary
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2, MasterPlaylist};
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
// use chrono::Duration;
|
||||||
|
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
|
||||||
|
use nestify::nest;
|
||||||
|
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum SortOrder {
|
pub enum SortOrder {
|
||||||
Manual,
|
Manual,
|
||||||
Tag(Tag)
|
Tag(Vec<Tag>),
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Playlist<'a> {
|
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,
|
title: String,
|
||||||
cover: Option<&'a AlbumArt>,
|
cover: Option<AlbumArt>,
|
||||||
tracks: Vec<Uuid>,
|
tracks: Vec<Uuid>,
|
||||||
sort_order: SortOrder,
|
sort_order: SortOrder,
|
||||||
play_count: i32,
|
play_count: i32,
|
||||||
play_time: Duration,
|
play_time: Duration,
|
||||||
}
|
}
|
||||||
impl<'a> Playlist<'a> {
|
impl Playlist {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
pub fn play_count(&self) -> i32 {
|
pub fn play_count(&self) -> i32 {
|
||||||
self.play_count
|
self.play_count
|
||||||
}
|
}
|
||||||
pub fn play_time(&self) -> chrono::Duration {
|
pub fn play_time(&self) -> Duration {
|
||||||
self.play_time
|
self.play_time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> &String {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cover(&self) -> Option<&AlbumArt> {
|
||||||
|
match &self.cover {
|
||||||
|
Some(e) => Some(e),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tracks(&self) -> Vec<Uuid> {
|
||||||
|
self.tracks.to_owned()
|
||||||
|
}
|
||||||
pub fn set_tracks(&mut self, tracks: Vec<Uuid>) {
|
pub fn set_tracks(&mut self, tracks: Vec<Uuid>) {
|
||||||
self.tracks = tracks;
|
self.tracks = tracks;
|
||||||
}
|
}
|
||||||
pub fn add_track(&mut self, track: Uuid) -> Result<(), Error> {
|
pub fn add_track(&mut self, track: Uuid) {
|
||||||
self.tracks.push(track);
|
self.tracks.push(track);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
pub fn remove_track(&mut self, index: i32) -> Result<(), Error> {
|
pub fn remove_track(&mut self, index: i32) {
|
||||||
let index = index as usize;
|
let index = index as usize;
|
||||||
if (self.tracks.len() - 1) >= index {
|
if (self.tracks.len() - 1) >= index {
|
||||||
self.tracks.remove(index);
|
self.tracks.remove(index);
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
// pub fn get_index(&self, song_name: &str) -> Option<usize> {
|
// pub fn get_index(&self, song_name: &str) -> Option<usize> {
|
||||||
// let mut index = 0;
|
// let mut index = 0;
|
||||||
|
@ -68,24 +106,65 @@ impl<'a> Playlist<'a> {
|
||||||
// }
|
// }
|
||||||
// None
|
// None
|
||||||
// }
|
// }
|
||||||
pub fn contains_value(&self, tag: &Tag, value: &str) -> bool {
|
pub fn contains_value(
|
||||||
&self.tracks.iter().for_each(|track| {
|
&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,
|
||||||
|
};
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
for uuid in &self.tracks {
|
||||||
|
if uuid == &item.uuid {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
pub fn to_m3u8(&mut self, tracks: Vec<Song>) {
|
|
||||||
let seg = tracks
|
|
||||||
.iter()
|
|
||||||
.map({
|
|
||||||
|track| {
|
|
||||||
|
|
||||||
MediaSegment {
|
pub fn to_file(&self, path: &str) -> Result<(), Box<dyn Error>> {
|
||||||
uri: track.location.to_string().into(),
|
super::utils::write_file(self, PathBuf::from(path))?;
|
||||||
duration: track.duration.as_millis() as f32,
|
Ok(())
|
||||||
title: Some(track.tags.get_key_value(&Tag::Title).unwrap().1.into()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_file(path: &str) -> Result<Playlist, Box<dyn Error>> {
|
||||||
|
super::utils::read_file(PathBuf::from(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.iter()
|
||||||
|
.filter_map(|uuid| {
|
||||||
|
// TODO: The Unwraps need to be handled here
|
||||||
|
if let Some((track, _)) = lib.query_uuid(uuid) {
|
||||||
|
if let URI::Local(_) = track.primary_uri().unwrap().0 {
|
||||||
|
Some(MediaSegment {
|
||||||
|
uri: track.primary_uri().unwrap().0.to_string(),
|
||||||
|
duration: track.duration.as_millis() as f32,
|
||||||
|
title: track
|
||||||
|
.tags
|
||||||
|
.get_key_value(&Tag::Title)
|
||||||
|
.map(|tag| tag.1.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<MediaSegment>>();
|
.collect::<Vec<MediaSegment>>();
|
||||||
|
@ -100,19 +179,24 @@ impl<'a> Playlist<'a> {
|
||||||
segments: seg.clone(),
|
segments: seg.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
//TODO: change this to put in a real file path
|
|
||||||
let mut file = std::fs::OpenOptions::new()
|
let mut file = std::fs::OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open("F:\\Dango Music Player\\playlist.m3u8")
|
.open(location)?;
|
||||||
.unwrap();
|
m3u8.write_to(&mut file)?;
|
||||||
m3u8.write_to(&mut file).unwrap();
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn from_m3u8(path: &str) -> Result<Playlist<'a>, Error> {
|
|
||||||
|
pub fn from_m3u8(
|
||||||
|
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),
|
Err(e) => return Err(e.into()),
|
||||||
};
|
};
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
file.read_to_end(&mut bytes).unwrap();
|
file.read_to_end(&mut bytes).unwrap();
|
||||||
|
@ -120,56 +204,174 @@ impl<'a> Playlist<'a> {
|
||||||
let parsed = m3u8_rs::parse_playlist(&bytes);
|
let parsed = m3u8_rs::parse_playlist(&bytes);
|
||||||
|
|
||||||
let playlist = match parsed {
|
let playlist = match parsed {
|
||||||
Result::Ok((i, playlist)) => playlist,
|
Result::Ok((_, playlist)) => playlist,
|
||||||
Result::Err(e) => panic!("Parsing error: \n{}", e),
|
Result::Err(e) => panic!("Parsing error: \n{}", e),
|
||||||
};
|
};
|
||||||
|
|
||||||
match playlist {
|
match playlist {
|
||||||
List2::MasterPlaylist(_) => panic!(),
|
List2::MasterPlaylist(_) => {
|
||||||
List2::MediaPlaylist(pl) => {
|
Err("This is a Master Playlist!\nPlase input a Media Playlist".into())
|
||||||
let values = pl.segments.iter().map(|seg| seg.uri.to_owned() ).collect::<Vec<String>>();
|
}
|
||||||
|
List2::MediaPlaylist(playlist_) => {
|
||||||
|
let mut uuids = Vec::new();
|
||||||
|
for seg in playlist_.segments {
|
||||||
|
let path_ = PathBuf::from(seg.uri.to_owned());
|
||||||
|
let mut lib = lib.write().unwrap();
|
||||||
|
|
||||||
|
let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) {
|
||||||
|
song.uuid
|
||||||
|
} else {
|
||||||
|
let song_ = Song::from_file(&path_)?;
|
||||||
|
let uuid = song_.uuid.to_owned();
|
||||||
|
lib.add_song(song_)?;
|
||||||
|
uuid
|
||||||
|
};
|
||||||
|
uuids.push(uuid);
|
||||||
|
}
|
||||||
|
let mut playlist = Playlist::new();
|
||||||
|
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
{
|
||||||
|
playlist.title = path
|
||||||
|
.split("\\")
|
||||||
|
.last()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.strip_suffix(".m3u8")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
{
|
||||||
|
playlist.title = path
|
||||||
|
.split("/")
|
||||||
|
.last()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.strip_suffix(".m3u8")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist.set_tracks(uuids);
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
todo!()
|
pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) {
|
||||||
}
|
let lib = lib.read().unwrap();
|
||||||
fn title(&self) -> &String {
|
let mut songs = vec![];
|
||||||
&self.title
|
let mut invalid_uuids = vec![];
|
||||||
}
|
|
||||||
fn cover(&self) -> Option<&AlbumArt> {
|
for uuid in &self.tracks {
|
||||||
match self.cover {
|
if let Some((track, _)) = lib.query_uuid(uuid) {
|
||||||
Some(e) => Some(e),
|
songs.push(track.to_owned());
|
||||||
None => None,
|
} else {
|
||||||
}
|
invalid_uuids.push(uuid);
|
||||||
}
|
|
||||||
fn tracks(&self) -> Vec<Uuid> {
|
|
||||||
self.tracks.to_owned()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let SortOrder::Tag(sort_by) = &self.sort_order {
|
||||||
|
println!("sorting by: {:?}", sort_by);
|
||||||
|
|
||||||
|
songs.par_sort_by(|a, b| {
|
||||||
|
for (i, sort_option) in sort_by.iter().enumerate() {
|
||||||
|
dbg!(&i);
|
||||||
|
let tag_a = match sort_option {
|
||||||
|
Tag::Field(field_selection) => {
|
||||||
|
match a.get_field(field_selection.as_str()) {
|
||||||
|
Some(field_value) => field_value.to_string(),
|
||||||
|
None => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => match a.get_tag(sort_option) {
|
||||||
|
Some(tag_value) => tag_value.to_owned(),
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
impl Default for Playlist<'_> {
|
let tag_b = match sort_option {
|
||||||
|
Tag::Field(field_selection) => match b.get_field(field_selection) {
|
||||||
|
Some(field_value) => field_value.to_string(),
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
|
_ => match b.get_tag(sort_option) {
|
||||||
|
Some(tag_value) => tag_value.to_owned(),
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
dbg!(&i);
|
||||||
|
|
||||||
|
if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::<i32>(), tag_b.parse::<i32>()) {
|
||||||
|
// If parsing succeeds, compare as numbers
|
||||||
|
return dbg!(num_a.cmp(&num_b));
|
||||||
|
} else {
|
||||||
|
// If parsing fails, compare as strings
|
||||||
|
return dbg!(tag_a.cmp(&tag_b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all tags are equal, sort by Track number
|
||||||
|
let path_a = PathBuf::from(a.get_field("location").unwrap().to_string());
|
||||||
|
let path_b = PathBuf::from(b.get_field("location").unwrap().to_string());
|
||||||
|
|
||||||
|
path_a.file_name().cmp(&path_b.file_name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
(songs, invalid_uuids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Playlist {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Playlist {
|
Playlist {
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
title: String::default(),
|
title: String::default(),
|
||||||
cover: None,
|
cover: None,
|
||||||
tracks: Vec::default(),
|
tracks: Vec::default(),
|
||||||
sort_order: SortOrder::Manual,
|
sort_order: SortOrder::Manual,
|
||||||
play_count: 0,
|
play_count: 0,
|
||||||
play_time: Duration::zero(),
|
play_time: Duration::from_secs(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[test]
|
#[cfg(test)]
|
||||||
// fn list_to_m3u8() {
|
mod test_super {
|
||||||
// let lib = ITunesLibrary::from_file(Path::new(
|
use super::*;
|
||||||
// "F:\\Music\\Mp3\\Music Main\\iTunes Music Library.xml",
|
use crate::{config::config::tests::read_config_lib, music_storage::playlist};
|
||||||
// ));
|
|
||||||
// let mut a = Playlist::new();
|
#[test]
|
||||||
// let c = lib.to_songs();
|
fn list_to_m3u8() {
|
||||||
// let mut b = c.iter().map(|song| song.to_owned()).collect::<Vec<Song>>();
|
let (_, lib) = read_config_lib();
|
||||||
// a.tracks.append(&mut b);
|
let mut playlist = Playlist::new();
|
||||||
// a.to_m3u8()
|
let tracks = lib.library.iter().map(|track| track.uuid).collect();
|
||||||
// }
|
playlist.set_tracks(tracks);
|
||||||
|
|
||||||
|
_ = playlist.to_m3u8(
|
||||||
|
Arc::new(RwLock::from(lib)),
|
||||||
|
".\\test-config\\playlists\\playlist.m3u8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn m3u8_to_list() -> Playlist {
|
||||||
|
let (_, lib) = read_config_lib();
|
||||||
|
let arc = Arc::new(RwLock::from(lib));
|
||||||
|
let playlist =
|
||||||
|
Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap();
|
||||||
|
|
||||||
|
playlist.to_file(".\\test-config\\playlists\\playlist");
|
||||||
|
dbg!(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn out_queue_sort() {
|
||||||
|
let (_, lib) = read_config_lib();
|
||||||
|
let mut list = m3u8_to_list();
|
||||||
|
list.sort_order = SortOrder::Tag(vec![Tag::Album]);
|
||||||
|
|
||||||
|
let songs = &list.out_tracks(Arc::new(RwLock::from(lib)));
|
||||||
|
|
||||||
|
dbg!(songs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
use std::fs::{File, self};
|
use deunicode::deunicode_with_tofu;
|
||||||
use std::io::{BufReader, BufWriter};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
use file_format::{FileFormat, Kind};
|
use file_format::{FileFormat, Kind};
|
||||||
use snap;
|
use snap;
|
||||||
use deunicode::deunicode_with_tofu;
|
use std::error::Error;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{BufReader, BufWriter};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use super::library::{AlbumArt, URI};
|
use super::library::{AlbumArt, URI};
|
||||||
|
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
use std::os::windows::fs::MetadataExt;
|
||||||
|
|
||||||
pub(super) fn normalize(input_string: &str) -> String {
|
pub(super) fn normalize(input_string: &str) -> String {
|
||||||
// Normalize the string to latin characters... this needs a lot of work
|
// Normalize the string to latin characters... this needs a lot of work
|
||||||
let mut normalized = deunicode_with_tofu(input_string, " ");
|
let mut normalized = deunicode_with_tofu(input_string, " ");
|
||||||
|
@ -23,12 +25,15 @@ 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>(
|
pub(super) fn write_file<
|
||||||
|
T: serde::Serialize,
|
||||||
|
U: std::convert::AsRef<Path> + std::convert::AsRef<std::ffi::OsStr> + Clone,
|
||||||
|
>(
|
||||||
library: T,
|
library: T,
|
||||||
path: PathBuf,
|
path: U,
|
||||||
) -> 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 = path.clone();
|
let mut writer_name = PathBuf::from(&path);
|
||||||
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
|
||||||
|
@ -50,7 +55,9 @@ pub(super) fn write_file<T: serde::Serialize>(
|
||||||
|
|
||||||
/// Read a file serialized out with [write_file] and turn it into
|
/// Read a file serialized out with [write_file] and turn it into
|
||||||
/// the desired structure
|
/// the desired structure
|
||||||
pub(super) fn read_file<T: for<'de> serde::Deserialize<'de>>(path: PathBuf) -> Result<T, Box<dyn Error>> {
|
pub(super) fn read_file<T: for<'de> serde::Deserialize<'de>>(
|
||||||
|
path: PathBuf,
|
||||||
|
) -> Result<T, Box<dyn Error>> {
|
||||||
// Create a new snap reader over the file
|
// Create a new snap reader over the file
|
||||||
let file_reader = BufReader::new(File::open(path)?);
|
let file_reader = BufReader::new(File::open(path)?);
|
||||||
let mut d = snap::read::FrameDecoder::new(file_reader);
|
let mut d = snap::read::FrameDecoder::new(file_reader);
|
||||||
|
@ -74,11 +81,12 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> {
|
||||||
.follow_links(true)
|
.follow_links(true)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| e.depth() < 3) // Don't recurse very deep
|
.filter(|e| e.depth() < 3)
|
||||||
|
// Don't recurse very deep
|
||||||
{
|
{
|
||||||
println!("{:?}", target_file);
|
println!("{:?}", target_file);
|
||||||
let path = target_file.path();
|
let path = target_file.path();
|
||||||
if !path.is_file() {
|
if !path.is_file() || !path.exists() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +95,11 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
if (4 & path.metadata().unwrap().file_attributes()) == 4 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let image_uri = URI::Local(path.to_path_buf().canonicalize()?);
|
let image_uri = URI::Local(path.to_path_buf().canonicalize()?);
|
||||||
|
|
||||||
images.push(AlbumArt::External(image_uri));
|
images.push(AlbumArt::External(image_uri));
|
||||||
|
|
Loading…
Reference in a new issue