Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

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

Make metadata parsing thread-safe and async

Add a global mutex and call the C get_metadata to fill a MaybeUninit
Mp3Entry
in place to avoid thread-safety and dangling pointer issues. Export a
new
extern get_metadata symbol.

Run metadata FFI on Tokio's blocking pool (spawn_blocking) and reduce
the
audio scan concurrency to 1 to avoid unsafe concurrent calls.

Convert artists::update_metadata to async (remove background thread) and
await it from the server handler.

Improve Typesense client error logging for JSON parse failures.

Fix CLI PATH formatting to include the user's ~/.rockbox/bin and tidy
imports.

+93 -70
+3 -2
crates/cli/src/lib.rs
··· 162 162 let data_dir = homedir.join(".config/rockbox.org/typesense"); 163 163 164 164 let path = format!( 165 - "{}:{}", 165 + "{}:{}/{}", 166 166 std::env::var("PATH").unwrap_or_default(), 167 - "~/.rockbox/bin" 167 + homedir.display(), 168 + ".rockbox/bin" 168 169 ); 169 170 170 171 std::process::Command::new("sh")
+51 -62
crates/library/src/artists.rs
··· 1 - use std::{collections::HashMap, thread}; 1 + use std::collections::HashMap; 2 2 3 3 use anyhow::Error; 4 4 use cuid::cuid1; ··· 30 30 31 31 const ROCKSKY_API: &str = "https://api.rocksky.app"; 32 32 33 - pub fn update_metadata(pool: Pool<Sqlite>) -> Result<(), Error> { 34 - thread::spawn(move || { 35 - let runtime = tokio::runtime::Runtime::new().unwrap(); 36 - let result = runtime.block_on(async { 37 - let local_artists = repo::artist::all(pool.clone()).await?; 38 - let local_artists = local_artists.into_iter().filter(|v| v.image.is_none()); 39 - let local_artists = local_artists.map(|mut artist| { 40 - if artist.name == "Theory Of A Deadman" { 41 - artist.name = "Theory of a Deadman".to_string(); 42 - } 43 - if artist.name == "Yonaka" { 44 - artist.name = "YONAKA".to_string(); 45 - } 46 - artist 47 - }); 48 - let mut artist_map: HashMap<String, Artist> = HashMap::new(); 49 - let names = local_artists 50 - .clone() 51 - .map(|artist| artist.name) 52 - .collect::<Vec<String>>(); 33 + pub async fn update_metadata(pool: Pool<Sqlite>) -> Result<(), Error> { 34 + let local_artists = repo::artist::all(pool.clone()).await?; 35 + let local_artists = local_artists.into_iter().filter(|v| v.image.is_none()); 36 + let local_artists = local_artists.map(|mut artist| { 37 + if artist.name == "Theory Of A Deadman" { 38 + artist.name = "Theory of a Deadman".to_string(); 39 + } 40 + if artist.name == "Yonaka" { 41 + artist.name = "YONAKA".to_string(); 42 + } 43 + artist 44 + }); 45 + let mut artist_map: HashMap<String, Artist> = HashMap::new(); 46 + let names = local_artists 47 + .clone() 48 + .map(|artist| artist.name) 49 + .collect::<Vec<String>>(); 53 50 54 - let client = reqwest::Client::new(); 55 - let response = client 56 - .get(format!( 57 - "{}/xrpc/app.rocksky.artist.getArtists", 58 - ROCKSKY_API 59 - )) 60 - .query(&[("names", names.join(","))]) 61 - .send() 62 - .await?; 63 - let text = response.text().await?; 64 - let response: Artists = serde_json::from_str(&text)?; 65 - let artists = response.artists; 51 + let client = reqwest::Client::new(); 52 + let response = client 53 + .get(format!( 54 + "{}/xrpc/app.rocksky.artist.getArtists", 55 + ROCKSKY_API 56 + )) 57 + .query(&[("names", names.join(","))]) 58 + .send() 59 + .await?; 60 + let text = response.text().await?; 61 + let response: Artists = serde_json::from_str(&text)?; 62 + let artists = response.artists; 66 63 67 - for artist in artists.clone() { 68 - println!("Loading artist: {}", artist.name.bright_green()); 69 - artist_map.insert(artist.name.clone(), artist); 70 - } 64 + for artist in artists.clone() { 65 + println!("Loading artist: {}", artist.name.bright_green()); 66 + artist_map.insert(artist.name.clone(), artist); 67 + } 71 68 72 - println!("Loaded {} artists", artists.len()); 69 + println!("Loaded {} artists", artists.len()); 73 70 74 - for artist in local_artists { 75 - println!("Updating artist: {}", artist.name.bright_green()); 76 - let artist_id = artist.id; 77 - if let Some(artist) = artist_map.get(&artist.name) { 78 - repo::artist::update_genres(&pool, &artist_id, &artist.genres.join(", ")) 79 - .await?; 80 - if let Some(picture) = artist.picture.clone() { 81 - repo::artist::update_picture(&pool, &artist_id, &picture).await?; 82 - } 83 - for genre in &artist.genres { 84 - println!("Saving genre: {}", genre.bright_green()); 85 - let id = cuid1()?; 86 - repo::genre::save(&pool, &id, genre).await?; 87 - repo::artist::save_artist_genre(&pool, &cuid1()?, &artist_id, &id).await?; 88 - } 89 - } 71 + for artist in local_artists { 72 + println!("Updating artist: {}", artist.name.bright_green()); 73 + let artist_id = artist.id; 74 + if let Some(artist) = artist_map.get(&artist.name) { 75 + repo::artist::update_genres(&pool, &artist_id, &artist.genres.join(", ")) 76 + .await?; 77 + if let Some(picture) = artist.picture.clone() { 78 + repo::artist::update_picture(&pool, &artist_id, &picture).await?; 90 79 } 91 - 92 - Ok::<(), Error>(()) 93 - }); 94 - 95 - match result { 96 - Ok(_) => {} 97 - Err(e) => eprintln!("Error updating artists: {}", e), 80 + for genre in &artist.genres { 81 + println!("Saving genre: {}", genre.bright_green()); 82 + let id = cuid1()?; 83 + repo::genre::save(&pool, &id, genre).await?; 84 + repo::artist::save_artist_genre(&pool, &cuid1()?, &artist_id, &id).await?; 85 + } 98 86 } 99 - }); 87 + } 88 + 100 89 Ok(()) 101 90 }
+10 -2
crates/library/src/audio_scan.rs
··· 22 22 "opus", "spx", "sid", "ape", "wma", 23 23 ]; 24 24 25 - const MAX_CONCURRENT_SCANS: usize = 8; 25 + const MAX_CONCURRENT_SCANS: usize = 1; 26 26 27 27 pub fn scan_audio_files( 28 28 pool: Pool<Sqlite>, ··· 109 109 .as_ref() 110 110 .map(|probe| probe.path.as_str()) 111 111 .unwrap_or(path); 112 - let entry = rb::metadata::get_metadata(-1, metadata_path); 112 + 113 + // Run C FFI call on blocking thread pool to avoid thread-safety issues 114 + let metadata_path_owned = metadata_path.to_string(); 115 + let entry = tokio::task::spawn_blocking(move || { 116 + rb::metadata::get_metadata(-1, &metadata_path_owned) 117 + }) 118 + .await 119 + .map_err(|e| anyhow!("Failed to get metadata: {}", e))?; 120 + 113 121 let title = track_title(&entry, path); 114 122 let artist = track_artist(&entry); 115 123 let album_artist = track_album_artist(&entry, &artist);
+1 -1
crates/server/src/handlers/system.rs
··· 47 47 return Ok(()); 48 48 } 49 49 50 - update_metadata(ctx.pool.clone())?; 50 + update_metadata(ctx.pool.clone()).await?; 51 51 52 52 if !rebuild_index { 53 53 res.text("0");
+1
crates/sys/src/lib.rs
··· 1237 1237 1238 1238 // Metadata 1239 1239 fn rb_get_metadata(fd: i32, trackname: *const c_char) -> Mp3Entry; 1240 + fn get_metadata(id3: *mut Mp3Entry, fd: i32, trackname: *const c_char) -> bool; 1240 1241 fn get_codec_string(codectype: c_int) -> *const c_char; 1241 1242 fn count_mp3_frames( 1242 1243 fd: c_int,
+18 -2
crates/sys/src/metadata.rs
··· 1 1 use std::ffi::{c_uchar, CStr, CString}; 2 + use std::sync::Mutex; 2 3 3 4 use crate::{types::mp3_entry::Mp3Entry, ProgressFunc}; 4 5 6 + // Global mutex to protect non-thread-safe C metadata parsing code 7 + static METADATA_MUTEX: Mutex<()> = Mutex::new(()); 8 + 5 9 pub fn get_metadata(fd: i32, trackname: &str) -> Mp3Entry { 6 10 let trackname = CString::new(trackname).unwrap(); 7 - let id3 = unsafe { crate::rb_get_metadata(fd, trackname.as_ptr()) }; 8 - id3.into() 11 + let _guard = METADATA_MUTEX.lock().unwrap(); 12 + 13 + // Create a properly initialized mp3entry struct 14 + // Use MaybeUninit to avoid undefined behavior 15 + let mut id3 = std::mem::MaybeUninit::<crate::Mp3Entry>::zeroed(); 16 + 17 + // Call C function directly to fill the struct in place 18 + // This avoids the dangling pointer issue from the Zig wrapper 19 + unsafe { 20 + crate::get_metadata(id3.as_mut_ptr(), fd, trackname.as_ptr()); 21 + let id3 = id3.assume_init(); 22 + // Convert immediately while pointers are still valid 23 + id3.into() 24 + } 9 25 } 10 26 11 27 pub fn get_codec_string(codectype: i32) -> String {
+9 -1
crates/typesense/src/client.rs
··· 268 268 .send() 269 269 .await?; 270 270 271 - Ok(Some(res.json::<TrackResult>().await?)) 271 + let text = res.text().await?; 272 + match serde_json::from_str::<TrackResult>(&text) { 273 + Ok(result) => Ok(Some(result)), 274 + Err(e) => { 275 + eprintln!("Failed to parse Typesense response: {}", e); 276 + eprintln!("Response body: {}", text); 277 + Err(e.into()) 278 + } 279 + } 272 280 } 273 281 274 282 pub async fn search_albums(query: &str) -> Result<Option<AlbumResult>, Error> {