Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
- name: Checkout companion light-sdk
uses: actions/checkout@v4
with:
repository: LooseWireDev/light-sdk
ref: codex/kelp-official-sdk
repository: gavdevs/light-sdk
ref: 251237ef3778c907c5dbd89e84685532c9d99a28
path: light-sdk

- name: Set up JDK 17
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
- name: Checkout companion light-sdk
uses: actions/checkout@v4
with:
repository: LooseWireDev/light-sdk
ref: codex/kelp-official-sdk
repository: gavdevs/light-sdk
ref: 251237ef3778c907c5dbd89e84685532c9d99a28
path: light-sdk

- name: Set up JDK 17
Expand Down
12 changes: 7 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kelp – agent notes

Light Phone III TIDAL client built on `lightphone/light-sdk` (composite build from `../light-sdk`, branch `codex/kelp-official-sdk`, based on official v0.1.1). Kotlin + Compose throughout.
Light Phone III TIDAL client built on the `gavdevs/light-sdk` fork (composite build from `../light-sdk`, pinned to `main` at `251237e`). Kotlin + Compose throughout.

## Module map

Expand All @@ -17,9 +17,9 @@ Distribution lane: **sideload-only, experimental/local.** Not eligible for Light
- **`:app` never imports TIDAL SDK, okhttp, Media3, or `:server` types.** Everything crosses the Light binder via `KelpRemoteMethod` IDs (`com.loosewire.kelp.*`). Add a feature by extending `:protocol` contracts, dispatch in `KelpServiceMethods`, and a client method in `KelpClient.kt`.
- **`:protocol` stays Android/TIDAL-free.** Serializable models only, no platform imports.
- **Two TIDAL sessions** (`KelpRuntime`): developer-app auth3 login for the v2 Public API (catalog) + `TidalStreamingAuth` first-party PKCE for playback. Catalog endpoints only accept the developer token; full-length streams only resolve with the first-party token. Do not collapse them.
- **Playback is service-owned**: `KelpPlaybackService` owns the MediaSession and `KelpPlayerController`/ExoPlayer. It explicitly calls `addSession` because Binder RPCs start playback without a MediaController connection. `TidalMediaSource` lazily resolves BTS/clear-DASH through `TidalStreamResolver`. Never create a second player from a screen or reintroduce the official TIDAL Player SDK.
- **Playback threading**: queue mutations and ExoPlayer calls are main-looper confined; Binder reads an immutable `@Volatile` snapshot. MediaSource lifecycle uses ExoPlayer's playback looper and resolution runs on IO. Cancellation must prevent stale stream/continuous-queue results; screen hide must not release playback.
- **Media3 is strict-pinned to 1.5.0** in both `app` and `server` (Light SDK resolves a newer version). Keep both pins in sync.
- **The Light SDK owns playback** through `LightAudioQueueSessions` and its detached `MediaSessionService`. Kelp owns TIDAL stream resolution (`playbackinfopostpaywall`, BTS/clear-DASH) and catalog recommendations. Do not create a second player or reintroduce the official TIDAL Player SDK.
- **Playback threading**: the SDK queue is main-thread confined. TIDAL resolver callbacks use IO. Synchronous playback RPCs must stay off Main. Never hold `catalogLock` while waiting for the SDK. Playback commands and sign-out serialize under `playerLock`.
- **The SDK owns Media3 versions and decoder dependencies**, including DASH. The old app/server 1.5.0 pins are removed. Both Kelp modules exclude desktop Tink from their SDK-client dependency because authentication requires `tink-android`.
- **Only implemented settings**: continuous playback is applied at queue start. Quality selection, normalization, explicit filtering, and offline playback are not implemented; do not reintroduce preference-only controls.
- **`lighttool.toml` is the identity source** (app id, version, capabilities, `serverPackage = "com.loosewire.kelp"`). Do not hand-write what the plugin generates.
- **Secrets**: `local.properties` holds the developer-app client id and is git-ignored. Never embed it. The first-party client id in `TidalStreamingAuth` is intentionally embedded (public, shared with orpheusdl/phono; rotation procedure in `docs/TIDAL_ARCHITECTURE.md` and README).
Expand All @@ -35,9 +35,11 @@ scripts/build-release.sh # tests + minified release APK in dist/ (handl

Verification for any change: the narrowest useful unit tests first (`:protocol:test`, `:app:testDebugUnitTest`, `:server:testDebugUnitTest`), then `:app:assembleDebug`; run `:app:assembleRelease` for anything touching server code (R8 path) — or just run `scripts/build-release.sh`. Install: `adb -s <serial> install -r app/build/outputs/apk/debug/app-debug.apk`; pick an explicit serial when devices are ambiguous.

Playback changes also require the companion SDK's `:sdk:client:testDebugUnitTest` and a device lifecycle check. Verify screen-off playback across track transitions after the Activity and controller are released. A stock emulator cannot prove LP3-specific power behavior.

## CI / releases

- `.github/workflows/build.yml`: tests + debug assemble on push/PR. CI checks out the companion `LooseWireDev/light-sdk` (branch `codex/kelp-official-sdk`) next to the repo; locally that checkout must sit at `../light-sdk` (override with `-Ptide.sdkPath=...`).
- `.github/workflows/build.yml`: tests + debug assemble on push/PR. CI checks out the companion `gavdevs/light-sdk` (pinned revision on `main`) next to the repo; locally that checkout must sit at `../light-sdk` (override with `-Ptide.sdkPath=...`).
- `.github/workflows/release.yml`: tag-triggered (`v*`) signed release. Tag must match `versionName` in `app/lighttool.toml`; bump `versionCode` each release. Signing uses Actions secrets `TIDE_RELEASE_*`; the keystore backup lives at `~/.tide-keys/tide-release.jks` (storepass in `tide-release.storepass`, 600), outside the repo.
- GitHub Release APKs are signed with the release key; dev-key-signed installs must be uninstalled first (signature mismatch on upgrade).

Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

## What is it?

Kelp streams TIDAL through saved songs, albums, artists, playlists, daily mixes, and search in a text-first Light UI. The Light SDK owns the tool UI and navigation; a Kelp-owned Media3 media-session service owns background playback and publishes system transport controls. Full-length stream resolution uses the unofficial first-party-client technique used by [phono](https://github.com/jonathancaudill/phono), not the official TIDAL Player SDK.
Kelp provides saved songs, albums, artists, playlists, daily mixes, and search in a monochrome interface. Kelp resolves full-length TIDAL streams with first-party credentials, using the approach of [phono](https://github.com/jonathancaudill/phono). The Light SDK owns the audio player and its background service.

**You need a paid TIDAL subscription.** Like phono, kelp never works around that and never will.

Expand All @@ -43,6 +43,7 @@ Kelp is **sideload-only** today. It can't go through Light's hosted tool builder
- Foreground-service playback outside Kelp, with track metadata and system play/pause/previous/next controls
- Full-width navigation and transport touch targets, with Now Playing accessible from the top-left waveform
- LOSSLESS streaming by default (falls back to lower quality if needed)
- SDK-owned background playback, track transitions, and system media controls
- Follows LightOS theming and hardware-button navigation

Loading audio is distinct from playing; preparation failures remain visible in Now Playing. Returning from playback keeps loaded browse results. Settings exposes continuous playback and sign-out, with read-only information about the actual streaming policy. Nonfunctional quality, normalization, explicit-filter, and offline controls are not presented as working features.
Expand All @@ -51,7 +52,7 @@ Background playback is not persistent playback recovery: force-stop, reboot, or

## Build it yourself

You'll need JDK 17, an Android SDK (platform 36), and the companion Light SDK checkout at `../light-sdk` (branch `codex/kelp-official-sdk`, based on upstream v0.1.1 plus kelp's small compatibility patches):
Builds require JDK 17, Android SDK platform 36, and the companion `gavdevs/light-sdk` fork at `../light-sdk` on `main`. The fork carries the `LightAudioQueueSessions` streaming API and the companion cert override Kelp needs; Kelp and its companion SDK changes must ship together.

```bash
./gradlew :app:assembleDebug
Expand All @@ -70,6 +71,16 @@ Local builds default to one worker, two JVM-visible processors, a 1.5 GiB heap,

CI runs tests + assemble on every push; pushing a matching `v*` tag publishes a signed release automatically.

### Background playback

`detached-audio` declares the SDK media service and its required permissions. Kelp starts a service-owned streaming queue through `LightAudioQueueSessions`. The service resolves each track when needed and loads recommendations without a screen or controller.

Closing Kelp disconnects the UI without stopping playback. Reopening Kelp reads the existing queue and position. Sign-out clears the queue and cancels pending playback work. An abandoned paused session stops after 60 seconds. Queue restoration after service or process destruction is not supported.

The SDK owns Media3 and the DASH decoder dependency. Kelp no longer pins the older Media3 runtime used by its separate player.

For device verification, play several tracks with the screen off for at least 15 minutes. Verify automatic transitions, system pause/resume and next controls, reconnection, and sign-out. JVM tests cannot establish LightOS power-management behavior.

## Legal / disclaimers

Unofficial project, unaffiliated with TIDAL or Light. A paid TIDAL subscription is required for everything to work. If TIDAL rotates their first-party client id (you'll notice as playback suddenly failing), the fix procedure is documented in [docs/TIDAL_ARCHITECTURE.md](docs/TIDAL_ARCHITECTURE.md).
Expand Down
25 changes: 9 additions & 16 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ android {
error += "RestrictedApi"
}

packaging {
resources {
// Legacy jar indexes and version manifests from the SDK server's
// Netty/ktor stack; nothing reads them at runtime.
excludes += "META-INF/INDEX.LIST"
pickFirsts += "META-INF/io.netty.versions.properties"
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
Expand All @@ -87,22 +96,6 @@ dependencies {
}
implementation(libs.kotlinx.coroutines)

// Keep the SDK client and Kelp's foreground media session on one tested
// Media3 runtime; TIDAL Auth/API do not own the player.
listOf(
"media3-common",
"media3-exoplayer",
"media3-session",
"media3-exoplayer-dash",
"media3-exoplayer-hls",
"media3-datasource-okhttp",
).forEach { module ->
implementation("androidx.media3:$module") {
version { strictly("1.5.0") }
because("Keep the SDK client and Kelp playback service on the same Media3 runtime")
}
}

testImplementation(libs.kotlin.test)
testImplementation(libs.kotlinx.coroutines.test)

Expand Down
4 changes: 2 additions & 2 deletions docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Following Lightious's lifecycle lessons, browse screens retain loaded results on

1. **Shell:** authentication, five-icon catalog navigation, search entry, persistent local playback settings, and Light-native styling.
2. **Catalog:** album details and tracks, playlists, artists, saved tracks, and live search results.
3. **Player:** TIDAL stream resolution, service-owned media queue, Now Playing, native system transports, and media-state reconnection. Per-network quality selection remains unimplemented.
3. **Player:** TIDAL stream resolution through an SDK-owned detached queue, Now Playing, queue controls, and media-state reconnection. Quality application remains separate work.
4. **Offline:** downloads and storage management, only after online playback is reliable.

The current implementation includes the shell, paginated collection reads, artist/album/playlist details, grouped search, and basic online playback from every song list. Offline controls must only appear as active actions when their backing behavior is connected.
The current implementation includes the shell, paginated collection reads, artist/album/playlist details, grouped search, and SDK-owned online playback from song lists. The audio service owns background track transitions and continuous playback. Offline controls must only appear as active actions when their backing behavior is connected.
Loading
Loading