summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md51
1 files changed, 43 insertions, 8 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index b32083e..df5b251 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -168,14 +168,17 @@ clip files on disk, and unsubscribing is the only thing that clears them.
columns the on-device detector used to, which is why playback needed no changes when detection
moved. No migration, no schema change, nothing for a real library to go wrong over.
-Two things the deletion left standing that a future session should look at rather than assume:
-
-- **The per-show detection readout has nothing feeding it.** `ShowSettingsViewModel.loadDetection`
- reads `ShowFingerprintDao.get` and `MidrollDao.getCandidates`, and nothing has written a
- *candidate* row since detection moved: the server writes per-episode ranges and offsets, not the
- confirmation state the readout describes. So "2 of 3 episodes agree" can never appear, and a
- phone that never ran the old version shows "nothing found yet" for every show while the server
- is finding and publishing breaks. Predates this deletion; it is the same migration's doing.
+Two things the deletion left standing. The first is fixed; the second is not:
+
+- **The per-show detection readout had nothing feeding it — fixed 2026-11-05.** It read
+ `ShowFingerprintDao.get` and `MidrollDao.getCandidates`, the confirmation state the *on-device*
+ detector kept, and nothing had written a candidate row since detection moved. So every show
+ reported "Nothing found yet" for ever, including shows the server was actively publishing breaks
+ for — seen on Peter's phone. It is counted off the episodes now (`detectedIntroMs`,
+ `detectedOutroMs`, and the midroll ranges `ServerSkips` writes), worded by `SkipSummary.showState`,
+ and it keeps the distinction that was the point: *not checked* and *checked, nothing found* are
+ different answers. `show_midrolls` is consequently an entity nothing reads or writes; the table
+ stays because dropping it needs a migration and nothing is gained by one.
- **`RetentionSweeper`'s pending-episode guard protects nothing.** It reads
`getAllPendingEpisodeIds` and `getAllMidrollPendingEpisodeIds` so retention cannot reclaim a file
mid-detection. Nothing sets a pending basis any more, so both are always empty — harmless, but a
@@ -553,6 +556,38 @@ by default and correct; it is simply waiting for a publisher who marks breaks.
## Things a future session should know
+- **Nothing the listener reads is written for a machine, and the build enforces it**
+ (`PlainLanguageTest`, added 2026-11-05). A red banner on Peter's phone read "Playback failed:
+ ERROR_CODE_IO_BAD_HTTP_STATUS"; his words were "arcane and sketchy" and both halves were fair.
+ The same pass found a downloader printing `${it.message}`, an OPML import printing
+ `permission denied`, and `FeedFetcher` putting the literal word `UnknownHostException` under a
+ podcast's name in the library — **and two tests that asserted the machine text came through**,
+ which is how it survived review. Causes go to logcat; the screen gets a sentence, from
+ `PlaybackFailureText`, `FeedFailureText` or `FailureText`. Accuracy is the harder half of the
+ rule: "it will be retried" would read better than "try the download button again" and would be
+ a lie, because a failed download is deliberately not retried.
+
+- **Every outbound request goes through one client, and the build enforces that too**
+ (`OutboundRequestsTest`). One `OkHttpClient`, built in `AppGraph`, carrying
+ `SecureSchemeInterceptor`; everything else is handed it, Coil's artwork fetches included. The
+ same arrangement the server has in `safefetch/audit_test.go`, for the same reason: this app
+ fetches URLs chosen by podcast publishers.
+
+- **Playback plays the app's own copy, never the publisher's** (changed 2026-11-05).
+ `toMediaItem` falls back to the enclosure URL when there is no local file, and that fallback was
+ reachable from every play: pressing play on an undownloaded episode streamed it straight from
+ the publisher's CDN. That is the automatic fallback the download path had removed on purpose; it
+ is an unasked-for request to a third party; and because nothing in playback checks
+ `audioFromServer`, the breaks the server measured on *its* copy were being applied to a copy with
+ different advertising in different places. `PlaybackAdvancer.start` now returns `NeedsFetch` for
+ any episode with no file, so the app fetches first.
+
+ **One caller still reaches the fallback: Android Auto.** `onAddMediaItems` resolves a browse id
+ straight to a media item without going through the advancer, and a car that silently plays
+ nothing is worse than a car that streams. Fixing it properly means fetching inside the session
+ callback. `OutboundRequestsTest` pins the fallback to exactly one line so it cannot spread back.
+
+
- **The episode screen is the player; there is no separate player screen** (merged 2026-10-19).
Opening an episode puts you in its transport at the stored position, and pressing play changes
nothing about the layout. Playback state still has one owner, `PlayerViewModel`; the screen