From ab529f4c9b82e1a376d93fbccaf86ec5dad0a5d6 Mon Sep 17 00:00:00 2001 From: MrDulfin Date: Fri, 5 Apr 2024 20:26:13 -0400 Subject: [PATCH] added `out_queue` function, rudamentary listenbrainz scrobbling and other small changes --- Cargo.toml | 3 +- src/config/config.rs | 15 +++- src/music_controller/connections.rs | 93 +++++++++++++++++++- src/music_controller/controller.rs | 47 +++++----- src/music_controller/queue.rs | 8 +- src/music_storage/playlist.rs | 132 +++++++++++++++++++++++----- 6 files changed, 244 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d106a4..49ac4e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,5 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] } serde_json = "1.0.111" deunicode = "1.4.2" opener = { version = "0.7.0", features = ["reveal"] } -tempfile = "3.10.1" \ No newline at end of file +tempfile = "3.10.1" +listenbrainz = "0.7.0" diff --git a/src/config/config.rs b/src/config/config.rs index 5a209ae..1c19b15 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -90,11 +90,18 @@ impl ConfigLibraries { } #[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct ConfigConnections { + pub listenbrainz_token: Option +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +#[serde(default)] pub struct Config { pub path: PathBuf, pub backup_folder: Option, 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); } } diff --git a/src/music_controller/connections.rs b/src/music_controller/connections.rs index ac4abd9..dec723c 100644 --- a/src/music_controller/connections.rs +++ b/src/music_controller/connections.rs @@ -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> { + 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> { + 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> { + 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(); + } } diff --git a/src/music_controller/controller.rs b/src/music_controller/controller.rs index ebcc27f..d13a62f 100644 --- a/src/music_controller/controller.rs +++ b/src/music_controller/controller.rs @@ -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, - config: Arc>, + pub config: Arc>, // library: MusicLibrary, - controller_mail: MailMan, - db_mail: MailMan, - queue_mail: Vec>, + pub(super) controller_mail: MailMan, + pub(super) db_mail: MailMan, + pub(super) queue_mail: Vec>, } #[derive(Debug)] -pub enum ControllerCmd { +pub(super) enum ControllerCmd { Default, Test } #[derive(Debug)] -enum ControllerResponse { +pub(super) enum ControllerResponse { Empty, QueueMailMan(MailMan), } #[derive(Debug)] -pub enum DatabaseCmd { +pub(super) enum DatabaseCmd { Default, Test, SaveLibrary, @@ -49,11 +51,10 @@ pub enum DatabaseCmd { QueryUuid(Uuid), QueryUuids(Vec), ReadFolder(String), - } #[derive(Debug)] -enum DatabaseResponse { +pub(super) enum DatabaseResponse { Empty, Song(Song), Songs(Vec), @@ -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 { +pub(super) struct MailMan { pub tx: Sender, rx: Receiver } -impl MailMan { +impl MailMan { pub fn new() -> Self { let (tx, rx) = unbounded::(); MailMan { tx, rx } } } -impl MailMan { +impl MailMan { pub fn double() -> (MailMan, MailMan) { let (tx, rx) = unbounded::(); let (tx1, rx1) = unbounded::(); @@ -108,14 +110,16 @@ impl MailMan { } pub fn recv(&self) -> Result> { - 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> { + pub fn start

(config_path: P) -> Result> + where std::path::PathBuf: std::convert::From

+ { 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 { + pub fn lib_get_songs(&self) -> Vec { 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> { + pub fn lib_scan_folder(&self, folder: String) -> Result<(), Box> { 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> { + pub fn q_play(&self, index: usize) -> Result<(), Box> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::Play)?; dbg!(mail.recv()?); Ok(()) } - fn q_pause(&self, index: usize) -> Result<(), Box> { + pub fn q_pause(&self, index: usize) -> Result<(), Box> { 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> { + pub fn q_enqueue(&self, index: usize, uri: URI) -> Result<(), Box> { let mail = &self.queue_mail[index]; mail.send(QueueCmd::Enqueue(uri))?; // dbg!(mail.recv()?); diff --git a/src/music_controller/queue.rs b/src/music_controller/queue.rs index 6180e9d..483010f 100644 --- a/src/music_controller/queue.rs +++ b/src/music_controller/queue.rs @@ -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 { diff --git a/src/music_storage/playlist.rs b/src/music_storage/playlist.rs index bd40b6a..63876fb 100644 --- a/src/music_storage/playlist.rs +++ b/src/music_storage/playlist.rs @@ -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) } -#[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 { + self.tracks.to_owned() + } pub fn set_tracks(&mut self, tracks: Vec) { self.tracks = tracks; } @@ -75,6 +93,15 @@ impl Playlist { false } + pub fn to_file(&self, path: &str) -> Result<(), Box> { + super::utils::write_file(self, PathBuf::from(path))?; + Ok(()) + } + + pub fn from_file(path: &str) -> Result> { + super::utils::read_file(PathBuf::from(path)) + } + pub fn to_m3u8(&mut self, lib: Arc>, location: &str) -> Result<(), Box> { 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>) -> (Vec, 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 { - 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::(), tag_b.parse::()) { + // 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); } }