diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-09-25 09:23:02 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-09-25 09:23:02 +0000 |
| commit | f04a01c672c01f4913d4b9bbaf2fc4febbaec08c (patch) | |
| tree | 5ac86711fc5101896e58c70aca78177d3badcffc /CLAUDE.md | |
| parent | d9585c40e7f3b6de8b5f0c37e27edec49d565aa8 (diff) | |
Auto-queue rules: decide what happens to an episode as it arrives
The first of Phase 2's three tracks. A rule is a set of conditions on a newly
discovered episode -- show, title contains/excludes, duration range, age, season
-- and an action: queue at the top or bottom, mark played, and optionally
download it or pin it to a speed. Rules are ordered, individually switchable,
and the first one that matches claims the episode.
Every claimed episode records which rule fired and the account that rule gave,
shown on the episode screen as `Handled by "News to the queue" - title contains
"news"`. That is not decoration. Automation that fills a queue and cannot say
why is the thing that makes people stop trusting it, so the reason string is
tested as hard as the matching is.
Three decisions worth keeping:
Rules never fire on a first subscribe. That import discovers the entire back
catalogue -- 2,753 episodes on a real feed -- and a catch-all rule would enqueue
all of it. `applyEpisodes` takes a `report` flag and `subscribe` passes false.
Verified live: subscribing to NPR News Now stored four episodes and queued none;
deleting the newest and refreshing queued exactly that one, with its reason
recorded.
A rule's speed lands on the episode, not the show. Writing it to
`SubscriptionEntity.playbackSpeed` would let one matching episode silently reset
the speed of everything else the show publishes.
Auto-downloads go through a new DownloadWorker rather than the refresh. A
refresh should finish in seconds whatever the rules decided; a worker that spent
twenty minutes pulling audio would be killed mid-flight with the feed state half
written. The drainer caps a batch at ten and leaves a failure FAILED rather than
retrying forever against a 404.
Schema v1 -> v2, this app's first migration, with MIGRATION_1_2 validated on a
device against the exported v1 schema and a seeded playback position read back
afterwards. Two pieces of plumbing were needed and both fail obscurely: the
schemas directory wired into androidTest assets, and kotlinx-serialization
raised to 1.8.1 because Room 2.8's bundle parser calls a method 1.7.3 lacks and
dies with AbstractMethodError at runtime. Both are written up in CLAUDE.md,
along with the trap that cost the most time -- changing an entity before bumping
the version rewrites the exported schema for the old version, destroying the
only record of what is on the phone.
No permission changes. 298 unit tests green, detekt and lint clean, coverage
61.60 -> 63.63.
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 | 57 |
1 files changed, 47 insertions, 10 deletions
@@ -63,6 +63,13 @@ and a logcat capture started before launch: app/build/outputs/apk/debug/app-debug.apk listen-debug.apk ``` +Migrations only prove themselves on a device. Every version bump runs: + +``` +/workspace/devtools/bin/gradle-verify /workspace/listen connectedDebugAndroidTest -- \ + -Pandroid.testInstrumentationRunnerArguments.class=org.terst.listen.data.MigrationTest +``` + **Never read `app/build/reports/detekt/detekt.xml` directly.** An invalid config aborts detekt before it analyses anything and leaves the previous run's report in place; it was read as current findings three times. `gradle-report` refuses a report older than the run. @@ -94,19 +101,24 @@ claims three separate times across these repos. queue, play, variable speed, sleep timer, retention, unsubscribe, Android Auto with a browsable library and voice search. 207 unit tests plus instrumented tests covering the media browse tree. -**Phase 2, not started:** the auto-queue rule engine, slow-drip subscriptions, and intro/outro -skipping. `skipIntroMs`/`skipOutroMs` exist on `SubscriptionEntity` and nothing reads them. +**Phase 2 is in progress**, in three tracks. Peter deferred the audio-fingerprint skip detection +(the design plan's Tier 3); the simple skip work comes last. + +- **Track 1, auto-queue rules — done.** Named, ordered, enable-able rules with conditions on show, + title, duration, age and season, and actions to queue top/bottom, mark played, auto-download, or + pin a speed. `RuleEngine` decides, `RuleApplier` carries it out, `RuleSummary` describes a rule + in one line, and every claimed episode records which rule fired and why. +- **Track 2, slow-drip archives — not started.** Includes walking RFC 5005 `prev-archive` pages; + `ParsedFeed.previousArchiveUrl` is already parsed and tested and nothing consumes it. +- **Track 3, simple skip — not started.** Manual per-show offsets (`skipIntroMs`/`skipOutroMs` + exist on `SubscriptionEntity` and nothing reads them) and Podcasting 2.0 chapter JSON via + `EpisodeEntity.chaptersUrl`, which the parser already captures. **Known gaps in what is built** (deliberate, not forgotten): -- **No artwork anywhere.** The parser captures `imageUrl` at channel and item level and the - media session passes it to the lock screen, but no screen renders it. Needs an image-loading - dependency (Coil) and caching. -- **No player screen.** There is a transport bar only: play/pause, skip, title. No scrubbing, - no per-show speed (the field exists on `SubscriptionEntity`, only the global is reachable). -- **No queue reordering.** `QueueOrdering.move` and `QueueRepository.move` are written and - tested with no UI, which the reachability guard tolerates because they are on a class rather - than an object. +- **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. @@ -178,6 +190,31 @@ skipping. `skipIntroMs`/`skipOutroMs` exist on `SubscriptionEntity` and nothing available and they take the slots, pushing skip-forward off the screen. They are removed in `onConnect` so the seek buttons get the room. +- **Bump `version` before you change an entity, not after.** Room rewrites the exported schema + for whatever version is declared, so editing `EpisodeEntity` while the database still says + `version = 1` silently overwrites `schemas/1.json` with the new shape. That file is the only + record of what is on the phone, and once it matches the new code the migration test validates + happily against a schema that never existed. It happened building the rules table; the fix was + `git checkout -- app/schemas/`. + +- **The migration test needs two pieces of plumbing that fail obscurely.** `MigrationTestHelper` + reads the exported schemas from the instrumented test APK's assets, so `schemas/` is wired into + `androidTest`'s asset dirs in `build.gradle.kts`. And Room 2.8's schema-bundle parser calls a + kotlinx-serialization method 1.7.3 does not have -- the version `lifecycle-viewmodel-savedstate` + drags in -- which surfaces as `AbstractMethodError` on `GeneratedSerializer` at runtime, not as + a build failure. The app's own serialization dependency is pinned at 1.8.1 to raise the floor; + AGP resolves the androidTest classpath consistently with the app's, so pinning it only in + `androidTestImplementation` does nothing. + +- **Rules must never fire on a first subscribe.** `SubscriptionRepository.applyEpisodes` takes a + `report` flag for exactly this: the first import discovers the entire back catalogue -- 2,753 + episodes on a real feed -- and a catch-all rule would enqueue all of it. Verified on a live NPR + feed: subscribing stored four episodes and queued none. + +- **`android-ui dump` does not list every rendered Text.** The rule attribution line on the + episode screen is absent from the dump and plainly visible in a screenshot. Treat a missing + line in a dump as inconclusive, not as absence. + - **XML comments cannot contain `--`.** The house prose style uses it constantly and it makes the manifest unparseable, with a merge error that doesn't name the line. |
