Skip to content

Commit

Permalink
Generate thumbnails in rust
Browse files Browse the repository at this point in the history
  • Loading branch information
nukeop committed Nov 3, 2023
1 parent 2ac4191 commit db7f94f
Show file tree
Hide file tree
Showing 9 changed files with 614 additions and 16 deletions.
512 changes: 511 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/main/src/controllers/local-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class LocalIpcCtrl {
.map(folder => this.localLibraryDb.addFolder(this.normalizeFolderPath(folder)))
);

const cache = await scanFolders(directories, ['mp3'], (scanProgress, scanTotal) => {
const cache = await scanFolders(directories, ['mp3'], this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => {
this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal});
});

Expand Down Expand Up @@ -84,7 +84,7 @@ class LocalIpcCtrl {
try {
const folders = await this.localLibraryDb.getLocalFolders();

const cache = await scanFolders(folders.map(folder => folder.path), ['mp3'], (scanProgress, scanTotal) => {
const cache = await scanFolders(folders.map(folder => folder.path), ['mp3'], this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => {
this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal});
});

Expand Down
4 changes: 4 additions & 0 deletions packages/main/src/services/local-library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class LocalLibrary {
}
}

getThumbnailsDir() {
return this.mediaDir;
}

/**
* Format metadata from files to nuclear format
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/scanner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ crate-type = ["cdylib"]

[dependencies]
id3 = "1.7.0"
md5 = "0.7.0"
mockall = "0.11.4"

[dependencies.image]
version = "0.24.7"
features = ["webp-encoder"]

[dependencies.uuid]
version = "1.3.4"
features = [
Expand Down
8 changes: 6 additions & 2 deletions packages/scanner/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type LocalTrack = {
title?: string;
album?: string;
duration?: number;
thumbnail?: Buffer;
thumbnail?: string;
position?: number;
year?: string;

Expand All @@ -16,6 +16,10 @@ export type LocalTrack = {
declare const scanFolders = (
folders: string[],
supportedFormats: string[],
thumbnailsDir: string,
onProgress: (progress: number, total: number, lastScanned?: string) => void
) => new Promise<LocalTrack[]>;
export { scanFolders };

declare const generateThumbnail = (filename: string, thumbnailsDir: string) => new Promise<string>;

export { scanFolders, generateThumbnail };
19 changes: 15 additions & 4 deletions packages/scanner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ mod error;
mod js;
mod local_track;
mod scanner;
mod thumbnails;
use id3::Tag;
use js::{set_optional_field_buffer, set_optional_field_str, set_optional_field_u32};
use js::{set_optional_field_str, set_optional_field_u32};
use neon::prelude::*;
use scanner::{visit_directory, visit_file};
use std::collections::LinkedList;
use thumbnails::create_thumbnails_dir;

fn scan_folders(mut cx: FunctionContext) -> JsResult<JsArray> {
let folders: Handle<JsArray> = cx.argument(0)?;
let supported_formats: Handle<JsArray> = cx.argument(1)?;
let on_progress_callback: Handle<JsFunction> = cx.argument(2)?;
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 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 @@ -52,14 +56,21 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult<JsArray> {
on_progress_callback.call(&mut cx, this, args)?;
}

// First, create a directory for thumbnails
create_thumbnails_dir(thumbnails_dir_str.as_str());

// All folders have been scanned, now scan the files
total_files_to_scan_num = files_to_scan_queue.len();
while !files_to_scan_queue.is_empty() {
// Get the next file to scan
let file = files_to_scan_queue.pop_front().unwrap();

// Scan the file
let track = visit_file(file.clone(), |path| Tag::read_from_path(path));
let track = visit_file(
file.clone(),
|path| Tag::read_from_path(path),
thumbnails_dir_str.as_str(),
);

if track.is_err() {
// Call the progress callback
Expand Down Expand Up @@ -89,7 +100,7 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult<JsArray> {
let track_duration_js_number = cx.number(track.duration);
track_js_object.set(&mut cx, "duration", track_duration_js_number)?;

set_optional_field_buffer(&mut cx, &mut track_js_object, "thumbnail", track.thumbnail);
set_optional_field_str(&mut cx, &mut track_js_object, "thumbnail", track.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);
Expand Down
2 changes: 1 addition & 1 deletion packages/scanner/src/local_track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pub struct LocalTrack {
pub title: Option<String>,
pub album: Option<String>,
pub duration: u32,
pub thumbnail: Option<Vec<u8>>,
pub thumbnail: Option<String>,
pub disc: Option<u32>,
pub position: Option<u32>,
pub year: Option<u32>,
Expand Down
17 changes: 11 additions & 6 deletions packages/scanner/src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ use uuid::Uuid;

use crate::error::ScannerError;
use crate::local_track::LocalTrack;
use crate::thumbnails::generate_thumbnail;

pub trait TagReader {
fn read_from_path(path: impl AsRef<Path>) -> Result<Tag, Error>;
}

pub fn visit_file<F>(path: String, tag_reader: F) -> Result<LocalTrack, ScannerError>
pub fn visit_file<F>(
path: String,
tag_reader: F,
thumbnails_dir: &str,
) -> Result<LocalTrack, ScannerError>
where
F: FnOnce(&str) -> Result<Tag, id3::Error>,
{
Expand All @@ -23,10 +28,7 @@ where
title: tag.title().map(|s| s.to_string()),
album: tag.album().map(|s| s.to_string()),
duration: tag.duration().unwrap_or(0),
thumbnail: tag
.pictures()
.find(|p| p.picture_type == id3::frame::PictureType::CoverFront)
.map(|p| p.data.clone()),
thumbnail: generate_thumbnail(&path, thumbnails_dir),
position: tag.track(),
disc: tag.disc(),
year: tag.year().map(|s| s as u32),
Expand Down Expand Up @@ -110,7 +112,10 @@ mod tests {
assert_eq!(track.year, Some(2020));
assert_eq!(track.filename, String::from("file.mp3"));
assert_eq!(track.path, path);
assert_eq!(track.thumbnail, Some(vec![1, 2, 3]));
assert_eq!(
track.thumbnail,
Some("file://path/to/valid/file.webp".to_string())
);
} else {
panic!("Result is not ok");
}
Expand Down
59 changes: 59 additions & 0 deletions packages/scanner/src/thumbnails.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use id3::Tag;
use image::{imageops::resize, imageops::FilterType, io::Reader as ImageReader, ImageFormat};
use md5;
use std::io::Cursor;
use std::path::{Path, PathBuf};

fn hash_thumb_filename(path: &str) -> String {
let filename = Path::new(path).file_name().unwrap();
let hash = md5::compute(filename.to_string_lossy().as_bytes());
format!("{:x}.webp", hash)
}

pub fn create_thumbnails_dir(thumbnails_dir: &str) {
let thumbnails_dir_path = Path::new(thumbnails_dir);

if !thumbnails_dir_path.exists() {
std::fs::create_dir(thumbnails_dir_path).unwrap();
}
}

fn url_path_from_path(path: &str) -> String {
let path = path.replace("\\", "/");
let path = path.replace(" ", "%20");
format!("file://{}", path)
}

pub fn generate_thumbnail(filename: &str, thumbnails_dir: &str) -> Option<String> {
let mut thumbnail_path = PathBuf::from(thumbnails_dir);

thumbnail_path.push(hash_thumb_filename(filename));

let thumbnail_path_str = thumbnail_path.to_str().unwrap();

if Path::new(thumbnail_path_str).exists() {
return Some(url_path_from_path(thumbnail_path_str));
}

let tag = Tag::read_from_path(filename).unwrap();
let thumbnail = tag
.pictures()
.find(|p| p.picture_type == id3::frame::PictureType::CoverFront)
.map(|p| p.data.clone());

if let Some(thumbnail) = thumbnail {
let img = ImageReader::new(Cursor::new(&thumbnail))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();

let img = resize(&img, 192, 192, FilterType::Lanczos3);
img.save_with_format(thumbnail_path_str, ImageFormat::WebP)
.unwrap();
} else {
return None;
}

Some(url_path_from_path(thumbnail_path_str))
}

0 comments on commit db7f94f

Please sign in to comment.