From 7e64c67b46055e88ff9b2b26ba5ed5e0a8b179fc Mon Sep 17 00:00:00 2001 From: G2-Games Date: Fri, 19 Jan 2024 17:45:57 -0600 Subject: [PATCH] Added preliminary listenbrainz scrobbling, removed cue support. Closes #1 --- Cargo.toml | 1 - src/music_controller/config.rs | 19 +++-- src/music_controller/music_controller.rs | 5 +- src/music_player/music_output.rs | 5 +- src/music_player/music_player.rs | 17 +++-- src/music_storage/music_db.rs | 60 ---------------- src/music_tracker/music_tracker.rs | 92 ++++++++++++++++++++++-- 7 files changed, 118 insertions(+), 81 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b312946..3ee99fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ heapless = "0.7.16" rb = "0.4.1" symphonia = { version = "0.5.3", features = ["all-codecs"] } serde_json = "1.0.104" -cue = "2.0.0" async-std = "1.12.0" async-trait = "0.1.73" md-5 = "0.10.5" diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index c77a02a..034f12d 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -4,13 +4,14 @@ use std::fs; use serde::{Deserialize, Serialize}; -use crate::music_tracker::music_tracker::{LastFM, LastFMConfig, DiscordRPCConfig}; +use crate::music_tracker::music_tracker::{LastFMConfig, DiscordRPCConfig, ListenBrainzConfig}; #[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { pub db_path: Box, pub lastfm: Option, pub discord: Option, + pub listenbrainz: Option, } impl Default for Config { @@ -21,18 +22,24 @@ impl Default for Config { db_path: Box::new(path), lastfm: None, - + discord: Some(DiscordRPCConfig { enabled: true, dango_client_id: 1144475145864499240, dango_icon: String::from("flat"), }), + + listenbrainz: Some(ListenBrainzConfig { + enabled: false, + api_url: String::from("https://api.listenbrainz.org"), + auth_token: String::from(""), + }) }; } } impl Config { - // Creates and saves a new config with default values + /// Creates and saves a new config with default values pub fn new(config_file: &PathBuf) -> std::io::Result { let config = Config::default(); config.save(config_file)?; @@ -40,14 +47,14 @@ impl Config { Ok(config) } - // Loads config from given file path + /// Loads config from given file path pub fn from(config_file: &PathBuf) -> std::result::Result { return toml::from_str(&read_to_string(config_file) .expect("Failed to initalize music config: File not found!")); } - // Saves config to given path - // Saves -> temp file, if successful, removes old config, and renames temp to given path + /// Saves config to given path + /// Saves -> temp file, if successful, removes old config, and renames temp to given path pub fn save(&self, config_file: &PathBuf) -> std::io::Result<()> { let toml = toml::to_string_pretty(self).unwrap(); diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 29f4fb7..da4f676 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -1,12 +1,11 @@ use std::path::PathBuf; -use std::sync::{RwLock, Arc, Mutex}; +use std::sync::{RwLock, Arc}; use rusqlite::Result; use crate::music_controller::config::Config; use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; -use crate::music_processor::music_processor::MusicProcessor; -use crate::music_storage::music_db::{URI, Song}; +use crate::music_storage::music_db::Song; pub struct MusicController { pub config: Arc>, diff --git a/src/music_player/music_output.rs b/src/music_player/music_output.rs index b774b0f..795e9e9 100644 --- a/src/music_player/music_output.rs +++ b/src/music_player/music_output.rs @@ -74,8 +74,7 @@ pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result AudioOutput::::create_stream(spec, &device, &config.into(), duration), _ => todo!(), }; - } - +} impl AudioOutput { // Creates the stream (TODO: Merge w/open_stream?) @@ -83,7 +82,7 @@ impl AudioOutput { let num_channels = config.channels as usize; // Ring buffer is created with 200ms audio capacity - let ring_len = ((200 * config.sample_rate.0 as usize) / 1000) * num_channels; + let ring_len = ((50 * config.sample_rate.0 as usize) / 1000) * num_channels; let ring_buf= rb::SpscRb::new(ring_len); let ring_buf_producer = ring_buf.producer(); diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 2944788..5ab0ad9 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -21,7 +21,7 @@ use crate::music_controller::config::Config; use crate::music_player::music_output::AudioStream; use crate::music_processor::music_processor::MusicProcessor; use crate::music_storage::music_db::{URI, Song}; -use crate::music_tracker::music_tracker::{MusicTracker, LastFM, DiscordRPCConfig, DiscordRPC, TrackerError}; +use crate::music_tracker::music_tracker::{MusicTracker, TrackerError, LastFM, DiscordRPC, ListenBrainz}; // Struct that controls playback of music pub struct MusicPlayer { @@ -146,13 +146,22 @@ impl MusicPlayer { // Sets local config for trackers to detect changes let local_config = global_config.clone(); let mut trackers: Vec> = Vec::new(); - // Updates local trackers to the music controller config + // Updates local trackers to the music controller config // TODO: refactor let update_trackers = |trackers: &mut Vec>|{ if let Some(lastfm_config) = global_config.lastfm.clone() { - trackers.push(Box::new(LastFM::new(&lastfm_config))); + if lastfm_config.enabled { + trackers.push(Box::new(LastFM::new(&lastfm_config))); + } } if let Some(discord_config) = global_config.discord.clone() { - trackers.push(Box::new(DiscordRPC::new(&discord_config))); + if discord_config.enabled { + trackers.push(Box::new(DiscordRPC::new(&discord_config))); + } + } + if let Some(listenbz_config) = global_config.listenbrainz.clone() { + if listenbz_config.enabled { + trackers.push(Box::new(ListenBrainz::new(&listenbz_config))); + } } }; update_trackers(&mut trackers); diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 7ed59fe..093d7c5 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -2,7 +2,6 @@ use file_format::{FileFormat, Kind}; use serde::Deserialize; use lofty::{Accessor, AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; use rusqlite::{params, Connection}; -use cue::{cd_text::PTI, cd::CD}; use std::fs; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -105,64 +104,6 @@ fn path_in_db(query_path: &Path, connection: &Connection) -> bool { } } -/// Parse a cuesheet given a path and a directory it is located in, -/// returning a Vec of Song objects -fn parse_cuesheet( - cuesheet_path: &Path, - current_dir: &PathBuf -) -> Result, Box>{ - let cuesheet = CD::parse_file(cuesheet_path.to_path_buf())?; - - let album = cuesheet.get_cdtext().read(PTI::Title); - - let mut song_list:Vec = vec![]; - - for (index, track) in cuesheet.tracks().iter().enumerate() { - let track_string_path = format!("{}/{}", current_dir.to_string_lossy(), track.get_filename()); - let track_path = Path::new(&track_string_path); - - if !track_path.exists() {continue}; - - // Get the format as a string - let short_format = match FileFormat::from_file(track_path) { - Ok(fmt) => Some(fmt), - Err(_) => None - }; - - let duration = Duration::from_secs(track.get_length().unwrap_or(-1) as u64); - - let custom_index_start = Tag::Custom{ - tag: String::from("dango_cue_index_start"), - tag_value: track.get_index(0).unwrap_or(-1).to_string() - }; - let custom_index_end = Tag::Custom{ - tag: String::from("dango_cue_index_end"), - tag_value: track.get_index(0).unwrap_or(-1).to_string() - }; - - let custom_tags: Vec = vec![custom_index_start, custom_index_end]; - - let tags = track.get_cdtext(); - let cue_song = Song { - path: URI::Local(String::from("URI")), - title: tags.read(PTI::Title), - album: album.clone(), - tracknum: Some(index + 1), - artist: tags.read(PTI::Performer), - date: None, - genre: tags.read(PTI::Genre), - plays: Some(0), - favorited: Some(false), - format: short_format, - duration: Some(duration), - custom_tags: Some(custom_tags) - }; - - song_list.push(cue_song); - } - - Ok(song_list) -} pub fn find_all_music( config: &Config, @@ -196,7 +137,6 @@ pub fn find_all_music( add_file_to_db(target_file.path(), &db_connection) } else if extension.to_ascii_lowercase() == "cue" { // TODO: implement cuesheet support - parse_cuesheet(target_file.path(), ¤t_dir); } } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 7dd1d8a..213a5f1 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -1,10 +1,11 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::collections::BTreeMap; +use serde_json::json; use async_trait::async_trait; -use serde::{Serialize, Deserialize, Serializer}; +use serde::{Serialize, Deserialize}; use md5::{Md5, Digest}; -use discord_presence::{ Event, DiscordError}; +use discord_presence::{Event}; use surf::StatusCode; use crate::music_storage::music_db::Song; @@ -38,7 +39,6 @@ pub enum TrackerError { Unknown, } - impl TrackerError { pub fn from_surf_error(error: surf::Error) -> TrackerError { return match error.status() { @@ -300,4 +300,88 @@ impl MusicTracker for DiscordRPC { async fn get_times_tracked(&mut self, song: &Song) -> Result { return Ok(0); } -} \ No newline at end of file +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct ListenBrainzConfig { + pub enabled: bool, + pub api_url: String, + pub auth_token: String, +} + +pub struct ListenBrainz { + config: ListenBrainzConfig, +} + +#[async_trait] +impl MusicTracker for ListenBrainz { + async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { + let (artist, track) = match (song.artist, song.title) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + // Creates a json to submit a single song as defined in the listenbrainz documentation + let json_req = json!({ + "listen_type": "playing_now", + "payload": [ + { + "track_metadata": { + "artist_name": artist, + "track_name": track, + } + } + ] + }); + + return match self.api_request(&json_req.to_string(), &String::from("/1/submit-listens")).await { + Ok(_) => Ok(()), + Err(err) => Err(TrackerError::from_surf_error(err)) + } + } + + async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; + + let (artist, track) = match (song.artist, song.title) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + + let json_req = json!({ + "listen_type": "single", + "payload": [ + { + "listened_at": timestamp, + "track_metadata": { + "artist_name": artist, + "track_name": track, + } + } + ] + }); + + return match self.api_request(&json_req.to_string(), &String::from("/1/submit-listens")).await { + Ok(_) => Ok(()), + Err(err) => Err(TrackerError::from_surf_error(err)) + } + } + async fn test_tracker(&mut self) -> Result<(), TrackerError> { + todo!() + } + async fn get_times_tracked(&mut self, song: &Song) -> Result { + todo!() + } +} + +impl ListenBrainz { + pub fn new(config: &ListenBrainzConfig) -> Self { + ListenBrainz { + config: config.clone() + } + } + // Makes an api request to configured url with given json + pub async fn api_request(&self, request: &String, endpoint: &String) -> Result { + let reponse = surf::post(format!("{}{}", &self.config.api_url, endpoint)).body_string(request.clone()).header("Authorization", format!("Token {}", self.config.auth_token)).await; + return reponse + } +}