From 00144d41ed6a1ea08684d80f69c0e7fe497e568a Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Thu, 9 Nov 2023 01:26:00 +0100 Subject: [PATCH] Implement FLAC scanning --- Cargo.lock | 24 ++++ .../main/src/controllers/local-library.ts | 12 +- packages/scanner/Cargo.toml | 1 + packages/scanner/index.d.ts | 3 +- packages/scanner/src/error.rs | 13 ++ packages/scanner/src/lib.rs | 38 ++++-- packages/scanner/src/local_track.rs | 12 +- packages/scanner/src/metadata.rs | 113 ++++++++++++++++++ packages/scanner/src/scanner.rs | 9 +- 9 files changed, 200 insertions(+), 25 deletions(-) create mode 100644 packages/scanner/src/metadata.rs diff --git a/Cargo.lock b/Cargo.lock index 0bee7dda5d..6ff33d8b98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "id3" version = "1.7.0" @@ -342,6 +348,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "md5" version = "0.7.0" @@ -363,6 +375,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metaflac" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1470d3cc1bb0d692af5eb3afb594330b8ba09fd91c32c4e1c6322172a5ba750" +dependencies = [ + "byteorder", + "hex", + "log", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -639,6 +662,7 @@ dependencies = [ "id3", "image", "md5", + "metaflac", "mockall", "neon", "uuid", diff --git a/packages/main/src/controllers/local-library.ts b/packages/main/src/controllers/local-library.ts index 97a3c90f21..3b5c5eefc9 100644 --- a/packages/main/src/controllers/local-library.ts +++ b/packages/main/src/controllers/local-library.ts @@ -8,13 +8,17 @@ import { ipcController, ipcEvent } from '../utils/decorators'; import LocalLibraryDb from '../services/local-library/db'; import Platform from '../services/platform'; import Window from '../services/window'; +import Config from '../services/config'; +import Logger, { $mainLogger } from '../services/logger'; @ipcController() class LocalIpcCtrl { constructor( + @inject(Config) private config: Config, @inject(LocalLibrary) private localLibrary: LocalLibrary, @inject(LocalLibraryDb) private localLibraryDb: LocalLibraryDb, @inject(Platform) private platform: Platform, + @inject($mainLogger) private logger: Logger, @inject(Window) private window: Window ) {} @@ -54,9 +58,9 @@ class LocalIpcCtrl { .map(folder => this.localLibraryDb.addFolder(this.normalizeFolderPath(folder))) ); - const cache = await scanFolders(directories, ['mp3'], this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => { + const cache = await scanFolders(directories, this.config.supportedFormats, this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => { this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal}); - }); + }, () => {}); this.window.send(IpcEvents.LOCAL_FILES, Object.values(cache).reduce((acc, track) => ({ ...acc, @@ -84,9 +88,9 @@ class LocalIpcCtrl { try { const folders = await this.localLibraryDb.getLocalFolders(); - const cache = await scanFolders(folders.map(folder => folder.path), ['mp3'], this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => { + const cache = await scanFolders(folders.map(folder => folder.path), this.config.supportedFormats, this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => { this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal}); - }); + }, () => {}); this.window.send(IpcEvents.LOCAL_FILES, cache); } catch (err) { diff --git a/packages/scanner/Cargo.toml b/packages/scanner/Cargo.toml index b622f93c15..d06811c93e 100644 --- a/packages/scanner/Cargo.toml +++ b/packages/scanner/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [dependencies] id3 = "1.7.0" md5 = "0.7.0" +metaflac = "0.2.5" mockall = "0.11.4" [dependencies.image] diff --git a/packages/scanner/index.d.ts b/packages/scanner/index.d.ts index 96b0ac9158..2ddbc0d594 100644 --- a/packages/scanner/index.d.ts +++ b/packages/scanner/index.d.ts @@ -17,7 +17,8 @@ declare const scanFolders = ( folders: string[], supportedFormats: string[], thumbnailsDir: string, - onProgress: (progress: number, total: number, lastScanned?: string) => void + onProgress: (progress: number, total: number, lastScanned?: string) => void, + onError: (track: string, error: string) => void ) => new Promise; declare const generateThumbnail = (filename: string, thumbnailsDir: string) => new Promise; diff --git a/packages/scanner/src/error.rs b/packages/scanner/src/error.rs index 30c9d6a73a..dafbd940cb 100644 --- a/packages/scanner/src/error.rs +++ b/packages/scanner/src/error.rs @@ -13,3 +13,16 @@ impl fmt::Display for ScannerError { write!(f, "ScannerError: {}", self.message) } } + +#[derive(Debug)] +pub struct MetadataError { + pub message: String, +} + +impl Error for MetadataError {} + +impl fmt::Display for MetadataError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MetadataError: {}", self.message) + } +} diff --git a/packages/scanner/src/lib.rs b/packages/scanner/src/lib.rs index f21afdf4a8..20993c3587 100644 --- a/packages/scanner/src/lib.rs +++ b/packages/scanner/src/lib.rs @@ -1,6 +1,8 @@ +#![forbid(unsafe_code)] mod error; mod js; mod local_track; +mod metadata; mod scanner; mod thumbnails; use id3::Tag; @@ -16,6 +18,7 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult { let thumbnails_dir: Handle = cx.argument(2)?; let thumbnails_dir_str = thumbnails_dir.value(&mut cx); let on_progress_callback: Handle = cx.argument(3)?; + let on_error_callback: Handle = cx.argument(4)?; let result: Handle = cx.empty_array(); // Copy all the starting folders to a queue, which holds all the folders left to scan @@ -83,6 +86,10 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult { ]; on_progress_callback.call(&mut cx, this, args)?; + let error = track.err().unwrap(); + let error_string = cx.string(error.message); + let on_error_args = vec![cx.string(file.clone()).upcast(), error_string.upcast()]; + on_error_callback.call(&mut cx, this, on_error_args)?; continue; } @@ -93,18 +100,33 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult { let track_uuid_js_string = cx.string(track.uuid); track_js_object.set(&mut cx, "uuid", track_uuid_js_string)?; - set_optional_field_str(&mut cx, &mut track_js_object, "artist", track.artist); - set_optional_field_str(&mut cx, &mut track_js_object, "title", track.title); - set_optional_field_str(&mut cx, &mut track_js_object, "album", track.album); + set_optional_field_str( + &mut cx, + &mut track_js_object, + "artist", + track.metadata.artist, + ); + set_optional_field_str(&mut cx, &mut track_js_object, "title", track.metadata.title); + set_optional_field_str(&mut cx, &mut track_js_object, "album", track.metadata.album); - let track_duration_js_number = cx.number(track.duration); + let track_duration_js_number = cx.number(track.metadata.duration); track_js_object.set(&mut cx, "duration", track_duration_js_number)?; - set_optional_field_str(&mut cx, &mut track_js_object, "thumbnail", track.thumbnail); + set_optional_field_str( + &mut cx, + &mut track_js_object, + "thumbnail", + track.metadata.thumbnail, + ); - set_optional_field_u32(&mut cx, &mut track_js_object, "position", track.position); - set_optional_field_u32(&mut cx, &mut track_js_object, "disc", track.disc); - set_optional_field_u32(&mut cx, &mut track_js_object, "year", track.year); + set_optional_field_u32( + &mut cx, + &mut track_js_object, + "position", + track.metadata.position, + ); + set_optional_field_u32(&mut cx, &mut track_js_object, "disc", track.metadata.disc); + set_optional_field_u32(&mut cx, &mut track_js_object, "year", track.metadata.year); let track_filename_js_string = cx.string(track.filename); track_js_object.set(&mut cx, "filename", track_filename_js_string)?; diff --git a/packages/scanner/src/local_track.rs b/packages/scanner/src/local_track.rs index 4aeec16cfb..c1d974c228 100644 --- a/packages/scanner/src/local_track.rs +++ b/packages/scanner/src/local_track.rs @@ -1,14 +1,10 @@ +use crate::metadata::AudioMetadata; + #[derive(Debug, Clone)] pub struct LocalTrack { pub uuid: String, - pub artist: Option, - pub title: Option, - pub album: Option, - pub duration: u32, - pub thumbnail: Option, - pub disc: Option, - pub position: Option, - pub year: Option, + + pub metadata: AudioMetadata, pub filename: String, pub path: String, diff --git a/packages/scanner/src/metadata.rs b/packages/scanner/src/metadata.rs new file mode 100644 index 0000000000..d39ed283c6 --- /dev/null +++ b/packages/scanner/src/metadata.rs @@ -0,0 +1,113 @@ +use id3::TagLike; +use metaflac; + +use crate::{error::MetadataError, thumbnails::generate_thumbnail}; + +pub struct AudioMetadata { + pub artist: Option, + pub title: Option, + pub album: Option, + pub duration: u32, + pub disc: Option, + pub position: Option, + pub year: Option, + pub thumbnail: Option, +} + +impl AudioMetadata { + pub fn new() -> Self { + Self { + artist: None, + title: None, + album: None, + duration: 0, + disc: None, + position: None, + year: None, + thumbnail: None, + } + } +} + +pub trait MetadataExtractor { + fn extract_metadata( + &self, + path: &str, + thumbnails_dir: &str, + ) -> Result; +} + +#[derive(Debug, Clone)] + +pub struct Mp3MetadataExtractor; +impl MetadataExtractor for Mp3MetadataExtractor { + fn extract_metadata( + &self, + path: &str, + thumbnails_dir: &str, + ) -> Result { + let tag = id3::Tag::read_from_path(path).unwrap(); + let mut metadata = AudioMetadata::new(); + + metadata.artist = tag.artist().map(|s| s.to_string()); + metadata.title = tag.title().map(|s| s.to_string()); + metadata.album = tag.album().map(|s| s.to_string()); + metadata.duration = tag.duration().unwrap_or(0); + metadata.position = tag.track(); + metadata.disc = tag.disc(); + metadata.year = tag.year().map(|s| s as u32); + metadata.thumbnail = generate_thumbnail(&path, thumbnails_dir); + + Ok(metadata) + } +} + +pub struct FlacMetadataExtractor; +impl FlacMetadataExtractor { + fn extract_string_metadata( + tag: &metaflac::Tag, + key: &str, + fallback_key: Option<&str>, + ) -> Option { + tag.get_vorbis(key) + .and_then(|mut iter| iter.next()) + .map(|s| s.to_string()) + .or_else(|| { + fallback_key.and_then(|key| { + tag.get_vorbis(key) + .and_then(|mut iter| iter.next()) + .map(|s| s.to_string()) + }) + }) + } + + fn extract_numeric_metadata(tag: &metaflac::Tag, key: &str) -> Option { + tag.get_vorbis(key) + .and_then(|mut iter| iter.next()) + .and_then(|s| s.parse::().ok()) + } +} + +impl MetadataExtractor for FlacMetadataExtractor { + fn extract_metadata( + &self, + path: &str, + thumbnails_dir: &str, + ) -> Result { + // Extract metadata from a FLAC file. + let tag = metaflac::Tag::read_from_path(path).unwrap(); + let mut metadata = AudioMetadata::new(); + metadata.artist = Self::extract_string_metadata(&tag, "ARTIST", Some("ALBUMARTIST")); + metadata.title = Self::extract_string_metadata(&tag, "TITLE", None); + metadata.album = Self::extract_string_metadata(&tag, "ALBUM", None); + metadata.duration = Self::extract_numeric_metadata(&tag, "LENGTH").unwrap_or(0); + metadata.position = Self::extract_numeric_metadata(&tag, "TRACKNUMBER"); + metadata.disc = Self::extract_numeric_metadata(&tag, "DISCNUMBER"); + metadata.year = Self::extract_numeric_metadata(&tag, "DATE"); + let thumbnail_content = tag.pictures().next().map(|p| p.data.clone()).unwrap(); + + //TODO: add thumbnail generation + + Ok(metadata) + } +} diff --git a/packages/scanner/src/scanner.rs b/packages/scanner/src/scanner.rs index 42e54ed0e7..79098f3e2d 100644 --- a/packages/scanner/src/scanner.rs +++ b/packages/scanner/src/scanner.rs @@ -3,8 +3,9 @@ use std::collections::LinkedList; use std::path::Path; use uuid::Uuid; -use crate::error::ScannerError; +use crate::error::{MetadataError, ScannerError}; use crate::local_track::LocalTrack; +use crate::metadata::AudioMetadata; use crate::thumbnails::generate_thumbnail; pub trait TagReader { @@ -13,13 +14,13 @@ pub trait TagReader { pub fn visit_file( path: String, - tag_reader: F, + metadata_reader: F, thumbnails_dir: &str, ) -> Result where - F: FnOnce(&str) -> Result, + F: FnOnce(&str) -> Result, { - let tag = tag_reader(&path); + let meta = metadata_reader(&path); match tag { Ok(tag) => Ok(LocalTrack {