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.

Add TS SDK examples and Bluetooth & volume APIs

Add 15 runnable TypeScript examples under sdk/typescript/examples with a
shared `_client` helper and README. Implement a Bluetooth API and add a
volume endpoint (VolumeInfo + getVolume). Extend types and GraphQL
fields (browse.displayName, album.copyrightMessage).

+814 -2
+32
sdk/typescript/examples/01-basic-playback.ts
··· 1 + // 01 — Basic playback 2 + // 3 + // Inspect the current track, then either pause or resume based on the current 4 + // state. Idempotent: run it twice and it toggles between Playing and Paused. 5 + // 6 + // bun run examples/01-basic-playback.ts 7 + 8 + import { PlaybackStatus } from '../src/index.js'; 9 + import { createClient, fmtTime } from './_client.js'; 10 + 11 + const client = createClient(); 12 + 13 + const status = await client.playback.status(); 14 + console.log(`Status: ${PlaybackStatus[status]}`); 15 + 16 + const track = await client.playback.currentTrack(); 17 + if (track) { 18 + const pct = track.length > 0 ? Math.round((track.elapsed / track.length) * 100) : 0; 19 + console.log(`Now: ${track.title} — ${track.artist}`); 20 + console.log(` ${fmtTime(track.elapsed)} / ${fmtTime(track.length)} (${pct}%)`); 21 + } else { 22 + console.log('Nothing is playing.'); 23 + } 24 + 25 + // Toggle playback 26 + if (status === PlaybackStatus.Playing) { 27 + await client.playback.pause(); 28 + console.log('→ paused'); 29 + } else if (status === PlaybackStatus.Paused) { 30 + await client.playback.resume(); 31 + console.log('→ resumed'); 32 + }
+37
sdk/typescript/examples/02-now-playing.ts
··· 1 + // 02 — Now playing (real-time subscriptions) 2 + // 3 + // Opens a WebSocket and prints every track / status / queue change as it 4 + // happens. Press Ctrl+C to exit. 5 + // 6 + // bun run examples/02-now-playing.ts 7 + 8 + import { PlaybackStatus } from '../src/index.js'; 9 + import { createClient } from './_client.js'; 10 + 11 + const client = createClient(); 12 + client.connect(); 13 + 14 + client.on('track:changed', (track) => { 15 + console.log(`▶ ${track.title} — ${track.artist} [${track.album}]`); 16 + }); 17 + 18 + client.on('status:changed', (raw) => { 19 + console.log(`◐ ${PlaybackStatus[raw] ?? raw}`); 20 + }); 21 + 22 + client.on('playlist:changed', (queue) => { 23 + console.log(`☰ queue updated — ${queue.amount} tracks (index ${queue.index})`); 24 + }); 25 + 26 + client.on('ws:error', (err) => { 27 + console.error(`✗ websocket error: ${err.message}`); 28 + }); 29 + 30 + console.log('Listening for events. Press Ctrl+C to exit.'); 31 + 32 + // Tear down cleanly on Ctrl+C 33 + process.on('SIGINT', () => { 34 + console.log('\nDisconnecting...'); 35 + client.disconnect(); 36 + process.exit(0); 37 + });
+36
sdk/typescript/examples/03-library-search.ts
··· 1 + // 03 — Search the library 2 + // 3 + // Search the library for a term passed on the command line, print a summary, 4 + // and start playing the first matching album (if any). 5 + // 6 + // bun run examples/03-library-search.ts "pink floyd" 7 + 8 + import { createClient } from './_client.js'; 9 + 10 + const term = process.argv[2]; 11 + if (!term) { 12 + console.error('usage: bun run examples/03-library-search.ts <search term>'); 13 + process.exit(1); 14 + } 15 + 16 + const client = createClient(); 17 + 18 + const { artists, albums, tracks, likedAlbums, likedTracks } = await client.library.search(term); 19 + 20 + console.log(`Search: "${term}"`); 21 + console.log(` Artists : ${artists.length}`); 22 + console.log(` Albums : ${albums.length}`); 23 + console.log(` Tracks : ${tracks.length}`); 24 + console.log(` Liked albums : ${likedAlbums.length}`); 25 + console.log(` Liked tracks : ${likedTracks.length}\n`); 26 + 27 + console.log('Top albums:'); 28 + for (const a of albums.slice(0, 5)) { 29 + const copyright = a.copyrightMessage ? ` © ${a.copyrightMessage}` : ''; 30 + console.log(` • ${a.title} — ${a.artist} (${a.year})${copyright}`); 31 + } 32 + 33 + if (albums[0]) { 34 + console.log(`\nPlaying: ${albums[0].title}`); 35 + await client.playback.playAlbum(albums[0].id, { shuffle: false }); 36 + }
+34
sdk/typescript/examples/04-queue-management.ts
··· 1 + // 04 — Queue management 2 + // 3 + // Print the current queue, demonstrate inserting tracks at different positions, 4 + // and removing entries. 5 + // 6 + // bun run examples/04-queue-management.ts 7 + 8 + import { InsertPosition } from '../src/index.js'; 9 + import { createClient, fmtTime } from './_client.js'; 10 + 11 + const client = createClient(); 12 + 13 + const queue = await client.playlist.current(); 14 + console.log(`Queue: ${queue.amount} tracks, currently at index ${queue.index}\n`); 15 + 16 + queue.tracks.slice(0, 10).forEach((t, i) => { 17 + const marker = i === queue.index ? '▶' : ' '; 18 + console.log(`${marker} ${(i + 1).toString().padStart(3)}. ${t.title} — ${t.artist} (${fmtTime(t.length)})`); 19 + }); 20 + if (queue.tracks.length > 10) console.log(` ... and ${queue.tracks.length - 10} more`); 21 + 22 + // --- Append a track at the end of the queue (no playback change) ---------- 23 + // Replace this path with one that exists on your system before running. 24 + const PATH_TO_INSERT = process.argv[2]; 25 + 26 + if (PATH_TO_INSERT) { 27 + console.log(`\nAppending: ${PATH_TO_INSERT}`); 28 + await client.playlist.insertTracks([PATH_TO_INSERT], InsertPosition.Last); 29 + 30 + const after = await client.playlist.amount(); 31 + console.log(`→ queue now has ${after} tracks`); 32 + } else { 33 + console.log('\n(Pass a file path to insert it at the end of the queue.)'); 34 + }
+54
sdk/typescript/examples/05-saved-playlists.ts
··· 1 + // 05 — Saved playlists 2 + // 3 + // Create a new playlist, populate it with a few tracks from the library, list 4 + // existing playlists, then clean up after itself. 5 + // 6 + // bun run examples/05-saved-playlists.ts 7 + 8 + import { createClient } from './_client.js'; 9 + 10 + const client = createClient(); 11 + 12 + // 1) Pick the first 3 track IDs from the library 13 + const tracks = await client.library.tracks(); 14 + const seedIds = tracks 15 + .slice(0, 3) 16 + .map((t) => t.id) 17 + .filter((id): id is string => Boolean(id)); 18 + 19 + if (seedIds.length === 0) { 20 + console.error('No tracks in the library — cannot create a playlist.'); 21 + process.exit(1); 22 + } 23 + 24 + // 2) Create the playlist with those tracks 25 + const created = await client.savedPlaylists.create({ 26 + name: `SDK example — ${new Date().toISOString()}`, 27 + description: 'Demo playlist created by examples/05-saved-playlists.ts', 28 + trackIds: seedIds, 29 + }); 30 + console.log(`Created playlist ${created.id} — "${created.name}" (${created.trackCount} tracks)`); 31 + 32 + // 3) Show all playlists 33 + const all = await client.savedPlaylists.list(); 34 + console.log(`\nAll playlists (${all.length}):`); 35 + for (const p of all.slice(0, 10)) { 36 + console.log(` • ${p.name} — ${p.trackCount} tracks`); 37 + } 38 + 39 + // 4) Add one more track, then remove the original first track 40 + if (tracks[3]?.id) { 41 + await client.savedPlaylists.addTracks(created.id, [tracks[3].id]); 42 + console.log(`\nAdded one more track`); 43 + } 44 + if (seedIds[0]) { 45 + await client.savedPlaylists.removeTrack(created.id, seedIds[0]); 46 + console.log(`Removed the first seed track`); 47 + } 48 + 49 + const refreshed = await client.savedPlaylists.get(created.id); 50 + console.log(`Now has ${refreshed?.trackCount ?? 0} tracks`); 51 + 52 + // 5) Cleanup — comment this out to keep the demo playlist around 53 + await client.savedPlaylists.delete(created.id); 54 + console.log(`\nCleaned up demo playlist.`);
+44
sdk/typescript/examples/06-smart-playlist.ts
··· 1 + // 06 — Smart playlist 2 + // 3 + // Build a smart playlist that resolves to the user's most-played tracks, then 4 + // resolve the rules right now and print the matches. 5 + // 6 + // bun run examples/06-smart-playlist.ts 7 + 8 + import { createClient } from './_client.js'; 9 + 10 + const client = createClient(); 11 + 12 + // Smart-playlist rules are server-evaluated and serialized as JSON. The 13 + // schema is documented in the rockbox_playlists::rules module. 14 + const rules = JSON.stringify({ 15 + operator: 'AND', 16 + rules: [{ field: 'play_count', op: 'gte', value: 1 }], 17 + sort: { field: 'play_count', dir: 'desc' }, 18 + limit: 25, 19 + }); 20 + 21 + const sp = await client.smartPlaylists.create({ 22 + name: 'Most played (demo)', 23 + description: 'Top 25 most-played tracks', 24 + rules, 25 + }); 26 + console.log(`Created smart playlist: ${sp.id}`); 27 + 28 + // Resolve the rules right now and inspect the results. 29 + const ids = await client.smartPlaylists.trackIds(sp.id); 30 + console.log(`Currently resolves to ${ids.length} tracks`); 31 + 32 + // Show stats for the first match 33 + if (ids[0]) { 34 + const stats = await client.smartPlaylists.trackStats(ids[0]); 35 + if (stats) { 36 + console.log( 37 + `Top track stats: played ${stats.playCount}× (skipped ${stats.skipCount}×)`, 38 + ); 39 + } 40 + } 41 + 42 + // Cleanup 43 + await client.smartPlaylists.delete(sp.id); 44 + console.log('Cleaned up demo smart playlist.');
+24
sdk/typescript/examples/07-volume-control.ts
··· 1 + // 07 — Volume control 2 + // 3 + // Read the current volume (with min/max range) and bump it up by one step. 4 + // 5 + // bun run examples/07-volume-control.ts # show + step up 6 + // bun run examples/07-volume-control.ts -3 # step down 3 7 + // 8 + // Volume is in firmware-defined steps (typically dB on PortalPlayer targets). 9 + 10 + import { createClient } from './_client.js'; 11 + 12 + const client = createClient(); 13 + const delta = process.argv[2] ? Number(process.argv[2]) : 1; 14 + 15 + const before = await client.sound.getVolume(); 16 + const range = before.max - before.min; 17 + const filled = range > 0 ? Math.round(((before.volume - before.min) / range) * 20) : 0; 18 + const bar = '█'.repeat(filled) + '░'.repeat(Math.max(0, 20 - filled)); 19 + 20 + console.log(`Volume: ${before.volume} dB (range ${before.min} … ${before.max})`); 21 + console.log(` ${bar}`); 22 + 23 + const after = await client.sound.adjustVolume(delta); 24 + console.log(`\nAdjusted by ${delta >= 0 ? `+${delta}` : delta} → ${after} dB`);
+42
sdk/typescript/examples/08-eq-config.ts
··· 1 + // 08 — Equalizer & replaygain configuration 2 + // 3 + // Reads the current settings, prints a summary, then applies a "warm + bass 4 + // boost" 5-band EQ preset and enables album-mode ReplayGain. 5 + // 6 + // bun run examples/08-eq-config.ts 7 + 8 + import { ReplaygainType } from '../src/index.js'; 9 + import { createClient } from './_client.js'; 10 + 11 + const client = createClient(); 12 + 13 + const before = await client.settings.get(); 14 + console.log(`Before:`); 15 + console.log(` EQ enabled : ${before.eqEnabled}`); 16 + console.log(` EQ precut : ${before.eqPrecut}`); 17 + console.log(` Replaygain type : ${before.replaygainSettings.type}`); 18 + console.log(` Replaygain noclip: ${before.replaygainSettings.noclip}`); 19 + 20 + await client.settings.save({ 21 + eqEnabled: true, 22 + eqPrecut: -3, 23 + eqBandSettings: [ 24 + { cutoff: 60, q: 7, gain: 4 }, // bass boost 25 + { cutoff: 200, q: 7, gain: 1 }, 26 + { cutoff: 800, q: 7, gain: 0 }, 27 + { cutoff: 4_000, q: 7, gain: -2 }, // tame harshness 28 + { cutoff: 12_000, q: 7, gain: 2 }, // air 29 + ], 30 + replaygainSettings: { 31 + noclip: true, 32 + type: ReplaygainType.Album, 33 + preamp: 0, 34 + }, 35 + }); 36 + 37 + const after = await client.settings.get(); 38 + console.log(`\nAfter:`); 39 + console.log(` EQ enabled : ${after.eqEnabled}`); 40 + console.log(` EQ precut : ${after.eqPrecut}`); 41 + console.log(` Replaygain type : ${after.replaygainSettings.type}`); 42 + console.log(` Replaygain noclip: ${after.replaygainSettings.noclip}`);
+31
sdk/typescript/examples/09-browse-filesystem.ts
··· 1 + // 09 — Browse the local filesystem 2 + // 3 + // Walk `music_dir` like a tree and print directories and files, demonstrating 4 + // the `isDirectory()` helper. 5 + // 6 + // bun run examples/09-browse-filesystem.ts # browse music_dir root 7 + // bun run examples/09-browse-filesystem.ts /Music # browse a specific path 8 + 9 + import { isDirectory } from '../src/index.js'; 10 + import { createClient } from './_client.js'; 11 + 12 + const client = createClient(); 13 + const path = process.argv[2]; 14 + 15 + const entries = await client.browse.entries(path); 16 + console.log(`Browsing: ${path ?? '(music_dir root)'}\n`); 17 + 18 + const dirs = entries.filter(isDirectory); 19 + const files = entries.filter((e) => !isDirectory(e)); 20 + 21 + console.log(`📁 Directories (${dirs.length}):`); 22 + for (const d of dirs.slice(0, 15)) { 23 + console.log(` ${d.displayName ?? d.name}`); 24 + } 25 + if (dirs.length > 15) console.log(` ... and ${dirs.length - 15} more`); 26 + 27 + console.log(`\n🎵 Files (${files.length}):`); 28 + for (const f of files.slice(0, 15)) { 29 + console.log(` ${f.displayName ?? f.name}`); 30 + } 31 + if (files.length > 15) console.log(` ... and ${files.length - 15} more`);
+35
sdk/typescript/examples/10-browse-upnp.ts
··· 1 + // 10 — Browse UPnP media servers 2 + // 3 + // `treeGetEntries` accepts paths starting with `upnp://` to discover and walk 4 + // UPnP ContentDirectory servers on the local network. 5 + // 6 + // bun run examples/10-browse-upnp.ts # discover servers 7 + // bun run examples/10-browse-upnp.ts <path> # browse a server / object 8 + // 9 + // Discovered servers come back as entries with names like `upnp://<encoded>`. 10 + // Pass that name back as the path argument to descend into one. The `name` 11 + // field of a child entry is the canonical path you can navigate into next. 12 + 13 + import { isDirectory } from '../src/index.js'; 14 + import { createClient } from './_client.js'; 15 + 16 + const client = createClient(); 17 + const path = process.argv[2] ?? 'upnp://'; 18 + 19 + const entries = await client.browse.entries(path); 20 + console.log(`UPnP browse: ${path}`); 21 + console.log(`Found ${entries.length} entries.\n`); 22 + 23 + for (const e of entries.slice(0, 30)) { 24 + const icon = isDirectory(e) ? '📁' : '🎵'; 25 + // displayName carries the human-readable title for UPnP entries 26 + const label = e.displayName ?? e.name; 27 + console.log(`${icon} ${label}`); 28 + console.log(` → ${e.name}`); 29 + } 30 + if (entries.length > 30) console.log(`\n... and ${entries.length - 30} more`); 31 + 32 + if (entries.length === 0) { 33 + console.log('No UPnP servers responded. Make sure a UPnP/DLNA server is running'); 34 + console.log('on the local network (e.g. minidlna, Plex, Kodi).'); 35 + }
+48
sdk/typescript/examples/11-bluetooth.ts
··· 1 + // 11 — Bluetooth (Linux only) 2 + // 3 + // List paired devices, optionally scan for new ones, and connect/disconnect by 4 + // MAC address. Works on Linux hosts where rockboxd has access to BlueZ. 5 + // 6 + // bun run examples/11-bluetooth.ts # list paired devices 7 + // bun run examples/11-bluetooth.ts scan # scan 10s for devices 8 + // bun run examples/11-bluetooth.ts connect AA:BB:.. # connect by address 9 + // bun run examples/11-bluetooth.ts disconnect AA:BB:.. 10 + 11 + import type { BluetoothDevice } from '../src/index.js'; 12 + import { createClient } from './_client.js'; 13 + 14 + function format(d: BluetoothDevice): string { 15 + const flags = [ 16 + d.connected ? 'connected' : '', 17 + d.paired ? 'paired' : '', 18 + d.trusted ? 'trusted' : '', 19 + ].filter(Boolean).join(', '); 20 + const rssi = d.rssi != null ? ` ${d.rssi} dBm` : ''; 21 + return ` ${d.address} ${d.name.padEnd(28)}${rssi} [${flags}]`; 22 + } 23 + 24 + const client = createClient(); 25 + const cmd = process.argv[2] ?? 'list'; 26 + 27 + try { 28 + if (cmd === 'scan') { 29 + console.log('Scanning for 10s...'); 30 + const found = await client.bluetooth.scan(10); 31 + console.log(`Found ${found.length} devices:`); 32 + found.forEach((d) => console.log(format(d))); 33 + } else if (cmd === 'connect' && process.argv[3]) { 34 + await client.bluetooth.connect(process.argv[3]); 35 + console.log(`Connected to ${process.argv[3]}`); 36 + } else if (cmd === 'disconnect' && process.argv[3]) { 37 + await client.bluetooth.disconnect(process.argv[3]); 38 + console.log(`Disconnected from ${process.argv[3]}`); 39 + } else { 40 + const devices = await client.bluetooth.devices(); 41 + console.log(`Paired devices (${devices.length}):`); 42 + devices.forEach((d) => console.log(format(d))); 43 + } 44 + } catch (err) { 45 + // Non-Linux hosts return "Bluetooth is only supported on Linux". 46 + console.error('Bluetooth call failed:', err instanceof Error ? err.message : err); 47 + process.exit(1); 48 + }
+36
sdk/typescript/examples/12-devices.ts
··· 1 + // 12 — Output devices (Chromecast / AirPlay / source devices) 2 + // 3 + // List discovered output sinks and optionally connect/disconnect one. 4 + // 5 + // bun run examples/12-devices.ts # list devices 6 + // bun run examples/12-devices.ts connect <id> # connect a device 7 + // bun run examples/12-devices.ts disconnect <id> # disconnect a device 8 + 9 + import { createClient } from './_client.js'; 10 + 11 + const client = createClient(); 12 + const cmd = process.argv[2] ?? 'list'; 13 + 14 + if (cmd === 'connect' && process.argv[3]) { 15 + await client.devices.connect(process.argv[3]); 16 + console.log(`Connected to device ${process.argv[3]}`); 17 + } else if (cmd === 'disconnect' && process.argv[3]) { 18 + await client.devices.disconnect(process.argv[3]); 19 + console.log(`Disconnected device ${process.argv[3]}`); 20 + } else { 21 + const devices = await client.devices.list(); 22 + console.log(`Discovered ${devices.length} device(s):\n`); 23 + 24 + for (const d of devices) { 25 + const kind = d.isCastDevice 26 + ? 'cast' 27 + : d.isSourceDevice 28 + ? 'source' 29 + : 'output'; 30 + const dot = d.isConnected ? '●' : '○'; 31 + const cur = d.isCurrentDevice ? ' (current)' : ''; 32 + console.log(`${dot} [${kind.padEnd(6)}] ${d.name}${cur}`); 33 + console.log(` id=${d.id} ${d.ip}:${d.port} ${d.service}`); 34 + if (d.baseUrl) console.log(` baseUrl=${d.baseUrl}`); 35 + } 36 + }
+61
sdk/typescript/examples/13-plugin-sleep-timer.ts
··· 1 + // 13 — Plugin: sleep timer 2 + // 3 + // Stops playback after N minutes. If the user stops playback manually before 4 + // the timer fires, the plugin cancels itself. 5 + // 6 + // bun run examples/13-plugin-sleep-timer.ts # default 30 minutes 7 + // bun run examples/13-plugin-sleep-timer.ts 5 # 5 minutes 8 + 9 + import type { RockboxPlugin } from '../src/index.js'; 10 + import { PlaybackStatus } from '../src/index.js'; 11 + import { createClient } from './_client.js'; 12 + 13 + function sleepTimer(minutes: number): RockboxPlugin { 14 + let timer: ReturnType<typeof setTimeout> | null = null; 15 + 16 + return { 17 + name: 'sleep-timer', 18 + version: '1.0.0', 19 + description: `Stop playback after ${minutes} minute(s)`, 20 + 21 + install({ events, query }) { 22 + const fireAt = new Date(Date.now() + minutes * 60_000); 23 + console.log(`💤 Sleep timer armed — will stop playback at ${fireAt.toLocaleTimeString()}`); 24 + 25 + timer = setTimeout(async () => { 26 + console.log('💤 Time’s up — stopping playback.'); 27 + await query('mutation { hardStop }'); 28 + }, minutes * 60_000); 29 + 30 + // If the user already pressed stop manually, cancel the timer. 31 + events.on('status:changed', (status) => { 32 + if (status === PlaybackStatus.Stopped && timer) { 33 + clearTimeout(timer); 34 + timer = null; 35 + console.log('💤 Playback stopped manually — sleep timer cancelled.'); 36 + } 37 + }); 38 + }, 39 + 40 + uninstall() { 41 + if (timer) { 42 + clearTimeout(timer); 43 + timer = null; 44 + } 45 + }, 46 + }; 47 + } 48 + 49 + const minutes = process.argv[2] ? Number(process.argv[2]) : 30; 50 + const client = createClient(); 51 + client.connect(); 52 + 53 + await client.use(sleepTimer(minutes)); 54 + 55 + console.log('Plugin installed. Press Ctrl+C to cancel and exit.'); 56 + 57 + process.on('SIGINT', async () => { 58 + await client.unuse('sleep-timer'); 59 + client.disconnect(); 60 + process.exit(0); 61 + });
+52
sdk/typescript/examples/14-plugin-scrobbler.ts
··· 1 + // 14 — Plugin: minimal scrobbler 2 + // 3 + // Records a "play" for the previous track when a new track starts, but only if 4 + // the previous one played for at least 30 seconds (Last.fm's classic rule). 5 + // Stats are stored on the rockbox server via `recordTrackPlayed` and feed the 6 + // smart-playlist rules engine. 7 + // 8 + // bun run examples/14-plugin-scrobbler.ts 9 + 10 + import type { RockboxPlugin, Track } from '../src/index.js'; 11 + import { createClient } from './_client.js'; 12 + 13 + const Scrobbler: RockboxPlugin = { 14 + name: 'scrobbler', 15 + version: '1.0.0', 16 + description: 'Record played tracks after 30s of playback', 17 + 18 + install({ events, query }) { 19 + let current: Track | null = null; 20 + let startedAt = 0; 21 + 22 + events.on('track:changed', async (track) => { 23 + const playedFor = current ? Date.now() - startedAt : 0; 24 + if (current?.id && playedFor >= 30_000) { 25 + console.log(`✓ scrobbling: ${current.title} — ${current.artist} (${Math.round(playedFor / 1000)}s)`); 26 + try { 27 + await query<{ recordTrackPlayed: boolean }>( 28 + `mutation Played($id: String!) { recordTrackPlayed(trackId: $id) }`, 29 + { id: current.id }, 30 + ); 31 + } catch (err) { 32 + console.error(' scrobble failed:', err instanceof Error ? err.message : err); 33 + } 34 + } 35 + current = track; 36 + startedAt = Date.now(); 37 + console.log(`▶ now playing: ${track.title} — ${track.artist}`); 38 + }); 39 + }, 40 + }; 41 + 42 + const client = createClient(); 43 + client.connect(); 44 + await client.use(Scrobbler); 45 + 46 + console.log('Scrobbler installed. Press Ctrl+C to exit.'); 47 + 48 + process.on('SIGINT', async () => { 49 + await client.unuse('scrobbler'); 50 + client.disconnect(); 51 + process.exit(0); 52 + });
+90
sdk/typescript/examples/15-cli-remote.ts
··· 1 + // 15 — Tiny interactive CLI remote 2 + // 3 + // A no-frills terminal remote control. Renders the now-playing line and 4 + // reacts to single-key presses without needing Enter. 5 + // 6 + // bun run examples/15-cli-remote.ts 7 + // 8 + // Keys: 9 + // space play / pause 10 + // n next track 11 + // p previous track 12 + // + volume up 13 + // - volume down 14 + // l like current track 15 + // q quit 16 + 17 + import { PlaybackStatus } from '../src/index.js'; 18 + import { createClient, fmtTime } from './_client.js'; 19 + 20 + const client = createClient(); 21 + client.connect(); 22 + 23 + async function render(): Promise<void> { 24 + const [status, track, vol] = await Promise.all([ 25 + client.playback.status(), 26 + client.playback.currentTrack(), 27 + client.sound.getVolume(), 28 + ]); 29 + 30 + const label = PlaybackStatus[status] ?? '?'; 31 + const line = track 32 + ? `${track.title} — ${track.artist} [${fmtTime(track.elapsed)} / ${fmtTime(track.length)}]` 33 + : '(nothing playing)'; 34 + 35 + process.stdout.write(`\r\x1b[2K[${label}] ${line} vol=${vol.volume}dB`); 36 + } 37 + 38 + await render(); 39 + 40 + // Re-render whenever something changes 41 + client.on('track:changed', () => { void render(); }); 42 + client.on('status:changed', () => { void render(); }); 43 + 44 + // Raw stdin: single keypress without Enter 45 + const stdin = process.stdin; 46 + stdin.setRawMode?.(true); 47 + stdin.resume(); 48 + stdin.setEncoding('utf8'); 49 + 50 + stdin.on('data', async (key) => { 51 + const k = key.toString(); 52 + 53 + if (k === 'q' || k === '' /* Ctrl-C */) { 54 + stdin.setRawMode?.(false); 55 + client.disconnect(); 56 + console.log('\nbye'); 57 + process.exit(0); 58 + } 59 + 60 + try { 61 + switch (k) { 62 + case ' ': { 63 + const status = await client.playback.status(); 64 + if (status === PlaybackStatus.Playing) await client.playback.pause(); 65 + else await client.playback.resume(); 66 + break; 67 + } 68 + case 'n': await client.playback.next(); break; 69 + case 'p': await client.playback.previous(); break; 70 + case '+': 71 + case '=': await client.sound.volumeUp(); break; 72 + case '-': 73 + case '_': await client.sound.volumeDown(); break; 74 + case 'l': { 75 + const t = await client.playback.currentTrack(); 76 + if (t?.id) { 77 + await client.library.likeTrack(t.id); 78 + process.stdout.write(' ♥'); 79 + } 80 + break; 81 + } 82 + } 83 + } catch (err) { 84 + process.stdout.write(`\n[error] ${err instanceof Error ? err.message : err}\n`); 85 + } 86 + 87 + await render(); 88 + }); 89 + 90 + console.log('Press [space] play/pause [n] next [p] prev [+/-] vol [l] like [q] quit');
+52
sdk/typescript/examples/README.md
··· 1 + # Examples 2 + 3 + Runnable sample programs for `@rockbox-zig/sdk`. 4 + 5 + ## Prerequisites 6 + 7 + - `bun` installed (https://bun.sh) 8 + - `rockboxd` running and reachable at `http://localhost:6062/graphql` 9 + ```sh 10 + ./zig/zig-out/bin/rockboxd 11 + ``` 12 + - SDK dependencies installed (run once from `sdk/typescript/`): 13 + ```sh 14 + bun install 15 + ``` 16 + 17 + ## Running an example 18 + 19 + From the `sdk/typescript/` directory: 20 + 21 + ```sh 22 + bun run examples/01-basic-playback.ts 23 + ``` 24 + 25 + Override the host/port with environment variables: 26 + 27 + ```sh 28 + ROCKBOX_HOST=192.168.1.42 ROCKBOX_PORT=6062 bun run examples/01-basic-playback.ts 29 + ``` 30 + 31 + ## Index 32 + 33 + | File | Demonstrates | 34 + |---------------------------------------|---------------------------------------------------------| 35 + | `01-basic-playback.ts` | Status, transport controls, current track | 36 + | `02-now-playing.ts` | Real-time WebSocket subscriptions | 37 + | `03-library-search.ts` | Search the library and play results | 38 + | `04-queue-management.ts` | Inspect and manipulate the playback queue | 39 + | `05-saved-playlists.ts` | Create, edit, and play saved playlists | 40 + | `06-smart-playlist.ts` | Build smart playlists from rule sets | 41 + | `07-volume-control.ts` | Read `VolumeInfo` and adjust relative volume | 42 + | `08-eq-config.ts` | Configure the equalizer and replaygain | 43 + | `09-browse-filesystem.ts` | Walk `music_dir` like a tree | 44 + | `10-browse-upnp.ts` | Discover and browse UPnP media servers | 45 + | `11-bluetooth.ts` | Scan, connect, and disconnect Bluetooth devices (Linux) | 46 + | `12-devices.ts` | List and switch Chromecast / AirPlay output sinks | 47 + | `13-plugin-sleep-timer.ts` | Plugin: stop playback after N minutes | 48 + | `14-plugin-scrobbler.ts` | Plugin: log every fully-played track | 49 + | `15-cli-remote.ts` | Tiny interactive remote control in the terminal | 50 + 51 + Each example is self-contained — pick the one closest to what you need, copy 52 + it into your project, and adapt.
+18
sdk/typescript/examples/_client.ts
··· 1 + // Shared client factory used by every example. 2 + // Override the host/port via env: ROCKBOX_HOST, ROCKBOX_PORT. 3 + import { RockboxClient } from '../src/index.js'; 4 + 5 + export function createClient(): RockboxClient { 6 + return new RockboxClient({ 7 + host: process.env.ROCKBOX_HOST ?? 'localhost', 8 + port: process.env.ROCKBOX_PORT ? Number(process.env.ROCKBOX_PORT) : 6062, 9 + }); 10 + } 11 + 12 + /** Format milliseconds as M:SS */ 13 + export function fmtTime(ms: number): string { 14 + const total = Math.max(0, Math.floor(ms / 1000)); 15 + const m = Math.floor(total / 60); 16 + const s = total % 60; 17 + return `${m}:${s.toString().padStart(2, '0')}`; 18 + }
+46
sdk/typescript/src/api/bluetooth.ts
··· 1 + import type { HttpTransport } from '../transport.js'; 2 + import type { BluetoothDevice } from '../types.js'; 3 + 4 + const BLUETOOTH_DEVICE_FIELDS = /* GraphQL */ ` 5 + fragment BluetoothDeviceFields on BluetoothDevice { 6 + address name paired trusted connected rssi 7 + } 8 + `; 9 + 10 + export class BluetoothApi { 11 + constructor(private readonly http: HttpTransport) {} 12 + 13 + /** List paired/known Bluetooth devices (Linux only) */ 14 + async devices(): Promise<BluetoothDevice[]> { 15 + const data = await this.http.execute<{ bluetoothDevices: BluetoothDevice[] }>(/* GraphQL */ ` 16 + ${BLUETOOTH_DEVICE_FIELDS} 17 + query BluetoothDevices { bluetoothDevices { ...BluetoothDeviceFields } } 18 + `); 19 + return data.bluetoothDevices; 20 + } 21 + 22 + /** Scan for nearby Bluetooth devices (Linux only) */ 23 + async scan(timeoutSecs?: number): Promise<BluetoothDevice[]> { 24 + const data = await this.http.execute<{ bluetoothScan: BluetoothDevice[] }>(/* GraphQL */ ` 25 + ${BLUETOOTH_DEVICE_FIELDS} 26 + mutation BluetoothScan($timeoutSecs: Int) { 27 + bluetoothScan(timeoutSecs: $timeoutSecs) { ...BluetoothDeviceFields } 28 + } 29 + `, { timeoutSecs }); 30 + return data.bluetoothScan; 31 + } 32 + 33 + /** Connect to a Bluetooth device by address (Linux only) */ 34 + async connect(address: string): Promise<void> { 35 + await this.http.execute(/* GraphQL */ ` 36 + mutation BluetoothConnect($address: String!) { bluetoothConnect(address: $address) } 37 + `, { address }); 38 + } 39 + 40 + /** Disconnect a Bluetooth device by address (Linux only) */ 41 + async disconnect(address: string): Promise<void> { 42 + await this.http.execute(/* GraphQL */ ` 43 + mutation BluetoothDisconnect($address: String!) { bluetoothDisconnect(address: $address) } 44 + `, { address }); 45 + } 46 + }
+1 -1
sdk/typescript/src/api/browse.ts
··· 8 8 async entries(path?: string): Promise<Entry[]> { 9 9 const data = await this.http.execute<{ treeGetEntries: Entry[] }>(/* GraphQL */ ` 10 10 query Browse($path: String) { 11 - treeGetEntries(path: $path) { name attr timeWrite customaction } 11 + treeGetEntries(path: $path) { name attr timeWrite customaction displayName } 12 12 } 13 13 `, { path }); 14 14 return data.treeGetEntries;
+1 -1
sdk/typescript/src/api/library.ts
··· 13 13 14 14 const ALBUM_FIELDS = /* GraphQL */ ` 15 15 fragment AlbumFields on Album { 16 - id title artist year yearString albumArt md5 artistId 16 + id title artist year yearString albumArt md5 artistId copyrightMessage 17 17 } 18 18 `; 19 19
+9
sdk/typescript/src/api/sound.ts
··· 1 1 import type { HttpTransport } from '../transport.js'; 2 + import type { VolumeInfo } from '../types.js'; 2 3 3 4 export class SoundApi { 4 5 constructor(private readonly http: HttpTransport) {} 6 + 7 + /** Get current volume with min/max range */ 8 + async getVolume(): Promise<VolumeInfo> { 9 + const data = await this.http.execute<{ volume: VolumeInfo }>(/* GraphQL */ ` 10 + query Volume { volume { volume min max } } 11 + `); 12 + return data.volume; 13 + } 5 14 6 15 /** Adjust volume by a relative number of steps (positive = louder, negative = quieter) */ 7 16 async adjustVolume(steps: number): Promise<number> {
+3
sdk/typescript/src/client.ts
··· 12 12 import { SystemApi } from './api/system.js'; 13 13 import { BrowseApi } from './api/browse.js'; 14 14 import { DevicesApi } from './api/devices.js'; 15 + import { BluetoothApi } from './api/bluetooth.js'; 15 16 16 17 import type { Track, Playlist } from './types.js'; 17 18 ··· 47 48 readonly system: SystemApi; 48 49 readonly browse: BrowseApi; 49 50 readonly devices: DevicesApi; 51 + readonly bluetooth: BluetoothApi; 50 52 51 53 private readonly http: HttpTransport; 52 54 private readonly ws: WsTransport; ··· 75 77 this.system = new SystemApi(this.http); 76 78 this.browse = new BrowseApi(this.http); 77 79 this.devices = new DevicesApi(this.http); 80 + this.bluetooth = new BluetoothApi(this.http); 78 81 } 79 82 80 83 // ---------------------------------------------------------------------------
+2
sdk/typescript/src/index.ts
··· 27 27 SavedPlaylistFolder, 28 28 SmartPlaylist, 29 29 TrackStats, 30 + BluetoothDevice, 31 + VolumeInfo, 30 32 Device, 31 33 Entry, 32 34 SystemStatus,
+26
sdk/typescript/src/types.ts
··· 75 75 albumArt?: string; 76 76 md5: string; 77 77 artistId: string; 78 + copyrightMessage?: string; 78 79 tracks: Track[]; 79 80 } 80 81 ··· 151 152 } 152 153 153 154 // --------------------------------------------------------------------------- 155 + // Bluetooth types 156 + // --------------------------------------------------------------------------- 157 + 158 + export interface BluetoothDevice { 159 + address: string; 160 + name: string; 161 + paired: boolean; 162 + trusted: boolean; 163 + connected: boolean; 164 + rssi?: number; 165 + } 166 + 167 + // --------------------------------------------------------------------------- 168 + // Sound types 169 + // --------------------------------------------------------------------------- 170 + 171 + export interface VolumeInfo { 172 + volume: number; 173 + min: number; 174 + max: number; 175 + } 176 + 177 + // --------------------------------------------------------------------------- 154 178 // Device types 155 179 // --------------------------------------------------------------------------- 156 180 ··· 179 203 attr: number; 180 204 timeWrite: number; 181 205 customaction: number; 206 + /** Human-readable display name (used for UPnP entries) */ 207 + displayName?: string; 182 208 } 183 209 184 210 export function isDirectory(entry: Entry): boolean {