From b1d8647f5dc655faffd8b95a7a96b78c83eeffea Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 14 Oct 2026 08:21:59 +0000 Subject: Give a stored break its kind Existing rows become advertising, which is what they behaved as: every row in this table was skipped unconditionally before kinds existed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- .../java/org/terst/listen/data/MigrationTest.kt | 40 ++++++++++++++++++++++ .../main/java/org/terst/listen/data/AppDatabase.kt | 17 +++++++-- .../main/java/org/terst/listen/data/Entities.kt | 8 +++++ 3 files changed, 63 insertions(+), 2 deletions(-) (limited to 'app/src') diff --git a/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt b/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt index 98f5ad1..fa070de 100644 --- a/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt +++ b/app/src/androidTest/java/org/terst/listen/data/MigrationTest.kt @@ -517,4 +517,44 @@ class MigrationTest { assertEquals("the crown jewel must survive", 616161L, cursor.getLong(2)) } } + + /** + * Existing rows were written before anything knew what kind of break they were, and the only + * safe assumption is the one that matches old behaviour: everything was skipped, so call it + * advertising. The server overwrites each episode's ranges on its next download anyway. + */ + @Test + fun migrate12To13KeepsExistingBreaksAndCallsThemAdvertising() { + helper.createDatabase(name, 12).apply { + execSQL( + """INSERT INTO subscriptions + (id, feedUrl, kind, title, addedAtMs, skipIntroMs, skipOutroMs, dripEnabled, + dripIntervalMs, dripCount, dripOldestFirst, skipAdChapters, bedtime) + VALUES ('s1', 'https://example.com/feed.xml', 'PODCAST', 'A Show', 1000, 0, 0, + 0, 86400000, 1, 1, 1, 0)""" + ) + execSQL( + """INSERT INTO episodes + (id, subscriptionId, guid, title, mediaUrl, mediaType, discoveredAtMs, + positionMs, downloadState) + VALUES ('e1', 's1', 'g1', 'One', 'https://cdn/1.mp3', 'audio/mpeg', 1000, + 0, 'NONE')""" + ) + execSQL( + "INSERT INTO episode_midrolls (episodeId, candidateId, startMs, endMs) " + + "VALUES ('e1', 'server-0', 1000, 2000)" + ) + close() + } + + val db = helper.runMigrationsAndValidate(name, 13, true, AppDatabase.MIGRATION_12_13) + + db.query("SELECT startMs, endMs, kind FROM episode_midrolls WHERE episodeId = 'e1'").use { + assertEquals("the break was lost", 1, it.count) + it.moveToFirst() + assertEquals(1000L, it.getLong(0)) + assertEquals(2000L, it.getLong(1)) + assertEquals("ad", it.getString(2)) + } + } } diff --git a/app/src/main/java/org/terst/listen/data/AppDatabase.kt b/app/src/main/java/org/terst/listen/data/AppDatabase.kt index 443a23d..6bc7df4 100644 --- a/app/src/main/java/org/terst/listen/data/AppDatabase.kt +++ b/app/src/main/java/org/terst/listen/data/AppDatabase.kt @@ -30,7 +30,7 @@ class Converters { ShowMidrollEntity::class, EpisodeMidrollEntity::class, ], - version = 12, + version = 13, exportSchema = true, ) @TypeConverters(Converters::class) @@ -346,6 +346,19 @@ abstract class AppDatabase : RoomDatabase() { } } + /** + * Breaks gain a kind, so advertising and a show's own intro, break cue and credits can + * be skipped on different terms. + * + * Existing rows become "ad" because that is what they behaved as: everything in this + * table was skipped unconditionally before the kind existed. + */ + val MIGRATION_12_13 = object : Migration(12, 13) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `episode_midrolls` ADD COLUMN `kind` TEXT NOT NULL DEFAULT 'ad'") + } + } + fun get(context: Context): AppDatabase = instance ?: synchronized(this) { instance ?: build(context.applicationContext).also { instance = it } } @@ -357,7 +370,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, - MIGRATION_10_11, MIGRATION_11_12, + MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13, ) .addCallback(SEED_DEFAULT_FILTERS) // No fallbackToDestructiveMigration. A listener's playback positions and play diff --git a/app/src/main/java/org/terst/listen/data/Entities.kt b/app/src/main/java/org/terst/listen/data/Entities.kt index b14ed2b..7480101 100644 --- a/app/src/main/java/org/terst/listen/data/Entities.kt +++ b/app/src/main/java/org/terst/listen/data/Entities.kt @@ -441,6 +441,14 @@ data class EpisodeMidrollEntity( val candidateId: String, val startMs: Long, val endMs: Long, + /** + * "ad" for inserted advertising, or "intro", "cue" or "outro" for the show's own audio. + * + * Defaulted to "ad" rather than left null: a row written before kinds existed was skipped + * unconditionally, and advertising is the kind that is still skipped unconditionally, so the + * default preserves exactly what those rows already did. + */ + @ColumnInfo(defaultValue = "ad") val kind: String = "ad", ) /** -- cgit v1.2.3