Thanks to Vandam Dinh — especially Echo — for the Light Phone UI patterns and product direction this client builds on.
An independent, minimal Spotify client for LightOS. Playback runs in-process via a patched fork of librespot (Rust). Metadata (search, library, albums, artists, playlists) uses the official Spotify Web API with your own developer-app credentials.
Requires a Spotify Premium account. This is not something we have any interest in working around, so please do not ask!
New developer? Agent? (ugh): Read docs/README.md and AGENTS.md before changing code.
vandam rocks. Basically, this has a few extra features, less album art and doesn't require the Spotify app to be installed.
The app uses dual authentication:
- Step 1 — Playback (librespot): WebView login with Spotify's first-party Keymaster client for audio streaming. No developer dashboard setup needed for this step.
- Step 2 — Web API: Create your own app at developer.spotify.com/dashboard and enter the Client ID and Client Secret on the Step 2 gate screen after playback login.
- Go to developer.spotify.com/dashboard
- Click Create App
- Fill in app name and description
- Set Redirect URI to
http://127.0.0.1:43821/callback(must match exactly!!) - Select Web API under "Which API/SDKs are you planning to use?"
- Accept terms and click Save
- Open Settings and copy your Client ID and Client Secret
- Click Save
- Complete Step 1 (playback login) in the app
- On Step 2, enter your Client ID and Client Secret:
- Type manually, or
- On a computer, open jonathancaudill.github.io/phono to generate a QR code, then tap Scan QR on the phone
- Tap Connect Web API and authorize when prompted
The setup page runs entirely in your browser; it's just a static page. The QR code encodes your client secret in plain text, so be careful sharing it :)
PLEASE NOTE: Credentials may expire around 6 months depending on Spotify dev app restrictions. Just rotate the secret and redo steps 2-3!
rust/
spotify-core/ # UniFFI engine: session, player, queue, AudioTrack sink
librespot-core-patched/ # Keymaster/desktop identity (PATCHES.md)
librespot-playback-patched/ # Buffering API, sink lifecycle (PATCHES.md)
librespot-audio-patched/ # CDN fetch resilience (PATCHES.md)
app/ # Android (Kotlin Web API + Jetpack Compose)
setup/ # GitHub Pages: Web API credential QR generator
docs/ # Architecture, field tests, future research
scripts/build-rust.sh # Cross-compile + UniFFI Kotlin bindings
All librespot crates are pinned to =0.8.0. Do not bump without re-validating every patch.
LibrespotEngine(UniFFI) owns session, player, and queue.- Keymaster OAuth via WebView (
http://127.0.0.1:8898/login). Three client-identity surfaces (session, stored credentials, client-token) must agree as Keymaster/desktop — seeAGENTS.md. - Audio output (Path C): decode on the librespot player thread → SPSC ring → dedicated
drain thread → JNI → Kotlin
PhonoAudioTrackSink→AudioTrack(USAGE_MEDIA). Details: docs/audio-sink.md. - Session recovery: seamless
Activerebuild with queue/position restore (not librespot-java in-place reconnect). Details: docs/future/session-reconnect.md.
PlaybackServicecreates the native engine and callsstartForeground()immediately.PlaybackController— audio focus, network-tier streaming policy, stall UX, DelayMs for lock-screen position.SpotifyWebApi+SpotifyRepository— search, liked/saved albums, album detail via dev-app OAuth (http://127.0.0.1:43821/callback). Single combined/searchper query; client-side ranking (SearchRanking.kt).- Playlists and artists — native spclient via
NativeMetadataGateway(Step 1 session required for browse, edit, follow/unfollow, and artist pages). - Library writes via Web API (
PUT/DELETE /me/library). - Daily mixes: native librespot
context-resolvewith Web API fallback.
NativeInit order: loadLibrary → initAndroidContext → registerAudioSink (Path C).
Liked tracks, saved albums, playlists — head-check delta sync, parallel page fill. UI reads from
disk via Flow.
Pinned Room cache for saved albums / owned playlists (24 h TTL); ephemeral in-memory for browsed content.
Per-query in-memory cache (5 min); filter chips reuse cached response.
- Playback: librespot stored credentials in
filesDir/spotify-cache/ - Web API:
EncryptedSharedPreferenceswith proactive refresh
Ogg chunks under filesDir/spotify-cache/. Opportunistic buffering on good Wi‑Fi:
buffer_current_to_end() + prefetch_upcoming() via patched librespot player API.
Prerequisites:
- Rust (rustup) with Android targets:
rustup target add aarch64-linux-android x86_64-linux-android cargo install cargo-ndk- Android NDK installed; export
ANDROID_NDK_HOME - JDK 17, Android SDK (compileSdk 35), Gradle
# Cross-compile Rust + generate Kotlin bindings (AudioTrack backend by default)
bash scripts/build-rust.sh
# Build and install
./gradlew :app:installDebugRodio fallback (emulator debug): USE_AUDIOTRACK_SINK=0 ./scripts/build-rust.sh and match
USE_AUDIOTRACK_SINK in app/build.gradle.kts.
Host tests (ring buffer): cd rust/spotify-core && cargo test pcm_ring
- Call
NativeInit.initAndroidContextbefore constructing the engine (ndk_context/ identity). - Do not mix redirect URIs: Step 1 →
127.0.0.1:8898/login; Step 2 →127.0.0.1:43821/callback. - Do not use the Keymaster token for Web API — search/library metadata must use the dev-app bearer.
- Playlist/artist screens require Step 1 (playback login) — they use native spclient, not Web API.
- minSdk 26. Audio focus in
PlaybackController. PlaybackServicemuststartForeground()within seconds ofstartForegroundService().
| Layer | Mechanism |
|---|---|
| Session | Monitor + seamless rebuild; force_reconnect_check() on network change |
| Decode buffer | buffer_current_to_end, prefetch_upcoming, network-tier presets |
| Audio output | Ring + drain; recreateAudioSink(); Kotlin DEAD_OBJECT / stall recovery |
| Web API | Token refresh, HTTP 429 Retry-After, invalid_grant handling |
Field validation: docs/audio-sink-baseline-metrics.md
| Doc | Contents |
|---|---|
| AGENTS.md | Hard rules, auth, diagnostics — read before coding |
| docs/README.md | Developer onboarding index |
| docs/audio-sink.md | Phase C AudioTrack architecture |
| docs/future/ | Researched future work (session reconnect, backend move) |