Merge pull request #17 from Dangoware/database

Merge database work to main
This commit is contained in:
G2 2023-11-11 16:16:44 -06:00 committed by GitHub
commit 438605875a
13 changed files with 1396 additions and 755 deletions

View file

@ -12,13 +12,12 @@ keywords = ["audio", "music"]
categories = ["multimedia::audio"] categories = ["multimedia::audio"]
[dependencies] [dependencies]
file-format = { version = "0.17.3", features = ["reader", "serde"] } file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] }
lofty = "0.14.0" lofty = "0.16.1"
rusqlite = { version = "0.29.0", features = ["bundled"] } serde = { version = "1.0.191", features = ["derive"] }
serde = { version = "1.0.164", features = ["derive"] }
time = "0.3.22" time = "0.3.22"
toml = "0.7.5" toml = "0.7.5"
walkdir = "2.3.3" walkdir = "2.4.0"
cpal = "0.15.2" cpal = "0.15.2"
heapless = "0.7.16" heapless = "0.7.16"
rb = "0.4.1" rb = "0.4.1"
@ -32,3 +31,12 @@ futures = "0.3.28"
rubato = "0.12.0" rubato = "0.12.0"
arrayvec = "0.7.4" arrayvec = "0.7.4"
discord-presence = "0.5.18" discord-presence = "0.5.18"
chrono = { version = "0.4.31", features = ["serde"] }
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
unidecode = "0.3.0"
rayon = "1.8.0"
log = "0.4"
pretty_env_logger = "0.4"
base64 = "0.21.5"
snap = "1.1.0"
rcue = "0.1.3"

View file

@ -5,20 +5,17 @@ pub mod music_tracker {
pub mod music_storage { pub mod music_storage {
pub mod music_db; pub mod music_db;
pub mod playlist; pub mod playlist;
} mod utils;
pub mod music_processor {
pub mod music_processor;
} }
pub mod music_player { pub mod music_player {
pub mod music_player;
pub mod music_output; pub mod music_output;
pub mod music_player;
pub mod music_resampler; pub mod music_resampler;
} }
pub mod music_controller { pub mod music_controller {
pub mod music_controller;
pub mod config; pub mod config;
pub mod init; pub mod init;
} pub mod music_controller;
}

View file

@ -1,10 +1,10 @@
use std::path::PathBuf;
use std::fs::read_to_string;
use std::fs; use std::fs;
use std::fs::read_to_string;
use std::path::PathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::music_tracker::music_tracker::{LastFMConfig, DiscordRPCConfig, ListenBrainzConfig}; use crate::music_tracker::music_tracker::{DiscordRPCConfig, LastFMConfig, ListenBrainzConfig};
#[derive(Serialize, Deserialize, PartialEq, Eq)] #[derive(Serialize, Deserialize, PartialEq, Eq)]
pub struct Config { pub struct Config {
@ -16,57 +16,59 @@ pub struct Config {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
let path = PathBuf::from("./music_database.db3"); let path = PathBuf::from("./music_database");
return Config { return Config {
db_path: Box::new(path), db_path: Box::new(path),
lastfm: None, lastfm: None,
discord: Some(DiscordRPCConfig { discord: Some(DiscordRPCConfig {
enabled: true, enabled: true,
dango_client_id: 1144475145864499240, dango_client_id: 1144475145864499240,
dango_icon: String::from("flat"), dango_icon: String::from("flat"),
}), }),
listenbrainz: Some(ListenBrainzConfig { listenbrainz: Some(ListenBrainzConfig {
enabled: false, enabled: false,
api_url: String::from("https://api.listenbrainz.org"), api_url: String::from("https://api.listenbrainz.org"),
auth_token: String::from(""), auth_token: String::from(""),
}) }),
}; };
} }
} }
impl Config { 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<Config> { pub fn new(config_file: &PathBuf) -> std::io::Result<Config> {
let config = Config::default(); let config = Config::default();
config.save(config_file)?; config.save(config_file)?;
Ok(config) Ok(config)
} }
/// Loads config from given file path /// Loads config from given file path
pub fn from(config_file: &PathBuf) -> std::result::Result<Config, toml::de::Error> { pub fn from(config_file: &PathBuf) -> std::result::Result<Config, toml::de::Error> {
return toml::from_str(&read_to_string(config_file) return toml::from_str(
.expect("Failed to initalize music config: File not found!")); &read_to_string(config_file)
.expect("Failed to initalize music config: File not found!"),
);
} }
/// Saves config to given path /// Saves config to given path
/// Saves -> temp file, if successful, removes old config, and renames temp 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<()> { pub fn save(&self, config_file: &PathBuf) -> std::io::Result<()> {
let toml = toml::to_string_pretty(self).unwrap(); let toml = toml::to_string_pretty(self).unwrap();
let mut temp_file = config_file.clone(); let mut temp_file = config_file.clone();
temp_file.set_extension("tomltemp"); temp_file.set_extension("tomltemp");
fs::write(&temp_file, toml)?; fs::write(&temp_file, toml)?;
// If configuration file already exists, delete it // If configuration file already exists, delete it
match fs::metadata(config_file) { match fs::metadata(config_file) {
Ok(_) => fs::remove_file(config_file)?, Ok(_) => fs::remove_file(config_file)?,
Err(_) => {}, Err(_) => {}
} }
fs::rename(temp_file, config_file)?; fs::rename(temp_file, config_file)?;

View file

@ -1,9 +1,7 @@
use std::path::Path;
use std::fs::File; use std::fs::File;
use std::path::Path;
pub fn init() { pub fn init() {}
}
fn init_config() { fn init_config() {
let config_path = "./config.toml"; let config_path = "./config.toml";
@ -13,7 +11,4 @@ fn init_config() {
} }
} }
fn init_db() { fn init_db() {}
}

View file

@ -1,68 +1,77 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{RwLock, Arc}; use std::sync::{Arc, RwLock};
use rusqlite::Result;
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; use crate::music_player::music_player::{DecoderMessage, MusicPlayer, PlayerStatus};
use crate::music_storage::music_db::Song; use crate::music_storage::music_db::{MusicLibrary, Song, Tag};
pub struct MusicController { pub struct MusicController {
pub config: Arc<RwLock<Config>>, pub config: Arc<RwLock<Config>>,
pub library: MusicLibrary,
music_player: MusicPlayer, music_player: MusicPlayer,
} }
impl MusicController { impl MusicController {
/// Creates new MusicController with config at given path /// Creates new MusicController with config at given path
pub fn new(config_path: &PathBuf) -> Result<MusicController, std::io::Error>{ pub fn new(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> {
let config = Arc::new(RwLock::new(Config::new(config_path)?)); let config = Arc::new(RwLock::new(Config::new(config_path)?));
let music_player = MusicPlayer::new(config.clone()); let music_player = MusicPlayer::new(config.clone());
let library = match MusicLibrary::init(config.clone()) {
Ok(library) => library,
Err(error) => return Err(error),
};
let controller = MusicController { let controller = MusicController {
config, config,
library,
music_player, music_player,
}; };
return Ok(controller) return Ok(controller);
} }
/// Creates new music controller from a config at given path /// Creates new music controller from a config at given path
pub fn from(config_path: &PathBuf) -> std::result::Result<MusicController, toml::de::Error> { pub fn from(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> {
let config = Arc::new(RwLock::new(Config::from(config_path)?)); let config = Arc::new(RwLock::new(Config::from(config_path)?));
let music_player = MusicPlayer::new(config.clone()); let music_player = MusicPlayer::new(config.clone());
let library = match MusicLibrary::init(config.clone()) {
Ok(library) => library,
Err(error) => return Err(error),
};
let controller = MusicController { let controller = MusicController {
config, config,
library,
music_player, music_player,
}; };
return Ok(controller) return Ok(controller);
} }
/// Sends given message to control music player /// Sends given message to control music player
pub fn song_control(&mut self, message: DecoderMessage) { pub fn song_control(&mut self, message: DecoderMessage) {
self.music_player.send_message(message); self.music_player.send_message(message);
} }
/// Gets status of the music player /// Gets status of the music player
pub fn player_status(&mut self) -> PlayerStatus { pub fn player_status(&mut self) -> PlayerStatus {
return self.music_player.get_status(); return self.music_player.get_status();
} }
/// Gets current song being controlled, if any /// Gets current song being controlled, if any
pub fn get_current_song(&self) -> Option<Song> { pub fn get_current_song(&self) -> Option<Song> {
return self.music_player.get_current_song(); return self.music_player.get_current_song();
} }
/// Gets audio playback volume /// Queries the [MusicLibrary], returning a `Vec<Song>`
pub fn get_vol(&self) -> f32 { pub fn query_library(
return self.music_player.music_processor.audio_volume; &self,
query_string: &String,
target_tags: Vec<Tag>,
search_location: bool,
sort_by: Vec<Tag>,
) -> Option<Vec<&Song>> {
self.library
.query_tracks(query_string, &target_tags, &sort_by)
} }
/// Sets audio playback volume on a scale of 0.0 to 1.0
pub fn set_vol(&mut self, volume: f32) {
self.music_player.music_processor.audio_volume = volume;
self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone()))));
}
} }

View file

@ -1,7 +1,7 @@
use std::{result, thread}; use std::{result, thread};
use symphonia::core::audio::{AudioBufferRef, SignalSpec, RawSample, SampleBuffer}; use symphonia::core::audio::{AudioBufferRef, RawSample, SampleBuffer, SignalSpec};
use symphonia::core::conv::{ConvertibleSample, IntoSample, FromSample}; use symphonia::core::conv::{ConvertibleSample, FromSample, IntoSample};
use symphonia::core::units::Duration; use symphonia::core::units::Duration;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
@ -25,10 +25,21 @@ pub enum AudioOutputError {
pub type Result<T> = result::Result<T, AudioOutputError>; pub type Result<T> = result::Result<T, AudioOutputError>;
pub trait OutputSample: SizedSample + FromSample<f32> + IntoSample<f32> +cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static {} pub trait OutputSample:
SizedSample
+ FromSample<f32>
+ IntoSample<f32>
+ cpal::Sample
+ ConvertibleSample
+ RawSample
+ std::marker::Send
+ 'static
{
}
pub struct AudioOutput<T> pub struct AudioOutput<T>
where T: OutputSample, where
T: OutputSample,
{ {
ring_buf_producer: rb::Producer<T>, ring_buf_producer: rb::Producer<T>,
sample_buf: SampleBuffer<T>, sample_buf: SampleBuffer<T>,
@ -48,80 +59,112 @@ impl OutputSample for f64 {}
//create a new trait with functions, then impl that somehow //create a new trait with functions, then impl that somehow
pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result<Box<dyn AudioStream>> { pub fn open_stream(spec: SignalSpec, duration: Duration) -> Result<Box<dyn AudioStream>> {
let host = cpal::default_host(); let host = cpal::default_host();
// Uses default audio device // Uses default audio device
let device = match host.default_output_device() { let device = match host.default_output_device() {
Some(device) => device, Some(device) => device,
_ => return Err(AudioOutputError::OpenStreamError), _ => return Err(AudioOutputError::OpenStreamError),
}; };
let config = match device.default_output_config() { let config = match device.default_output_config() {
Ok(config) => config, Ok(config) => config,
Err(err) => return Err(AudioOutputError::OpenStreamError), Err(err) => return Err(AudioOutputError::OpenStreamError),
}; };
return match config.sample_format(){ return match config.sample_format() {
cpal::SampleFormat::I8 => AudioOutput::<i8>::create_stream(spec, &device, &config.into(), duration), cpal::SampleFormat::I8 => {
cpal::SampleFormat::I16 => AudioOutput::<i16>::create_stream(spec, &device, &config.into(), duration), AudioOutput::<i8>::create_stream(spec, &device, &config.into(), duration)
cpal::SampleFormat::I32 => AudioOutput::<i32>::create_stream(spec, &device, &config.into(), duration), }
//cpal::SampleFormat::I64 => AudioOutput::<i64>::create_stream(spec, &device, &config.into(), duration), cpal::SampleFormat::I16 => {
cpal::SampleFormat::U8 => AudioOutput::<u8>::create_stream(spec, &device, &config.into(), duration), AudioOutput::<i16>::create_stream(spec, &device, &config.into(), duration)
cpal::SampleFormat::U16 => AudioOutput::<u16>::create_stream(spec, &device, &config.into(), duration), }
cpal::SampleFormat::U32 => AudioOutput::<u32>::create_stream(spec, &device, &config.into(), duration), cpal::SampleFormat::I32 => {
//cpal::SampleFormat::U64 => AudioOutput::<u64>::create_stream(spec, &device, &config.into(), duration), AudioOutput::<i32>::create_stream(spec, &device, &config.into(), duration)
cpal::SampleFormat::F32 => AudioOutput::<f32>::create_stream(spec, &device, &config.into(), duration), }
cpal::SampleFormat::F64 => AudioOutput::<f64>::create_stream(spec, &device, &config.into(), duration), //cpal::SampleFormat::I64 => AudioOutput::<i64>::create_stream(spec, &device, &config.into(), duration),
_ => todo!(), cpal::SampleFormat::U8 => {
}; AudioOutput::<u8>::create_stream(spec, &device, &config.into(), duration)
}
cpal::SampleFormat::U16 => {
AudioOutput::<u16>::create_stream(spec, &device, &config.into(), duration)
}
cpal::SampleFormat::U32 => {
AudioOutput::<u32>::create_stream(spec, &device, &config.into(), duration)
}
//cpal::SampleFormat::U64 => AudioOutput::<u64>::create_stream(spec, &device, &config.into(), duration),
cpal::SampleFormat::F32 => {
AudioOutput::<f32>::create_stream(spec, &device, &config.into(), duration)
}
cpal::SampleFormat::F64 => {
AudioOutput::<f64>::create_stream(spec, &device, &config.into(), duration)
}
_ => todo!(),
};
} }
impl<T: OutputSample> AudioOutput<T> { impl<T: OutputSample> AudioOutput<T> {
// Creates the stream (TODO: Merge w/open_stream?) // Creates the stream (TODO: Merge w/open_stream?)
fn create_stream(spec: SignalSpec, device: &cpal::Device, config: &cpal::StreamConfig, duration: Duration) -> Result<Box<dyn AudioStream>> { fn create_stream(
spec: SignalSpec,
device: &cpal::Device,
config: &cpal::StreamConfig,
duration: Duration,
) -> Result<Box<dyn AudioStream>> {
let num_channels = config.channels as usize; let num_channels = config.channels as usize;
// Ring buffer is created with 200ms audio capacity // Ring buffer is created with 200ms audio capacity
let ring_len = ((50 * 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 = rb::SpscRb::new(ring_len);
let ring_buf_producer = ring_buf.producer(); let ring_buf_producer = ring_buf.producer();
let ring_buf_consumer = ring_buf.consumer(); let ring_buf_consumer = ring_buf.consumer();
let stream_result = device.build_output_stream( let stream_result = device.build_output_stream(
config, config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| { move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
// Writes samples in the ring buffer to the audio output // Writes samples in the ring buffer to the audio output
let written = ring_buf_consumer.read(data).unwrap_or(0); let written = ring_buf_consumer.read(data).unwrap_or(0);
// Mutes non-written samples // Mutes non-written samples
data[written..].iter_mut().for_each(|sample| *sample = T::MID); data[written..]
.iter_mut()
.for_each(|sample| *sample = T::MID);
}, },
//TODO: Handle error here properly //TODO: Handle error here properly
move |err| println!("Yeah we erroring out here"), move |err| println!("Yeah we erroring out here"),
None None,
); );
if let Err(err) = stream_result { if let Err(err) = stream_result {
return Err(AudioOutputError::OpenStreamError); return Err(AudioOutputError::OpenStreamError);
} }
let stream = stream_result.unwrap(); let stream = stream_result.unwrap();
//Start output stream //Start output stream
if let Err(err) = stream.play() { if let Err(err) = stream.play() {
return Err(AudioOutputError::PlayStreamError); return Err(AudioOutputError::PlayStreamError);
} }
let sample_buf = SampleBuffer::<T>::new(duration, spec); let sample_buf = SampleBuffer::<T>::new(duration, spec);
let mut resampler = None; let mut resampler = None;
if spec.rate != config.sample_rate.0 { if spec.rate != config.sample_rate.0 {
println!("Resampling enabled"); println!("Resampling enabled");
resampler = Some(Resampler::new(spec, config.sample_rate.0 as usize, duration)) resampler = Some(Resampler::new(
spec,
config.sample_rate.0 as usize,
duration,
))
} }
Ok(Box::new(AudioOutput { ring_buf_producer, sample_buf, stream, resampler})) Ok(Box::new(AudioOutput {
ring_buf_producer,
sample_buf,
stream,
resampler,
}))
} }
} }
@ -131,7 +174,7 @@ impl<T: OutputSample> AudioStream for AudioOutput<T> {
if decoded.frames() == 0 { if decoded.frames() == 0 {
return Ok(()); return Ok(());
} }
let mut samples: &[T] = if let Some(resampler) = &mut self.resampler { let mut samples: &[T] = if let Some(resampler) = &mut self.resampler {
// Resamples if required // Resamples if required
match resampler.resample(decoded) { match resampler.resample(decoded) {
@ -142,25 +185,25 @@ impl<T: OutputSample> AudioStream for AudioOutput<T> {
self.sample_buf.copy_interleaved_ref(decoded); self.sample_buf.copy_interleaved_ref(decoded);
self.sample_buf.samples() self.sample_buf.samples()
}; };
// Write samples into ring buffer // Write samples into ring buffer
while let Some(written) = self.ring_buf_producer.write_blocking(samples) { while let Some(written) = self.ring_buf_producer.write_blocking(samples) {
samples = &samples[written..]; samples = &samples[written..];
} }
Ok(()) Ok(())
} }
// Flushes resampler if needed // Flushes resampler if needed
fn flush(&mut self) { fn flush(&mut self) {
if let Some(resampler) = &mut self.resampler { if let Some(resampler) = &mut self.resampler {
let mut stale_samples = resampler.flush().unwrap_or_default(); let mut stale_samples = resampler.flush().unwrap_or_default();
while let Some(written) = self.ring_buf_producer.write_blocking(stale_samples) { while let Some(written) = self.ring_buf_producer.write_blocking(stale_samples) {
stale_samples = &stale_samples[written..]; stale_samples = &stale_samples[written..];
} }
} }
let _ = self.stream.pause(); let _ = self.stream.pause();
} }
} }

View file

@ -1,31 +1,31 @@
use std::sync::mpsc::{self, Sender, Receiver}; use std::io::SeekFrom;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread; use std::thread;
use std::io::SeekFrom;
use async_std::io::ReadExt; use async_std::io::ReadExt;
use async_std::task; use async_std::task;
use futures::future::join_all; use futures::future::join_all;
use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions, Decoder}; use symphonia::core::codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL};
use symphonia::core::errors::Error;
use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo}; use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo};
use symphonia::core::io::{MediaSourceStream, MediaSource}; use symphonia::core::io::{MediaSource, MediaSourceStream};
use symphonia::core::meta::MetadataOptions; use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint; use symphonia::core::probe::Hint;
use symphonia::core::errors::Error;
use symphonia::core::units::{Time, TimeBase}; use symphonia::core::units::{Time, TimeBase};
use futures::AsyncBufRead; use futures::AsyncBufRead;
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use crate::music_player::music_output::AudioStream; use crate::music_player::music_output::AudioStream;
use crate::music_processor::music_processor::MusicProcessor; use crate::music_storage::music_db::{Song, URI};
use crate::music_storage::music_db::{URI, Song}; use crate::music_tracker::music_tracker::{
use crate::music_tracker::music_tracker::{MusicTracker, TrackerError, LastFM, DiscordRPC, ListenBrainz}; DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError,
};
// Struct that controls playback of music // Struct that controls playback of music
pub struct MusicPlayer { pub struct MusicPlayer {
pub music_processor: MusicProcessor,
player_status: PlayerStatus, player_status: PlayerStatus,
music_trackers: Vec<Box<dyn MusicTracker + Send>>, music_trackers: Vec<Box<dyn MusicTracker + Send>>,
current_song: Arc<RwLock<Option<Song>>>, current_song: Arc<RwLock<Option<Song>>>,
@ -49,18 +49,12 @@ pub enum DecoderMessage {
Pause, Pause,
Stop, Stop,
SeekTo(u64), SeekTo(u64),
DSP(DSPMessage)
} }
#[derive(Clone)] #[derive(Clone)]
pub enum TrackerMessage { pub enum TrackerMessage {
Track(Song), Track(Song),
TrackNow(Song) TrackNow(Song),
}
#[derive(Debug, Clone)]
pub enum DSPMessage {
UpdateProcessor(Box<MusicProcessor>)
} }
// Holds a song decoder reader, etc // Holds a song decoder reader, etc
@ -75,48 +69,59 @@ struct SongHandler {
impl SongHandler { impl SongHandler {
pub fn new(uri: &URI) -> Result<Self, ()> { pub fn new(uri: &URI) -> Result<Self, ()> {
// Opens remote/local source and creates MediaSource for symphonia // Opens remote/local source and creates MediaSource for symphonia
let config = RemoteOptions {media_buffer_len: 10000, forward_buffer_len: 10000}; let config = RemoteOptions {
media_buffer_len: 10000,
forward_buffer_len: 10000,
};
let src: Box<dyn MediaSource> = match uri { let src: Box<dyn MediaSource> = match uri {
URI::Local(path) => { URI::Local(path) => match std::fs::File::open(path) {
match std::fs::File::open(path) { Ok(file) => Box::new(file),
Ok(file) => Box::new(file), Err(_) => return Err(()),
Err(_) => return Err(()),
}
}, },
URI::Remote(_, location) => { URI::Remote(_, location) => {
match RemoteSource::new(location.as_ref(), &config) { match RemoteSource::new(location.to_str().unwrap(), &config) {
Ok(remote_source) => Box::new(remote_source), Ok(remote_source) => Box::new(remote_source),
Err(_) => return Err(()), Err(_) => return Err(()),
} }
}, }
_ => todo!(),
}; };
let mss = MediaSourceStream::new(src, Default::default()); let mss = MediaSourceStream::new(src, Default::default());
// Use default metadata and format options // Use default metadata and format options
let meta_opts: MetadataOptions = Default::default(); let meta_opts: MetadataOptions = Default::default();
let fmt_opts: FormatOptions = Default::default(); let fmt_opts: FormatOptions = Default::default();
let mut hint = Hint::new(); let hint = Hint::new();
let probed = symphonia::default::get_probe().format(&hint, mss, &fmt_opts, &meta_opts).expect("Unsupported format"); let probed = symphonia::default::get_probe()
.format(&hint, mss, &fmt_opts, &meta_opts)
let mut reader = probed.format; .expect("Unsupported format");
let track = reader.tracks() let reader = probed.format;
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL) let track = reader
.expect("no supported audio tracks"); .tracks()
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.expect("no supported audio tracks");
let time_base = track.codec_params.time_base; let time_base = track.codec_params.time_base;
let duration = track.codec_params.n_frames; let duration = track.codec_params.n_frames;
let dec_opts: DecoderOptions = Default::default(); let dec_opts: DecoderOptions = Default::default();
let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) let decoder = symphonia::default::get_codecs()
.expect("unsupported codec"); .make(&track.codec_params, &dec_opts)
.expect("unsupported codec");
return Ok(SongHandler {reader, decoder, time_base, duration});
return Ok(SongHandler {
reader,
decoder,
time_base,
duration,
});
} }
} }
@ -126,11 +131,15 @@ impl MusicPlayer {
let (message_sender, message_receiver) = mpsc::channel(); let (message_sender, message_receiver) = mpsc::channel();
let (status_sender, status_receiver) = mpsc::channel(); let (status_sender, status_receiver) = mpsc::channel();
let current_song = Arc::new(RwLock::new(None)); let current_song = Arc::new(RwLock::new(None));
MusicPlayer::start_player(message_receiver, status_sender, config.clone(), current_song.clone()); MusicPlayer::start_player(
message_receiver,
status_sender,
config.clone(),
current_song.clone(),
);
MusicPlayer { MusicPlayer {
music_processor: MusicProcessor::new(),
music_trackers: Vec::new(), music_trackers: Vec::new(),
player_status: PlayerStatus::Stopped, player_status: PlayerStatus::Stopped,
current_song, current_song,
@ -139,15 +148,19 @@ impl MusicPlayer {
config, config,
} }
} }
fn start_tracker(status_sender: Sender<Result<(), TrackerError>>, tracker_receiver: Receiver<TrackerMessage>, config: Arc<RwLock<Config>>) { fn start_tracker(
status_sender: Sender<Result<(), TrackerError>>,
tracker_receiver: Receiver<TrackerMessage>,
config: Arc<RwLock<Config>>,
) {
thread::spawn(move || { thread::spawn(move || {
let global_config = &*config.read().unwrap(); let global_config = &*config.read().unwrap();
// Sets local config for trackers to detect changes // Sets local config for trackers to detect changes
let local_config = global_config.clone(); let local_config = global_config.clone();
let mut trackers: Vec<Box<dyn MusicTracker>> = Vec::new(); let mut trackers: Vec<Box<dyn MusicTracker>> = Vec::new();
// Updates local trackers to the music controller config // TODO: refactor // Updates local trackers to the music controller config // TODO: refactor
let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>|{ let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>| {
if let Some(lastfm_config) = global_config.lastfm.clone() { if let Some(lastfm_config) = global_config.lastfm.clone() {
if lastfm_config.enabled { if lastfm_config.enabled {
trackers.push(Box::new(LastFM::new(&lastfm_config))); trackers.push(Box::new(LastFM::new(&lastfm_config)));
@ -170,20 +183,24 @@ impl MusicPlayer {
if local_config != global_config { if local_config != global_config {
update_trackers(&mut trackers); update_trackers(&mut trackers);
} }
let mut results = Vec::new(); let mut results = Vec::new();
task::block_on(async { task::block_on(async {
let mut futures = Vec::new(); let mut futures = Vec::new();
for tracker in trackers.iter_mut() { for tracker in trackers.iter_mut() {
match message.clone() { match message.clone() {
Ok(TrackerMessage::Track(song)) => futures.push(tracker.track_song(song)), Ok(TrackerMessage::Track(song)) => {
Ok(TrackerMessage::TrackNow(song)) => futures.push(tracker.track_now(song)), futures.push(tracker.track_song(song))
Err(_) => {}, }
Ok(TrackerMessage::TrackNow(song)) => {
futures.push(tracker.track_now(song))
}
Err(_) => {}
} }
} }
results = join_all(futures).await; results = join_all(futures).await;
}); });
for result in results { for result in results {
status_sender.send(result).unwrap_or_default() status_sender.send(result).unwrap_or_default()
} }
@ -191,30 +208,39 @@ impl MusicPlayer {
} }
}); });
} }
// Opens and plays song with given path in separate thread // Opens and plays song with given path in separate thread
fn start_player(message_receiver: Receiver<DecoderMessage>, status_sender: Sender<PlayerStatus>, config: Arc<RwLock<Config>>, current_song: Arc<RwLock<Option<Song>>>) { fn start_player(
message_receiver: Receiver<DecoderMessage>,
status_sender: Sender<PlayerStatus>,
config: Arc<RwLock<Config>>,
current_song: Arc<RwLock<Option<Song>>>,
) {
// Creates thread that audio is decoded in // Creates thread that audio is decoded in
thread::spawn(move || { thread::spawn(move || {
let current_song = current_song; let current_song = current_song;
let mut song_handler = None; let mut song_handler = None;
let mut seek_time: Option<u64> = None; let mut seek_time: Option<u64> = None;
let mut audio_output: Option<Box<dyn AudioStream>> = None; let mut audio_output: Option<Box<dyn AudioStream>> = None;
let mut music_processor = MusicProcessor::new(); let (tracker_sender, tracker_receiver): (
Sender<TrackerMessage>,
let (tracker_sender, tracker_receiver): (Sender<TrackerMessage>, Receiver<TrackerMessage>) = mpsc::channel(); Receiver<TrackerMessage>,
let (tracker_status_sender, tracker_status_receiver): (Sender<Result<(), TrackerError>>, Receiver<Result<(), TrackerError>>) = mpsc::channel(); ) = mpsc::channel();
let (tracker_status_sender, tracker_status_receiver): (
Sender<Result<(), TrackerError>>,
Receiver<Result<(), TrackerError>>,
) = mpsc::channel();
MusicPlayer::start_tracker(tracker_status_sender, tracker_receiver, config); MusicPlayer::start_tracker(tracker_status_sender, tracker_receiver, config);
let mut song_tracked = false; let mut song_tracked = false;
let mut song_time = 0.0; let mut song_time = 0.0;
let mut paused = true; let mut paused = true;
'main_decode: loop { 'main_decode: loop {
'handle_message: loop { 'handle_message: loop {
let message = if paused { let message = if paused {
// Pauses playback by blocking on waiting for new player messages // Pauses playback by blocking on waiting for new player messages
@ -232,7 +258,7 @@ impl MusicPlayer {
// Handles message received from MusicPlayer struct // Handles message received from MusicPlayer struct
match message { match message {
Some(DecoderMessage::OpenSong(song)) => { Some(DecoderMessage::OpenSong(song)) => {
let song_uri = song.path.clone(); let song_uri = song.location.clone();
match SongHandler::new(&song_uri) { match SongHandler::new(&song_uri) {
Ok(new_handler) => { Ok(new_handler) => {
song_handler = Some(new_handler); song_handler = Some(new_handler);
@ -253,26 +279,29 @@ impl MusicPlayer {
status_sender.send(PlayerStatus::Paused).unwrap(); status_sender.send(PlayerStatus::Paused).unwrap();
} }
Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time), Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time),
Some(DecoderMessage::DSP(dsp_message)) => {
match dsp_message {
DSPMessage::UpdateProcessor(new_processor) => music_processor = *new_processor,
}
}
// Exits main decode loop and subsequently ends thread // Exits main decode loop and subsequently ends thread
Some(DecoderMessage::Stop) => { Some(DecoderMessage::Stop) => {
status_sender.send(PlayerStatus::Stopped).unwrap(); status_sender.send(PlayerStatus::Stopped).unwrap();
break 'main_decode break 'main_decode;
} }
None => {}, None => {}
} }
status_sender.send(PlayerStatus::Error).unwrap(); status_sender.send(PlayerStatus::Error).unwrap();
} }
// In theory this check should not need to occur? // In theory this check should not need to occur?
if let (Some(song_handler), current_song) = (&mut song_handler, &*current_song.read().unwrap()) { if let (Some(song_handler), current_song) =
(&mut song_handler, &*current_song.read().unwrap())
{
match seek_time { match seek_time {
Some(time) => { Some(time) => {
let seek_to = SeekTo::Time { time: Time::from(time), track_id: Some(0) }; let seek_to = SeekTo::Time {
song_handler.reader.seek(SeekMode::Accurate, seek_to).unwrap(); time: Time::from(time),
track_id: Some(0),
};
song_handler
.reader
.seek(SeekMode::Accurate, seek_to)
.unwrap();
seek_time = None; seek_time = None;
} }
None => {} //Nothing to do! None => {} //Nothing to do!
@ -285,60 +314,58 @@ impl MusicPlayer {
panic!("{}", err); panic!("{}", err);
} }
}; };
if let (Some(time_base), Some(song)) = (song_handler.time_base, current_song) { if let (Some(time_base), Some(song)) = (song_handler.time_base, current_song) {
let time_units = time_base.calc_time(packet.ts); let time_units = time_base.calc_time(packet.ts);
song_time = time_units.seconds as f64 + time_units.frac; song_time = time_units.seconds as f64 + time_units.frac;
// Tracks song now if song has just started // Tracks song now if song has just started
if song_time == 0.0 { if song_time == 0.0 {
tracker_sender.send(TrackerMessage::TrackNow(song.clone())).unwrap(); tracker_sender
.send(TrackerMessage::TrackNow(song.clone()))
.unwrap();
} }
if let Some(duration) = song_handler.duration { if let Some(duration) = song_handler.duration {
let song_duration = time_base.calc_time(duration); let song_duration = time_base.calc_time(duration);
let song_duration_secs = song_duration.seconds as f64 + song_duration.frac; let song_duration_secs =
song_duration.seconds as f64 + song_duration.frac;
// Tracks song if current time is past half of total song duration or past 4 minutes // Tracks song if current time is past half of total song duration or past 4 minutes
if (song_duration_secs / 2.0 < song_time || song_time > 240.0) && !song_tracked { if (song_duration_secs / 2.0 < song_time || song_time > 240.0)
&& !song_tracked
{
song_tracked = true; song_tracked = true;
tracker_sender.send(TrackerMessage::Track(song.clone())).unwrap(); tracker_sender
.send(TrackerMessage::Track(song.clone()))
.unwrap();
} }
} }
} }
status_sender.send(PlayerStatus::Playing(song_time)).unwrap(); status_sender
.send(PlayerStatus::Playing(song_time))
.unwrap();
match song_handler.decoder.decode(&packet) { match song_handler.decoder.decode(&packet) {
Ok(decoded) => { Ok(decoded) => {
// Opens audio stream if there is not one // Opens audio stream if there is not one
if audio_output.is_none() { if audio_output.is_none() {
let spec = *decoded.spec(); let spec = *decoded.spec();
let duration = decoded.capacity() as u64; let duration = decoded.capacity() as u64;
audio_output.replace(crate::music_player::music_output::open_stream(spec, duration).unwrap()); audio_output.replace(
crate::music_player::music_output::open_stream(spec, duration)
.unwrap(),
);
} }
// Handles audio normally provided there is an audio stream }
if let Some(ref mut audio_output) = audio_output {
// Changes buffer of the MusicProcessor if the packet has a differing capacity or spec
if music_processor.audio_buffer.capacity() != decoded.capacity() ||music_processor.audio_buffer.spec() != decoded.spec() {
let spec = *decoded.spec();
let duration = decoded.capacity() as u64;
music_processor.set_buffer(duration, spec);
}
let transformed_audio = music_processor.process(&decoded);
// Writes transformed packet to audio out
audio_output.write(transformed_audio).unwrap()
}
},
Err(Error::IoError(_)) => { Err(Error::IoError(_)) => {
// rest in peace packet // rest in peace packet
continue; continue;
}, }
Err(Error::DecodeError(_)) => { Err(Error::DecodeError(_)) => {
// may you one day be decoded // may you one day be decoded
continue; continue;
}, }
Err(err) => { Err(err) => {
// Unrecoverable, though shouldn't panic here // Unrecoverable, though shouldn't panic here
panic!("{}", err); panic!("{}", err);
@ -348,28 +375,28 @@ impl MusicPlayer {
} }
}); });
} }
// Updates status by checking on messages from spawned thread // Updates status by checking on messages from spawned thread
fn update_player(&mut self) { fn update_player(&mut self) {
for message in self.status_receiver.try_recv() { for message in self.status_receiver.try_recv() {
self.player_status = message; self.player_status = message;
} }
} }
pub fn get_current_song(&self) -> Option<Song>{ pub fn get_current_song(&self) -> Option<Song> {
match self.current_song.try_read() { match self.current_song.try_read() {
Ok(song) => return (*song).clone(), Ok(song) => return (*song).clone(),
Err(_) => return None, Err(_) => return None,
} }
} }
// Sends message to spawned thread // Sends message to spawned thread
pub fn send_message(&mut self, message: DecoderMessage) { pub fn send_message(&mut self, message: DecoderMessage) {
self.update_player(); self.update_player();
// Checks that message sender exists before sending a message off // Checks that message sender exists before sending a message off
self.message_sender.send(message).unwrap(); self.message_sender.send(message).unwrap();
} }
pub fn get_status(&mut self) -> PlayerStatus { pub fn get_status(&mut self) -> PlayerStatus {
self.update_player(); self.update_player();
return self.player_status; return self.player_status;
@ -393,7 +420,7 @@ impl Default for RemoteOptions {
media_buffer_len: 100000, media_buffer_len: 100000,
forward_buffer_len: 1024, forward_buffer_len: 1024,
} }
} }
} }
/// A remote source of media /// A remote source of media
@ -407,12 +434,12 @@ struct RemoteSource {
impl RemoteSource { impl RemoteSource {
/// Creates a new RemoteSource with given uri and configuration /// Creates a new RemoteSource with given uri and configuration
pub fn new(uri: &str, config: &RemoteOptions) -> Result<Self, surf::Error> { pub fn new(uri: &str, config: &RemoteOptions) -> Result<Self, surf::Error> {
let mut response = task::block_on(async { let mut response = task::block_on(async {
return surf::get(uri).await; return surf::get(uri).await;
})?; })?;
let reader = response.take_body().into_reader(); let reader = response.take_body().into_reader();
Ok(RemoteSource { Ok(RemoteSource {
reader, reader,
media_buffer: Vec::new(), media_buffer: Vec::new(),
@ -432,16 +459,16 @@ impl std::io::Read for RemoteSource {
Ok(_) => { Ok(_) => {
self.media_buffer.extend_from_slice(&buffer); self.media_buffer.extend_from_slice(&buffer);
return Ok(()); return Ok(());
}, }
Err(err) => return Err(err), Err(err) => return Err(err),
} }
}); });
match read_bytes { match read_bytes {
Err(err) => return Err(err), Err(err) => return Err(err),
_ => {}, _ => {}
} }
} }
// Reads bytes from the media buffer into the buffer given by // Reads bytes from the media buffer into the buffer given by
let mut bytes_read = 0; let mut bytes_read = 0;
for location in 0..1024 { for location in 0..1024 {
if (location + self.offset as usize) < self.media_buffer.len() { if (location + self.offset as usize) < self.media_buffer.len() {
@ -449,7 +476,7 @@ impl std::io::Read for RemoteSource {
bytes_read += 1; bytes_read += 1;
} }
} }
self.offset += bytes_read; self.offset += bytes_read;
return Ok(bytes_read as usize); return Ok(bytes_read as usize);
} }
@ -462,13 +489,13 @@ impl std::io::Seek for RemoteSource {
match pos { match pos {
// Offset is set to given position // Offset is set to given position
SeekFrom::Start(pos) => { SeekFrom::Start(pos) => {
if pos > self.media_buffer.len() as u64{ if pos > self.media_buffer.len() as u64 {
self.offset = self.media_buffer.len() as u64; self.offset = self.media_buffer.len() as u64;
} else { } else {
self.offset = pos; self.offset = pos;
} }
return Ok(self.offset); return Ok(self.offset);
}, }
// Offset is set to length of buffer + given position // Offset is set to length of buffer + given position
SeekFrom::End(pos) => { SeekFrom::End(pos) => {
if self.media_buffer.len() as u64 + pos as u64 > self.media_buffer.len() as u64 { if self.media_buffer.len() as u64 + pos as u64 > self.media_buffer.len() as u64 {
@ -477,16 +504,16 @@ impl std::io::Seek for RemoteSource {
self.offset = self.media_buffer.len() as u64 + pos as u64; self.offset = self.media_buffer.len() as u64 + pos as u64;
} }
return Ok(self.offset); return Ok(self.offset);
}, }
// Offset is set to current offset + given position // Offset is set to current offset + given position
SeekFrom::Current(pos) => { SeekFrom::Current(pos) => {
if self.offset + pos as u64 > self.media_buffer.len() as u64{ if self.offset + pos as u64 > self.media_buffer.len() as u64 {
self.offset = self.media_buffer.len() as u64; self.offset = self.media_buffer.len() as u64;
} else { } else {
self.offset += pos as u64 self.offset += pos as u64
} }
return Ok(self.offset); return Ok(self.offset);
}, }
} }
} }
} }
@ -495,7 +522,7 @@ impl MediaSource for RemoteSource {
fn is_seekable(&self) -> bool { fn is_seekable(&self) -> bool {
return true; return true;
} }
fn byte_len(&self) -> Option<u64> { fn byte_len(&self) -> Option<u64> {
return None; return None;
} }

View file

@ -48,7 +48,8 @@ where
// Interleave the planar samples from Rubato. // Interleave the planar samples from Rubato.
let num_channels = self.output.len(); let num_channels = self.output.len();
self.interleaved.resize(num_channels * self.output[0].len(), T::MID); self.interleaved
.resize(num_channels * self.output[0].len(), T::MID);
for (i, frame) in self.interleaved.chunks_exact_mut(num_channels).enumerate() { for (i, frame) in self.interleaved.chunks_exact_mut(num_channels).enumerate() {
for (ch, s) in frame.iter_mut().enumerate() { for (ch, s) in frame.iter_mut().enumerate() {
@ -81,7 +82,13 @@ where
let input = vec![Vec::with_capacity(duration); num_channels]; let input = vec![Vec::with_capacity(duration); num_channels];
Self { resampler, input, output, duration, interleaved: Default::default() } Self {
resampler,
input,
output,
duration,
interleaved: Default::default(),
}
} }
/// Resamples a planar/non-interleaved input. /// Resamples a planar/non-interleaved input.
@ -144,4 +151,4 @@ where
let src = input.chan(c); let src = input.chan(c);
dst.extend(src.iter().map(|&s| s.into_sample())); dst.extend(src.iter().map(|&s| s.into_sample()));
} }
} }

View file

@ -1,43 +0,0 @@
use std::fmt::Debug;
use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec};
#[derive(Clone)]
pub struct MusicProcessor {
pub audio_buffer: AudioBuffer<f32>,
pub audio_volume: f32,
}
impl Debug for MusicProcessor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MusicProcessor").field("audio_volume", &self.audio_volume).finish()
}
}
impl MusicProcessor {
/// Returns new MusicProcessor with blank buffer and 100% volume
pub fn new() -> Self {
MusicProcessor {
audio_buffer: AudioBuffer::unused(),
audio_volume: 1.0,
}
}
/// Processes audio samples
///
/// Currently only supports transformations of volume
pub fn process(&mut self, audio_buffer_ref: &AudioBufferRef) -> AudioBufferRef {
audio_buffer_ref.convert(&mut self.audio_buffer);
let process = |sample| sample * self.audio_volume;
self.audio_buffer.transform(process);
return self.audio_buffer.as_audio_buffer_ref();
}
/// Sets buffer of the MusicProcessor
pub fn set_buffer(&mut self, duration: u64, spec: SignalSpec) {
self.audio_buffer = AudioBuffer::new(duration, spec);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,27 +1,6 @@
use std::path::Path;
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use rusqlite::{params, Connection}; use std::path::Path;
pub fn playlist_add( pub fn playlist_add(config: &Config, playlist_name: &str, song_paths: &Vec<&Path>) {
config: &Config, unimplemented!()
playlist_name: &str,
song_paths: &Vec<&Path>
) {
let db_connection = Connection::open(&*config.db_path).unwrap();
for song_path in song_paths {
db_connection.execute(
"INSERT INTO playlists (
playlist_name,
song_path
) VALUES (
?1,
?2
)",
params![
playlist_name,
song_path.to_str().unwrap()
],
).unwrap();
}
} }

View file

@ -0,0 +1,97 @@
use std::io::{BufReader, BufWriter};
use std::{error::Error, fs, path::PathBuf};
use walkdir::WalkDir;
use file_format::{FileFormat, Kind};
use snap;
use super::music_db::{Song, AlbumArt, URI};
use unidecode::unidecode;
pub(super) fn normalize(input_string: &String) -> String {
let mut normalized = unidecode(input_string);
// Remove non alphanumeric characters
normalized.retain(|c| c.is_alphanumeric());
normalized
}
pub(super) fn read_library(path: PathBuf) -> Result<Vec<Song>, Box<dyn Error>> {
// Create a new snap reader over the database file
let database = fs::File::open(path)?;
let reader = BufReader::new(database);
let mut d = snap::read::FrameDecoder::new(reader);
// Decode the library from the serialized data into the vec
let library: Vec<Song> = bincode::serde::decode_from_std_read(
&mut d,
bincode::config::standard()
.with_little_endian()
.with_variable_int_encoding(),
)?;
Ok(library)
}
pub(super) fn write_library(
library: &Vec<Song>,
path: PathBuf,
take_backup: bool,
) -> Result<(), Box<dyn Error>> {
// Create 2 new names for the file, a temporary one for writing out, and a backup
let mut writer_name = path.clone();
writer_name.set_extension("tmp");
let mut backup_name = path.clone();
backup_name.set_extension("bkp");
// Create a new BufWriter on the file and make a snap frame encoer for it too
let writer = BufWriter::new(fs::File::create(writer_name.to_path_buf())?);
let mut e = snap::write::FrameEncoder::new(writer);
// Write out the data using bincode
bincode::serde::encode_into_std_write(
&library,
&mut e,
bincode::config::standard()
.with_little_endian()
.with_variable_int_encoding(),
)?;
if path.exists() && take_backup {
fs::rename(&path, backup_name)?;
}
fs::rename(writer_name, &path)?;
Ok(())
}
pub fn find_images(song_path: &PathBuf) -> Result<Vec<AlbumArt>, Box<dyn Error>> {
let mut images: Vec<AlbumArt> = Vec::new();
let song_dir = song_path.parent().ok_or("")?;
for target_file in WalkDir::new(song_dir)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
if target_file.depth() >= 3 { // Don't recurse very deep
break
}
let path = target_file.path();
if !path.is_file() {
continue;
}
let format = FileFormat::from_file(&path)?.kind();
if format != Kind::Image {
break
}
let image_uri = URI::Local(path.to_path_buf().canonicalize().unwrap());
images.push(AlbumArt::External(image_uri));
}
Ok(images)
}

View file

@ -1,26 +1,26 @@
use std::time::{SystemTime, UNIX_EPOCH};
use std::collections::BTreeMap;
use serde_json::json; use serde_json::json;
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use async_trait::async_trait; use async_trait::async_trait;
use serde::{Serialize, Deserialize}; use discord_presence::Event;
use md5::{Md5, Digest}; use md5::{Digest, Md5};
use discord_presence::{Event}; use serde::{Deserialize, Serialize};
use surf::StatusCode; use surf::StatusCode;
use crate::music_storage::music_db::Song; use crate::music_storage::music_db::{Song, Tag};
#[async_trait] #[async_trait]
pub trait MusicTracker { pub trait MusicTracker {
/// Adds one listen to a song halfway through playback /// Adds one listen to a song halfway through playback
async fn track_song(&mut self, song: Song) -> Result<(), TrackerError>; async fn track_song(&mut self, song: Song) -> Result<(), TrackerError>;
/// Adds a 'listening' status to the music tracker service of choice /// Adds a 'listening' status to the music tracker service of choice
async fn track_now(&mut self, song: Song) -> Result<(), TrackerError>; async fn track_now(&mut self, song: Song) -> Result<(), TrackerError>;
/// Reads config files, and attempts authentication with service /// Reads config files, and attempts authentication with service
async fn test_tracker(&mut self) -> Result<(), TrackerError>; async fn test_tracker(&mut self) -> Result<(), TrackerError>;
/// Returns plays for a given song according to tracker service /// Returns plays for a given song according to tracker service
async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError>; async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError>;
} }
@ -50,7 +50,7 @@ impl TrackerError {
StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable, StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable,
StatusCode::NotFound => TrackerError::ServiceUnavailable, StatusCode::NotFound => TrackerError::ServiceUnavailable,
_ => TrackerError::Unknown, _ => TrackerError::Unknown,
} };
} }
} }
@ -63,62 +63,66 @@ pub struct LastFMConfig {
} }
pub struct LastFM { pub struct LastFM {
config: LastFMConfig config: LastFMConfig,
} }
#[async_trait] #[async_trait]
impl MusicTracker for LastFM { impl MusicTracker for LastFM {
async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> {
let mut params: BTreeMap<&str, &str> = BTreeMap::new(); let mut params: BTreeMap<&str, &str> = BTreeMap::new();
// Sets timestamp of song beginning play time // Sets timestamp of song beginning play time
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Your time is off.")
.as_secs()
- 30;
let string_timestamp = timestamp.to_string(); let string_timestamp = timestamp.to_string();
let (artist, track) = match (song.artist, song.title) { let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) {
(Some(artist), Some(track)) => (artist, track), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong),
}; };
params.insert("method", "track.scrobble"); params.insert("method", "track.scrobble");
params.insert("artist", &artist); params.insert("artist", &artist);
params.insert("track", &track); params.insert("track", &track);
params.insert("timestamp", &string_timestamp); params.insert("timestamp", &string_timestamp);
return match self.api_request(params).await { return match self.api_request(params).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(TrackerError::from_surf_error(err)), Err(err) => Err(TrackerError::from_surf_error(err)),
} };
} }
async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
let mut params: BTreeMap<&str, &str> = BTreeMap::new(); let mut params: BTreeMap<&str, &str> = BTreeMap::new();
let (artist, track) = match (song.artist, song.title) { let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) {
(Some(artist), Some(track)) => (artist, track), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong),
}; };
params.insert("method", "track.updateNowPlaying"); params.insert("method", "track.updateNowPlaying");
params.insert("artist", &artist); params.insert("artist", &artist);
params.insert("track", &track); params.insert("track", &track);
return match self.api_request(params).await { return match self.api_request(params).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(TrackerError::from_surf_error(err)), Err(err) => Err(TrackerError::from_surf_error(err)),
} };
} }
async fn test_tracker(&mut self) -> Result<(), TrackerError> { async fn test_tracker(&mut self) -> Result<(), TrackerError> {
let mut params: BTreeMap<&str, &str> = BTreeMap::new(); let mut params: BTreeMap<&str, &str> = BTreeMap::new();
params.insert("method", "chart.getTopArtists"); params.insert("method", "chart.getTopArtists");
return match self.api_request(params).await { return match self.api_request(params).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(TrackerError::from_surf_error(err)), Err(err) => Err(TrackerError::from_surf_error(err)),
} };
} }
async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> {
todo!(); todo!();
} }
@ -126,7 +130,7 @@ impl MusicTracker for LastFM {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct AuthToken { struct AuthToken {
token: String token: String,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -138,72 +142,85 @@ struct SessionResponse {
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
struct Session { struct Session {
session: SessionResponse session: SessionResponse,
} }
impl LastFM { impl LastFM {
/// Returns a url to be approved by the user along with the auth token /// Returns a url to be approved by the user along with the auth token
pub async fn get_auth(api_key: &String) -> Result<String, surf::Error> { pub async fn get_auth(api_key: &String) -> Result<String, surf::Error> {
let method = String::from("auth.gettoken"); let method = String::from("auth.gettoken");
let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&format=json"); let api_request_url = format!(
"http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&format=json"
);
let auth_token: AuthToken = surf::get(api_request_url).await?.body_json().await?; let auth_token: AuthToken = surf::get(api_request_url).await?.body_json().await?;
let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token); let auth_url = format!(
"http://www.last.fm/api/auth/?api_key={api_key}&token={}",
auth_token.token
);
return Ok(auth_url); return Ok(auth_url);
} }
/// Returns a LastFM session key /// Returns a LastFM session key
pub async fn get_session_key(api_key: &String, shared_secret: &String, auth_token: &String) -> Result<String, surf::Error> { pub async fn get_session_key(
api_key: &String,
shared_secret: &String,
auth_token: &String,
) -> Result<String, surf::Error> {
let method = String::from("auth.getSession"); let method = String::from("auth.getSession");
// Creates api_sig as defined in last.fm documentation // Creates api_sig as defined in last.fm documentation
let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}"); let api_sig =
format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}");
// Creates insecure MD5 hash for last.fm api sig // Creates insecure MD5 hash for last.fm api sig
let mut hasher = Md5::new(); let mut hasher = Md5::new();
hasher.update(api_sig); hasher.update(api_sig);
let hash_result = hasher.finalize(); let hash_result = hasher.finalize();
let hex_string_hash = format!("{:#02x}", hash_result); let hex_string_hash = format!("{:#02x}", hash_result);
let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&token={auth_token}&api_sig={hex_string_hash}&format=json"); let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&token={auth_token}&api_sig={hex_string_hash}&format=json");
let response = surf::get(api_request_url).recv_string().await?; let response = surf::get(api_request_url).recv_string().await?;
// Sets session key from received response // Sets session key from received response
let session_response: Session = serde_json::from_str(&response)?; let session_response: Session = serde_json::from_str(&response)?;
return Ok(session_response.session.key); return Ok(session_response.session.key);
} }
/// Creates a new LastFM struct with a given config /// Creates a new LastFM struct with a given config
pub fn new(config: &LastFMConfig) -> LastFM { pub fn new(config: &LastFMConfig) -> LastFM {
let last_fm = LastFM { let last_fm = LastFM {
config: config.clone() config: config.clone(),
}; };
return last_fm; return last_fm;
} }
// Creates an api request with the given parameters // Creates an api request with the given parameters
pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result<surf::Response, surf::Error> { pub async fn api_request(
&self,
mut params: BTreeMap<&str, &str>,
) -> Result<surf::Response, surf::Error> {
params.insert("api_key", &self.config.dango_api_key); params.insert("api_key", &self.config.dango_api_key);
params.insert("sk", &self.config.session_key); params.insert("sk", &self.config.session_key);
// Creates and sets api call signature // Creates and sets api call signature
let api_sig = LastFM::request_sig(&params, &self.config.shared_secret); let api_sig = LastFM::request_sig(&params, &self.config.shared_secret);
params.insert("api_sig", &api_sig); params.insert("api_sig", &api_sig);
let mut string_params = String::from(""); let mut string_params = String::from("");
// Creates method call string // Creates method call string
// Just iterate over values??? // Just iterate over values???
for key in params.keys() { for key in params.keys() {
let param_value = params.get(key).unwrap(); let param_value = params.get(key).unwrap();
string_params.push_str(&format!("{key}={param_value}&")); string_params.push_str(&format!("{key}={param_value}&"));
} }
string_params.pop(); string_params.pop();
let url = "http://ws.audioscrobbler.com/2.0/"; let url = "http://ws.audioscrobbler.com/2.0/";
let response = surf::post(url).body_string(string_params).await; let response = surf::post(url).body_string(string_params).await;
return response; return response;
@ -218,16 +235,16 @@ impl LastFM {
sig_string.push_str(&format!("{key}{}", param_value.unwrap())); sig_string.push_str(&format!("{key}{}", param_value.unwrap()));
} }
sig_string.push_str(shared_secret); sig_string.push_str(shared_secret);
// Hashes signature using **INSECURE** MD5 (Required by last.fm api) // Hashes signature using **INSECURE** MD5 (Required by last.fm api)
let mut md5_hasher = Md5::new(); let mut md5_hasher = Md5::new();
md5_hasher.update(sig_string); md5_hasher.update(sig_string);
let hash_result = md5_hasher.finalize(); let hash_result = md5_hasher.finalize();
let hashed_sig = format!("{:#02x}", hash_result); let hashed_sig = format!("{:#02x}", hash_result);
return hashed_sig; return hashed_sig;
} }
// Removes last.fm account from dango-music-player // Removes last.fm account from dango-music-player
pub fn reset_account() { pub fn reset_account() {
todo!(); todo!();
@ -237,13 +254,13 @@ impl LastFM {
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct DiscordRPCConfig { pub struct DiscordRPCConfig {
pub enabled: bool, pub enabled: bool,
pub dango_client_id: u64, pub dango_client_id: u64,
pub dango_icon: String, pub dango_icon: String,
} }
pub struct DiscordRPC { pub struct DiscordRPC {
config: DiscordRPCConfig, config: DiscordRPCConfig,
pub client: discord_presence::client::Client pub client: discord_presence::client::Client,
} }
impl DiscordRPC { impl DiscordRPC {
@ -259,44 +276,57 @@ impl DiscordRPC {
#[async_trait] #[async_trait]
impl MusicTracker for DiscordRPC { impl MusicTracker for DiscordRPC {
async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
let unknown = String::from("Unknown");
// Sets song title // Sets song title
let song_name = if let Some(song_name) = song.title { let song_name = if let Some(song_name) = song.get_tag(&Tag::Title) {
song_name song_name
} else { } else {
String::from("Unknown") &unknown
}; };
// Sets album
let album = if let Some(album) = song.get_tag(&Tag::Album) {
album
} else {
&unknown
};
let _client_thread = self.client.start(); let _client_thread = self.client.start();
// Blocks thread execution until it has connected to local discord client // Blocks thread execution until it has connected to local discord client
let ready = self.client.block_until_event(Event::Ready); let ready = self.client.block_until_event(Event::Ready);
if ready.is_err() { if ready.is_err() {
return Err(TrackerError::ServiceUnavailable); return Err(TrackerError::ServiceUnavailable);
} }
let start_time = std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64; let start_time = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
// Sets discord account activity to current playing song // Sets discord account activity to current playing song
let send_activity = self.client.set_activity(|activity| { let send_activity = self.client.set_activity(|activity| {
activity activity
.state(format!("Listening to: {}", song_name)) .state(format!("{}", album))
.details(format!("{}", song_name))
.assets(|assets| assets.large_image(&self.config.dango_icon)) .assets(|assets| assets.large_image(&self.config.dango_icon))
.timestamps(|time| time.start(start_time)) .timestamps(|time| time.start(start_time))
}); });
match send_activity { match send_activity {
Ok(_) => return Ok(()), Ok(_) => return Ok(()),
Err(_) => return Err(TrackerError::ServiceUnavailable), Err(_) => return Err(TrackerError::ServiceUnavailable),
} }
} }
async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> {
return Ok(()) return Ok(());
} }
async fn test_tracker(&mut self) -> Result<(), TrackerError> { async fn test_tracker(&mut self) -> Result<(), TrackerError> {
return Ok(()) return Ok(());
} }
async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> { async fn get_times_tracked(&mut self, song: &Song) -> Result<u32, TrackerError> {
return Ok(0); return Ok(0);
} }
@ -316,9 +346,9 @@ pub struct ListenBrainz {
#[async_trait] #[async_trait]
impl MusicTracker for ListenBrainz { impl MusicTracker for ListenBrainz {
async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
let (artist, track) = match (song.artist, song.title) { let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) {
(Some(artist), Some(track)) => (artist, track), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong),
}; };
// Creates a json to submit a single song as defined in the listenbrainz documentation // Creates a json to submit a single song as defined in the listenbrainz documentation
let json_req = json!({ let json_req = json!({
@ -332,21 +362,28 @@ impl MusicTracker for ListenBrainz {
} }
] ]
}); });
return match self.api_request(&json_req.to_string(), &String::from("/1/submit-listens")).await { return match self
.api_request(&json_req.to_string(), &String::from("/1/submit-listens"))
.await
{
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(TrackerError::from_surf_error(err)) 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)
}; };
}
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.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) {
(Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong),
};
let json_req = json!({ let json_req = json!({
"listen_type": "single", "listen_type": "single",
"payload": [ "payload": [
@ -359,11 +396,14 @@ impl MusicTracker for ListenBrainz {
} }
] ]
}); });
return match self.api_request(&json_req.to_string(), &String::from("/1/submit-listens")).await { return match self
.api_request(&json_req.to_string(), &String::from("/1/submit-listens"))
.await
{
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(TrackerError::from_surf_error(err)) Err(err) => Err(TrackerError::from_surf_error(err)),
} };
} }
async fn test_tracker(&mut self) -> Result<(), TrackerError> { async fn test_tracker(&mut self) -> Result<(), TrackerError> {
todo!() todo!()
@ -376,12 +416,19 @@ impl MusicTracker for ListenBrainz {
impl ListenBrainz { impl ListenBrainz {
pub fn new(config: &ListenBrainzConfig) -> Self { pub fn new(config: &ListenBrainzConfig) -> Self {
ListenBrainz { ListenBrainz {
config: config.clone() config: config.clone(),
} }
} }
// Makes an api request to configured url with given json // Makes an api request to configured url with given json
pub async fn api_request(&self, request: &String, endpoint: &String) -> Result<surf::Response, surf::Error> { pub async fn api_request(
let reponse = surf::post(format!("{}{}", &self.config.api_url, endpoint)).body_string(request.clone()).header("Authorization", format!("Token {}", self.config.auth_token)).await; &self,
return reponse request: &String,
endpoint: &String,
) -> Result<surf::Response, surf::Error> {
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;
} }
} }