mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
added out_queue
function, rudamentary listenbrainz scrobbling and other small changes
This commit is contained in:
parent
a75081d4fc
commit
ab529f4c9b
6 changed files with 244 additions and 54 deletions
|
@ -37,3 +37,4 @@ serde_json = "1.0.111"
|
|||
deunicode = "1.4.2"
|
||||
opener = { version = "0.7.0", features = ["reveal"] }
|
||||
tempfile = "3.10.1"
|
||||
listenbrainz = "0.7.0"
|
||||
|
|
|
@ -90,11 +90,18 @@ impl ConfigLibraries {
|
|||
}
|
||||
|
||||
#[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 path: PathBuf,
|
||||
pub backup_folder: Option<PathBuf>,
|
||||
pub libraries: ConfigLibraries,
|
||||
pub volume: f32,
|
||||
pub connections: ConfigConnections,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
@ -212,10 +219,10 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test3() {
|
||||
let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap();
|
||||
let uuid = config.libraries.get_default().unwrap().uuid;
|
||||
let lib = MusicLibrary::init(Arc::new(RwLock::from(config.clone())), uuid).unwrap();
|
||||
let (config, lib) = read_config_lib();
|
||||
|
||||
dbg!(lib);
|
||||
_ = config.write_file();
|
||||
|
||||
dbg!(config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,95 @@
|
|||
use super::controller::Controller;
|
||||
use std::{
|
||||
sync::{Arc, RwLock},
|
||||
error::Error,
|
||||
};
|
||||
|
||||
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
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, RwLock};
|
||||
use crossbeam_channel::{Sender, Receiver};
|
||||
use crossbeam_channel;
|
||||
use listenbrainz::ListenBrainz;
|
||||
use std::thread::spawn;
|
||||
|
||||
use std::error::Error;
|
||||
use crossbeam_channel::unbounded;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::music_controller::queue::{QueueItem, QueueItemType};
|
||||
use crate::music_storage::library::{Tag, URI};
|
||||
use crate::{
|
||||
music_storage::library::{MusicLibrary, Song},
|
||||
|
@ -21,27 +23,27 @@ use crate::{
|
|||
|
||||
pub struct Controller {
|
||||
// queues: Vec<Queue>,
|
||||
config: Arc<RwLock<Config>>,
|
||||
pub config: Arc<RwLock<Config>>,
|
||||
// library: MusicLibrary,
|
||||
controller_mail: MailMan<ControllerCmd, ControllerResponse>,
|
||||
db_mail: MailMan<DatabaseCmd, DatabaseResponse>,
|
||||
queue_mail: Vec<MailMan<QueueCmd, QueueResponse>>,
|
||||
pub(super) controller_mail: MailMan<ControllerCmd, ControllerResponse>,
|
||||
pub(super) db_mail: MailMan<DatabaseCmd, DatabaseResponse>,
|
||||
pub(super) queue_mail: Vec<MailMan<QueueCmd, QueueResponse>>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum ControllerCmd {
|
||||
pub(super) enum ControllerCmd {
|
||||
Default,
|
||||
Test
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ControllerResponse {
|
||||
pub(super) enum ControllerResponse {
|
||||
Empty,
|
||||
QueueMailMan(MailMan<QueueCmd, QueueResponse>),
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DatabaseCmd {
|
||||
pub(super) enum DatabaseCmd {
|
||||
Default,
|
||||
Test,
|
||||
SaveLibrary,
|
||||
|
@ -49,11 +51,10 @@ pub enum DatabaseCmd {
|
|||
QueryUuid(Uuid),
|
||||
QueryUuids(Vec<Uuid>),
|
||||
ReadFolder(String),
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DatabaseResponse {
|
||||
pub(super) enum DatabaseResponse {
|
||||
Empty,
|
||||
Song(Song),
|
||||
Songs(Vec<Song>),
|
||||
|
@ -61,7 +62,7 @@ enum DatabaseResponse {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum QueueCmd {
|
||||
pub(super) enum QueueCmd {
|
||||
Default,
|
||||
Test,
|
||||
Play,
|
||||
|
@ -73,25 +74,26 @@ enum QueueCmd {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum QueueResponse {
|
||||
pub(super) enum QueueResponse {
|
||||
Default,
|
||||
Test,
|
||||
Index(i32),
|
||||
Uuid(Uuid),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MailMan<T, U> {
|
||||
pub(super) struct MailMan<T: Send, U: Send> {
|
||||
pub tx: Sender<T>,
|
||||
rx: Receiver<U>
|
||||
}
|
||||
|
||||
impl<T> MailMan<T, T> {
|
||||
impl<T: Send> MailMan<T, T> {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = unbounded::<T>();
|
||||
MailMan { tx, rx }
|
||||
}
|
||||
}
|
||||
impl<T, U> MailMan<T, U> {
|
||||
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>();
|
||||
|
@ -108,14 +110,16 @@ impl<T, U> MailMan<T, U> {
|
|||
}
|
||||
|
||||
pub fn recv(&self) -> Result<U, Box<dyn Error>> {
|
||||
let u = self.rx.recv().unwrap();
|
||||
let u = self.rx.recv()?;
|
||||
Ok(u)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl Controller {
|
||||
pub fn start(config_path: String) -> 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>
|
||||
{
|
||||
let config_path = PathBuf::from(config_path);
|
||||
let config = Config::read_file(config_path)?;
|
||||
let uuid = config.libraries.get_default()?.uuid;
|
||||
|
@ -185,7 +189,6 @@ impl Controller {
|
|||
});
|
||||
|
||||
|
||||
|
||||
Ok(
|
||||
Controller {
|
||||
// queues: Vec::new(),
|
||||
|
@ -197,7 +200,7 @@ impl Controller {
|
|||
)
|
||||
}
|
||||
|
||||
fn lib_get_songs(&self) -> Vec<Song> {
|
||||
pub fn lib_get_songs(&self) -> Vec<Song> {
|
||||
self.db_mail.send(DatabaseCmd::GetSongs);
|
||||
match self.db_mail.recv().unwrap() {
|
||||
DatabaseResponse::Songs(songs) => songs,
|
||||
|
@ -205,7 +208,7 @@ impl Controller {
|
|||
}
|
||||
}
|
||||
|
||||
fn lib_scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> {
|
||||
pub fn lib_scan_folder(&self, folder: String) -> Result<(), Box<dyn Error>> {
|
||||
let mail = &self.db_mail;
|
||||
mail.send(DatabaseCmd::ReadFolder(folder))?;
|
||||
dbg!(mail.recv()?);
|
||||
|
@ -259,14 +262,14 @@ impl Controller {
|
|||
Ok(self.queue_mail.len() - 1)
|
||||
}
|
||||
|
||||
fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> {
|
||||
pub fn q_play(&self, index: usize) -> Result<(), Box<dyn Error>> {
|
||||
let mail = &self.queue_mail[index];
|
||||
mail.send(QueueCmd::Play)?;
|
||||
dbg!(mail.recv()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn q_pause(&self, index: usize) -> Result<(), Box<dyn Error>> {
|
||||
pub fn q_pause(&self, index: usize) -> Result<(), Box<dyn Error>> {
|
||||
let mail = &self.queue_mail[index];
|
||||
mail.send(QueueCmd::Pause)?;
|
||||
dbg!(mail.recv()?);
|
||||
|
@ -286,7 +289,7 @@ impl Controller {
|
|||
// Ok(())
|
||||
// }
|
||||
|
||||
fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> {
|
||||
pub fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box<dyn Error>> {
|
||||
let mail = &self.queue_mail[index];
|
||||
mail.send(QueueCmd::Enqueue(uri))?;
|
||||
// dbg!(mail.recv()?);
|
||||
|
|
|
@ -89,10 +89,10 @@ pub enum PlayerLocation {
|
|||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct QueueItem<'a> {
|
||||
item: QueueItemType<'a>,
|
||||
state: QueueState,
|
||||
source: PlayerLocation,
|
||||
by_human: bool
|
||||
pub(super) item: QueueItemType<'a>,
|
||||
pub(super) state: QueueState,
|
||||
pub(super) source: PlayerLocation,
|
||||
pub(super) by_human: bool
|
||||
}
|
||||
impl QueueItem<'_> {
|
||||
fn new() -> Self {
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
use std::{fs::File, io::Read, path:: PathBuf, sync::{Arc, RwLock}};
|
||||
use std::error::Error;
|
||||
|
||||
use chrono::Duration;
|
||||
use std::time::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
||||
|
||||
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
use rayon::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum SortOrder {
|
||||
Manual,
|
||||
Tag(Tag)
|
||||
Tag(Vec<Tag>)
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Playlist {
|
||||
uuid: Uuid,
|
||||
title: String,
|
||||
|
@ -29,9 +32,24 @@ impl Playlist {
|
|||
pub fn play_count(&self) -> i32 {
|
||||
self.play_count
|
||||
}
|
||||
pub fn play_time(&self) -> chrono::Duration {
|
||||
pub fn play_time(&self) -> Duration {
|
||||
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>) {
|
||||
self.tracks = tracks;
|
||||
}
|
||||
|
@ -75,6 +93,15 @@ impl Playlist {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn to_file(&self, path: &str) -> Result<(), Box<dyn Error>> {
|
||||
super::utils::write_file(self, PathBuf::from(path))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -174,22 +201,72 @@ impl Playlist {
|
|||
}
|
||||
}
|
||||
}
|
||||
fn title(&self) -> &String {
|
||||
&self.title
|
||||
}
|
||||
fn cover(&self) -> Option<&AlbumArt> {
|
||||
match &self.cover {
|
||||
Some(e) => Some(e),
|
||||
None => None,
|
||||
|
||||
|
||||
pub fn out_tracks(&self, lib: Arc<RwLock<MusicLibrary>>) -> (Vec<Song>, Vec<&Uuid>) {
|
||||
let lib = lib.read().unwrap();
|
||||
let mut songs = vec![];
|
||||
let mut invalid_uuids = vec![];
|
||||
|
||||
for uuid in &self.tracks {
|
||||
if let Some((track, _)) = lib.query_uuid(uuid) {
|
||||
songs.push(track.to_owned());
|
||||
}else {
|
||||
invalid_uuids.push(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
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 {
|
||||
Playlist {
|
||||
|
@ -199,7 +276,7 @@ impl Default for Playlist {
|
|||
tracks: Vec::default(),
|
||||
sort_order: SortOrder::Manual,
|
||||
play_count: 0,
|
||||
play_time: Duration::zero(),
|
||||
play_time: Duration::from_millis(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +284,7 @@ impl Default for Playlist {
|
|||
#[cfg(test)]
|
||||
mod test_super {
|
||||
use super::*;
|
||||
use crate::config::config::tests::read_config_lib;
|
||||
use crate::{config::config::tests::read_config_lib, music_storage::playlist};
|
||||
|
||||
#[test]
|
||||
fn list_to_m3u8() {
|
||||
|
@ -219,11 +296,24 @@ mod test_super {
|
|||
_ = playlist.to_m3u8(Arc::new(RwLock::from(lib)), ".\\test-config\\playlists\\playlist.m3u8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn m3u8_to_list() {
|
||||
|
||||
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();
|
||||
dbg!(playlist);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue