Skip to content

Commit

Permalink
Implement FLAC scanning
Browse files Browse the repository at this point in the history
  • Loading branch information
nukeop committed Nov 9, 2023
1 parent db7f94f commit 00144d4
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 25 deletions.
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions packages/main/src/controllers/local-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/scanner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion packages/scanner/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocalTrack[]>;

declare const generateThumbnail = (filename: string, thumbnailsDir: string) => new Promise<string>;
Expand Down
13 changes: 13 additions & 0 deletions packages/scanner/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
38 changes: 30 additions & 8 deletions packages/scanner/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![forbid(unsafe_code)]
mod error;
mod js;
mod local_track;
mod metadata;
mod scanner;
mod thumbnails;
use id3::Tag;
Expand All @@ -16,6 +18,7 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult<JsArray> {
let thumbnails_dir: Handle<JsString> = cx.argument(2)?;
let thumbnails_dir_str = thumbnails_dir.value(&mut cx);
let on_progress_callback: Handle<JsFunction> = cx.argument(3)?;
let on_error_callback: Handle<JsFunction> = cx.argument(4)?;
let result: Handle<JsArray> = cx.empty_array();

// Copy all the starting folders to a queue, which holds all the folders left to scan
Expand Down Expand Up @@ -83,6 +86,10 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult<JsArray> {
];
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;
}

Expand All @@ -93,18 +100,33 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult<JsArray> {
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)?;
Expand Down
12 changes: 4 additions & 8 deletions packages/scanner/src/local_track.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
use crate::metadata::AudioMetadata;

#[derive(Debug, Clone)]
pub struct LocalTrack {
pub uuid: String,
pub artist: Option<String>,
pub title: Option<String>,
pub album: Option<String>,
pub duration: u32,
pub thumbnail: Option<String>,
pub disc: Option<u32>,
pub position: Option<u32>,
pub year: Option<u32>,

pub metadata: AudioMetadata,

pub filename: String,
pub path: String,
Expand Down
113 changes: 113 additions & 0 deletions packages/scanner/src/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use id3::TagLike;
use metaflac;

use crate::{error::MetadataError, thumbnails::generate_thumbnail};

pub struct AudioMetadata {
pub artist: Option<String>,
pub title: Option<String>,
pub album: Option<String>,
pub duration: u32,
pub disc: Option<u32>,
pub position: Option<u32>,
pub year: Option<u32>,
pub thumbnail: Option<String>,
}

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<AudioMetadata, MetadataError>;
}

#[derive(Debug, Clone)]

pub struct Mp3MetadataExtractor;
impl MetadataExtractor for Mp3MetadataExtractor {
fn extract_metadata(
&self,
path: &str,
thumbnails_dir: &str,
) -> Result<AudioMetadata, MetadataError> {
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<String> {
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<T: std::str::FromStr>(tag: &metaflac::Tag, key: &str) -> Option<T> {
tag.get_vorbis(key)
.and_then(|mut iter| iter.next())
.and_then(|s| s.parse::<T>().ok())
}
}

impl MetadataExtractor for FlacMetadataExtractor {
fn extract_metadata(
&self,
path: &str,
thumbnails_dir: &str,
) -> Result<AudioMetadata, MetadataError> {
// 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)
}
}
9 changes: 5 additions & 4 deletions packages/scanner/src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -13,13 +14,13 @@ pub trait TagReader {

pub fn visit_file<F>(
path: String,
tag_reader: F,
metadata_reader: F,
thumbnails_dir: &str,
) -> Result<LocalTrack, ScannerError>
where
F: FnOnce(&str) -> Result<Tag, id3::Error>,
F: FnOnce(&str) -> Result<AudioMetadata, MetadataError>,
{
let tag = tag_reader(&path);
let meta = metadata_reader(&path);

match tag {
Ok(tag) => Ok(LocalTrack {
Expand Down

0 comments on commit 00144d4

Please sign in to comment.