mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
Merge branch 'Library-Rework' into controller
This commit is contained in:
commit
b04a166c1b
7 changed files with 326 additions and 170 deletions
11
Cargo.toml
11
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"
|
||||||
|
@ -39,3 +47,4 @@ opener = { version = "0.7.0", features = ["reveal"] }
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
listenbrainz = "0.7.0"
|
listenbrainz = "0.7.0"
|
||||||
discord-rpc-client = "0.4.0"
|
discord-rpc-client = "0.4.0"
|
||||||
|
nestify = "0.3.3"
|
||||||
|
|
|
@ -2,37 +2,37 @@
|
||||||
//! player. It manages queues, playback, library access, and
|
//! player. It manages queues, playback, library access, and
|
||||||
//! other functions
|
//! other functions
|
||||||
|
|
||||||
|
use crossbeam_channel;
|
||||||
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
|
use listenbrainz::ListenBrainz;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use crossbeam_channel::{Sender, Receiver};
|
|
||||||
use crossbeam_channel;
|
|
||||||
use listenbrainz::ListenBrainz;
|
|
||||||
use std::thread::spawn;
|
use std::thread::spawn;
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use crossbeam_channel::unbounded;
|
use crossbeam_channel::unbounded;
|
||||||
|
use std::error::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::music_controller::queue::QueueItem;
|
use crate::music_controller::queue::QueueItem;
|
||||||
use crate::music_player::gstreamer::GStreamer;
|
use crate::music_player::gstreamer::GStreamer;
|
||||||
use crate::music_storage::library::{Tag, URI};
|
use crate::music_storage::library::{Tag, URI};
|
||||||
use crate::{
|
use crate::{
|
||||||
music_storage::library::{MusicLibrary, Song},
|
|
||||||
config::config::Config,
|
config::config::Config,
|
||||||
music_controller::queue::Queue,
|
music_controller::queue::Queue,
|
||||||
|
music_storage::library::{MusicLibrary, Song},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
pub queue: Queue,
|
pub queue: Queue,
|
||||||
pub config: Arc<RwLock<Config>>,
|
pub config: Arc<RwLock<Config>>,
|
||||||
pub library: MusicLibrary,
|
pub library: MusicLibrary,
|
||||||
player_mail: MailMan<PlayerCmd, PlayerRes>
|
player_mail: MailMan<PlayerCmd, PlayerRes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) struct MailMan<T: Send, U: Send> {
|
pub(super) struct MailMan<T: Send, U: Send> {
|
||||||
pub tx: Sender<T>,
|
pub tx: Sender<T>,
|
||||||
rx: Receiver<U>
|
rx: Receiver<U>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send> MailMan<T, T> {
|
impl<T: Send> MailMan<T, T> {
|
||||||
|
@ -46,10 +46,7 @@ impl<T: Send, U: Send> MailMan<T, U> {
|
||||||
let (tx, rx) = unbounded::<T>();
|
let (tx, rx) = unbounded::<T>();
|
||||||
let (tx1, rx1) = unbounded::<U>();
|
let (tx1, rx1) = unbounded::<U>();
|
||||||
|
|
||||||
(
|
(MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx })
|
||||||
MailMan { tx, rx: rx1 },
|
|
||||||
MailMan { tx: tx1, rx }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&self, mail: T) -> Result<(), Box<dyn Error>> {
|
pub fn send(&self, mail: T) -> Result<(), Box<dyn Error>> {
|
||||||
|
@ -64,17 +61,18 @@ impl<T: Send, U: Send> MailMan<T, U> {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlayerCmd {
|
enum PlayerCmd {
|
||||||
Test(URI)
|
Test(URI),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlayerRes {
|
enum PlayerRes {
|
||||||
Test
|
Test,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
impl Controller {
|
impl Controller {
|
||||||
pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>>
|
pub fn start<P>(config_path: P) -> Result<Self, Box<dyn Error>>
|
||||||
where std::path::PathBuf: std::convert::From<P>
|
where
|
||||||
|
std::path::PathBuf: std::convert::From<P>,
|
||||||
{
|
{
|
||||||
let config_path = PathBuf::from(config_path);
|
let config_path = PathBuf::from(config_path);
|
||||||
|
|
||||||
|
@ -99,36 +97,43 @@ impl Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Ok(Controller {
|
||||||
Ok(
|
queue: Queue::new(),
|
||||||
Controller {
|
config: config_.clone(),
|
||||||
queue: Queue::new(),
|
library,
|
||||||
config: config_.clone(),
|
player_mail,
|
||||||
library,
|
})
|
||||||
player_mail
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn q_add(&self, item: Uuid, source:super::queue::PlayerLocation , by_human: bool) {
|
pub fn q_add(&self, item: Uuid, source: super::queue::PlayerLocation, by_human: bool) {
|
||||||
self.queue.add_item(item, source, by_human)
|
self.queue.add_item(item, source, by_human)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_super {
|
mod test_super {
|
||||||
use std::{thread::sleep, time::Duration};
|
use std::{thread::sleep, time::Duration};
|
||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_() {
|
fn test_() {
|
||||||
let c = Controller::start("F:\\Dangoware\\Dango Music Player\\dmp-core\\test-config\\config_test.json").unwrap();
|
let c = Controller::start(
|
||||||
|
"F:\\Dangoware\\Dango Music Player\\dmp-core\\test-config\\config_test.json",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
sleep(Duration::from_secs(60));
|
sleep(Duration::from_secs(60));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use uuid::Uuid;
|
|
||||||
use crate::music_storage::library::{MusicLibrary, Song, URI};
|
use crate::music_storage::library::{MusicLibrary, Song, URI};
|
||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
sync::{Arc, RwLock}
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -12,8 +12,7 @@ pub enum QueueError {
|
||||||
#[error("Index out of bounds! Index {0} is over len {1}")]
|
#[error("Index out of bounds! Index {0} is over len {1}")]
|
||||||
OutOfBounds(usize, usize),
|
OutOfBounds(usize, usize),
|
||||||
#[error("The Queue is empty!")]
|
#[error("The Queue is empty!")]
|
||||||
EmptyQueue
|
EmptyQueue,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
@ -32,7 +31,7 @@ pub enum PlayerLocation {
|
||||||
Library,
|
Library,
|
||||||
Playlist(Uuid),
|
Playlist(Uuid),
|
||||||
File,
|
File,
|
||||||
Custom
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -41,7 +40,7 @@ pub struct QueueItem {
|
||||||
pub(super) item: Song,
|
pub(super) item: Song,
|
||||||
pub(super) state: QueueState,
|
pub(super) state: QueueState,
|
||||||
pub(super) source: PlayerLocation,
|
pub(super) source: PlayerLocation,
|
||||||
pub(super) by_human: bool
|
pub(super) by_human: bool,
|
||||||
}
|
}
|
||||||
impl From<Song> for QueueItem {
|
impl From<Song> for QueueItem {
|
||||||
fn from(song: Song) -> Self {
|
fn from(song: Song) -> Self {
|
||||||
|
@ -49,41 +48,46 @@ impl From<Song> for QueueItem {
|
||||||
item: song,
|
item: song,
|
||||||
state: QueueState::NoState,
|
state: QueueState::NoState,
|
||||||
source: PlayerLocation::Library,
|
source: PlayerLocation::Library,
|
||||||
by_human: false
|
by_human: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Queue {
|
pub struct Queue {
|
||||||
pub items: Vec<QueueItem>,
|
pub items: Vec<QueueItem>,
|
||||||
pub played: Vec<QueueItem>,
|
pub played: Vec<QueueItem>,
|
||||||
pub loop_: bool,
|
pub loop_: bool,
|
||||||
pub shuffle: bool
|
pub shuffle: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Queue {
|
impl Queue {
|
||||||
fn has_addhere(&self) -> bool {
|
fn has_addhere(&self) -> bool {
|
||||||
for item in &self.items {
|
for item in &self.items {
|
||||||
if item.state == QueueState::AddHere {
|
if item.state == QueueState::AddHere {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dbg_items(&self) {
|
fn dbg_items(&self) {
|
||||||
dbg!(self.items.iter().map(|item| item.item.clone() ).collect::<Vec<Song>>(), self.items.len());
|
dbg!(
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.item.clone())
|
||||||
|
.collect::<Vec<Song>>(),
|
||||||
|
self.items.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
//TODO: Make the queue take settings from config/state if applicable
|
//TODO: Make the queue take settings from config/state if applicable
|
||||||
Queue {
|
Queue {
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
played: Vec::new(),
|
played: Vec::new(),
|
||||||
loop_: false,
|
loop_: false,
|
||||||
shuffle: false,
|
shuffle: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,22 +100,30 @@ impl Queue {
|
||||||
pub fn add_item(&mut self, item: Song, source: PlayerLocation, by_human: bool) {
|
pub fn add_item(&mut self, item: Song, source: PlayerLocation, by_human: bool) {
|
||||||
let mut i: usize = 0;
|
let mut i: usize = 0;
|
||||||
|
|
||||||
self.items = self.items.iter().enumerate().map(|(j, item_)| {
|
self.items = self
|
||||||
let mut item_ = item_.to_owned();
|
.items
|
||||||
// get the index of the current AddHere item and give it to i
|
.iter()
|
||||||
if item_.state == QueueState::AddHere {
|
.enumerate()
|
||||||
i = j;
|
.map(|(j, item_)| {
|
||||||
item_.state = QueueState::NoState;
|
let mut item_ = item_.to_owned();
|
||||||
}
|
// get the index of the current AddHere item and give it to i
|
||||||
item_
|
if item_.state == QueueState::AddHere {
|
||||||
}).collect::<Vec<QueueItem>>();
|
i = j;
|
||||||
|
item_.state = QueueState::NoState;
|
||||||
|
}
|
||||||
|
item_
|
||||||
|
})
|
||||||
|
.collect::<Vec<QueueItem>>();
|
||||||
|
|
||||||
self.items.insert(i + if self.items.is_empty() { 0 } else { 1 }, QueueItem {
|
self.items.insert(
|
||||||
item,
|
i + if self.items.is_empty() { 0 } else { 1 },
|
||||||
state: QueueState::AddHere,
|
QueueItem {
|
||||||
source,
|
item,
|
||||||
by_human
|
state: QueueState::AddHere,
|
||||||
});
|
source,
|
||||||
|
by_human,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_item_next(&mut self, item: Song, source: PlayerLocation) {
|
pub fn add_item_next(&mut self, item: Song, source: PlayerLocation) {
|
||||||
|
@ -122,19 +134,23 @@ impl Queue {
|
||||||
(if empty { 0 } else { 1 }),
|
(if empty { 0 } else { 1 }),
|
||||||
QueueItem {
|
QueueItem {
|
||||||
item,
|
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,
|
source,
|
||||||
by_human: true
|
by_human: true,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) {
|
pub fn add_multi(&mut self, items: Vec<Song>, source: PlayerLocation, by_human: bool) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> {
|
pub fn remove_item(&mut self, remove_index: usize) -> Result<(), QueueError> {
|
||||||
|
|
||||||
// dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]);
|
// dbg!(/*&remove_index, self.current_index(), &index,*/ &self.items[remove_index]);
|
||||||
|
|
||||||
if remove_index < self.items.len() {
|
if remove_index < self.items.len() {
|
||||||
|
@ -145,7 +161,7 @@ impl Queue {
|
||||||
self.items[remove_index].state = QueueState::NoState;
|
self.items[remove_index].state = QueueState::NoState;
|
||||||
self.items.remove(remove_index);
|
self.items.remove(remove_index);
|
||||||
Ok(())
|
Ok(())
|
||||||
}else {
|
} else {
|
||||||
Err(QueueError::EmptyQueue)
|
Err(QueueError::EmptyQueue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,11 +176,11 @@ impl Queue {
|
||||||
|
|
||||||
if !empty && index < self.items.len() {
|
if !empty && index < self.items.len() {
|
||||||
let i = self.items[index].clone();
|
let i = self.items[index].clone();
|
||||||
self.items.retain(|item| *item == i );
|
self.items.retain(|item| *item == i);
|
||||||
self.items[0].state = AddHere;
|
self.items[0].state = AddHere;
|
||||||
}else if empty {
|
} else if empty {
|
||||||
return Err("Queue is empty!".into());
|
return Err("Queue is empty!".into());
|
||||||
}else {
|
} else {
|
||||||
return Err("index out of bounds!".into());
|
return Err("index out of bounds!".into());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -179,17 +195,19 @@ impl Queue {
|
||||||
self.played.clear();
|
self.played.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: uh, fix this?
|
// TODO: uh, fix this?
|
||||||
fn move_to(&mut self, index: usize) -> Result<(), QueueError> {
|
fn move_to(&mut self, index: usize) -> Result<(), QueueError> {
|
||||||
use QueueState::*;
|
use QueueState::*;
|
||||||
|
|
||||||
let empty = self.items.is_empty();
|
let empty = self.items.is_empty();
|
||||||
|
|
||||||
let index = if !empty { index } else { return Err(QueueError::EmptyQueue); };
|
let index = if !empty {
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
return Err(QueueError::EmptyQueue);
|
||||||
|
};
|
||||||
|
|
||||||
if !empty && index < self.items.len() {
|
if !empty && index < self.items.len() {
|
||||||
|
|
||||||
let to_item = self.items[index].clone();
|
let to_item = self.items[index].clone();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -201,16 +219,18 @@ impl Queue {
|
||||||
self.items[1].state = AddHere;
|
self.items[1].state = AddHere;
|
||||||
}
|
}
|
||||||
if let Err(e) = self.remove_item(0) {
|
if let Err(e) = self.remove_item(0) {
|
||||||
dbg!(&e); self.dbg_items(); return Err(e);
|
dbg!(&e);
|
||||||
|
self.dbg_items();
|
||||||
|
return Err(e);
|
||||||
}
|
}
|
||||||
// dbg!(&to_item.item, &self.items[ind].item);
|
// dbg!(&to_item.item, &self.items[ind].item);
|
||||||
}else if empty {
|
} else if empty {
|
||||||
return Err(QueueError::EmptyQueue);
|
return Err(QueueError::EmptyQueue);
|
||||||
}else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else {
|
} else {
|
||||||
return Err(QueueError::EmptyQueue);
|
return Err(QueueError::EmptyQueue);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -230,17 +250,15 @@ impl Queue {
|
||||||
|
|
||||||
#[allow(clippy::should_implement_trait)]
|
#[allow(clippy::should_implement_trait)]
|
||||||
pub fn next(&mut self) -> Result<&QueueItem, Box<dyn Error>> {
|
pub fn next(&mut self) -> Result<&QueueItem, Box<dyn Error>> {
|
||||||
|
|
||||||
if self.items.is_empty() {
|
if self.items.is_empty() {
|
||||||
if self.loop_ {
|
if self.loop_ {
|
||||||
return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue
|
return Err(QueueError::EmptyQueue.into()); // TODO: add function to loop the queue
|
||||||
}else {
|
} else {
|
||||||
return Err(QueueError::EmptyQueue.into());
|
return Err(QueueError::EmptyQueue.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: add an algorithm to detect if the song should be skipped
|
// TODO: add an algorithm to detect if the song should be skipped
|
||||||
let item = self.items[0].clone();
|
let item = self.items[0].clone();
|
||||||
|
|
||||||
if self.items[0].state == QueueState::AddHere || !self.has_addhere() {
|
if self.items[0].state == QueueState::AddHere || !self.has_addhere() {
|
||||||
self.items[1].state = QueueState::AddHere;
|
self.items[1].state = QueueState::AddHere;
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,14 +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(),
|
uuid: Uuid::new_v4(),
|
||||||
plays: 0,
|
plays: 0,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: false,
|
favorited: false,
|
||||||
// banned: None,
|
banned: None,
|
||||||
rating: None,
|
rating: None,
|
||||||
format: None,
|
format: None,
|
||||||
duration: self.duration,
|
duration: self.duration,
|
||||||
|
@ -193,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ use std::vec::Vec;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
|
||||||
use crate::music_storage::db_reader::extern_library::ExternalLibrary;
|
use crate::music_storage::db_reader::extern_library::ExternalLibrary;
|
||||||
use crate::music_storage::library::{AlbumArt, 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;
|
||||||
|
@ -166,17 +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,
|
location: vec![location],
|
||||||
uuid: Uuid::new_v4(),
|
uuid: Uuid::new_v4(),
|
||||||
plays: track.plays,
|
plays: track.plays,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: track.favorited,
|
favorited: track.favorited,
|
||||||
// banned: if track.banned {
|
banned: if track.banned {
|
||||||
// Some(BannedType::All)
|
Some(BannedType::All)
|
||||||
// }else {
|
}else {
|
||||||
// None
|
None
|
||||||
// },
|
},
|
||||||
rating: track.rating,
|
rating: track.rating,
|
||||||
format: match FileFormat::from_file(PathBuf::from(&loc)) {
|
format: match FileFormat::from_file(PathBuf::from(&loc)) {
|
||||||
Ok(e) => Some(e),
|
Ok(e) => Some(e),
|
||||||
|
@ -192,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);
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -117,35 +118,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)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum BannedType {
|
pub enum BannedType {
|
||||||
Shuffle,
|
Shuffle,
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum DoNotTrack {
|
pub enum DoNotTrack {
|
||||||
// TODO: add services to not track
|
// TODO: add services to not track
|
||||||
|
LastFM,
|
||||||
|
LibreFM,
|
||||||
|
MusicBrainz,
|
||||||
|
Discord,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
enum SongType {
|
#[non_exhaustive]
|
||||||
// TODO: add song types
|
pub enum SongType {
|
||||||
|
// TODO: add MORE?! song types
|
||||||
Main,
|
Main,
|
||||||
Instrumental,
|
Instrumental,
|
||||||
Remix,
|
Remix,
|
||||||
Custom(String)
|
Custom(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SongType {
|
||||||
|
fn default() -> Self {
|
||||||
|
SongType::Main
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stores information about a single song
|
/// Stores information about a single song
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct Song {
|
pub struct Song {
|
||||||
pub location: URI,
|
pub location: Vec<URI>,
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub plays: i32,
|
pub plays: i32,
|
||||||
pub skips: i32,
|
pub skips: i32,
|
||||||
pub favorited: bool,
|
pub favorited: bool,
|
||||||
// pub banned: Option<BannedType>,
|
pub banned: Option<BannedType>,
|
||||||
pub rating: Option<u8>,
|
pub rating: Option<u8>,
|
||||||
pub format: Option<FileFormat>,
|
pub format: Option<FileFormat>,
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
|
@ -158,6 +182,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>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,7 +205,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)),
|
||||||
|
@ -279,13 +304,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(),
|
uuid: Uuid::new_v4(),
|
||||||
plays: 0,
|
plays: 0,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: false,
|
favorited: false,
|
||||||
// banned: None,
|
banned: None,
|
||||||
rating: None,
|
rating: None,
|
||||||
format,
|
format,
|
||||||
duration,
|
duration,
|
||||||
|
@ -295,6 +322,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)
|
||||||
}
|
}
|
||||||
|
@ -399,17 +427,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(),
|
uuid: Uuid::new_v4(),
|
||||||
plays: 0,
|
plays: 0,
|
||||||
skips: 0,
|
skips: 0,
|
||||||
favorited: false,
|
favorited: false,
|
||||||
// banned: None,
|
banned: None,
|
||||||
rating: None,
|
rating: None,
|
||||||
format,
|
format,
|
||||||
duration,
|
duration,
|
||||||
|
@ -419,6 +447,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: Vec::new()
|
||||||
};
|
};
|
||||||
tracks.push((new_song, audio_location.clone()));
|
tracks.push((new_song, audio_location.clone()));
|
||||||
}
|
}
|
||||||
|
@ -448,16 +477,17 @@ impl Song {
|
||||||
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
|
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
|
||||||
let tagged_file: lofty::TaggedFile;
|
let tagged_file: lofty::TaggedFile;
|
||||||
|
|
||||||
|
// TODO: add support for other URI types... or don't
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
let uri = urlencoding::decode(
|
let uri = urlencoding::decode(
|
||||||
match self.location.as_uri().strip_prefix("file:///") {
|
match self.primary_uri()?.0.as_uri().strip_prefix("file:///") {
|
||||||
Some(str) => str,
|
Some(str) => str,
|
||||||
None => return Err("invalid path.. again?".into())
|
None => return Err("invalid path.. again?".into())
|
||||||
})?.into_owned();
|
})?.into_owned();
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
let uri = urlencoding::decode(
|
let uri = urlencoding::decode(
|
||||||
match self.location.as_uri().strip_prefix("file://") {
|
match self.primary_uri()?.as_uri().strip_prefix("file://") {
|
||||||
Some(str) => str,
|
Some(str) => str,
|
||||||
None => return Err("invalid path.. again?".into())
|
None => return Err("invalid path.. again?".into())
|
||||||
})?.into_owned();
|
})?.into_owned();
|
||||||
|
@ -492,6 +522,26 @@ impl Song {
|
||||||
dbg!(open(dbg!(uri))?);
|
dbg!(open(dbg!(uri))?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
@ -652,14 +702,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("test_config/config_test.json")).unwrap();
|
|
||||||
let target_uuid = config.libraries.libraries[0].uuid;
|
|
||||||
let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap();
|
|
||||||
dbg!(a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicLibrary {
|
impl MusicLibrary {
|
||||||
|
@ -669,6 +713,8 @@ impl MusicLibrary {
|
||||||
name,
|
name,
|
||||||
uuid,
|
uuid,
|
||||||
library: Vec::new(),
|
library: Vec::new(),
|
||||||
|
playlists: PlaylistFolder::new(),
|
||||||
|
backup_songs: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -737,8 +783,11 @@ 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 {
|
||||||
return std::ops::ControlFlow::Break((track, i));
|
//TODO: check that this works
|
||||||
|
if path == location {
|
||||||
|
return std::ops::ControlFlow::Break((track, i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Continue(())
|
Continue(())
|
||||||
});
|
});
|
||||||
|
@ -774,7 +823,7 @@ impl MusicLibrary {
|
||||||
fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> {
|
fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> {
|
||||||
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
|
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
self.library.par_iter().for_each(|track| {
|
self.library.par_iter().for_each(|track| {
|
||||||
if path == track.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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -886,13 +935,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())
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
@ -915,17 +965,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);
|
||||||
|
@ -1143,10 +1194,12 @@ impl MusicLibrary {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod 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 tempfile::TempDir;
|
||||||
|
|
||||||
|
use crate::{config::config::Config, music_storage::library::MusicLibrary};
|
||||||
|
|
||||||
use super::Song;
|
use super::Song;
|
||||||
|
|
||||||
|
|
||||||
|
@ -1162,4 +1215,12 @@ mod test {
|
||||||
|
|
||||||
sleep(Duration::from_secs(20));
|
sleep(Duration::from_secs(20));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn library_init() {
|
||||||
|
let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap();
|
||||||
|
let target_uuid = config.libraries.libraries[0].uuid;
|
||||||
|
let a = MusicLibrary::init(Arc::new(RwLock::from(config)), target_uuid).unwrap();
|
||||||
|
dbg!(a);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,21 +1,52 @@
|
||||||
use std::{fs::File, io::Read, path:: PathBuf, sync::{Arc, RwLock}};
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::Read,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// use chrono::Duration;
|
||||||
|
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
|
||||||
|
|
||||||
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
|
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
|
||||||
|
use nestify::nest;
|
||||||
|
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum SortOrder {
|
pub enum SortOrder {
|
||||||
Manual,
|
Manual,
|
||||||
Tag(Vec<Tag>)
|
Tag(Vec<Tag>),
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
|
nest! {
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]*
|
||||||
|
pub struct PlaylistFolder {
|
||||||
|
name: String,
|
||||||
|
items: Vec<
|
||||||
|
pub enum PlaylistFolderItem {
|
||||||
|
Folder(PlaylistFolder),
|
||||||
|
List(Playlist)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaylistFolder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PlaylistFolder {
|
||||||
|
name: String::new(),
|
||||||
|
items: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
title: String,
|
title: String,
|
||||||
|
@ -75,11 +106,16 @@ impl Playlist {
|
||||||
// }
|
// }
|
||||||
// None
|
// 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 lib = lib.read().unwrap();
|
||||||
let items = match lib.query_tracks(value, &vec![tag.to_owned()], &vec![tag.to_owned()]) {
|
let items = match lib.query_tracks(value, &vec![tag.to_owned()], &vec![tag.to_owned()]) {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
None => return false
|
None => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
for item in items {
|
for item in items {
|
||||||
|
@ -102,23 +138,35 @@ impl Playlist {
|
||||||
super::utils::read_file(PathBuf::from(path))
|
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 lib = lib.read().unwrap();
|
||||||
let seg = self.tracks
|
let seg = self
|
||||||
|
.tracks
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map( |uuid| {
|
.filter_map(|uuid| {
|
||||||
if let Some((track, _)) = lib.query_uuid(uuid) {
|
// TODO: The Unwraps need to be handled here
|
||||||
if let URI::Local(_) = track.location {
|
if let Some((track, _)) = lib.query_uuid(uuid) {
|
||||||
Some(MediaSegment {
|
if let URI::Local(_) = track.primary_uri().unwrap().0 {
|
||||||
uri: track.location.to_string(),
|
Some(MediaSegment {
|
||||||
duration: track.duration.as_millis() as f32,
|
uri: track.primary_uri().unwrap().0.to_string(),
|
||||||
title: track.tags.get_key_value(&Tag::Title).map(|tag| tag.1.into()),
|
duration: track.duration.as_millis() as f32,
|
||||||
..Default::default()
|
title: track
|
||||||
})
|
.tags
|
||||||
}else { None }
|
.get_key_value(&Tag::Title)
|
||||||
}else { None }
|
.map(|tag| tag.1.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
.collect::<Vec<MediaSegment>>();
|
.collect::<Vec<MediaSegment>>();
|
||||||
|
|
||||||
let m3u8 = MediaPlaylist {
|
let m3u8 = MediaPlaylist {
|
||||||
|
@ -142,7 +190,10 @@ impl Playlist {
|
||||||
Ok(())
|
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) {
|
let mut file = match File::open(path) {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
|
@ -158,7 +209,9 @@ impl Playlist {
|
||||||
};
|
};
|
||||||
|
|
||||||
match 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_) => {
|
List2::MediaPlaylist(playlist_) => {
|
||||||
let mut uuids = Vec::new();
|
let mut uuids = Vec::new();
|
||||||
for seg in playlist_.segments {
|
for seg in playlist_.segments {
|
||||||
|
@ -167,7 +220,7 @@ impl Playlist {
|
||||||
|
|
||||||
let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) {
|
let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) {
|
||||||
song.uuid
|
song.uuid
|
||||||
}else {
|
} else {
|
||||||
let song_ = Song::from_file(&path_)?;
|
let song_ = Song::from_file(&path_)?;
|
||||||
let uuid = song_.uuid.to_owned();
|
let uuid = song_.uuid.to_owned();
|
||||||
lib.add_song(song_)?;
|
lib.add_song(song_)?;
|
||||||
|
@ -179,21 +232,23 @@ impl Playlist {
|
||||||
|
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
{
|
{
|
||||||
playlist.title = path.split("\\")
|
playlist.title = path
|
||||||
.last()
|
.split("\\")
|
||||||
.unwrap_or_default()
|
.last()
|
||||||
.strip_suffix(".m3u8")
|
.unwrap_or_default()
|
||||||
.unwrap_or_default()
|
.strip_suffix(".m3u8")
|
||||||
.to_string();
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
{
|
{
|
||||||
playlist.title = path.split("/")
|
playlist.title = path
|
||||||
.last()
|
.split("/")
|
||||||
.unwrap_or_default()
|
.last()
|
||||||
.strip_suffix(".m3u8")
|
.unwrap_or_default()
|
||||||
.unwrap_or_default()
|
.strip_suffix(".m3u8")
|
||||||
.to_string();
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist.set_tracks(uuids);
|
playlist.set_tracks(uuids);
|
||||||
|
@ -202,7 +257,6 @@ impl Playlist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) {
|
pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) {
|
||||||
let lib = lib.read().unwrap();
|
let lib = lib.read().unwrap();
|
||||||
let mut songs = vec![];
|
let mut songs = vec![];
|
||||||
|
@ -211,7 +265,7 @@ impl Playlist {
|
||||||
for uuid in &self.tracks {
|
for uuid in &self.tracks {
|
||||||
if let Some((track, _)) = lib.query_uuid(uuid) {
|
if let Some((track, _)) = lib.query_uuid(uuid) {
|
||||||
songs.push(track.to_owned());
|
songs.push(track.to_owned());
|
||||||
}else {
|
} else {
|
||||||
invalid_uuids.push(uuid);
|
invalid_uuids.push(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,10 +277,12 @@ impl Playlist {
|
||||||
for (i, sort_option) in sort_by.iter().enumerate() {
|
for (i, sort_option) in sort_by.iter().enumerate() {
|
||||||
dbg!(&i);
|
dbg!(&i);
|
||||||
let tag_a = match sort_option {
|
let tag_a = match sort_option {
|
||||||
Tag::Field(field_selection) => match a.get_field(field_selection.as_str()) {
|
Tag::Field(field_selection) => {
|
||||||
Some(field_value) => field_value.to_string(),
|
match a.get_field(field_selection.as_str()) {
|
||||||
None => continue,
|
Some(field_value) => field_value.to_string(),
|
||||||
},
|
None => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => match a.get_tag(sort_option) {
|
_ => match a.get_tag(sort_option) {
|
||||||
Some(tag_value) => tag_value.to_owned(),
|
Some(tag_value) => tag_value.to_owned(),
|
||||||
None => continue,
|
None => continue,
|
||||||
|
@ -266,7 +322,6 @@ impl Playlist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Default for Playlist {
|
impl Default for Playlist {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Playlist {
|
Playlist {
|
||||||
|
@ -276,7 +331,7 @@ impl Default for Playlist {
|
||||||
tracks: Vec::default(),
|
tracks: Vec::default(),
|
||||||
sort_order: SortOrder::Manual,
|
sort_order: SortOrder::Manual,
|
||||||
play_count: 0,
|
play_count: 0,
|
||||||
play_time: Duration::from_millis(0),
|
play_time: Duration::from_secs(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -290,17 +345,20 @@ mod test_super {
|
||||||
fn list_to_m3u8() {
|
fn list_to_m3u8() {
|
||||||
let (_, lib) = read_config_lib();
|
let (_, lib) = read_config_lib();
|
||||||
let mut playlist = Playlist::new();
|
let mut playlist = Playlist::new();
|
||||||
let tracks = lib.library.iter().map(|track| track.uuid ).collect();
|
let tracks = lib.library.iter().map(|track| track.uuid).collect();
|
||||||
playlist.set_tracks(tracks);
|
playlist.set_tracks(tracks);
|
||||||
|
|
||||||
_ = playlist.to_m3u8(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 {
|
fn m3u8_to_list() -> Playlist {
|
||||||
let (_, lib) = read_config_lib();
|
let (_, lib) = read_config_lib();
|
||||||
let arc = Arc::new(RwLock::from(lib));
|
let arc = Arc::new(RwLock::from(lib));
|
||||||
let playlist = 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");
|
playlist.to_file(".\\test-config\\playlists\\playlist");
|
||||||
dbg!(playlist)
|
dbg!(playlist)
|
||||||
|
|
Loading…
Reference in a new issue