feat(train): the Jellyfin token in Train's own store, sealed by the Keystore (ADR 0030, D52)

The token moves from flutter_secure_storage into JellyfinTokens.kt: a
file per person in the no-backup folder, sealed with AES-GCM under a key
of Android's Keystore, the person's ID as associated data, so that the
music job to come can read it as the app does. A token the keyring held
is moved over as it is first read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-29 18:42:46 +01:00
commit 6bde74f0ba
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1310,6 +1310,44 @@ Strava. What was found, 27 Oct:
ride starts with), and GPS's speed is ignored while the wheel sent within 5 s; a
ride without a circumference (no speed sensor) pauses by GPS as before. Not tried
with a real sensor or Health Connect.
- **D52 Music kept in the background, and smaller copies** (recommended, the
developer's; 29 Oct; step 14, what D47's 10b left open):
- **Downloads by a WorkManager job of Train's, in Kotlin, on the app's list** (14b;
over the job also reading the marked playlists, which writes part of the Jellyfin
client a second time; over the workmanager plugin running today's Dart in a
background engine, a new plugin and a whole engine for each run; over Android's
DownloadManager, which keeps the token in the system's download database): the
app, as it opens and resumes, reads the playlists as before, deletes what they no
longer hold and writes which tracks are missing; the job downloads those and works
out their tempo, a line each in the person's music folder, which the app takes in
as it opens and, while open, as each line comes. A playlist changed on the server
while the app is closed is noticed as it opens. The job does every download, the
open app's too, so that one code path runs and two never fetch the same track.
- **The token in one store of Train's, in Kotlin** (14a; over a second copy beside
flutter_secure_storage's, two to keep in step, and over the job reading that
plugin's own files, whose format has changed between versions): a file per person
in the no-backup folder, sealed with AES-GCM under a key in Android's Keystore
that can't be exported; the app reads it over the music channel; a token kept
before is moved over once and deleted from the keyring.
- **When**: every 6 hours and once whenever the list changes, on an unmetered network,
with the battery and the storage not low (over daily while charging, which leaves
a new playlist waiting overnight, and over every 12 hours).
- **Smaller copies** (14c): the music screen chooses *As they are* or *Smaller*, the
default *Smaller*: files above 256 kbps (FLAC and other lossless) are transcoded by
Jellyfin to Opus at 128 kbps in WebM, which Android's extractor and decoder read
on every version Train runs on; MP3s and AACs come as they are (over *As they are*
by default, and over always transcoding, which loses an MP3's quality twice and
has the server's processor do every track). Switching downloads the tracks again;
a file is kept until its new copy is whole, its tempo kept.
- Built: 14a. `JellyfinTokens.kt` keeps each person's token in the no-backup
folder's `jellyfin/`, a file named by their ID: a version byte, the IV the
Keystore makes and the token sealed with AES-GCM under `train_jellyfin`, a
256-bit key of Android's Keystore made once, the person's ID its associated data;
a file that doesn't open is deleted and counts as none, so the person signs in
again. Tested on the JVM with a software key. `AndroidJellyfinTokenStore` asks it
over the music channel and moves a token the keyring holds under
`train_jellyfin:` over as it is first read; keeping one, and forgetting others',
clears the keyring's too.
## Consequences