feat(core,app,android,cli,ios): sync the library and settings, verify a share before adding it

Sync carried only watch positions, so a second device started empty. The
per-installation history file now also carries per-key registers
(pstr-core::profile): every share with its link and custom password, the
playback, appearance and metadata settings, the TMDB key, hand-picked
matches and per-title track choices. Newer updated_at wins per key, a
removal travels as an empty register, and a first sync stamps at 0 so a
fresh install takes the library the other devices have instead of
overwriting it with defaults. Volume, mute and the device-only settings
(cache budget, Wi-Fi only, background audio, hardware decoding) stay out.
The secrets live only in the end-to-end encrypted Drive file, never in
shares.json. FORMAT_VERSION stays 1; an older build ignores the registers.

Adding a share stored it before opening the link, so a first failure left a
broken entry and every retry said "already configured". add_verified opens
the link first and stores nothing on failure, and only the new share is
crawled afterwards, so another broken share cannot fail the add.

Android drops picture-in-picture: the window sized the video wrongly and
background audio already covers leaving the app.

iOS sizes the Metal drawable on every layout, so the picture no longer
stretches after a rotation or a Split View resize. Settings show a save at
once rather than after the match run, and a failed save reverts and says
why. Both phone clients gain an activity banner naming what is running, and
the add-share, metadata and change-match forms show progress and errors in
place.
This commit is contained in:
nvrl committed 2026-10-09 03:15:39 +02:00
1 parent dc70cafbb7
commit 8e42906a7f
37 files changed
+2018 -594

No files matched your search

+5 -5
View File
@@ -55,15 +55,15 @@ installs the signed minified APK and runs instrumentation against it, which is
the only path that exercises R8 — the step that has broken this app five distinct
ways that a debug build cannot reproduce. Without the `ANDROID_RELEASE_*`
variables below, drop `--release` and the debug APK is used instead. The matrix — phone and tablet layouts, rotation,
process recreation, playback and background controls, Picture-in-Picture,
process recreation, playback and background controls,
download cancellation/resume, and an offline launch with networking disabled —
is `scripts/android-acceptance.sh`; `--list` prints every case and what it
needs, and `PSTR_ACCEPTANCE_ONLY=name` runs one. Five cases need a real share,
given as `PSTR_ACCEPTANCE_SHARE_URL` and `PSTR_ACCEPTANCE_SHARE_PASSWORD`; they
reach the device as instrumentation arguments and are stored nowhere. Without
them those cases report `skip`. `picture-in-picture` is still `pending`, so the
run tells you the matrix is incomplete — but no longer because of B40, which is
fixed; the case now just needs writing. `docs/TESTING.md` has the layer-by-layer
them those cases report `skip`. There is no Picture-in-Picture: it was removed
after a PiP window that never sized its video right proved less use than
background audio and the lock-screen transport. `docs/TESTING.md` has the layer-by-layer
detail, including why lint runs against a baseline.
Requires JDK 17. `build-android.sh` locates it, because a newer JVM does not
@@ -110,6 +110,6 @@ ABIs, plus a source archive and revision record, under
packages the staged libraries, and compiles `pstr_mpv`, the JNI/EGL adapter. It
feeds libmpv through the Rust stream C ABI; decrypted bytes remain in native
memory. `PlaybackService` owns the mpv core for background audio and media
controls, while the activity supplies the current `Surface` and enters PiP.
controls, while the activity supplies the current `Surface`.
Do not publish an Android binary until this exact pipeline is verified on real
arm64 hardware and an x86_64 emulator.
+14 -1
View File
@@ -118,6 +118,18 @@ an edge case.
including trashing the file of an installation quiet for 90 days once its
history is merged into this one's. Folders of the account added as shares
are only ever read.
- The same file carries the rest of what makes every device look alike
(`pstr-core::profile`): the shares, with each link and custom password so
another device can open them; the playback, appearance and metadata settings
except volume and mute; the TMDB key; hand-picked matches; per-show track
choices. Each is a register merged per key, newer wins, a removed share being
a register with no value. `sync-state.json` holds the registers as last
synced, and a sync stamps whatever differs from it, so nothing that writes a
setting has to know about sync. A first sync stamps what it finds at zero, so
a fresh install's defaults never outrank the library everyone else agreed on.
Automatic matches are not synced: every device with enrichment on finds them
again. The front end crawls what arrived (`uncrawled_shares` covers a sync
that ran in the background) and drops what was removed with its files.
- A CAPTCHA in front of a sign-in is shown in a webview, because the page
reports success by posting a message to its host. On the desktop that is a
child process of the app (`pstr-app::verify`, wry): WebKitGTK on Linux,
@@ -139,7 +151,8 @@ an edge case.
| Watch state | same DB, own table | **No** — deliberately survives a recrawl and removing the share; with an account, mirrored to its Drive |
| Proton session | OS credential store, `proton-account` | No — sign in again |
| Sync identity | config dir, `sync.json` | A new id if missing — the old history file is still read |
| Watch history in Drive | device `proton-stream`, `watch-history/<id>.json` | One file per installation; merged per episode, newer wins. A file unwritten for 90 days is retired by the next installation to sync, after merging it; a live installation whose file was retired writes it again |
| Sync state | config dir, `sync-state.json` | Yes — a missing one is a first sync, which takes everything other devices have |
| Watch history and profile in Drive | device `proton-stream`, `watch-history/<id>.json` | One file per installation; merged per episode and per register, newer wins. Holds share links and passwords, end-to-end encrypted by Drive. A file unwritten for 90 days is retired by the next installation to sync, after merging it; a live installation whose file was retired writes it again |
| Block cache | cache dir, `blocks/` | Yes — must work correctly after deletion |
| Poster thumbnails | cache dir, `thumbs/` | Yes — refetched per file, one small decrypt each; stored scaled to 640 px |
| Provider artwork | cache dir, `posters/` | Yes — refetched by URL; stored scaled to 640 px |
+1 -1
View File
@@ -75,7 +75,7 @@ Each of these is the platform's doing, not a choice to diverge.
boundary and is queued again; it resumes from its `.part` file when the app next
comes forward. "Download on Wi-Fi only" stops transfers on a cellular or
otherwise expensive network rather than holding them back.
- **Watch history syncs when the app leaves the screen**, inside the same grace, in
- **Sync runs when the app leaves the screen**, inside the same grace, in
place of Android's `WatchSyncWorker`, and every five minutes while it is open.
- **Background audio off pauses** the episode when the app leaves the screen, where
Android stops it.
+3 -4
View File
@@ -255,10 +255,9 @@ three dispositions, and the third is the one to read:
| `pending` | specified but not implemented. Never passes, and the summary reports the matrix as incomplete until it is gone |
| `skip` | needs a share that was not configured |
Today `process-recreation` is `xfail` against B35, and `picture-in-picture` is
`pending` against B40 — asserting today's behaviour would encode the bug rather
than catch it. **The matrix is not complete, and the runner says so on every
run.**
Today `process-recreation` is `xfail` against B35 — asserting today's behaviour
would encode the bug rather than catch it. The `picture-in-picture` case went
with Picture-in-Picture itself, so nothing is `pending`.
The five live cases need a share, given as environment variables that reach the
device as instrumentation arguments and are written nowhere: