Problem Statement
- Users want to sing from their OpenKara library on a phone. A phone cannot hold the whole library, so it must open the Remote Repository the desktop maintains and fetch songs on demand. OpenKara has no phone app, and its Remote Repository code cannot be reused in one.
- The Remote Repository code is desktop-only and is the largest part of the backend, about 27,000 lines. Much of it is publication recovery machinery (an operation executor, a recovery state machine, a control database, upload status tracking), three Remote Provider adapters, and about 1,500 lines of fault-injection code in the production build. It is slow to change, and its tests build the whole application.
- Google Drive repositories cannot Publish Changes: Drive ignores conditional writes, so publishing stays blocked. The adapter is still about 1,900 lines.
- When another device published first, the user hits a Pre-Publish Conflict for the whole repository and must pick a side, even when the two devices changed different songs. The side not picked loses its edits.
- Users in mainland China cannot reach Google Drive or Dropbox. WebDAV services such as Jianguoyun, a NAS or Nextcloud work there and everywhere else.
Solution
- A standalone Remote Repository core, shared by desktop and phone. It reads and writes the current repository format without migration. The repository manifest is the only visibility switch and the only commit point. Each generation's database snapshot is an immutable file checked against the manifest's digest. Assets in the managed media and stem directories are content-addressed by SHA-256.
- A phone app (Tauri 2, iOS and Android) opens an existing Remote Repository read-only over WebDAV. Refresh Repository downloads the database snapshot when the Remote Revision changed and verifies it. Users browse and search the library, play songs fetched on demand into a verified media cache with an LRU budget, pin songs for offline use, and get the accompaniment with vocal volume when the desktop published stems, plus stored lyrics. The phone does not Publish Changes, run Streaming Sources, or separate stems.
- The desktop first moves Refresh Repository and media fetching onto the core, with no behavior change. Then Publish Changes and Publish Song move onto it: upload the assets the remote lacks, upload the new database snapshot, then conditionally replace the manifest. Recovery after a crash is publishing again. Unreferenced uploads are cleaned up later.
- If the Remote Revision moved before the manifest replace, Publish Changes rebases. It applies the outbox's song-scoped changes to the newer remote database when the other device did not touch those songs. A Pre-Publish Conflict remains only for songs both devices changed and for whole-repository changes, and it names those songs.
- WebDAV is the only adapter in the core. Dropbox and Google Drive are not ported in this spec. They keep the current desktop code until a follow-up decides between porting and retiring them, based on usage.
- Fault injection leaves the production build. Tests script failures through the in-memory storage port instead.
User Stories
- As a phone user, I want to open my existing Remote Repository with its WebDAV Remote Repository Location and Repository Credentials, so that I can sing from my library away from my computer.
- As a phone user, I want the song list to appear after only the database snapshot downloads, so that I can browse thousands of songs without downloading audio.
- As a phone user, I want search, sort and artwork in the phone library, so that I find songs as quickly as on desktop.
- As a phone user, I want a song to download when I play it and stay in the media cache, so that repeat plays start at once.
- As a phone user, I want playback to start before the whole file arrives, so that I do not wait for large files.
- As a phone user, I want to set a cache budget with least-recently-played songs evicted first, so that the app does not fill my phone.
- As a phone user, I want to pin songs for offline use and have pinned songs exempt from eviction, so that I can sing without a network.
- As a phone user, I want to see which songs are available offline, so that I know what works without a network.
- As a phone user, I want the accompaniment with adjustable vocal volume when the desktop published stems, so that I get karaoke on the phone.
- As a phone user, I want songs without published stems to play the original audio and show that stems are missing, so that I know why vocal control is unavailable.
- As a phone user, I want stored Word-timed Lyrics and Line-timed Lyrics to show in sync, so that I can sing along.
- As a phone user, I want Refresh Repository to run when I open the app on a network, so that songs the desktop published appear without extra steps.
- As a phone user, I want the app to keep working from the last snapshot when offline, so that a dead network does not empty my library.
- As a phone user, I want every downloaded snapshot and asset verified against its digest before it is kept, so that a partial or corrupted download never replaces good data.
- As a phone user, I want interrupted downloads to resume, so that large songs finish on flaky mobile networks.
- As a phone user, I want an option to download only on Wi-Fi, so that I do not use mobile data by accident.
- As a phone user, I want expired Repository Credentials to lead me to Reauthorize Repository, so that I fix access without adding the repository again.
- As a phone user, I want Disconnect Repository to remove the phone's Local Working Copy, media cache and credentials without touching remote contents, so that I can remove it safely.
- As a user in mainland China, I want WebDAV services such as Jianguoyun or my NAS to work on desktop and phone, so that I can use a Remote Repository without Google Drive or Dropbox.
- As a desktop user, I want Refresh Repository and playback from a Remote Repository to behave as today after the core moves, so that the rewrite is invisible.
- As a desktop user, I want Publish Changes interrupted by a crash or network loss to finish by publishing again, so that I never need a manual recovery step.
- As a desktop user, I want Publish Song to upload only assets the Remote Repository lacks, so that re-publishing is fast and stores nothing twice.
- As a desktop user, I want Pre-Mutation Refresh to keep working as today, so that my edits start from the newest Remote Revision.
- As a multi-device user, I want my edits to publish automatically when another device changed only other songs, so that unrelated edits never cause a Pre-Publish Conflict.
- As a multi-device user, I want a Pre-Publish Conflict to name the songs both devices changed, so that I decide only where it matters.
- As a multi-device user, I want no change to be overwritten silently, so that I can trust publishing from several devices.
- As a desktop user, I want uploads left behind by interrupted publications cleaned up later, so that the Remote Repository does not grow without bound.
- As a phone user, I want cleanup never to delete a snapshot or asset my phone is still reading, so that cleanup on the desktop does not break playback on the phone.
- As a Dropbox or Google Drive user, I want my repository to keep working on desktop while the core covers WebDAV, so that the rewrite does not drop my storage before a follow-up decision.
- As the maintainer, I want the core in its own crate with its own tests, so that I can change and test Remote Repository code without building the Tauri application.
- As the maintainer, I want fault-injection code out of the production build, so that the shipped binary contains product code only.
- As the maintainer, I want existing Remote Repositories to stay readable and publishable without migration, so that no user rebuilds a repository.
- As a contributor, I want one storage port with one conformance suite, so that a new adapter only has to pass that suite.
- As a coding agent, I want core tests to run against an in-memory storage port without network or credentials, so that they are fast and deterministic.
Implementation Decisions
- New crate, the Remote Repository core: repository format, reader, writer and the WebDAV adapter. It has no Tauri dependency and builds for desktop and mobile targets. It keeps ADR 0018's ownership areas inside the crate: lifecycle (open, Reauthorize Repository, Relocate Repository, Disconnect Repository), content (verified downloads, range reads, cache leases, stem-set materialization), and publication (outbox, Publish Changes, Publish Song).
- Storage port: one port, the existing
RepositoryStorageandRemoteMediaSourcepair, trimmed to what the core uses: stat, read, range read, write a new object, conditional replace, list, delete. Adapters declare capabilities as today, and a Remote Provider without conditional replace still cannot publish. - Format: unchanged. The manifest keeps its fields (schema version, repository id, generation, database path, size and SHA-256, commit time, writer id, operation id). Database snapshots stay per generation and operation. Assets stay in the managed content-addressed directories.
- Reader: Refresh Repository reads the manifest. When its generation differs from the Local Working Copy's, it downloads the snapshot to a temporary file, checks size and digest, and swaps it in atomically. An asset fetch downloads with range resume to a temporary file, checks the digest named by the asset path, and moves the file into the media cache. Playback can start from a range stream before the download finishes; only a verified copy is kept. The cache enforces a byte budget with LRU eviction, and pinned songs are exempt.
- Writer (desktop): Publish Changes uploads the assets the remote lacks, uploads the new snapshot at its generation path, then replaces the manifest on the condition that the Remote Revision still matches. Uploads are idempotent. A crash at any step is recovered by running the same publication again from the existing outbox rows; no other journal state is kept.
- Rebase: outbox rows already carry song ids, a whole-repository flag, the expected generation and the source database digest. The writer keeps the base snapshot until publication succeeds. When the conditional replace fails, it downloads the newer snapshot. For each outbox song whose remote rows still equal the base rows, it applies the local rows. Every other song, and every whole-repository row, becomes part of a Pre-Publish Conflict.
- Pre-Mutation Refresh stays as today.
- Cleanup: a desktop-initiated cleanup keeps the snapshots of recent generations and deletes assets that no kept generation references, after a grace period long enough for a phone to finish reading an older generation. Phones never delete remote objects.
- Fault injection is removed from production code. The in-memory storage port scripts failures in tests.
- Phone app: a Tauri 2 iOS and Android shell with the library, player and lyrics parts of the existing React UI, the existing Rust audio engine (cpal, symphonia, rubato), and Repository Credentials in the platform keychain or keystore. The library marks songs available offline, and a setting limits downloads to Wi-Fi. Phones exclude ONNX separation, Streaming Sources, Publish Changes, Publish Song, Mirror Local Library and Delete Repository.
- Delivery order: (1) the core crate with the reader and the WebDAV adapter, used by desktop Refresh Repository and media fetching; (2) the read-only phone app; (3) Publish Changes and Publish Song on the core, with rebase, then deletion of the superseded executor, recovery, control and fault-injection code for WebDAV repositories.
Testing Decisions
- Seam 1, confirmed by the maintainer: the core's public operations (open, Refresh Repository, list and search songs in the Local Working Copy, fetch an asset, pin and evict, Publish Changes, Publish Song, and rebase) over an in-memory storage port that can script failures: a failed precondition, an interrupted transfer, a partial write, a stale listing, and a Remote Revision that moves between steps. Tests assert what a user or another device observes: Local Working Copy contents, cached files, manifest generation, remote objects, events and errors. They do not assert on internal state machines, outbox row layout, or call order.
- Seam 2, confirmed by the maintainer: the shared provider conformance suite from ADR 0018, run against the WebDAV adapter with the in-process WebDAV test server. Any future adapter passes the same suite.
- Compatibility: repositories written by the current code are fixtures. The core must refresh, read and publish to them.
- Prior art: the scripted provider in the conformance suite, the fake providers in the manifest, executor and download tests, the fake Remote Provider in the track-loading tests, the WebDAV test server, and the remote schema integration test.
- Core tests run with
cargo teston the core crate alone, without building the Tauri application. - Tests that pin superseded internals are deleted with that code.
- The phone shell has no automated UI tests in this spec. Device acceptance per release covers open, refresh, browse, search, play, stems with vocal volume, lyrics, offline pins, eviction, Reauthorize Repository and Disconnect Repository.
Out of Scope
- Editing or publishing anything from the phone.
- Streaming Sources and stem separation on the phone. #589 keeps phone builds free of Streaming Sources.
- Porting Dropbox or Google Drive to the core.
- A new repository format or a migration.
- App Store and Play Store submission.
- CDG playback on the phone.
- LAN or peer-to-peer transfer between devices.
Further Notes
- Status: design specification. The maintainer confirmed both testing seams on 2026-10-26. No implementation has started.
- ADRs: this spec keeps ADR 0011 (database first, files lazy, revision-tracked, no silent overwrite) and ADR 0018 (ownership areas, one provider port, shared conformance tests, existing data stays compatible). It changes the Pre-Publish Conflict relationship in CONTEXT.md: publication now rebases song-scoped changes and stops only for songs changed on both sides or for whole-repository changes. That change needs an ADR. Removing fault injection from production code changes how ADR 0018's restart-recovery behavior is tested, not the behavior.
- Glossary gaps: a phone's read-only Local Working Copy, and pinning songs for offline use.
- Why WebDAV first: it is reachable in mainland China and everywhere else, the current adapter already performs conditional replace with If-Match, and Google Drive cannot publish today.
- Size target: the core plus the WebDAV adapter should be a fraction of today's ~27,000 lines; a rough target is ~5,000.