From de3444c8ebd45491b389f97b96e97186d155041a Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 12 Aug 2024 19:52:42 +0800 Subject: [PATCH] :sparkles: feat: multithreading local scanner Signed-off-by: SimonShiki --- src-tauri/Cargo.lock | 11 ++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/local_scanner.rs | 68 +++++++++++++++++++++++++++++----- src/components/song-item.tsx | 2 +- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ffe286e..5733a33 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -552,6 +552,7 @@ version = "0.0.0" dependencies = [ "base64 0.22.1", "lofty", + "num_cpus", "rodio", "serde", "serde_json", @@ -2426,6 +2427,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "num_enum" version = "0.5.11" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 84dfef3..941779d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,5 +36,6 @@ tauri-plugin-dialog = "2.0.0-rc.0" rodio = { git = "https://github.com/SimonShiki/rodio.git", branch = "master", features = ["symphonia-all"] } souvlaki = "0.6" walkdir = "2.5.0" +num_cpus = "1.16.0" lofty = "0.21.0" base64 = "0.22.1" diff --git a/src-tauri/src/local_scanner.rs b/src-tauri/src/local_scanner.rs index be867eb..9dc5f6d 100644 --- a/src-tauri/src/local_scanner.rs +++ b/src-tauri/src/local_scanner.rs @@ -1,11 +1,14 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use lofty::{file::{AudioFile, TaggedFileExt}, probe::Probe, tag::{Accessor, ItemKey}}; use walkdir::WalkDir; use serde::{Serialize, Deserialize}; use base64::{Engine as _, engine::general_purpose}; use std::fs; +use std::sync::mpsc; +use std::thread; +use std::sync::{Arc, Mutex}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct Song { id: String, name: String, @@ -21,16 +24,64 @@ pub struct Song { #[tauri::command] pub fn scan_folder(path: &str) -> Result, String> { - let mut songs = Vec::new(); + let (tx, rx) = mpsc::channel(); + let path_clone = path.to_string(); + + let handle = thread::spawn(move || { + let (file_tx, file_rx) = mpsc::channel::(); + let file_rx = Arc::new(Mutex::new(file_rx)); + + // Spawn multiple worker threads + let num_threads = num_cpus::get(); + let thread_handles: Vec<_> = (0..num_threads).map(|_| { + let file_rx = Arc::clone(&file_rx); + let tx = tx.clone(); + thread::spawn(move || { + loop { + let path = { + let rx = file_rx.lock().unwrap(); + rx.recv() + }; + match path { + Ok(path) => { + if let Some(song) = process_file(&path) { + tx.send(song).unwrap(); + } + }, + Err(_) => break, // Channel closed, exit the loop + } + } + }) + }).collect(); - for entry in WalkDir::new(path).follow_links(true).into_iter().filter_map(|e| e.ok()) { - if entry.file_type().is_file() { - if let Some(song) = process_file(entry.path()) { - songs.push(song); + // Collect files + for entry in WalkDir::new(&path_clone).follow_links(true).into_iter().filter_map(|e| e.ok()) { + if entry.file_type().is_file() { + file_tx.send(entry.path().to_path_buf()).unwrap(); } } + + // Close the file channel + drop(file_tx); + + // Wait for all worker threads to complete + for handle in thread_handles { + handle.join().unwrap(); + } + + // Close the song channel + drop(tx); + }); + + // Collect all songs + let mut songs = Vec::new(); + for song in rx { + songs.push(song); } + // Wait for the main thread to finish + handle.join().unwrap(); + Ok(songs) } @@ -61,7 +112,6 @@ fn process_file(path: &Path) -> Option { t.get_string(&ItemKey::Lyrics).map(|s| s.to_string()) }); - let cover = tag.and_then(|t| t.pictures().first()).map(|p| { let b64 = general_purpose::STANDARD.encode(&p.data()); format!("data:{};base64,{}", p.mime_type().unwrap().as_str(), b64) @@ -73,7 +123,7 @@ fn process_file(path: &Path) -> Option { let mtime = metadata.modified().ok()?.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs(); Some(Song { - id: format!("local-{name}-{album}-{artist}", album = album.clone().unwrap(), artist = artist.clone().unwrap()), + id: format!("local-{name}-{album}-{artist}", album = album.clone().unwrap_or_default(), artist = artist.clone().unwrap_or_default()), name, artist, album, diff --git a/src/components/song-item.tsx b/src/components/song-item.tsx index 90efd8d..844a258 100644 --- a/src/components/song-item.tsx +++ b/src/components/song-item.tsx @@ -86,7 +86,7 @@ export default function SongItem (props: SongItemProps) { props.onSelect?.(!props.select); }} className={`flex flex-row items-center ${props.selectMode ? '' : 'active:scale-99'} py-2 gap-2 hover:!bg-black cursor-pointer hover:!bg-op-5 transition-all ${props.hideBg ? '!border-none !bg-transparent' : ''}`}> {props.selectMode && ()} - {props.song.name} + {props.song.name}
{props.song.name} {props.song.album}