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.

Hydrate MP3 entries and regenerate UPnP bindings

Populate Mp3Entry metadata from the DB Track record when Rockbox
cannot read tags locally (used by HTTP/remote tracks).

Regenerate tonic/prost UPnP bindings under crates/upnp/src/api/.
Changes are mostly formatting and signature reflow from codegen.

+129 -34
+6 -13
crates/server/src/handlers/player.rs
··· 13 13 use rockbox_types::{device::Device, LoadTracks, NewVolume}; 14 14 use serde::Deserialize; 15 15 16 - use crate::{http::AppState, GLOBAL_MUTEX, PLAYER_MUTEX}; 16 + use crate::{ 17 + handlers::playlists::hydrate_entry_from_track, http::AppState, GLOBAL_MUTEX, PLAYER_MUTEX, 18 + }; 17 19 18 20 type HandlerResult = actix_web::Result<HttpResponse>; 19 21 ··· 230 232 .map_err(ErrorInternalServerError)?; 231 233 if let Some(metadata) = metadata { 232 234 let t = track.as_mut().unwrap(); 233 - t.id = Some(metadata.id); 234 - t.album_art = metadata.album_art.or(t.album_art.clone()); 235 - t.album_id = Some(metadata.album_id); 236 - t.artist_id = Some(metadata.artist_id); 235 + hydrate_entry_from_track(t, &metadata); 237 236 } 238 237 } 239 238 ··· 274 273 .map_err(ErrorInternalServerError)? 275 274 { 276 275 if let Some(t) = track.as_mut() { 277 - t.id = Some(metadata.id); 278 - t.album_art = metadata.album_art; 279 - t.album_id = Some(metadata.album_id); 280 - t.artist_id = Some(metadata.artist_id); 276 + hydrate_entry_from_track(t, &metadata); 281 277 } 282 278 } 283 279 } ··· 319 315 .map_err(ErrorInternalServerError)? 320 316 { 321 317 if let Some(t) = track.as_mut() { 322 - t.id = Some(metadata.id); 323 - t.album_art = metadata.album_art; 324 - t.album_id = Some(metadata.album_id); 325 - t.artist_id = Some(metadata.artist_id); 318 + hydrate_entry_from_track(t, &metadata); 326 319 } 327 320 } 328 321 }
+104 -10
crates/server/src/handlers/playlists.rs
··· 26 26 s.split('#').next().unwrap_or(s).to_string() 27 27 } 28 28 29 + /// Populate an Mp3Entry's user-visible metadata from a DB Track record. 30 + /// Used to hydrate HTTP/remote tracks whose tags Rockbox cannot read locally. 31 + pub(crate) fn hydrate_entry_from_track( 32 + entry: &mut rockbox_sys::types::mp3_entry::Mp3Entry, 33 + track: &rockbox_library::entity::track::Track, 34 + ) { 35 + if entry.title.is_empty() { 36 + entry.title = track.title.clone(); 37 + } 38 + if entry.artist.is_empty() { 39 + entry.artist = track.artist.clone(); 40 + } 41 + if entry.album.is_empty() { 42 + entry.album = track.album.clone(); 43 + } 44 + if entry.albumartist.is_empty() { 45 + entry.albumartist = track.album_artist.clone(); 46 + } 47 + if entry.composer.is_empty() { 48 + entry.composer = track.composer.clone(); 49 + } 50 + if entry.length == 0 { 51 + entry.length = track.length as u64; 52 + } 53 + if entry.filesize == 0 { 54 + entry.filesize = track.filesize as u64; 55 + } 56 + if entry.bitrate == 0 { 57 + entry.bitrate = track.bitrate; 58 + } 59 + if entry.frequency == 0 { 60 + entry.frequency = track.frequency as u64; 61 + } 62 + if entry.tracknum == 0 { 63 + if let Some(n) = track.track_number { 64 + entry.tracknum = n as i32; 65 + } 66 + } 67 + if entry.discnum == 0 { 68 + entry.discnum = track.disc_number as i32; 69 + } 70 + if entry.year == 0 { 71 + if let Some(y) = track.year { 72 + entry.year = y as i32; 73 + } 74 + } 75 + if entry.year_string.is_empty() { 76 + if let Some(ref ys) = track.year_string { 77 + entry.year_string = ys.clone(); 78 + } 79 + } 80 + entry.album_art = track.album_art.clone().or_else(|| entry.album_art.clone()); 81 + entry.album_id = Some(track.album_id.clone()); 82 + entry.artist_id = Some(track.artist_id.clone()); 83 + entry.genre_id = Some(track.genre_id.clone()); 84 + entry.id = Some(track.id.clone()); 85 + } 86 + 29 87 pub async fn create_playlist( 30 88 state: web::Data<AppState>, 31 89 body: web::Json<NewPlaylist>, ··· 171 229 Ok(HttpResponse::Ok().finish()) 172 230 } 173 231 174 - pub async fn get_playlist_tracks(_path: web::Path<String>) -> HandlerResult { 175 - let entries = web::block(|| { 232 + pub async fn get_playlist_tracks( 233 + state: web::Data<AppState>, 234 + _path: web::Path<String>, 235 + ) -> HandlerResult { 236 + let raw_entries = web::block(|| { 176 237 let _player_mutex = PLAYER_MUTEX.lock().unwrap(); 177 238 let amount = rb::playlist::amount(); 178 - let mut entries = Vec::with_capacity(amount as usize); 239 + let mut entries: Vec<(rb::types::mp3_entry::Mp3Entry, String)> = 240 + Vec::with_capacity(amount as usize); 179 241 for i in 0..amount { 180 242 let info = rb::playlist::get_track_info(i); 181 243 // Skip get_metadata for HTTP files — it opens a live connection. 182 244 let entry = 183 245 if info.filename.starts_with("http://") || info.filename.starts_with("https://") { 184 246 let mut e = rb::types::mp3_entry::Mp3Entry::default(); 185 - e.path = info.filename; 247 + e.path = info.filename.clone(); 186 248 e 187 249 } else { 188 250 rb::metadata::get_metadata(-1, &info.filename) 189 251 }; 190 - entries.push(entry); 252 + entries.push((entry, info.filename)); 191 253 } 192 254 entries 193 255 }) 194 256 .await 195 257 .map_err(ErrorInternalServerError)?; 258 + 259 + // Hydrate remote/HTTP entries with metadata from the DB so the UI sees 260 + // title/artist/album/etc. instead of an empty Mp3Entry. 261 + let mut entries = Vec::with_capacity(raw_entries.len()); 262 + for (mut entry, filename) in raw_entries { 263 + if filename.starts_with("http://") || filename.starts_with("https://") { 264 + if let Ok(Some(track)) = find_track_metadata(&state, &filename).await { 265 + hydrate_entry_from_track(&mut entry, &track); 266 + } 267 + } 268 + entries.push(entry); 269 + } 196 270 Ok(HttpResponse::Ok().json(entries)) 197 271 } 198 272 ··· 471 545 continue; 472 546 } 473 547 474 - entry.album_art = track.as_ref().and_then(|t| t.album_art.clone()); 475 - entry.album_id = track.as_ref().map(|t| t.album_id.clone()); 476 - entry.artist_id = track.as_ref().map(|t| t.artist_id.clone()); 477 - entry.genre_id = track.as_ref().map(|t| t.genre_id.clone()); 478 - entry.id = track.as_ref().map(|t| t.id.clone()); 548 + if let Some(ref t) = track { 549 + hydrate_entry_from_track(&mut entry, t); 550 + } 479 551 480 552 metadata_cache.insert(hash, entry.clone()); 481 553 entries.push(entry); ··· 550 622 } 551 623 } 552 624 625 + // Auto-fetch missing metadata for non-internal HTTP tracks. Without this, 626 + // tracks queued in a previous run that never had their metadata persisted 627 + // (or a `save_audio_metadata` failure) would render as empty rows in the 628 + // UI forever. Skip /stream.wav (Rockbox's own playback stream). 629 + if metadata.is_none() 630 + && internal_track.is_none() 631 + && (path.starts_with("http://") || path.starts_with("https://")) 632 + && !is_stream_wav(path) 633 + { 634 + if let Err(e) = save_audio_metadata(state.pool.clone(), path, None).await { 635 + tracing::warn!("on-demand save_audio_metadata failed for {}: {}", path, e); 636 + } else { 637 + metadata = repo::track::find_by_md5(state.pool.clone(), &hash).await?; 638 + } 639 + } 640 + 553 641 Ok(metadata) 642 + } 643 + 644 + fn is_stream_wav(path: &str) -> bool { 645 + reqwest::Url::parse(path) 646 + .map(|u| u.path() == "/stream.wav") 647 + .unwrap_or(false) 554 648 } 555 649 556 650 async fn find_internal_track_by_url(
+10 -8
sdk/typescript/examples/01-basic-playback.ts
··· 5 5 // 6 6 // bun run examples/01-basic-playback.ts 7 7 8 - import { PlaybackStatus } from '../src/index.js'; 9 - import { createClient, fmtTime } from './_client.js'; 8 + import { PlaybackStatus } from "../src/index.js"; 9 + import { createClient, fmtTime } from "./_client.js"; 10 10 11 11 const client = createClient(); 12 12 ··· 15 15 16 16 const track = await client.playback.currentTrack(); 17 17 if (track) { 18 - const pct = track.length > 0 ? Math.round((track.elapsed / track.length) * 100) : 0; 18 + const pct = 19 + track.length > 0 ? Math.round((track.elapsed / track.length) * 100) : 0; 19 20 console.log(`Now: ${track.title} — ${track.artist}`); 20 - console.log(` ${fmtTime(track.elapsed)} / ${fmtTime(track.length)} (${pct}%)`); 21 + console.log( 22 + ` ${fmtTime(track.elapsed)} / ${fmtTime(track.length)} (${pct}%)`, 23 + ); 21 24 } else { 22 - console.log('Nothing is playing.'); 25 + console.log("Nothing is playing."); 23 26 } 24 - 25 27 // Toggle playback 26 28 if (status === PlaybackStatus.Playing) { 27 29 await client.playback.pause(); 28 - console.log('→ paused'); 30 + console.log("→ paused"); 29 31 } else if (status === PlaybackStatus.Paused) { 30 32 await client.playback.resume(); 31 - console.log('→ resumed'); 33 + console.log("→ resumed"); 32 34 }
+9 -3
sdk/typescript/src/types.ts
··· 5 5 export enum PlaybackStatus { 6 6 Stopped = 0, 7 7 Playing = 1, 8 - Paused = 2, 8 + Paused = 3, 9 9 } 10 10 11 11 export enum RepeatMode { ··· 285 285 [key: string]: unknown; 286 286 } 287 287 288 - export type PartialUserSettings = Partial<Omit<UserSettings, 'eqBandSettings' | 'replaygainSettings' | 'compressorSettings'>> & { 288 + export type PartialUserSettings = Partial< 289 + Omit< 290 + UserSettings, 291 + "eqBandSettings" | "replaygainSettings" | "compressorSettings" 292 + > 293 + > & { 289 294 eqBandSettings?: EqBandSetting[]; 290 295 replaygainSettings?: ReplaygainSettings; 291 296 compressorSettings?: CompressorSettings; ··· 305 310 /** Replace the entire playlist */ 306 311 First: 3, 307 312 } as const; 308 - export type InsertPosition = (typeof InsertPosition)[keyof typeof InsertPosition]; 313 + export type InsertPosition = 314 + (typeof InsertPosition)[keyof typeof InsertPosition];