mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-19 13:32:53 -05:00
cargo fmt
This commit is contained in:
parent
c5a631e30f
commit
085927caa1
11 changed files with 474 additions and 328 deletions
|
@ -12,13 +12,13 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -22,51 +22,53 @@ impl Default for 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)?;
|
||||||
|
|
|
@ -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() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{RwLock, Arc};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
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::{DSPMessage, DecoderMessage, MusicPlayer, PlayerStatus};
|
||||||
use crate::music_storage::music_db::{MusicLibrary, Song, Tag};
|
use crate::music_storage::music_db::{MusicLibrary, Song, Tag};
|
||||||
|
|
||||||
pub struct MusicController {
|
pub struct MusicController {
|
||||||
|
@ -13,75 +13,78 @@ pub struct MusicController {
|
||||||
|
|
||||||
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, Box<dyn std::error::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()) {
|
let library = match MusicLibrary::init(config.clone()) {
|
||||||
Ok(library) => library,
|
Ok(library) => library,
|
||||||
Err(error) => return Err(error)
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
let controller = MusicController {
|
let controller = MusicController {
|
||||||
config,
|
config,
|
||||||
library,
|
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) -> Result<MusicController, Box<dyn std::error::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()) {
|
let library = match MusicLibrary::init(config.clone()) {
|
||||||
Ok(library) => library,
|
Ok(library) => library,
|
||||||
Err(error) => return Err(error)
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
let controller = MusicController {
|
let controller = MusicController {
|
||||||
config,
|
config,
|
||||||
library,
|
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
|
/// Gets audio playback volume
|
||||||
pub fn get_vol(&self) -> f32 {
|
pub fn get_vol(&self) -> f32 {
|
||||||
return self.music_player.music_processor.audio_volume;
|
return self.music_player.music_processor.audio_volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets audio playback volume on a scale of 0.0 to 1.0
|
/// Sets audio playback volume on a scale of 0.0 to 1.0
|
||||||
pub fn set_vol(&mut self, volume: f32) {
|
pub fn set_vol(&mut self, volume: f32) {
|
||||||
self.music_player.music_processor.audio_volume = volume;
|
self.music_player.music_processor.audio_volume = volume;
|
||||||
self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone()))));
|
self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(
|
||||||
|
self.music_player.music_processor.clone(),
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queries the [MusicLibrary], returning a `Vec<Song>`
|
/// Queries the [MusicLibrary], returning a `Vec<Song>`
|
||||||
pub fn query_library(
|
pub fn query_library(
|
||||||
&self,
|
&self,
|
||||||
query_string: &String,
|
query_string: &String,
|
||||||
target_tags: Vec<Tag>,
|
target_tags: Vec<Tag>,
|
||||||
search_location: bool,
|
search_location: bool,
|
||||||
sort_by: Vec<Tag>
|
sort_by: Vec<Tag>,
|
||||||
) -> Option<Vec<&Song>> {
|
) -> Option<Vec<&Song>> {
|
||||||
self.library.query(query_string, &target_tags, search_location, &sort_by)
|
self.library
|
||||||
|
.query(query_string, &target_tags, search_location, &sort_by)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
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;
|
||||||
|
@ -20,8 +20,10 @@ 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_processor::music_processor::MusicProcessor;
|
||||||
use crate::music_storage::music_db::{URI, Song};
|
use crate::music_storage::music_db::{Song, URI};
|
||||||
use crate::music_tracker::music_tracker::{MusicTracker, TrackerError, LastFM, DiscordRPC, ListenBrainz};
|
use crate::music_tracker::music_tracker::{
|
||||||
|
DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError,
|
||||||
|
};
|
||||||
|
|
||||||
// Struct that controls playback of music
|
// Struct that controls playback of music
|
||||||
pub struct MusicPlayer {
|
pub struct MusicPlayer {
|
||||||
|
@ -49,18 +51,18 @@ pub enum DecoderMessage {
|
||||||
Pause,
|
Pause,
|
||||||
Stop,
|
Stop,
|
||||||
SeekTo(u64),
|
SeekTo(u64),
|
||||||
DSP(DSPMessage)
|
DSP(DSPMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum TrackerMessage {
|
pub enum TrackerMessage {
|
||||||
Track(Song),
|
Track(Song),
|
||||||
TrackNow(Song)
|
TrackNow(Song),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DSPMessage {
|
pub enum DSPMessage {
|
||||||
UpdateProcessor(Box<MusicProcessor>)
|
UpdateProcessor(Box<MusicProcessor>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holds a song decoder reader, etc
|
// Holds a song decoder reader, etc
|
||||||
|
@ -75,49 +77,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.to_str().unwrap(), &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!()
|
_ => 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 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 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 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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +139,14 @@ 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_processor: MusicProcessor::new(),
|
||||||
music_trackers: Vec::new(),
|
music_trackers: Vec::new(),
|
||||||
|
@ -140,15 +157,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)));
|
||||||
|
@ -177,9 +198,13 @@ impl MusicPlayer {
|
||||||
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;
|
||||||
|
@ -192,30 +217,41 @@ 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 mut music_processor = MusicProcessor::new();
|
||||||
|
|
||||||
let (tracker_sender, tracker_receiver): (Sender<TrackerMessage>, Receiver<TrackerMessage>) = mpsc::channel();
|
let (tracker_sender, tracker_receiver): (
|
||||||
let (tracker_status_sender, tracker_status_receiver): (Sender<Result<(), TrackerError>>, Receiver<Result<(), TrackerError>>) = mpsc::channel();
|
Sender<TrackerMessage>,
|
||||||
|
Receiver<TrackerMessage>,
|
||||||
|
) = 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
|
||||||
|
@ -254,26 +290,34 @@ 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)) => {
|
Some(DecoderMessage::DSP(dsp_message)) => match dsp_message {
|
||||||
match dsp_message {
|
DSPMessage::UpdateProcessor(new_processor) => {
|
||||||
DSPMessage::UpdateProcessor(new_processor) => music_processor = *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!
|
||||||
|
@ -286,60 +330,74 @@ 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
|
// Handles audio normally provided there is an audio stream
|
||||||
if let Some(ref mut audio_output) = audio_output {
|
if let Some(ref mut audio_output) = audio_output {
|
||||||
// Changes buffer of the MusicProcessor if the packet has a differing capacity or spec
|
// 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() {
|
if music_processor.audio_buffer.capacity() != decoded.capacity()
|
||||||
|
|| music_processor.audio_buffer.spec() != decoded.spec()
|
||||||
|
{
|
||||||
let spec = *decoded.spec();
|
let spec = *decoded.spec();
|
||||||
let duration = decoded.capacity() as u64;
|
let duration = decoded.capacity() as u64;
|
||||||
|
|
||||||
music_processor.set_buffer(duration, spec);
|
music_processor.set_buffer(duration, spec);
|
||||||
}
|
}
|
||||||
let transformed_audio = music_processor.process(&decoded);
|
let transformed_audio = music_processor.process(&decoded);
|
||||||
|
|
||||||
// Writes transformed packet to audio out
|
// Writes transformed packet to audio out
|
||||||
audio_output.write(transformed_audio).unwrap()
|
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);
|
||||||
|
@ -349,28 +407,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;
|
||||||
|
@ -394,7 +452,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
|
||||||
|
@ -408,12 +466,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(),
|
||||||
|
@ -433,16 +491,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() {
|
||||||
|
@ -450,7 +508,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);
|
||||||
}
|
}
|
||||||
|
@ -463,13 +521,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 {
|
||||||
|
@ -478,16 +536,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);
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -496,7 +554,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec};
|
use symphonia::core::audio::{AsAudioBufferRef, AudioBuffer, AudioBufferRef, Signal, SignalSpec};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MusicProcessor {
|
pub struct MusicProcessor {
|
||||||
|
@ -10,7 +10,9 @@ pub struct MusicProcessor {
|
||||||
|
|
||||||
impl Debug for MusicProcessor {
|
impl Debug for MusicProcessor {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("MusicProcessor").field("audio_volume", &self.audio_volume).finish()
|
f.debug_struct("MusicProcessor")
|
||||||
|
.field("audio_volume", &self.audio_volume)
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,22 +24,22 @@ impl MusicProcessor {
|
||||||
audio_volume: 1.0,
|
audio_volume: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes audio samples
|
/// Processes audio samples
|
||||||
///
|
///
|
||||||
/// Currently only supports transformations of volume
|
/// Currently only supports transformations of volume
|
||||||
pub fn process(&mut self, audio_buffer_ref: &AudioBufferRef) -> AudioBufferRef {
|
pub fn process(&mut self, audio_buffer_ref: &AudioBufferRef) -> AudioBufferRef {
|
||||||
audio_buffer_ref.convert(&mut self.audio_buffer);
|
audio_buffer_ref.convert(&mut self.audio_buffer);
|
||||||
|
|
||||||
let process = |sample| sample * self.audio_volume;
|
let process = |sample| sample * self.audio_volume;
|
||||||
|
|
||||||
self.audio_buffer.transform(process);
|
self.audio_buffer.transform(process);
|
||||||
|
|
||||||
return self.audio_buffer.as_audio_buffer_ref();
|
return self.audio_buffer.as_audio_buffer_ref();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets buffer of the MusicProcessor
|
/// Sets buffer of the MusicProcessor
|
||||||
pub fn set_buffer(&mut self, duration: u64, spec: SignalSpec) {
|
pub fn set_buffer(&mut self, duration: u64, spec: SignalSpec) {
|
||||||
self.audio_buffer = AudioBuffer::new(duration, spec);
|
self.audio_buffer = AudioBuffer::new(duration, spec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -362,7 +362,7 @@ impl MusicLibrary {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
//println!("{:?}", target_file.path());
|
//println!("{:?}", target_file.path());
|
||||||
total += 1
|
total += 1
|
||||||
},
|
}
|
||||||
Err(_error) => {
|
Err(_error) => {
|
||||||
//println!("{}, {:?}: {}", format, target_file.file_name(), _error)
|
//println!("{}, {:?}: {}", format, target_file.file_name(), _error)
|
||||||
} // TODO: Handle more of these errors
|
} // TODO: Handle more of these errors
|
||||||
|
@ -481,8 +481,14 @@ impl MusicLibrary {
|
||||||
let cue_data = CD::parse_file(cuesheet.to_owned()).unwrap();
|
let cue_data = CD::parse_file(cuesheet.to_owned()).unwrap();
|
||||||
|
|
||||||
// Get album level information
|
// Get album level information
|
||||||
let album_title = &cue_data.get_cdtext().read(cue::cd_text::PTI::Title).unwrap_or(String::new());
|
let album_title = &cue_data
|
||||||
let album_artist = &cue_data.get_cdtext().read(cue::cd_text::PTI::Performer).unwrap_or(String::new());
|
.get_cdtext()
|
||||||
|
.read(cue::cd_text::PTI::Title)
|
||||||
|
.unwrap_or(String::new());
|
||||||
|
let album_artist = &cue_data
|
||||||
|
.get_cdtext()
|
||||||
|
.read(cue::cd_text::PTI::Performer)
|
||||||
|
.unwrap_or(String::new());
|
||||||
|
|
||||||
let parent_dir = cuesheet.parent().expect("The file has no parent path??");
|
let parent_dir = cuesheet.parent().expect("The file has no parent path??");
|
||||||
for (i, track) in cue_data.tracks().iter().enumerate() {
|
for (i, track) in cue_data.tracks().iter().enumerate() {
|
||||||
|
@ -498,17 +504,17 @@ impl MusicLibrary {
|
||||||
// Get the track timing information
|
// Get the track timing information
|
||||||
let pregap = match track.get_zero_pre() {
|
let pregap = match track.get_zero_pre() {
|
||||||
Some(pregap) => Duration::from_micros((pregap as f32 * 13333.333333) as u64),
|
Some(pregap) => Duration::from_micros((pregap as f32 * 13333.333333) as u64),
|
||||||
None => Duration::from_secs(0)
|
None => Duration::from_secs(0),
|
||||||
};
|
};
|
||||||
let postgap = match track.get_zero_post() {
|
let postgap = match track.get_zero_post() {
|
||||||
Some(postgap) => Duration::from_micros((postgap as f32 * 13333.333333) as u64),
|
Some(postgap) => Duration::from_micros((postgap as f32 * 13333.333333) as u64),
|
||||||
None => Duration::from_secs(0)
|
None => Duration::from_secs(0),
|
||||||
};
|
};
|
||||||
let mut start = Duration::from_micros((track.get_start() as f32 * 13333.333333) as u64);
|
let mut start = Duration::from_micros((track.get_start() as f32 * 13333.333333) as u64);
|
||||||
start -= pregap;
|
start -= pregap;
|
||||||
|
|
||||||
let duration = match track.get_length() {
|
let duration = match track.get_length() {
|
||||||
Some(len) => Duration::from_micros((len as f32 * 13333.333333) as u64),
|
Some(len) => Duration::from_micros((len as f32 * 13333.333333) as u64),
|
||||||
None => {
|
None => {
|
||||||
let tagged_file = match lofty::read_from_path(&audio_location) {
|
let tagged_file = match lofty::read_from_path(&audio_location) {
|
||||||
Ok(tagged_file) => tagged_file,
|
Ok(tagged_file) => tagged_file,
|
||||||
|
@ -537,33 +543,31 @@ impl MusicLibrary {
|
||||||
tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone()));
|
tags.push((Tag::Key("AlbumArtist".to_string()), album_artist.clone()));
|
||||||
match track.get_cdtext().read(cue::cd_text::PTI::Title) {
|
match track.get_cdtext().read(cue::cd_text::PTI::Title) {
|
||||||
Some(title) => tags.push((Tag::Title, title)),
|
Some(title) => tags.push((Tag::Title, title)),
|
||||||
None => {
|
None => match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) {
|
||||||
match track.get_cdtext().read(cue::cd_text::PTI::UPC_ISRC) {
|
Some(title) => tags.push((Tag::Title, title)),
|
||||||
Some(title) => tags.push((Tag::Title, title)),
|
None => {
|
||||||
None => {
|
let namestr = format!("{} - {}", i, track.get_filename());
|
||||||
let namestr = format!("{} - {}", i, track.get_filename());
|
tags.push((Tag::Title, namestr))
|
||||||
tags.push((Tag::Title, namestr))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
match track.get_cdtext().read(cue::cd_text::PTI::Performer) {
|
match track.get_cdtext().read(cue::cd_text::PTI::Performer) {
|
||||||
Some(artist) => tags.push((Tag::Artist, artist)),
|
Some(artist) => tags.push((Tag::Artist, artist)),
|
||||||
None => ()
|
None => (),
|
||||||
};
|
};
|
||||||
match track.get_cdtext().read(cue::cd_text::PTI::Genre) {
|
match track.get_cdtext().read(cue::cd_text::PTI::Genre) {
|
||||||
Some(genre) => tags.push((Tag::Genre, genre)),
|
Some(genre) => tags.push((Tag::Genre, genre)),
|
||||||
None => ()
|
None => (),
|
||||||
};
|
};
|
||||||
match track.get_cdtext().read(cue::cd_text::PTI::Message) {
|
match track.get_cdtext().read(cue::cd_text::PTI::Message) {
|
||||||
Some(comment) => tags.push((Tag::Comment, comment)),
|
Some(comment) => tags.push((Tag::Comment, comment)),
|
||||||
None => ()
|
None => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
let album_art = Vec::new();
|
let album_art = Vec::new();
|
||||||
|
|
||||||
let new_song = Song {
|
let new_song = Song {
|
||||||
location: URI::Cue{
|
location: URI::Cue {
|
||||||
location: audio_location,
|
location: audio_location,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
@ -586,8 +590,8 @@ impl MusicLibrary {
|
||||||
Ok(_) => tracks_added += 1,
|
Ok(_) => tracks_added += 1,
|
||||||
Err(_error) => {
|
Err(_error) => {
|
||||||
//println!("{}", _error);
|
//println!("{}", _error);
|
||||||
continue
|
continue;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -604,8 +608,8 @@ impl MusicLibrary {
|
||||||
match new_song.location {
|
match new_song.location {
|
||||||
URI::Local(_) if self.query_path(&new_song.location.path()).is_some() => {
|
URI::Local(_) if self.query_path(&new_song.location.path()).is_some() => {
|
||||||
return Err(format!("Location exists for {:?}", new_song.location).into())
|
return Err(format!("Location exists for {:?}", new_song.location).into())
|
||||||
},
|
}
|
||||||
_ => ()
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.library.push(new_song);
|
self.library.push(new_song);
|
||||||
|
@ -652,7 +656,7 @@ impl MusicLibrary {
|
||||||
for tag in target_tags {
|
for tag in target_tags {
|
||||||
let track_result = match track.get_tag(&tag) {
|
let track_result = match track.get_tag(&tag) {
|
||||||
Some(value) => value,
|
Some(value) => value,
|
||||||
None => continue
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if normalize(track_result).contains(&normalize(&query_string)) {
|
if normalize(track_result).contains(&normalize(&query_string)) {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
use std::path::Path;
|
|
||||||
use crate::music_controller::config::Config;
|
use crate::music_controller::config::Config;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn playlist_add(
|
pub fn playlist_add(config: &Config, playlist_name: &str, song_paths: &Vec<&Path>) {
|
||||||
config: &Config,
|
|
||||||
playlist_name: &str,
|
|
||||||
song_paths: &Vec<&Path>
|
|
||||||
) {
|
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
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, Tag};
|
use crate::music_storage::music_db::{Song, Tag};
|
||||||
|
@ -14,13 +14,13 @@ use crate::music_storage::music_db::{Song, Tag};
|
||||||
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.get_tag(&Tag::Artist), song.get_tag(&Tag::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.get_tag(&Tag::Artist), song.get_tag(&Tag::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(¶ms, &self.config.shared_secret);
|
let api_sig = LastFM::request_sig(¶ms, &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 {
|
||||||
|
@ -256,7 +273,6 @@ 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> {
|
||||||
|
@ -275,16 +291,19 @@ impl MusicTracker for DiscordRPC {
|
||||||
} else {
|
} else {
|
||||||
&unknown
|
&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
|
||||||
|
@ -293,21 +312,21 @@ impl MusicTracker for DiscordRPC {
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
@ -329,7 +348,7 @@ 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.get_tag(&Tag::Artist), song.get_tag(&Tag::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!({
|
||||||
|
@ -343,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> {
|
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 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)) {
|
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),
|
||||||
};
|
};
|
||||||
|
|
||||||
let json_req = json!({
|
let json_req = json!({
|
||||||
"listen_type": "single",
|
"listen_type": "single",
|
||||||
"payload": [
|
"payload": [
|
||||||
|
@ -370,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!()
|
||||||
|
@ -387,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue