mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-19 13:22:54 -05:00
Merge pull request #18 from Dangoware/database
Merge new things from database branch
This commit is contained in:
commit
755c62f702
12 changed files with 84 additions and 1013 deletions
|
@ -13,7 +13,7 @@ categories = ["multimedia::audio"]
|
|||
|
||||
[dependencies]
|
||||
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"] }
|
||||
time = "0.3.22"
|
||||
toml = "0.7.5"
|
||||
|
|
|
@ -8,14 +8,7 @@ pub mod music_storage {
|
|||
mod utils;
|
||||
}
|
||||
|
||||
pub mod music_player {
|
||||
pub mod music_output;
|
||||
pub mod music_player;
|
||||
pub mod music_resampler;
|
||||
}
|
||||
|
||||
pub mod music_controller {
|
||||
pub mod config;
|
||||
pub mod init;
|
||||
pub mod music_controller;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ impl Default for Config {
|
|||
fn default() -> Self {
|
||||
let path = PathBuf::from("./music_database");
|
||||
|
||||
return Config {
|
||||
Config {
|
||||
db_path: Box::new(path),
|
||||
|
||||
lastfm: None,
|
||||
|
@ -34,7 +34,7 @@ impl Default for Config {
|
|||
api_url: String::from("https://api.listenbrainz.org"),
|
||||
auth_token: String::from(""),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,10 +49,10 @@ impl Config {
|
|||
|
||||
/// Loads config from given file path
|
||||
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)
|
||||
.expect("Failed to initalize music config: File not found!"),
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/// Saves config to given path
|
||||
|
|
|
@ -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() {}
|
|
@ -2,20 +2,17 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::music_controller::config::Config;
|
||||
use crate::music_player::music_player::{DecoderMessage, MusicPlayer, PlayerStatus};
|
||||
use crate::music_storage::music_db::{MusicLibrary, Song, Tag};
|
||||
|
||||
pub struct MusicController {
|
||||
pub config: Arc<RwLock<Config>>,
|
||||
pub library: MusicLibrary,
|
||||
music_player: MusicPlayer,
|
||||
}
|
||||
|
||||
impl MusicController {
|
||||
/// Creates new MusicController with config at given path
|
||||
pub fn new(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> {
|
||||
let config = Arc::new(RwLock::new(Config::new(config_path)?));
|
||||
let music_player = MusicPlayer::new(config.clone());
|
||||
let library = match MusicLibrary::init(config.clone()) {
|
||||
Ok(library) => library,
|
||||
Err(error) => return Err(error),
|
||||
|
@ -24,16 +21,14 @@ impl MusicController {
|
|||
let controller = MusicController {
|
||||
config,
|
||||
library,
|
||||
music_player,
|
||||
};
|
||||
|
||||
return Ok(controller);
|
||||
Ok(controller)
|
||||
}
|
||||
|
||||
/// Creates new music controller from a config at given path
|
||||
pub fn from(config_path: &PathBuf) -> Result<MusicController, Box<dyn std::error::Error>> {
|
||||
let config = Arc::new(RwLock::new(Config::from(config_path)?));
|
||||
let music_player = MusicPlayer::new(config.clone());
|
||||
let library = match MusicLibrary::init(config.clone()) {
|
||||
Ok(library) => library,
|
||||
Err(error) => return Err(error),
|
||||
|
@ -42,25 +37,9 @@ impl MusicController {
|
|||
let controller = MusicController {
|
||||
config,
|
||||
library,
|
||||
music_player,
|
||||
};
|
||||
|
||||
return 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();
|
||||
Ok(controller)
|
||||
}
|
||||
|
||||
/// Queries the [MusicLibrary], returning a `Vec<Song>`
|
||||
|
@ -68,7 +47,7 @@ impl MusicController {
|
|||
&self,
|
||||
query_string: &String,
|
||||
target_tags: Vec<Tag>,
|
||||
search_location: bool,
|
||||
_search_location: bool,
|
||||
sort_by: Vec<Tag>,
|
||||
) -> Option<Vec<&Song>> {
|
||||
self.library
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ use std::ops::ControlFlow::{Break, Continue};
|
|||
use rcue::parser::parse_from_file;
|
||||
use file_format::{FileFormat, Kind};
|
||||
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::path::{Path, PathBuf};
|
||||
|
||||
|
@ -115,10 +115,7 @@ impl Song {
|
|||
"location" => Some(self.location.clone().path_string()),
|
||||
"plays" => Some(self.plays.clone().to_string()),
|
||||
"format" => match self.format {
|
||||
Some(format) => match format.short_name() {
|
||||
Some(short) => Some(short.to_string()),
|
||||
None => None,
|
||||
},
|
||||
Some(format) => format.short_name().map(|short| short.to_string()),
|
||||
None => None,
|
||||
},
|
||||
_ => 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)]
|
||||
pub struct MusicLibrary {
|
||||
|
@ -321,10 +318,9 @@ impl MusicLibrary {
|
|||
/// with matching `PathBuf`s
|
||||
fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> {
|
||||
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() {
|
||||
result.clone().lock().unwrap().push(&track);
|
||||
return;
|
||||
result.clone().lock().unwrap().push(track);
|
||||
}
|
||||
});
|
||||
if result.lock().unwrap().len() > 0 {
|
||||
|
@ -341,6 +337,7 @@ impl MusicLibrary {
|
|||
config: &Config,
|
||||
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
let mut total = 0;
|
||||
let mut errors = 0;
|
||||
for target_file in WalkDir::new(target_path)
|
||||
.follow_links(true)
|
||||
.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() {
|
||||
Some(ext) => ext.to_string_lossy().to_ascii_lowercase(),
|
||||
None => String::new(),
|
||||
|
@ -378,9 +375,10 @@ impl MusicLibrary {
|
|||
if (format.kind() == Kind::Audio || format.kind() == Kind::Video)
|
||||
&& !BLOCKED_EXTENSIONS.contains(&extension.as_str())
|
||||
{
|
||||
match self.add_file(&target_file.path()) {
|
||||
match self.add_file(target_file.path()) {
|
||||
Ok(_) => total += 1,
|
||||
Err(_error) => {
|
||||
errors += 1;
|
||||
println!("{}, {:?}: {}", format, target_file.file_name(), _error)
|
||||
} // TODO: Handle more of these errors
|
||||
};
|
||||
|
@ -388,6 +386,7 @@ impl MusicLibrary {
|
|||
total += match self.add_cuesheet(&target_file.path().to_path_buf()) {
|
||||
Ok(added) => added,
|
||||
Err(error) => {
|
||||
errors += 1;
|
||||
println!("{}", error);
|
||||
0
|
||||
}
|
||||
|
@ -396,21 +395,21 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
// Save the database after scanning finishes
|
||||
self.save(&config).unwrap();
|
||||
self.save(config).unwrap();
|
||||
|
||||
println!("ERRORS: {}", errors);
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box<dyn Error>> {
|
||||
// TODO: Fix error handling here
|
||||
let tagged_file = match lofty::read_from_path(target_file) {
|
||||
Ok(tagged_file) => tagged_file,
|
||||
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
|
||||
|
||||
Err(_) => match Probe::open(target_file)?.read() {
|
||||
// TODO: Fix error handling here
|
||||
let tagged_file = match Probe::open(target_file)?.options(normal_options).read() {
|
||||
Ok(tagged_file) => tagged_file,
|
||||
|
||||
Err(error) => return Err(error.into()),
|
||||
},
|
||||
};
|
||||
|
||||
// Ensure the tags exist, if not, insert blank data
|
||||
|
@ -435,7 +434,7 @@ impl MusicLibrary {
|
|||
ItemKey::Comment => Tag::Comment,
|
||||
ItemKey::AlbumTitle => Tag::Album,
|
||||
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()),
|
||||
custom => Tag::Key(format!("{:?}", custom)),
|
||||
};
|
||||
|
@ -452,7 +451,7 @@ impl MusicLibrary {
|
|||
// Get all the album artwork information from the file
|
||||
let mut album_art: Vec<AlbumArt> = Vec::new();
|
||||
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)
|
||||
}
|
||||
|
@ -490,7 +489,9 @@ impl MusicLibrary {
|
|||
|
||||
match self.add_song(new_song) {
|
||||
Ok(_) => (),
|
||||
Err(error) => return Err(error),
|
||||
Err(_) => {
|
||||
//return Err(error)
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
@ -532,7 +533,9 @@ impl MusicLibrary {
|
|||
None => Duration::from_secs(0),
|
||||
};
|
||||
let mut start = track.indices[0].1;
|
||||
if !start.is_zero() {
|
||||
start -= pregap;
|
||||
}
|
||||
|
||||
let duration = match next_track.next() {
|
||||
Some(future) => match future.indices.get(0) {
|
||||
|
@ -540,23 +543,21 @@ impl MusicLibrary {
|
|||
None => Duration::from_secs(0)
|
||||
}
|
||||
None => {
|
||||
let tagged_file = match lofty::read_from_path(&audio_location) {
|
||||
Ok(tagged_file) => tagged_file,
|
||||
match lofty::read_from_path(audio_location) {
|
||||
Ok(tagged_file) => tagged_file.properties().duration() - start,
|
||||
|
||||
Err(_) => match Probe::open(&audio_location)?.read() {
|
||||
Ok(tagged_file) => tagged_file,
|
||||
Err(_) => match Probe::open(audio_location)?.read() {
|
||||
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;
|
||||
|
||||
// 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),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
@ -632,7 +633,7 @@ impl MusicLibrary {
|
|||
None => (),
|
||||
}
|
||||
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())
|
||||
}
|
||||
_ => (),
|
||||
|
@ -656,10 +657,14 @@ impl MusicLibrary {
|
|||
}
|
||||
|
||||
/// 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>> {
|
||||
match self.query_uri(&new_tags.location) {
|
||||
pub fn update_uri(&mut self, target_uri: &URI, new_tags: Vec<Tag>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self.query_uri(target_uri) {
|
||||
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!()
|
||||
|
@ -667,7 +672,7 @@ impl MusicLibrary {
|
|||
|
||||
/// 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.
|
||||
///
|
||||
/// Example:
|
||||
|
@ -700,11 +705,11 @@ impl MusicLibrary {
|
|||
self.library.par_iter().for_each(|track| {
|
||||
for tag in target_tags {
|
||||
let track_result = match tag {
|
||||
Tag::Field(target) => match track.get_field(&target) {
|
||||
Tag::Field(target) => match track.get_field(target) {
|
||||
Some(value) => value,
|
||||
None => continue,
|
||||
},
|
||||
_ => match track.get_tag(&tag) {
|
||||
_ => match track.get_tag(tag) {
|
||||
Some(value) => value.clone(),
|
||||
None => continue,
|
||||
},
|
||||
|
@ -740,7 +745,7 @@ impl MusicLibrary {
|
|||
Some(field_value) => field_value,
|
||||
None => continue,
|
||||
},
|
||||
_ => match a.get_tag(&sort_option) {
|
||||
_ => match a.get_tag(sort_option) {
|
||||
Some(tag_value) => tag_value.to_owned(),
|
||||
None => continue,
|
||||
},
|
||||
|
@ -751,7 +756,7 @@ impl MusicLibrary {
|
|||
Some(field_value) => field_value,
|
||||
None => continue,
|
||||
},
|
||||
_ => match b.get_tag(&sort_option) {
|
||||
_ => match b.get_tag(sort_option) {
|
||||
Some(tag_value) => tag_value.to_owned(),
|
||||
None => continue,
|
||||
},
|
||||
|
@ -773,7 +778,7 @@ impl MusicLibrary {
|
|||
path_a.file_name().cmp(&path_b.file_name())
|
||||
});
|
||||
|
||||
if new_songs.len() > 0 {
|
||||
if !new_songs.is_empty() {
|
||||
Some(new_songs)
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::music_controller::config::Config;
|
||||
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!()
|
||||
}
|
||||
|
|
|
@ -45,12 +45,12 @@ pub(super) fn write_library(
|
|||
backup_name.set_extension("bkp");
|
||||
|
||||
// Create a new BufWriter on the file and make a snap frame encoer for it too
|
||||
let writer = BufWriter::new(fs::File::create(writer_name.to_path_buf())?);
|
||||
let writer = BufWriter::new(fs::File::create(&writer_name)?);
|
||||
let mut e = snap::write::FrameEncoder::new(writer);
|
||||
|
||||
// Write out the data using bincode
|
||||
bincode::serde::encode_into_std_write(
|
||||
&library,
|
||||
library,
|
||||
&mut e,
|
||||
bincode::config::standard()
|
||||
.with_little_endian()
|
||||
|
@ -83,7 +83,7 @@ pub fn find_images(song_path: &PathBuf) -> Result<Vec<AlbumArt>, Box<dyn Error>>
|
|||
continue;
|
||||
}
|
||||
|
||||
let format = FileFormat::from_file(&path)?.kind();
|
||||
let format = FileFormat::from_file(path)?.kind();
|
||||
if format != Kind::Image {
|
||||
break
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ pub enum TrackerError {
|
|||
|
||||
impl TrackerError {
|
||||
pub fn from_surf_error(error: surf::Error) -> TrackerError {
|
||||
return match error.status() {
|
||||
match error.status() {
|
||||
StatusCode::Forbidden => TrackerError::InvalidAuth,
|
||||
StatusCode::Unauthorized => TrackerError::InvalidAuth,
|
||||
StatusCode::NetworkAuthenticationRequired => TrackerError::InvalidAuth,
|
||||
|
@ -50,7 +50,7 @@ impl TrackerError {
|
|||
StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable,
|
||||
StatusCode::NotFound => TrackerError::ServiceUnavailable,
|
||||
_ => TrackerError::Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,8 +85,8 @@ impl MusicTracker for LastFM {
|
|||
};
|
||||
|
||||
params.insert("method", "track.scrobble");
|
||||
params.insert("artist", &artist);
|
||||
params.insert("track", &track);
|
||||
params.insert("artist", artist);
|
||||
params.insert("track", track);
|
||||
params.insert("timestamp", &string_timestamp);
|
||||
|
||||
return match self.api_request(params).await {
|
||||
|
@ -104,8 +104,8 @@ impl MusicTracker for LastFM {
|
|||
};
|
||||
|
||||
params.insert("method", "track.updateNowPlaying");
|
||||
params.insert("artist", &artist);
|
||||
params.insert("track", &track);
|
||||
params.insert("artist", artist);
|
||||
params.insert("track", track);
|
||||
|
||||
return match self.api_request(params).await {
|
||||
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!();
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ impl LastFM {
|
|||
auth_token.token
|
||||
);
|
||||
|
||||
return Ok(auth_url);
|
||||
Ok(auth_url)
|
||||
}
|
||||
|
||||
/// Returns a LastFM session key
|
||||
|
@ -186,15 +186,15 @@ impl LastFM {
|
|||
|
||||
// Sets session key from received 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
|
||||
pub fn new(config: &LastFMConfig) -> LastFM {
|
||||
let last_fm = LastFM {
|
||||
|
||||
LastFM {
|
||||
config: config.clone(),
|
||||
};
|
||||
return last_fm;
|
||||
}
|
||||
}
|
||||
|
||||
// Creates an api request with the given parameters
|
||||
|
@ -221,9 +221,9 @@ impl LastFM {
|
|||
|
||||
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
|
||||
|
@ -242,7 +242,7 @@ impl LastFM {
|
|||
let hash_result = md5_hasher.finalize();
|
||||
let hashed_sig = format!("{:#02x}", hash_result);
|
||||
|
||||
return hashed_sig;
|
||||
hashed_sig
|
||||
}
|
||||
|
||||
// Removes last.fm account from dango-music-player
|
||||
|
@ -265,11 +265,11 @@ pub struct DiscordRPC {
|
|||
|
||||
impl DiscordRPC {
|
||||
pub fn new(config: &DiscordRPCConfig) -> Self {
|
||||
let rpc = DiscordRPC {
|
||||
|
||||
DiscordRPC {
|
||||
client: discord_presence::client::Client::new(config.dango_client_id),
|
||||
config: config.clone(),
|
||||
};
|
||||
return rpc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,8 +307,8 @@ impl MusicTracker for DiscordRPC {
|
|||
// Sets discord account activity to current playing song
|
||||
let send_activity = self.client.set_activity(|activity| {
|
||||
activity
|
||||
.state(format!("{}", album))
|
||||
.details(format!("{}", song_name))
|
||||
.state(album.to_string())
|
||||
.details(song_name.to_string())
|
||||
.assets(|assets| assets.large_image(&self.config.dango_icon))
|
||||
.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(());
|
||||
}
|
||||
|
||||
|
@ -327,7 +327,7 @@ impl MusicTracker for DiscordRPC {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
@ -408,7 +408,7 @@ impl MusicTracker for ListenBrainz {
|
|||
async fn test_tracker(&mut self) -> Result<(), TrackerError> {
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
@ -425,10 +425,10 @@ impl ListenBrainz {
|
|||
request: &String,
|
||||
endpoint: &String,
|
||||
) -> 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())
|
||||
.header("Authorization", format!("Token {}", self.config.auth_token))
|
||||
.await;
|
||||
return reponse;
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue