From f04a01c672c01f4913d4b9bbaf2fc4febbaec08c Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 25 Sep 2026 09:23:02 +0000 Subject: 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 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- app/build.gradle.kts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'app/build.gradle.kts') diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 558e1dc..e525eea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,11 @@ android { arg("room.schemaLocation", "$projectDir/schemas") } + // MigrationTestHelper reads the exported schemas off the device, so they have to ship with + // the instrumented test APK. Without this the migration test fails with a missing-asset + // error that does not name the schema it wanted. + sourceSets.getByName("androidTest").assets.srcDir("$projectDir/schemas") + namespace = "org.terst.listen" compileSdk = 36 @@ -169,7 +174,12 @@ dependencies { implementation("androidx.datastore:datastore-preferences:1.1.7") implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + // 1.8.1 rather than something older because Room 2.8's schema-bundle parser -- the thing + // MigrationTest leans on -- calls a GeneratedSerializer method that 1.7.3 does not have, + // and fails at runtime with an AbstractMethodError rather than at build time. AGP resolves + // the androidTest classpath consistently with the app's, so the floor has to be raised + // here; raising it in androidTestImplementation alone has no effect. + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") // Bridges suspend functions to the Guava ListenableFuture that Media3's browse callbacks // return. kotlinx-coroutines-jdk8's future builder produces a CompletableFuture, which is -- cgit v1.2.3