Merge pull request #18 from Dangoware/database

Merge new things from database branch
This commit is contained in:
G2 2023-11-24 14:28:37 -06:00 committed by GitHub
commit 755c62f702
12 changed files with 84 additions and 1013 deletions

View file

@ -13,7 +13,7 @@ categories = ["multimedia::audio"]
[dependencies] [dependencies]
file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] } file-format = { version = "0.22.0", features = ["reader-asf", "reader-ebml", "reader-mp4", "reader-rm", "reader-txt", "reader-xml", "serde"] }
lofty = "0.16.1" lofty = "0.17.0"
serde = { version = "1.0.191", features = ["derive"] } serde = { version = "1.0.191", features = ["derive"] }
time = "0.3.22" time = "0.3.22"
toml = "0.7.5" toml = "0.7.5"

View file

@ -8,14 +8,7 @@ pub mod music_storage {
mod utils; mod utils;
} }
pub mod music_player {
pub mod music_output;
pub mod music_player;
pub mod music_resampler;
}
pub mod music_controller { pub mod music_controller {
pub mod config; pub mod config;
pub mod init;
pub mod music_controller; pub mod music_controller;
} }

View file

@ -18,7 +18,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
let path = PathBuf::from("./music_database"); let path = PathBuf::from("./music_database");
return Config { Config {
db_path: Box::new(path), db_path: Box::new(path),
lastfm: None, lastfm: None,
@ -34,7 +34,7 @@ impl Default for Config {
api_url: String::from("https://api.listenbrainz.org"), api_url: String::from("https://api.listenbrainz.org"),
auth_token: String::from(""), auth_token: String::from(""),
}), }),
}; }
} }
} }
@ -49,10 +49,10 @@ impl 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( toml::from_str(
&read_to_string(config_file) &read_to_string(config_file)
.expect("Failed to initalize music config: File not found!"), .expect("Failed to initalize music config: File not found!"),
); )
} }
/// Saves config to given path /// Saves config to given path

View file

@ -1,14 +0,0 @@
use std::fs::File;
use std::path::Path;
pub fn init() {}
fn init_config() {
let config_path = "./config.toml";
if !Path::new(config_path).try_exists().unwrap() {
File::create("./config.toml").unwrap();
}
}
fn init_db() {}

View file

@ -2,20 +2,17 @@ use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use crate::music_player::music_player::{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 {
pub config: Arc<RwLock<Config>>, pub config: Arc<RwLock<Config>>,
pub library: MusicLibrary, pub library: MusicLibrary,
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, 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 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),
@ -24,16 +21,14 @@ impl MusicController {
let controller = MusicController { let controller = MusicController {
config, config,
library, library,
music_player,
}; };
return Ok(controller); 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 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),
@ -42,25 +37,9 @@ impl MusicController {
let controller = MusicController { let controller = MusicController {
config, config,
library, library,
music_player,
}; };
return Ok(controller); Ok(controller)
}
/// Sends given message to control music player
pub fn song_control(&mut self, message: DecoderMessage) {
self.music_player.send_message(message);
}
/// Gets status of the music player
pub fn player_status(&mut self) -> PlayerStatus {
return self.music_player.get_status();
}
/// Gets current song being controlled, if any
pub fn get_current_song(&self) -> Option<Song> {
return self.music_player.get_current_song();
} }
/// Queries the [MusicLibrary], returning a `Vec<Song>` /// Queries the [MusicLibrary], returning a `Vec<Song>`
@ -68,7 +47,7 @@ impl MusicController {
&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 self.library

View file

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

View file

@ -1,529 +0,0 @@
use std::io::SeekFrom;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, RwLock};
use std::thread;
use async_std::io::ReadExt;
use async_std::task;
use futures::future::join_all;
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::io::{MediaSource, MediaSourceStream};
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use symphonia::core::units::{Time, TimeBase};
use futures::AsyncBufRead;
use crate::music_controller::config::Config;
use crate::music_player::music_output::AudioStream;
use crate::music_storage::music_db::{Song, URI};
use crate::music_tracker::music_tracker::{
DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError,
};
// Struct that controls playback of music
pub struct MusicPlayer {
player_status: PlayerStatus,
music_trackers: Vec<Box<dyn MusicTracker + Send>>,
current_song: Arc<RwLock<Option<Song>>>,
message_sender: Sender<DecoderMessage>,
status_receiver: Receiver<PlayerStatus>,
config: Arc<RwLock<Config>>,
}
#[derive(Clone, Copy, Debug)]
pub enum PlayerStatus {
Playing(f64),
Paused,
Stopped,
Error,
}
#[derive(Debug, Clone)]
pub enum DecoderMessage {
OpenSong(Song),
Play,
Pause,
Stop,
SeekTo(u64),
}
#[derive(Clone)]
pub enum TrackerMessage {
Track(Song),
TrackNow(Song),
}
// Holds a song decoder reader, etc
struct SongHandler {
pub reader: Box<dyn FormatReader>,
pub decoder: Box<dyn Decoder>,
pub time_base: Option<TimeBase>,
pub duration: Option<u64>,
}
// TODO: actual error handling here
impl SongHandler {
pub fn new(uri: &URI) -> Result<Self, ()> {
// Opens remote/local source and creates MediaSource for symphonia
let config = RemoteOptions {
media_buffer_len: 10000,
forward_buffer_len: 10000,
};
let src: Box<dyn MediaSource> = match uri {
URI::Local(path) => match std::fs::File::open(path) {
Ok(file) => Box::new(file),
Err(_) => return Err(()),
},
URI::Remote(_, location) => {
match RemoteSource::new(location.to_str().unwrap(), &config) {
Ok(remote_source) => Box::new(remote_source),
Err(_) => return Err(()),
}
}
_ => todo!(),
};
let mss = MediaSourceStream::new(src, Default::default());
// Use default metadata and format options
let meta_opts: MetadataOptions = Default::default();
let fmt_opts: FormatOptions = Default::default();
let hint = Hint::new();
let probed = symphonia::default::get_probe()
.format(&hint, mss, &fmt_opts, &meta_opts)
.expect("Unsupported format");
let reader = probed.format;
let track = reader
.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 duration = track.codec_params.n_frames;
let dec_opts: DecoderOptions = Default::default();
let decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &dec_opts)
.expect("unsupported codec");
return Ok(SongHandler {
reader,
decoder,
time_base,
duration,
});
}
}
impl MusicPlayer {
pub fn new(config: Arc<RwLock<Config>>) -> Self {
// Creates mpsc channels to communicate with music player threads
let (message_sender, message_receiver) = mpsc::channel();
let (status_sender, status_receiver) = mpsc::channel();
let current_song = Arc::new(RwLock::new(None));
MusicPlayer::start_player(
message_receiver,
status_sender,
config.clone(),
current_song.clone(),
);
MusicPlayer {
music_trackers: Vec::new(),
player_status: PlayerStatus::Stopped,
current_song,
message_sender,
status_receiver,
config,
}
}
fn start_tracker(
status_sender: Sender<Result<(), TrackerError>>,
tracker_receiver: Receiver<TrackerMessage>,
config: Arc<RwLock<Config>>,
) {
thread::spawn(move || {
let global_config = &*config.read().unwrap();
// Sets local config for trackers to detect changes
let local_config = global_config.clone();
let mut trackers: Vec<Box<dyn MusicTracker>> = Vec::new();
// Updates local trackers to the music controller config // TODO: refactor
let update_trackers = |trackers: &mut Vec<Box<dyn MusicTracker>>| {
if let Some(lastfm_config) = global_config.lastfm.clone() {
if lastfm_config.enabled {
trackers.push(Box::new(LastFM::new(&lastfm_config)));
}
}
if let Some(discord_config) = global_config.discord.clone() {
if discord_config.enabled {
trackers.push(Box::new(DiscordRPC::new(&discord_config)));
}
}
if let Some(listenbz_config) = global_config.listenbrainz.clone() {
if listenbz_config.enabled {
trackers.push(Box::new(ListenBrainz::new(&listenbz_config)));
}
}
};
update_trackers(&mut trackers);
loop {
if let message = tracker_receiver.recv() {
if local_config != global_config {
update_trackers(&mut trackers);
}
let mut results = Vec::new();
task::block_on(async {
let mut futures = Vec::new();
for tracker in trackers.iter_mut() {
match message.clone() {
Ok(TrackerMessage::Track(song)) => {
futures.push(tracker.track_song(song))
}
Ok(TrackerMessage::TrackNow(song)) => {
futures.push(tracker.track_now(song))
}
Err(_) => {}
}
}
results = join_all(futures).await;
});
for result in results {
status_sender.send(result).unwrap_or_default()
}
}
}
});
}
// 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>>>,
) {
// Creates thread that audio is decoded in
thread::spawn(move || {
let current_song = current_song;
let mut song_handler = None;
let mut seek_time: Option<u64> = None;
let mut audio_output: Option<Box<dyn AudioStream>> = None;
let (tracker_sender, tracker_receiver): (
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);
let mut song_tracked = false;
let mut song_time = 0.0;
let mut paused = true;
'main_decode: loop {
'handle_message: loop {
let message = if paused {
// Pauses playback by blocking on waiting for new player messages
match message_receiver.recv() {
Ok(message) => Some(message),
Err(_) => None,
}
} else {
// Resumes playback by not blocking
match message_receiver.try_recv() {
Ok(message) => Some(message),
Err(_) => break 'handle_message,
}
};
// Handles message received from MusicPlayer struct
match message {
Some(DecoderMessage::OpenSong(song)) => {
let song_uri = song.location.clone();
match SongHandler::new(&song_uri) {
Ok(new_handler) => {
song_handler = Some(new_handler);
*current_song.write().unwrap() = Some(song);
paused = false;
song_tracked = false;
}
Err(_) => status_sender.send(PlayerStatus::Error).unwrap(),
}
}
Some(DecoderMessage::Play) => {
if song_handler.is_some() {
paused = false;
}
}
Some(DecoderMessage::Pause) => {
paused = true;
status_sender.send(PlayerStatus::Paused).unwrap();
}
Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time),
// Exits main decode loop and subsequently ends thread
Some(DecoderMessage::Stop) => {
status_sender.send(PlayerStatus::Stopped).unwrap();
break 'main_decode;
}
None => {}
}
status_sender.send(PlayerStatus::Error).unwrap();
}
// In theory this check should not need to occur?
if let (Some(song_handler), current_song) =
(&mut song_handler, &*current_song.read().unwrap())
{
match seek_time {
Some(time) => {
let seek_to = SeekTo::Time {
time: Time::from(time),
track_id: Some(0),
};
song_handler
.reader
.seek(SeekMode::Accurate, seek_to)
.unwrap();
seek_time = None;
}
None => {} //Nothing to do!
}
let packet = match song_handler.reader.next_packet() {
Ok(packet) => packet,
Err(Error::ResetRequired) => panic!(), //TODO,
Err(err) => {
// Unrecoverable?
panic!("{}", err);
}
};
if let (Some(time_base), Some(song)) = (song_handler.time_base, current_song) {
let time_units = time_base.calc_time(packet.ts);
song_time = time_units.seconds as f64 + time_units.frac;
// Tracks song now if song has just started
if song_time == 0.0 {
tracker_sender
.send(TrackerMessage::TrackNow(song.clone()))
.unwrap();
}
if let Some(duration) = song_handler.duration {
let song_duration = time_base.calc_time(duration);
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
if (song_duration_secs / 2.0 < song_time || song_time > 240.0)
&& !song_tracked
{
song_tracked = true;
tracker_sender
.send(TrackerMessage::Track(song.clone()))
.unwrap();
}
}
}
status_sender
.send(PlayerStatus::Playing(song_time))
.unwrap();
match song_handler.decoder.decode(&packet) {
Ok(decoded) => {
// Opens audio stream if there is not one
if audio_output.is_none() {
let spec = *decoded.spec();
let duration = decoded.capacity() as u64;
audio_output.replace(
crate::music_player::music_output::open_stream(spec, duration)
.unwrap(),
);
}
}
Err(Error::IoError(_)) => {
// rest in peace packet
continue;
}
Err(Error::DecodeError(_)) => {
// may you one day be decoded
continue;
}
Err(err) => {
// Unrecoverable, though shouldn't panic here
panic!("{}", err);
}
}
}
}
});
}
// Updates status by checking on messages from spawned thread
fn update_player(&mut self) {
for message in self.status_receiver.try_recv() {
self.player_status = message;
}
}
pub fn get_current_song(&self) -> Option<Song> {
match self.current_song.try_read() {
Ok(song) => return (*song).clone(),
Err(_) => return None,
}
}
// Sends message to spawned thread
pub fn send_message(&mut self, message: DecoderMessage) {
self.update_player();
// Checks that message sender exists before sending a message off
self.message_sender.send(message).unwrap();
}
pub fn get_status(&mut self) -> PlayerStatus {
self.update_player();
return self.player_status;
}
}
// TODO: Make the buffer length do anything
/// Options for remote sources
///
/// media_buffer_len is how many bytes are to be buffered in totala
///
/// forward_buffer is how many bytes can ahead of the seek position without the remote source being read from
pub struct RemoteOptions {
media_buffer_len: u64,
forward_buffer_len: u64,
}
impl Default for RemoteOptions {
fn default() -> Self {
RemoteOptions {
media_buffer_len: 100000,
forward_buffer_len: 1024,
}
}
}
/// A remote source of media
struct RemoteSource {
reader: Box<dyn AsyncBufRead + Send + Sync + Unpin>,
media_buffer: Vec<u8>,
forward_buffer_len: u64,
offset: u64,
}
impl RemoteSource {
/// Creates a new RemoteSource with given uri and configuration
pub fn new(uri: &str, config: &RemoteOptions) -> Result<Self, surf::Error> {
let mut response = task::block_on(async {
return surf::get(uri).await;
})?;
let reader = response.take_body().into_reader();
Ok(RemoteSource {
reader,
media_buffer: Vec::new(),
forward_buffer_len: config.forward_buffer_len,
offset: 0,
})
}
}
// TODO: refactor this + buffer into the buffer passed into the function, not a newly allocated one
impl std::io::Read for RemoteSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
// Reads bytes into the media buffer if the offset is within the specified distance from the end of the buffer
if self.media_buffer.len() as u64 - self.offset < self.forward_buffer_len {
let mut buffer = [0; 1024];
let read_bytes = task::block_on(async {
match self.reader.read_exact(&mut buffer).await {
Ok(_) => {
self.media_buffer.extend_from_slice(&buffer);
return Ok(());
}
Err(err) => return Err(err),
}
});
match read_bytes {
Err(err) => return Err(err),
_ => {}
}
}
// Reads bytes from the media buffer into the buffer given by
let mut bytes_read = 0;
for location in 0..1024 {
if (location + self.offset as usize) < self.media_buffer.len() {
buf[location] = self.media_buffer[location + self.offset as usize];
bytes_read += 1;
}
}
self.offset += bytes_read;
return Ok(bytes_read as usize);
}
}
impl std::io::Seek for RemoteSource {
// Seeks to a given position
// Seeking past the internal buffer's length results in the seeking to the end of content
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
match pos {
// Offset is set to given position
SeekFrom::Start(pos) => {
if pos > self.media_buffer.len() as u64 {
self.offset = self.media_buffer.len() as u64;
} else {
self.offset = pos;
}
return Ok(self.offset);
}
// Offset is set to length of buffer + given position
SeekFrom::End(pos) => {
if self.media_buffer.len() as u64 + pos as u64 > self.media_buffer.len() as u64 {
self.offset = self.media_buffer.len() as u64;
} else {
self.offset = self.media_buffer.len() as u64 + pos as u64;
}
return Ok(self.offset);
}
// Offset is set to current offset + given position
SeekFrom::Current(pos) => {
if self.offset + pos as u64 > self.media_buffer.len() as u64 {
self.offset = self.media_buffer.len() as u64;
} else {
self.offset += pos as u64
}
return Ok(self.offset);
}
}
}
}
impl MediaSource for RemoteSource {
fn is_seekable(&self) -> bool {
return true;
}
fn byte_len(&self) -> Option<u64> {
return None;
}
}

View file

@ -1,154 +0,0 @@
// Symphonia
// Copyright (c) 2019-2022 The Project Symphonia Developers.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, SignalSpec};
use symphonia::core::conv::{FromSample, IntoSample};
use symphonia::core::sample::Sample;
pub struct Resampler<T> {
resampler: rubato::FftFixedIn<f32>,
input: Vec<Vec<f32>>,
output: Vec<Vec<f32>>,
interleaved: Vec<T>,
duration: usize,
}
impl<T> Resampler<T>
where
T: Sample + FromSample<f32> + IntoSample<f32>,
{
fn resample_inner(&mut self) -> &[T] {
{
//let mut input = heapless::Vec::<f32, 32>::new();
let mut input: arrayvec::ArrayVec<&[f32], 32> = Default::default();
for channel in self.input.iter() {
input.push(&channel[..self.duration]);
}
// Resample.
rubato::Resampler::process_into_buffer(
&mut self.resampler,
&input,
&mut self.output,
None,
)
.unwrap();
}
// Remove consumed samples from the input buffer.
for channel in self.input.iter_mut() {
channel.drain(0..self.duration);
}
// Interleave the planar samples from Rubato.
let num_channels = self.output.len();
self.interleaved
.resize(num_channels * self.output[0].len(), T::MID);
for (i, frame) in self.interleaved.chunks_exact_mut(num_channels).enumerate() {
for (ch, s) in frame.iter_mut().enumerate() {
*s = self.output[ch][i].into_sample();
}
}
&self.interleaved
}
}
impl<T> Resampler<T>
where
T: Sample + FromSample<f32> + IntoSample<f32>,
{
pub fn new(spec: SignalSpec, to_sample_rate: usize, duration: u64) -> Self {
let duration = duration as usize;
let num_channels = spec.channels.count();
let resampler = rubato::FftFixedIn::<f32>::new(
spec.rate as usize,
to_sample_rate,
duration,
2,
num_channels,
)
.unwrap();
let output = rubato::Resampler::output_buffer_allocate(&resampler);
let input = vec![Vec::with_capacity(duration); num_channels];
Self {
resampler,
input,
output,
duration,
interleaved: Default::default(),
}
}
/// Resamples a planar/non-interleaved input.
///
/// Returns the resampled samples in an interleaved format.
pub fn resample(&mut self, input: AudioBufferRef<'_>) -> Option<&[T]> {
// Copy and convert samples into input buffer.
convert_samples_any(&input, &mut self.input);
// Check if more samples are required.
if self.input[0].len() < self.duration {
return None;
}
Some(self.resample_inner())
}
/// Resample any remaining samples in the resample buffer.
pub fn flush(&mut self) -> Option<&[T]> {
let len = self.input[0].len();
if len == 0 {
return None;
}
let partial_len = len % self.duration;
if partial_len != 0 {
// Fill each input channel buffer with silence to the next multiple of the resampler
// duration.
for channel in self.input.iter_mut() {
channel.resize(len + (self.duration - partial_len), f32::MID);
}
}
Some(self.resample_inner())
}
}
fn convert_samples_any(input: &AudioBufferRef<'_>, output: &mut [Vec<f32>]) {
match input {
AudioBufferRef::U8(input) => convert_samples(input, output),
AudioBufferRef::U16(input) => convert_samples(input, output),
AudioBufferRef::U24(input) => convert_samples(input, output),
AudioBufferRef::U32(input) => convert_samples(input, output),
AudioBufferRef::S8(input) => convert_samples(input, output),
AudioBufferRef::S16(input) => convert_samples(input, output),
AudioBufferRef::S24(input) => convert_samples(input, output),
AudioBufferRef::S32(input) => convert_samples(input, output),
AudioBufferRef::F32(input) => convert_samples(input, output),
AudioBufferRef::F64(input) => convert_samples(input, output),
}
}
fn convert_samples<S>(input: &AudioBuffer<S>, output: &mut [Vec<f32>])
where
S: Sample + IntoSample<f32>,
{
for (c, dst) in output.iter_mut().enumerate() {
let src = input.chan(c);
dst.extend(src.iter().map(|&s| s.into_sample()));
}
}

View file

@ -11,7 +11,7 @@ use std::ops::ControlFlow::{Break, Continue};
use rcue::parser::parse_from_file; use rcue::parser::parse_from_file;
use file_format::{FileFormat, Kind}; use file_format::{FileFormat, Kind};
use walkdir::WalkDir; use walkdir::WalkDir;
use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt, ParseOptions};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -115,10 +115,7 @@ impl Song {
"location" => Some(self.location.clone().path_string()), "location" => Some(self.location.clone().path_string()),
"plays" => Some(self.plays.clone().to_string()), "plays" => Some(self.plays.clone().to_string()),
"format" => match self.format { "format" => match self.format {
Some(format) => match format.short_name() { Some(format) => format.short_name().map(|short| short.to_string()),
Some(short) => Some(short.to_string()),
None => None,
},
None => None, None => None,
}, },
_ => todo!(), // Other field types are not yet supported _ => todo!(), // Other field types are not yet supported
@ -246,7 +243,7 @@ impl Album<'_> {
} }
} }
const BLOCKED_EXTENSIONS: [&str; 3] = ["vob", "log", "txt"]; const BLOCKED_EXTENSIONS: [&str; 5] = ["vob", "log", "txt", "sf2", "mid"];
#[derive(Debug)] #[derive(Debug)]
pub struct MusicLibrary { pub struct MusicLibrary {
@ -321,10 +318,9 @@ impl MusicLibrary {
/// with matching `PathBuf`s /// with matching `PathBuf`s
fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> { fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> {
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
let _ = self.library.par_iter().for_each(|track| { self.library.par_iter().for_each(|track| {
if path == track.location.path() { if path == track.location.path() {
result.clone().lock().unwrap().push(&track); result.clone().lock().unwrap().push(track);
return;
} }
}); });
if result.lock().unwrap().len() > 0 { if result.lock().unwrap().len() > 0 {
@ -341,6 +337,7 @@ impl MusicLibrary {
config: &Config, config: &Config,
) -> Result<usize, Box<dyn std::error::Error>> { ) -> Result<usize, Box<dyn std::error::Error>> {
let mut total = 0; let mut total = 0;
let mut errors = 0;
for target_file in WalkDir::new(target_path) for target_file in WalkDir::new(target_path)
.follow_links(true) .follow_links(true)
.into_iter() .into_iter()
@ -367,7 +364,7 @@ impl MusicLibrary {
} }
*/ */
let format = FileFormat::from_file(&path)?; let format = FileFormat::from_file(path)?;
let extension = match path.extension() { let extension = match path.extension() {
Some(ext) => ext.to_string_lossy().to_ascii_lowercase(), Some(ext) => ext.to_string_lossy().to_ascii_lowercase(),
None => String::new(), None => String::new(),
@ -378,9 +375,10 @@ impl MusicLibrary {
if (format.kind() == Kind::Audio || format.kind() == Kind::Video) if (format.kind() == Kind::Audio || format.kind() == Kind::Video)
&& !BLOCKED_EXTENSIONS.contains(&extension.as_str()) && !BLOCKED_EXTENSIONS.contains(&extension.as_str())
{ {
match self.add_file(&target_file.path()) { match self.add_file(target_file.path()) {
Ok(_) => total += 1, Ok(_) => total += 1,
Err(_error) => { Err(_error) => {
errors += 1;
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
}; };
@ -388,6 +386,7 @@ impl MusicLibrary {
total += match self.add_cuesheet(&target_file.path().to_path_buf()) { total += match self.add_cuesheet(&target_file.path().to_path_buf()) {
Ok(added) => added, Ok(added) => added,
Err(error) => { Err(error) => {
errors += 1;
println!("{}", error); println!("{}", error);
0 0
} }
@ -396,21 +395,21 @@ impl MusicLibrary {
} }
// Save the database after scanning finishes // Save the database after scanning finishes
self.save(&config).unwrap(); self.save(config).unwrap();
println!("ERRORS: {}", errors);
Ok(total) Ok(total)
} }
pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> { pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> {
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
// TODO: Fix error handling here // TODO: Fix error handling here
let tagged_file = match lofty::read_from_path(target_file) { let tagged_file = match Probe::open(target_file)?.options(normal_options).read() {
Ok(tagged_file) => tagged_file, Ok(tagged_file) => tagged_file,
Err(_) => match Probe::open(target_file)?.read() { Err(error) => return Err(error.into()),
Ok(tagged_file) => tagged_file,
Err(error) => return Err(error.into()),
},
}; };
// Ensure the tags exist, if not, insert blank data // Ensure the tags exist, if not, insert blank data
@ -435,7 +434,7 @@ impl MusicLibrary {
ItemKey::Comment => Tag::Comment, ItemKey::Comment => Tag::Comment,
ItemKey::AlbumTitle => Tag::Album, ItemKey::AlbumTitle => Tag::Album,
ItemKey::DiscNumber => Tag::Disk, ItemKey::DiscNumber => Tag::Disk,
ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" => continue, ItemKey::Unknown(unknown) if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" => continue,
ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()),
custom => Tag::Key(format!("{:?}", custom)), custom => Tag::Key(format!("{:?}", custom)),
}; };
@ -452,7 +451,7 @@ impl MusicLibrary {
// Get all the album artwork information from the file // Get all the album artwork information from the file
let mut album_art: Vec<AlbumArt> = Vec::new(); let mut album_art: Vec<AlbumArt> = Vec::new();
for (i, _art) in tag.pictures().iter().enumerate() { for (i, _art) in tag.pictures().iter().enumerate() {
let new_art = AlbumArt::Embedded(i as usize); let new_art = AlbumArt::Embedded(i);
album_art.push(new_art) album_art.push(new_art)
} }
@ -490,7 +489,9 @@ impl MusicLibrary {
match self.add_song(new_song) { match self.add_song(new_song) {
Ok(_) => (), Ok(_) => (),
Err(error) => return Err(error), Err(_) => {
//return Err(error)
},
}; };
Ok(()) Ok(())
@ -532,7 +533,9 @@ impl MusicLibrary {
None => Duration::from_secs(0), None => Duration::from_secs(0),
}; };
let mut start = track.indices[0].1; let mut start = track.indices[0].1;
start -= pregap; if !start.is_zero() {
start -= pregap;
}
let duration = match next_track.next() { let duration = match next_track.next() {
Some(future) => match future.indices.get(0) { Some(future) => match future.indices.get(0) {
@ -540,23 +543,21 @@ impl MusicLibrary {
None => Duration::from_secs(0) None => Duration::from_secs(0)
} }
None => { None => {
let tagged_file = match lofty::read_from_path(&audio_location) { match lofty::read_from_path(audio_location) {
Ok(tagged_file) => tagged_file, Ok(tagged_file) => tagged_file.properties().duration() - start,
Err(_) => match Probe::open(&audio_location)?.read() { Err(_) => match Probe::open(audio_location)?.read() {
Ok(tagged_file) => tagged_file, Ok(tagged_file) => tagged_file.properties().duration() - start,
Err(error) => return Err(error.into()), Err(_) => Duration::from_secs(0),
}, },
}; }
tagged_file.properties().duration() - start
} }
}; };
let end = start + duration + postgap; let end = start + duration + postgap;
// Get the format as a string // Get the format as a string
let format: Option<FileFormat> = match FileFormat::from_file(&audio_location) { let format: Option<FileFormat> = match FileFormat::from_file(audio_location) {
Ok(fmt) => Some(fmt), Ok(fmt) => Some(fmt),
Err(_) => None, Err(_) => None,
}; };
@ -632,7 +633,7 @@ impl MusicLibrary {
None => (), None => (),
} }
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())
} }
_ => (), _ => (),
@ -656,10 +657,14 @@ impl MusicLibrary {
} }
/// Scan the song by a location and update its tags /// Scan the song by a location and update its tags
pub fn update_by_file(&mut self, new_tags: Song) -> Result<(), Box<dyn std::error::Error>> { pub fn update_uri(&mut self, target_uri: &URI, new_tags: Vec<Tag>) -> Result<(), Box<dyn std::error::Error>> {
match self.query_uri(&new_tags.location) { match self.query_uri(target_uri) {
Some(_) => (), Some(_) => (),
None => return Err(format!("URI not in database!").into()), None => return Err("URI not in database!".to_string().into()),
}
for tag in new_tags {
println!("{:?}", tag);
} }
todo!() todo!()
@ -667,7 +672,7 @@ impl MusicLibrary {
/// Query the database, returning a list of [Song]s /// Query the database, returning a list of [Song]s
/// ///
/// The order in which the sort by Vec is arranged /// The order in which the `sort by` Vec is arranged
/// determines the output sorting. /// determines the output sorting.
/// ///
/// Example: /// Example:
@ -700,11 +705,11 @@ impl MusicLibrary {
self.library.par_iter().for_each(|track| { self.library.par_iter().for_each(|track| {
for tag in target_tags { for tag in target_tags {
let track_result = match tag { let track_result = match tag {
Tag::Field(target) => match track.get_field(&target) { Tag::Field(target) => match track.get_field(target) {
Some(value) => value, Some(value) => value,
None => continue, None => continue,
}, },
_ => match track.get_tag(&tag) { _ => match track.get_tag(tag) {
Some(value) => value.clone(), Some(value) => value.clone(),
None => continue, None => continue,
}, },
@ -740,7 +745,7 @@ impl MusicLibrary {
Some(field_value) => field_value, Some(field_value) => field_value,
None => continue, None => continue,
}, },
_ => match a.get_tag(&sort_option) { _ => match a.get_tag(sort_option) {
Some(tag_value) => tag_value.to_owned(), Some(tag_value) => tag_value.to_owned(),
None => continue, None => continue,
}, },
@ -751,7 +756,7 @@ impl MusicLibrary {
Some(field_value) => field_value, Some(field_value) => field_value,
None => continue, None => continue,
}, },
_ => match b.get_tag(&sort_option) { _ => match b.get_tag(sort_option) {
Some(tag_value) => tag_value.to_owned(), Some(tag_value) => tag_value.to_owned(),
None => continue, None => continue,
}, },
@ -773,7 +778,7 @@ impl MusicLibrary {
path_a.file_name().cmp(&path_b.file_name()) path_a.file_name().cmp(&path_b.file_name())
}); });
if new_songs.len() > 0 { if !new_songs.is_empty() {
Some(new_songs) Some(new_songs)
} else { } else {
None None

View file

@ -1,6 +1,6 @@
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use std::path::Path; use std::path::Path;
pub fn playlist_add(config: &Config, playlist_name: &str, song_paths: &Vec<&Path>) { pub fn playlist_add(_config: &Config, _playlist_name: &str, _song_paths: &Vec<&Path>) {
unimplemented!() unimplemented!()
} }

View file

@ -45,12 +45,12 @@ pub(super) fn write_library(
backup_name.set_extension("bkp"); backup_name.set_extension("bkp");
// Create a new BufWriter on the file and make a snap frame encoer for it too // 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 writer = BufWriter::new(fs::File::create(&writer_name)?);
let mut e = snap::write::FrameEncoder::new(writer); let mut e = snap::write::FrameEncoder::new(writer);
// Write out the data using bincode // Write out the data using bincode
bincode::serde::encode_into_std_write( bincode::serde::encode_into_std_write(
&library, library,
&mut e, &mut e,
bincode::config::standard() bincode::config::standard()
.with_little_endian() .with_little_endian()
@ -83,7 +83,7 @@ pub fn find_images(song_path: &PathBuf) -> Result<Vec<AlbumArt>, Box<dyn Error>>
continue; continue;
} }
let format = FileFormat::from_file(&path)?.kind(); let format = FileFormat::from_file(path)?.kind();
if format != Kind::Image { if format != Kind::Image {
break break
} }

View file

@ -41,7 +41,7 @@ pub enum TrackerError {
impl TrackerError { impl TrackerError {
pub fn from_surf_error(error: surf::Error) -> TrackerError { pub fn from_surf_error(error: surf::Error) -> TrackerError {
return match error.status() { match error.status() {
StatusCode::Forbidden => TrackerError::InvalidAuth, StatusCode::Forbidden => TrackerError::InvalidAuth,
StatusCode::Unauthorized => TrackerError::InvalidAuth, StatusCode::Unauthorized => TrackerError::InvalidAuth,
StatusCode::NetworkAuthenticationRequired => TrackerError::InvalidAuth, StatusCode::NetworkAuthenticationRequired => TrackerError::InvalidAuth,
@ -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,
}; }
} }
} }
@ -85,8 +85,8 @@ impl MusicTracker for LastFM {
}; };
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 {
@ -104,8 +104,8 @@ impl MusicTracker for LastFM {
}; };
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(()),
@ -123,7 +123,7 @@ impl MusicTracker for LastFM {
}; };
} }
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!();
} }
} }
@ -160,7 +160,7 @@ impl LastFM {
auth_token.token auth_token.token
); );
return Ok(auth_url); Ok(auth_url)
} }
/// Returns a LastFM session key /// Returns a LastFM session key
@ -186,15 +186,15 @@ impl LastFM {
// 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); 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 {
LastFM {
config: config.clone(), config: config.clone(),
}; }
return last_fm;
} }
// Creates an api request with the given parameters // Creates an api request with the given parameters
@ -221,9 +221,9 @@ impl LastFM {
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;
return response; surf::post(url).body_string(string_params).await
} }
// Returns an api signature as defined in the last.fm api documentation // Returns an api signature as defined in the last.fm api documentation
@ -242,7 +242,7 @@ impl LastFM {
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; hashed_sig
} }
// Removes last.fm account from dango-music-player // Removes last.fm account from dango-music-player
@ -265,11 +265,11 @@ pub struct DiscordRPC {
impl DiscordRPC { impl DiscordRPC {
pub fn new(config: &DiscordRPCConfig) -> Self { pub fn new(config: &DiscordRPCConfig) -> Self {
let rpc = DiscordRPC {
DiscordRPC {
client: discord_presence::client::Client::new(config.dango_client_id), client: discord_presence::client::Client::new(config.dango_client_id),
config: config.clone(), config: config.clone(),
}; }
return rpc;
} }
} }
@ -307,8 +307,8 @@ impl MusicTracker for DiscordRPC {
// 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!("{}", album)) .state(album.to_string())
.details(format!("{}", song_name)) .details(song_name.to_string())
.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))
}); });
@ -319,7 +319,7 @@ impl MusicTracker for DiscordRPC {
} }
} }
async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_song(&mut self, _song: Song) -> Result<(), TrackerError> {
return Ok(()); return Ok(());
} }
@ -327,7 +327,7 @@ impl MusicTracker for DiscordRPC {
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);
} }
} }
@ -408,7 +408,7 @@ impl MusicTracker for ListenBrainz {
async fn test_tracker(&mut self) -> Result<(), TrackerError> { async fn test_tracker(&mut self) -> Result<(), TrackerError> {
todo!() todo!()
} }
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!()
} }
} }
@ -425,10 +425,10 @@ impl ListenBrainz {
request: &String, request: &String,
endpoint: &String, endpoint: &String,
) -> Result<surf::Response, surf::Error> { ) -> Result<surf::Response, surf::Error> {
let reponse = surf::post(format!("{}{}", &self.config.api_url, endpoint))
surf::post(format!("{}{}", &self.config.api_url, endpoint))
.body_string(request.clone()) .body_string(request.clone())
.header("Authorization", format!("Token {}", self.config.auth_token)) .header("Authorization", format!("Token {}", self.config.auth_token))
.await; .await
return reponse;
} }
} }