diff --git a/Cargo.toml b/Cargo.toml index 86ab8a8..3376e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ rayon = "1.8.0" log = "0.4" pretty_env_logger = "0.4" cue = "2.0.0" -jwalk = "0.8.1" base64 = "0.21.5" zip = "0.6.6" flate2 = "1.0.28" diff --git a/src/lib.rs b/src/lib.rs index 9afe354..7aa37eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,7 @@ pub mod music_tracker { pub mod music_storage { pub mod music_db; pub mod playlist; - pub mod utils; -} - -pub mod music_processor { - pub mod music_processor; + mod utils; } pub mod music_player { diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index 9455e58..51be432 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use crate::music_controller::config::Config; -use crate::music_player::music_player::{DSPMessage, DecoderMessage, MusicPlayer, PlayerStatus}; +use crate::music_player::music_player::{DecoderMessage, MusicPlayer, PlayerStatus}; use crate::music_storage::music_db::{MusicLibrary, Song, Tag}; pub struct MusicController { @@ -63,19 +63,6 @@ impl MusicController { return self.music_player.get_current_song(); } - /// Gets audio playback volume - pub fn get_vol(&self) -> f32 { - return self.music_player.music_processor.audio_volume; - } - - /// Sets audio playback volume on a scale of 0.0 to 1.0 - pub fn set_vol(&mut self, volume: f32) { - self.music_player.music_processor.audio_volume = volume; - self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new( - self.music_player.music_processor.clone(), - )))); - } - /// Queries the [MusicLibrary], returning a `Vec` pub fn query_library( &self, diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 9ed99e5..e3c43ad 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -19,7 +19,6 @@ use futures::AsyncBufRead; use crate::music_controller::config::Config; use crate::music_player::music_output::AudioStream; -use crate::music_processor::music_processor::MusicProcessor; use crate::music_storage::music_db::{Song, URI}; use crate::music_tracker::music_tracker::{ DiscordRPC, LastFM, ListenBrainz, MusicTracker, TrackerError, @@ -27,7 +26,6 @@ use crate::music_tracker::music_tracker::{ // Struct that controls playback of music pub struct MusicPlayer { - pub music_processor: MusicProcessor, player_status: PlayerStatus, music_trackers: Vec>, current_song: Arc>>, @@ -51,7 +49,6 @@ pub enum DecoderMessage { Pause, Stop, SeekTo(u64), - DSP(DSPMessage), } #[derive(Clone)] @@ -60,11 +57,6 @@ pub enum TrackerMessage { TrackNow(Song), } -#[derive(Debug, Clone)] -pub enum DSPMessage { - UpdateProcessor(Box), -} - // Holds a song decoder reader, etc struct SongHandler { pub reader: Box, @@ -148,7 +140,6 @@ impl MusicPlayer { ); MusicPlayer { - music_processor: MusicProcessor::new(), music_trackers: Vec::new(), player_status: PlayerStatus::Stopped, current_song, @@ -235,8 +226,6 @@ impl MusicPlayer { let mut audio_output: Option> = None; - let mut music_processor = MusicProcessor::new(); - let (tracker_sender, tracker_receiver): ( Sender, Receiver, @@ -290,11 +279,6 @@ impl MusicPlayer { status_sender.send(PlayerStatus::Paused).unwrap(); } Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time), - Some(DecoderMessage::DSP(dsp_message)) => match dsp_message { - DSPMessage::UpdateProcessor(new_processor) => { - music_processor = *new_processor - } - }, // Exits main decode loop and subsequently ends thread Some(DecoderMessage::Stop) => { status_sender.send(PlayerStatus::Stopped).unwrap(); @@ -373,22 +357,6 @@ impl MusicPlayer { .unwrap(), ); } - // Handles audio normally provided there is an audio stream - if let Some(ref mut audio_output) = audio_output { - // Changes buffer of the MusicProcessor if the packet has a differing capacity or spec - if music_processor.audio_buffer.capacity() != decoded.capacity() - || music_processor.audio_buffer.spec() != decoded.spec() - { - let spec = *decoded.spec(); - let duration = decoded.capacity() as u64; - - music_processor.set_buffer(duration, spec); - } - let transformed_audio = music_processor.process(&decoded); - - // Writes transformed packet to audio out - audio_output.write(transformed_audio).unwrap() - } } Err(Error::IoError(_)) => { // rest in peace packet diff --git a/src/music_processor/music_processor.rs b/src/music_processor/music_processor.rs deleted file mode 100644 index 64f4eaa..0000000 --- a/src/music_processor/music_processor.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::fmt::Debug; - -use symphonia::core::audio::{AsAudioBufferRef, AudioBuffer, AudioBufferRef, Signal, SignalSpec}; - -#[derive(Clone)] -pub struct MusicProcessor { - pub audio_buffer: AudioBuffer, - pub audio_volume: f32, -} - -impl Debug for MusicProcessor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MusicProcessor") - .field("audio_volume", &self.audio_volume) - .finish() - } -} - -impl MusicProcessor { - /// Returns new MusicProcessor with blank buffer and 100% volume - pub fn new() -> Self { - MusicProcessor { - audio_buffer: AudioBuffer::unused(), - audio_volume: 1.0, - } - } - - /// Processes audio samples - /// - /// Currently only supports transformations of volume - pub fn process(&mut self, audio_buffer_ref: &AudioBufferRef) -> AudioBufferRef { - audio_buffer_ref.convert(&mut self.audio_buffer); - - let process = |sample| sample * self.audio_volume; - - self.audio_buffer.transform(process); - - return self.audio_buffer.as_audio_buffer_ref(); - } - - /// Sets buffer of the MusicProcessor - pub fn set_buffer(&mut self, duration: u64, spec: SignalSpec) { - self.audio_buffer = AudioBuffer::new(duration, spec); - } -} diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index c6a59be..9e96e44 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -1,17 +1,17 @@ // Crate things -use super::utils::{normalize, read_library, write_library}; +use super::utils::{normalize, read_library, write_library, find_images}; use crate::music_controller::config::Config; // Various std things use std::collections::BTreeMap; use std::error::Error; +use std::ops::ControlFlow::{Break, Continue}; // Files use cue::cd::CD; use file_format::{FileFormat, Kind}; -use jwalk::WalkDir; +use walkdir::WalkDir; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt}; -use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; @@ -28,9 +28,9 @@ use rayon::prelude::*; use std::sync::{Arc, Mutex, RwLock}; #[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AlbumArt { - pub index: u16, - pub path: Option, +pub enum AlbumArt { + Embedded(usize), + External(URI), } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] @@ -112,7 +112,7 @@ impl Song { }, None => None, }, - _ => None, // Other field types are not yet supported + _ => todo!(), // Other field types are not yet supported } } } @@ -182,6 +182,7 @@ pub enum Service { InternetRadio, Spotify, Youtube, + None, } #[derive(Clone, Debug)] @@ -236,6 +237,8 @@ impl Album<'_> { } } +const BLOCKED_EXTENSIONS: [&str; 3] = ["vob", "log", "txt"]; + #[derive(Debug)] pub struct MusicLibrary { pub library: Vec, @@ -272,8 +275,7 @@ impl MusicLibrary { Ok(Self { library }) } - /// Serializes the database out to the file - /// specified in the config + /// Serializes the database out to the file specified in the config pub fn save(&self, config: &Config) -> Result<(), Box> { match config.db_path.try_exists() { Ok(exists) => { @@ -293,19 +295,16 @@ impl MusicLibrary { /// Queries for a [Song] by its [URI], returning a single `Song` /// with the `URI` that matches fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { - let result = Arc::new(Mutex::new(None)); - let index = Arc::new(Mutex::new(0)); - let _ = &self.library.par_iter().enumerate().for_each(|(i, track)| { + let result = self.library.par_iter().enumerate().try_for_each(|(i, track)| { if path == &track.location { - *result.clone().lock().unwrap() = Some(track); - *index.clone().lock().unwrap() = i; - return; + return std::ops::ControlFlow::Break((track, i)); } + Continue(()) }); - let song = Arc::try_unwrap(result).unwrap().into_inner().unwrap(); - match song { - Some(song) => Some((song, Arc::try_unwrap(index).unwrap().into_inner().unwrap())), - None => None, + + match result { + Break(song) => Some(song), + Continue(_) => None, } } @@ -313,7 +312,7 @@ impl MusicLibrary { /// with matching `PathBuf`s fn query_path(&self, path: &PathBuf) -> Option> { let result: Arc>> = Arc::new(Mutex::new(Vec::new())); - let _ = &self.library.par_iter().for_each(|track| { + let _ = self.library.par_iter().for_each(|track| { if path == track.location.path() { result.clone().lock().unwrap().push(&track); return; @@ -326,20 +325,18 @@ impl MusicLibrary { } } - /// Finds all the music files within a specified folder + /// Finds all the audio files within a specified folder pub fn scan_folder( &mut self, target_path: &str, config: &Config, ) -> Result> { let mut total = 0; - let mut i = 0; - for entry in WalkDir::new(target_path) + for target_file in WalkDir::new(target_path) .follow_links(true) .into_iter() .filter_map(|e| e.ok()) { - let target_file = entry; let path = target_file.path(); // Ensure the target is a file and not a directory, @@ -348,6 +345,7 @@ impl MusicLibrary { continue; } + /* TODO: figure out how to increase the speed of this maybe // Check if the file path is already in the db if self.query_uri(&URI::Local(path.to_path_buf())).is_some() { continue; @@ -358,30 +356,27 @@ impl MusicLibrary { if i % 500 == 0 { self.save(config).unwrap(); } + */ let format = FileFormat::from_file(&path)?; - let extension: &OsStr = match path.extension() { - Some(ext) => ext, - None => OsStr::new(""), + let extension = match path.extension() { + Some(ext) => ext.to_string_lossy().to_ascii_lowercase(), + None => String::new(), }; // If it's a normal file, add it to the database // if it's a cuesheet, do a bunch of fancy stuff if (format.kind() == Kind::Audio || format.kind() == Kind::Video) - && extension.to_ascii_lowercase() != "log" - && extension.to_ascii_lowercase() != "vob" + && !BLOCKED_EXTENSIONS.contains(&extension.as_str()) { match self.add_file(&target_file.path()) { - Ok(_) => { - //println!("{:?}", target_file.path()); - total += 1 - } + Ok(_) => total += 1, Err(_error) => { - //println!("{}, {:?}: {}", format, target_file.file_name(), _error) + println!("{}, {:?}: {}", format, target_file.file_name(), _error) } // TODO: Handle more of these errors }; - } else if extension.to_ascii_lowercase() == "cue" { - total += match self.add_cuesheet(&target_file.path()) { + } else if extension == "cue" { + total += match self.add_cuesheet(&target_file.path().to_path_buf()) { Ok(added) => added, Err(error) => { println!("{}", error); @@ -437,25 +432,26 @@ impl MusicLibrary { }; let value = match item.value() { - ItemValue::Text(value) => String::from(value), - ItemValue::Locator(value) => String::from(value), + ItemValue::Text(value) => value.clone(), + ItemValue::Locator(value) => value.clone(), ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), }; tags.insert(key, value); } - // Get all the album artwork information + // Get all the album artwork information from the file let mut album_art: Vec = Vec::new(); for (i, _art) in tag.pictures().iter().enumerate() { - let new_art = AlbumArt { - index: i as u16, - path: None, - }; + let new_art = AlbumArt::Embedded(i as usize); album_art.push(new_art) } + // Find images around the music file that can be used + let mut found_images = find_images(&target_file.to_path_buf()).unwrap(); + album_art.append(&mut found_images); + // Get the format as a string let format: Option = match FileFormat::from_file(target_file) { Ok(fmt) => Some(fmt), @@ -515,7 +511,10 @@ impl MusicLibrary { } // Try to remove the original audio file from the db if it exists - let _ = self.remove_uri(&URI::Local(audio_location.clone())); + match self.remove_uri(&URI::Local(audio_location.clone())) { + Ok(_) => tracks_added -= 1, + Err(_) => () + }; // Get the track timing information let pregap = match track.get_zero_pre() { @@ -687,6 +686,7 @@ impl MusicLibrary { sort_by: &Vec, // Tags to sort the resulting data by ) -> Option> { let songs = Arc::new(Mutex::new(Vec::new())); + //let matcher = SkimMatcherV2::default(); self.library.par_iter().for_each(|track| { for tag in target_tags { @@ -701,9 +701,19 @@ impl MusicLibrary { }, }; - if normalize(&track_result.to_string()) - .contains(&normalize(&query_string.to_owned())) - { + /* + let match_level = match matcher.fuzzy_match(&normalize(&track_result), &normalize(query_string)) { + Some(conf) => conf, + None => continue + }; + + if match_level > 100 { + songs.lock().unwrap().push(track); + return; + } + */ + + if normalize(&track_result.to_string()).contains(&normalize(&query_string.to_owned())) { songs.lock().unwrap().push(track); return; } @@ -776,6 +786,7 @@ impl MusicLibrary { .unwrap_or(&"".to_string()) .parse::() .unwrap_or(1); + match albums.get_mut(&norm_title) { // If the album is in the list, add the track to the appropriate disc in it Some(album) => match album.discs.get_mut(&disc_num) { @@ -786,11 +797,13 @@ impl MusicLibrary { }, // If the album is not in the list, make a new one and add it None => { + let album_art = result.album_art.get(0); + let new_album = Album { title, artist: result.get_tag(&Tag::AlbumArtist), discs: BTreeMap::from([(disc_num, vec![result])]), - cover: None, + cover: album_art, }; albums.insert(norm_title, new_album); } @@ -824,6 +837,7 @@ impl MusicLibrary { albums } + /// Queries a list of albums by title pub fn query_albums( &self, query_string: &String, // The query itself diff --git a/src/music_storage/utils.rs b/src/music_storage/utils.rs index 590f8ce..0847ae0 100644 --- a/src/music_storage/utils.rs +++ b/src/music_storage/utils.rs @@ -1,13 +1,14 @@ use std::io::{BufReader, BufWriter}; use std::{error::Error, fs, path::PathBuf}; +use walkdir::WalkDir; +use file_format::{FileFormat, Kind}; use snap; -use crate::music_storage::music_db::Song; +use super::music_db::{Song, AlbumArt, URI}; use unidecode::unidecode; -pub fn normalize(input_string: &String) -> String { - // Normalize the unicode and convert everything to lowercase +pub(super) fn normalize(input_string: &String) -> String { let mut normalized = unidecode(input_string); // Remove non alphanumeric characters @@ -16,7 +17,7 @@ pub fn normalize(input_string: &String) -> String { normalized } -pub fn read_library(path: PathBuf) -> Result, Box> { +pub(super) fn read_library(path: PathBuf) -> Result, Box> { // Create a new snap reader over the database file let database = fs::File::open(path)?; let reader = BufReader::new(database); @@ -32,7 +33,7 @@ pub fn read_library(path: PathBuf) -> Result, Box> { Ok(library) } -pub fn write_library( +pub(super) fn write_library( library: &Vec, path: PathBuf, take_backup: bool, @@ -63,3 +64,34 @@ pub fn write_library( Ok(()) } + +pub fn find_images(song_path: &PathBuf) -> Result, Box> { + let mut images: Vec = Vec::new(); + + let song_dir = song_path.parent().ok_or("")?; + for target_file in WalkDir::new(song_dir) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + if target_file.depth() >= 3 { // Don't recurse very deep + break + } + + let path = target_file.path(); + if !path.is_file() { + continue; + } + + let format = FileFormat::from_file(&path)?.kind(); + if format != Kind::Image { + break + } + + let image_uri = URI::Local(path.to_path_buf().canonicalize().unwrap()); + + images.push(AlbumArt::External(image_uri)); + } + + Ok(images) +}