feat(core,stream,meta,app,android,cli): sign in to Proton and sync watch history

A Proton account is now optional alongside public links. Signing in (password,
then a second-factor code and a mailbox password where the account has them;
a CAPTCHA gate is solved in the Android app) stores one session blob in the OS
credential store under `proton-account`, writing back every refreshed token
since refresh tokens are single-use. With an account, folders of the viewer's
own Drive — My Files, other devices, shared with me — can be added as shares
through the same `ShareClient` surface as a public link.

Watch history syncs through a `proton-stream` device in that Drive: one
`watch-history/<installation>.json` per installation, so no two devices ever
write the same file. Each sync reads the others and merges per episode, newer
`updated_at` wins, with a deterministic tie-break. It runs at launch, after
sign-in, whenever the player closes and every five minutes. A session that can
no longer refresh signs the app out instead of failing on every launch.

The CLI gains `login`, `logout`, `account`, `browse` and `sync`.

Also in this change:
- Android offline downloads no longer look stuck (B64): two transfer at once,
  through a copy stream with no read-ahead, with a smoothed transfer rate.
- Choosing a metadata match by hand now enriches the title too, instead of
  keeping the old match's episodes and artwork.

The catalog schema is unchanged.
This commit is contained in:
nvrl committed 2026-10-06 13:06:54 +02:00
1 parent 6cdab2fde1
commit 0b2813cd42
37 files changed
+4111 -159

No files matched your search

+11 -1
View File
@@ -104,6 +104,13 @@ an edge case.
holds only the id, display name and token.
- Nothing this app holds can write to the share: a viewer link grants
`MemberRole::Viewer`, and the SDK's public-link write side is unported anyway.
- Signing in to a Proton account is optional (`pstr-core::account`). The
session — tokens, the mailbox password that unlocks the key chain, and the
key salts a refreshed token can no longer fetch — is one blob in the same
credential store, under `proton-account`. Refresh tokens are single-use, so
every refresh is written back. The account writes exactly one thing: the
watch-history files on its own `proton-stream` device (`pstr-core::sync`).
Folders of the account added as shares are only ever read.
- Signature verification is absent on the visitor path by design — an anonymous
visitor cannot read `core/v4/keys/all` to resolve a signer. Decryption still
proves the bytes were encrypted to the node key. Nodes come back with nothing
@@ -118,7 +125,10 @@ an edge case.
| Share list | config dir, `shares.json` | **No** — atomic writes, never overwritten when unparseable |
| Share secrets | OS credential store | **No** |
| Catalog | data dir, `catalog.db` | Yes — recrawl |
| Watch state | same DB, own table | **No** — deliberately survives a recrawl |
| Watch state | same DB, own table | **No** — deliberately survives a recrawl; 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 |
| 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 |