diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-09-28 07:17:39 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-09-28 07:17:39 +0000 |
| commit | b3620e5f60ed8b590286dcf8a2b27919973d8743 (patch) | |
| tree | 21a09a55a1fc9669580b5018ee3c63d88d894f46 /CLAUDE.md | |
| parent | 850479ac3ffbff353c12faadd76f9ae840575136 (diff) | |
Bedtime shows, dimmed caught-up shows, a show menu, and skip visibility
Four things Peter asked for, plus a crash found while verifying them.
- Bedtime shows. A per-show flag (schema 8->9) that takes a show out of the
library entirely; the moon icon in the top bar switches to listing only
those. Not a category system -- the one grouping worth building is the one
with behaviour attached, which is that entering bedtime mode also arms the
sleep timer. BedtimePolicy owns the two cases that make that safe: it will
not overwrite a timer set by hand, and leaving only cancels a timer bedtime
itself armed. The mode is deliberately not persisted; a restart is morning.
- Shows with nothing waiting are dimmed. Same condition the library sort
already used -- nothing unplayed and unqueued -- now returned alongside it
as SubscriptionListing.hasWaiting. That comes from a COUNT rather than the
sort key on purpose: an episode with no publish date ranks at 0, which is
right for ordering and would have greyed out a show with one sitting
unheard. LibraryOrderTest pins that case. The lastError line keeps its
error colour through the dimming, since a failing feed looks exactly like
a feed with nothing new.
- Per-show controls moved into an overflow menu and a settings sheet, and
the tutorial prose is gone. Reaching the episodes used to mean scrolling
past drip cadence and two paragraphs; Peter wrote the requirements and
does not need them explained back. Unsubscribe went into the same menu,
off the top bar where it sat one tap from Refresh.
- Skip detection is visible at last, per show and per episode. SkipSummary
is pure and its wording is tested, because the distinction that matters is
confirmed-and-acting versus still-gathering, and "not checked yet" is a
third state again -- midroll scanning runs nightly, so a fresh download
legitimately has nothing to say yet.
And the crash. Opening the new readout on a device threw
SQLiteBlobTooBigException, which turned out not to be the readout's fault:
show_fingerprints.pendingClipPcm held a whole decoded head/tail window --
240s at 8kHz is 3.84MB as 16-bit PCM -- and Android reads every Room row
through a CursorWindow that refuses anything over 2MB *on the way out*. The
insert succeeded and the next read threw. Intro/outro detection therefore
died the moment a show's second episode was fingerprinted and could never
have worked as shipped; nothing off-device could have caught it.
Migration 9->10 drops the column and keeps only pendingEpisodeId, re-decoding
on demand -- exactly what midrollPendingEpisodeId already did for the same
reason. StoredClip now owns the limit, MidrollUpdater caps a candidate
segment at 60s before persisting it (a long sponsor block would hit the same
wall) and refuses anything that still would not fit, and StoredClipSizeTest
pins both against the limit so the class cannot come back quietly. Trimming
under-skips a very long break rather than over-skipping into content, which
is the safe direction.
Verified on the emulator: 10 migration tests including the never-before-run
7->8, LibraryOrderTest, and the whole reported flow by hand -- bedtime split,
timer arming and cancelling, the dimmed row, both readouts. Re-ran the exact
sequence that crashed: three downloads, no crash, FingerprintWorker SUCCESS,
and the MP3 decoder visibly running, which is the first time
MediaCodecClipSource has executed on hardware at all.
Still open, and recorded in CLAUDE.md rather than glossed: detection has not
actually confirmed an intro or outro for a real show. Three Up First
downloads left both ends "Nothing found yet". Possibly correct -- the
threshold is strict by design -- or possibly the 8-second reference window
against a roughly 6-second sting. That wants the desktop prototype, not a
guess at a tuned constant.
Coverage 91.76% -> 91.84%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'CLAUDE.md')
| -rw-r--r-- | CLAUDE.md | 68 |
1 files changed, 59 insertions, 9 deletions
@@ -147,7 +147,25 @@ prototyped against real episodes before any Android code, per the design plan's that, and unlike a download it never touches the network. No per-show toggle, same as intro/outro -- the confirmation threshold is the safety mechanism. -Neither has been verified on a device yet -- see the known-gaps list below. +**Both halves have now run on a device**, and doing so found a blocking defect the unit tests +could never have caught. `show_fingerprints.pendingClipPcm` held a whole decoded head/tail +window -- 240 seconds at 8kHz is 3.84MB as 16-bit PCM -- and Android reads every Room row +through a CursorWindow, which refuses a row over 2MB *on the way out*. The insert succeeded; +the next read threw `SQLiteBlobTooBigException`. Intro/outro detection therefore crashed the +app the moment a show's second episode was fingerprinted, and could never have worked as +shipped. Fixed in migration 9 -> 10 by storing only `pendingEpisodeId` and re-decoding on +demand -- the pattern `midrollPendingEpisodeId` already used for the same reason. + +`StoredClip` now owns that limit, `MidrollUpdater` caps a candidate segment at 60 seconds +before persisting it (a long sponsor block would otherwise hit the same wall), and +`StoredClipSizeTest` pins both against the limit so the class of bug cannot come back quietly. + +What is proven on hardware: the MediaCodec decode loop runs, `FingerprintWorker` completes +SUCCESS per download, and the re-decode path is exercised. What is **not** yet proven is that +detection actually confirms anything -- three downloaded Up First episodes left both ends still +"Nothing found yet". That may be correct (the confirmation threshold is deliberately strict) or +it may mean the 8-second reference window is too wide for a ~6-second sting; the desktop +prototype is the thing to compare against, and nobody has. **ID3 `CHAP` frames are dropped, not deferred** — decided 2026-09-26 by measuring Peter's actual 61 subscriptions rather than guessing. Every feed's newest episode was fetched far enough to read @@ -176,14 +194,19 @@ by default and correct; it is simply waiting for a publisher who marks breaks. - **Auto-downloads are not retried.** A rule's download that fails is left FAILED rather than requeued: retrying forever against a 404 would burn the battery, and the failed row is visible in the episode list. Retry is the manual Download button. -- **Android Auto is unverified in a real car.** The session protocol is proven by - `MediaBrowseTest`; whether Auto's own compatibility checks pass on a head unit is untested, - and a sideloaded media app may need Auto's developer mode enabled. -- **Neither half of Tier 3 fingerprinting has run on a device.** The decode/decimate/correlate - pipeline is proven against synthetic fixtures and, separately, against real downloaded episodes - in a throwaway desktop prototype -- never against `MediaCodecClipSource`'s actual - MediaExtractor/MediaCodec decode loop on real hardware, and the `MigrationTest` 7→8 case has - only run on the JVM-backed `MigrationTestHelper`, not an emulator or device. +- **Tier 3 has never confirmed a real intro or outro.** The pipeline runs on hardware and the + worker succeeds, but three consecutive Up First downloads produced no candidate at either + end. Untested hypothesis: `FingerprintUpdater.DEFAULT_REFERENCE_SECONDS` is 8 and the + prototype measured Up First's sting at about 6, so every 8-second window compared also + contains two seconds of different speech. Do not change that constant without re-running the + desktop prototype -- it is tuned, and the confirmation threshold is what keeps a rotating ad + from being learned as an intro. + +- **Android Auto is verified in a real car**, as of 2026-09-28. It appears, and connects. The + notification Peter saw on first connect was Android Auto's own new-media-app announcement, + not Listen: the only notification this app can raise is the Media3 transport control, which + a foreground media service is required to post and which is on a low-importance channel. + What is still untested in a car is browsing and voice search. ## Domain notes worth keeping @@ -272,6 +295,33 @@ by default and correct; it is simply waiting for a publisher who marks breaks. come from. Only then: a title that was shared is the one that was meant, and the probe costs a network round trip. +- **Per-show controls live behind the show's overflow menu**, in a bottom sheet, not stacked + above the episode list where they used to be -- reaching the episodes meant scrolling past + drip cadence and two paragraphs of explanation. The explanation went too, at Peter's request: + this is a single-user app and he wrote the requirements, so labels are one line and nothing + is taught. Unsubscribe moved into the same menu, off the top bar where it sat one tap from + Refresh. + +- **Bedtime is a flag on a show, not a category system.** A bedtime show is absent from the + ordinary library entirely and listed only in bedtime mode, which the moon icon toggles. That + mode is deliberately *not* persisted -- waking to an app still in bedtime mode, library + apparently empty, is the failure being avoided; a restart is morning. Entering it also arms + the sleep timer, and `BedtimePolicy` owns the two cases that make that safe: it will not + overwrite a timer set by hand, and leaving only cancels a timer bedtime itself armed. + +- **A show with nothing waiting is dimmed, not hidden.** Same condition the library sort + already used -- nothing unplayed *and* unqueued. `hasWaiting` comes from a COUNT, not from + the sort key: an episode with no publish date ranks at 0, which is right for ordering and + would wrongly grey out a show that has one sitting unheard. The `lastError` line keeps its + error colour through the dimming, because a feed failing to refresh looks exactly like a feed + with nothing new. + +- **What the skip detection has worked out is visible**, per show in the settings sheet and per + episode on its detail screen. `SkipSummary` owns the wording and is tested, because the + distinction that matters -- confirmed-and-acting versus still-gathering -- is the whole point, + and "not checked yet" is a third state again (midroll scanning runs nightly, so a fresh + download legitimately has nothing to say). + - **Played episodes are hidden by default**, globally, via a DataStore preference the show screen toggles. Android Auto always hides them regardless: a car list is scrolled blind at a traffic light, and something already heard is pure noise there. |
