[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Avoid spawn_future instead of AsyncTask

Kasper (Sep 14, 2025, 11:59 PM +0200) 6ca8412c afef1cb9

+53 -41
+5 -1
ferrum-addon/addon.d.ts
··· 123 123 124 124 export declare function playlist_filter_duplicates(playlistId: TrackID, ids: Array<string>): Array<TrackID> 125 125 126 - export declare function read_cover_async(trackId: string, index: number): Promise<ArrayBuffer> 126 + export declare function read_cover_async(trackId: string, index: number): Promise<ReadCoverResult> 127 127 128 128 /** Returns `None` if the file does not have an image */ 129 129 export declare function read_small_cover_async(path: string, index: number, cacheDbPath: string): Promise<Buffer | null> 130 + 131 + export type ReadCoverResult = 132 + | { type: 'Ok', field0?: Buffer } 133 + | { type: 'Err', field0: string } 130 134 131 135 export declare function remove_from_playlist(playlistId: TrackID, itemIds: Array<ItemId>): void 132 136
+37 -36
src-native/tracks/cover.rs
··· 1 - use crate::data::Data; 2 - use crate::data_js::get_data; 3 - 4 1 use super::{Tag, id_to_track}; 2 + use crate::data_js::get_data; 5 3 use anyhow::{Context, Result, anyhow, bail}; 6 4 use fast_image_resize::images::Image; 7 5 use fast_image_resize::{IntoImageView, Resizer}; ··· 9 7 use image::codecs::png::PngEncoder; 10 8 use image::{ImageEncoder, ImageFormat, ImageReader}; 11 9 use lazy_static::lazy_static; 12 - use napi::bindgen_prelude::{AsyncTask, Buffer}; 13 - use napi::{Env, Task}; 10 + use napi::Env; 11 + use napi::bindgen_prelude::{Buffer, PromiseRaw}; 14 12 use redb::{Database, TableDefinition}; 15 13 use std::fs; 16 14 use std::io::{BufWriter, Cursor}; ··· 218 216 Ok(img_bytes) 219 217 } 220 218 221 - pub struct ReadCover { 222 - path: PathBuf, 223 - index: usize, 219 + // This enum is now exposed to JavaScript 220 + #[napi] 221 + pub enum ReadCoverResult { 222 + Ok(Option<Buffer>), 223 + Err(String), 224 224 } 225 - impl Task for ReadCover { 226 - type Output = Option<Vec<u8>>; 227 - type JsValue = Option<Buffer>; 228 - fn compute(&mut self) -> napi::Result<Self::Output> { 229 - let tag = Tag::read_from_path(&self.path)?; 230 - let image = match tag.get_image_consume(self.index)? { 231 - Some(image) => image, 232 - None => { 233 - return Ok(None); 234 - } 235 - }; 236 225 237 - Ok(Some(image.data)) 238 - } 239 - fn resolve(&mut self, _env: Env, output: Self::Output) -> napi::Result<Self::JsValue> { 240 - match output { 241 - Some(output) => Ok(Some(Buffer::from(output))), 242 - None => Ok(None), 243 - } 244 - } 245 - } 246 - #[napi(js_name = "read_cover_async", ts_return_type = "Promise<ArrayBuffer>")] 226 + #[napi( 227 + js_name = "read_cover_async", 228 + ts_return_type = "Promise<ReadCoverResult>" 229 + )] 247 230 #[allow(dead_code)] 248 - pub fn read_cover_async(track_id: String, index: u16, env: Env) -> AsyncTask<ReadCover> { 249 - let data: &mut Data = get_data(&env); 231 + pub fn read_cover_async<'env>( 232 + track_id: String, 233 + index: u16, 234 + env: &'env Env, 235 + ) -> Result<PromiseRaw<'env, ReadCoverResult>> { 236 + let data = get_data(&env); 250 237 let track = id_to_track(&env, &track_id).expect("Track ID not found"); 238 + 251 239 let tracks_dir = &data.paths.tracks_dir; 252 240 let file_path = tracks_dir.join(&track.file); 253 - let task = ReadCover { 254 - path: file_path, 255 - index: index.into(), 256 - }; 257 - AsyncTask::new(task) 241 + 242 + let result = env 243 + .spawn_future(async move { 244 + let tag = match Tag::read_from_path(&file_path) { 245 + Ok(tag) => tag, 246 + Err(err) => return Ok(ReadCoverResult::Err(err.to_string())), 247 + }; 248 + let image = match tag.get_image_consume(index.into()) { 249 + Ok(Some(image)) => image, 250 + Ok(None) => { 251 + return Ok(ReadCoverResult::Ok(None)); 252 + } 253 + Err(err) => return Ok(ReadCoverResult::Err(err.to_string())), 254 + }; 255 + Ok(ReadCoverResult::Ok(Some(Buffer::from(image.data)))) 256 + }) 257 + .context("Failed to spawn async future")?; 258 + Ok(result) 258 259 }
+11 -4
src/lib/player.ts
··· 91 91 return { 92 92 async newFromTrackId(id: TrackID) { 93 93 try { 94 - const buf = await read_cover_async(id, 0) 95 - if (buf === null) { 94 + const result = await read_cover_async(id, 0) 95 + if (result.type === 'Err') { 96 96 set(null) 97 97 return 98 + } else if (!result.field0) { 99 + set(null) 100 + return 101 + } else { 102 + // this should be zero-copy 103 + const uint8 = new Uint8Array(result.field0) 104 + // Blob() does copy 105 + const url = URL.createObjectURL(new Blob([uint8], {})) 106 + set(url) 98 107 } 99 - const url = URL.createObjectURL(new Blob([buf], {})) 100 - set(url) 101 108 } catch (_) { 102 109 set(null) 103 110 }