diff --git a/app/src/androidTest/kotlin/voice/app/misc/MediaAnalyzerTest.kt b/app/src/androidTest/kotlin/voice/app/misc/MediaAnalyzerInstrumentationTest.kt similarity index 84% rename from app/src/androidTest/kotlin/voice/app/misc/MediaAnalyzerTest.kt rename to app/src/androidTest/kotlin/voice/app/misc/MediaAnalyzerInstrumentationTest.kt index f006d1a1e8..5bd72417a9 100644 --- a/app/src/androidTest/kotlin/voice/app/misc/MediaAnalyzerTest.kt +++ b/app/src/androidTest/kotlin/voice/app/misc/MediaAnalyzerInstrumentationTest.kt @@ -1,7 +1,7 @@ package voice.app.misc import androidx.annotation.RawRes -import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -11,17 +11,18 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith +import voice.app.scanner.FFProbeAnalyze import voice.app.scanner.MediaAnalyzer import voice.app.test.R import java.io.File @RunWith(AndroidJUnit4::class) -class MediaAnalyzerTest { +class MediaAnalyzerInstrumentationTest { @get:Rule val temporaryFolder = TemporaryFolder() - private val mediaAnalyzer = MediaAnalyzer(ApplicationProvider.getApplicationContext()) + private val mediaAnalyzer = MediaAnalyzer(FFProbeAnalyze(ApplicationProvider.getApplicationContext())) @Test(timeout = 1000) fun defectFile_noDuration() { @@ -45,7 +46,7 @@ class MediaAnalyzerTest { private fun durationOfResource(@RawRes resource: Int): Long? { val file = resourceToTemporaryFile(resource) return runBlocking { - mediaAnalyzer.analyze(file.toUri())?.duration + mediaAnalyzer.analyze(DocumentFile.fromFile(file))?.duration } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 03f131e34d..adee044d1c 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("voice.library") id("kotlin-parcelize") + id("kotlin-kapt") alias(libs.plugins.kotlin.serialization) alias(libs.plugins.anvil) alias(libs.plugins.ksp) @@ -39,11 +40,13 @@ dependencies { implementation(libs.appCompat) implementation(libs.androidxCore) implementation(libs.serialization.json) + implementation(libs.prefs.core) api(libs.room.runtime) ksp(libs.room.compiler) implementation(libs.dagger.core) + kaptTest(libs.dagger.compiler) testImplementation(libs.room.testing) testImplementation(libs.androidX.test.core) @@ -52,6 +55,7 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.robolectric) testImplementation(libs.truth) + testImplementation(libs.prefs.inMemory) testImplementation(libs.koTest.assert) testImplementation(libs.coroutines.test) } diff --git a/data/schemas/voice.data.repo.internals.AppDb/53.json b/data/schemas/voice.data.repo.internals.AppDb/53.json new file mode 100644 index 0000000000..c94196e37d --- /dev/null +++ b/data/schemas/voice.data.repo.internals.AppDb/53.json @@ -0,0 +1,428 @@ +{ + "formatVersion": 1, + "database": { + "version": 53, + "identityHash": "0108f4d676f29cb092d96c03912c9c86", + "entities": [ + { + "tableName": "bookmark", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file` TEXT NOT NULL, `title` TEXT, `time` INTEGER NOT NULL, `addedAt` TEXT NOT NULL, `setBySleepTimer` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "mediaFile", + "columnName": "file", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "setBySleepTimer", + "columnName": "setBySleepTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file` TEXT NOT NULL, `name` TEXT NOT NULL, `duration` INTEGER NOT NULL, `fileLastModified` INTEGER NOT NULL, `marks` TEXT NOT NULL, `bookId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "file", + "columnName": "file", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileLastModified", + "columnName": "fileLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markData", + "columnName": "marks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_chapters_bookId", + "unique": false, + "columnNames": [ + "bookId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookId` ON `${TABLE_NAME}` (`bookId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookMetaData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `author` TEXT, `name` TEXT NOT NULL, `root` TEXT NOT NULL, `addedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "root", + "columnName": "root", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAtMillis", + "columnName": "addedAtMillis", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "bookSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `currentFile` TEXT NOT NULL, `positionInChapter` INTEGER NOT NULL, `playbackSpeed` REAL NOT NULL, `loudnessGain` INTEGER NOT NULL, `skipSilence` INTEGER NOT NULL, `active` INTEGER NOT NULL, `lastPlayedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentFile", + "columnName": "currentFile", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "positionInChapter", + "columnName": "positionInChapter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackSpeed", + "columnName": "playbackSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "loudnessGain", + "columnName": "loudnessGain", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "skipSilence", + "columnName": "skipSilence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlayedAtMillis", + "columnName": "lastPlayedAtMillis", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chapters2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `fileLastModified` TEXT NOT NULL, `markData` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileLastModified", + "columnName": "fileLastModified", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markData", + "columnName": "markData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "content2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playbackSpeed` REAL NOT NULL, `skipSilence` INTEGER NOT NULL, `isActive` INTEGER NOT NULL, `lastPlayedAt` TEXT NOT NULL, `author` TEXT, `name` TEXT NOT NULL, `addedAt` TEXT NOT NULL, `chapters` TEXT NOT NULL, `currentChapter` TEXT NOT NULL, `positionInChapter` INTEGER NOT NULL, `cover` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playbackSpeed", + "columnName": "playbackSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "skipSilence", + "columnName": "skipSilence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlayedAt", + "columnName": "lastPlayedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapters", + "columnName": "chapters", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentChapter", + "columnName": "currentChapter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "positionInChapter", + "columnName": "positionInChapter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "bookmark2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `chapterId` TEXT NOT NULL, `title` TEXT, `time` INTEGER NOT NULL, `addedAt` TEXT NOT NULL, `setBySleepTimer` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterId", + "columnName": "chapterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "setBySleepTimer", + "columnName": "setBySleepTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0108f4d676f29cb092d96c03912c9c86')" + ] + } +} \ No newline at end of file diff --git a/data/schemas/voice.data.repo.internals.AppDb/54.json b/data/schemas/voice.data.repo.internals.AppDb/54.json new file mode 100644 index 0000000000..e6773966e0 --- /dev/null +++ b/data/schemas/voice.data.repo.internals.AppDb/54.json @@ -0,0 +1,428 @@ +{ + "formatVersion": 1, + "database": { + "version": 54, + "identityHash": "0108f4d676f29cb092d96c03912c9c86", + "entities": [ + { + "tableName": "bookmark", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file` TEXT NOT NULL, `title` TEXT, `time` INTEGER NOT NULL, `addedAt` TEXT NOT NULL, `setBySleepTimer` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "mediaFile", + "columnName": "file", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "setBySleepTimer", + "columnName": "setBySleepTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file` TEXT NOT NULL, `name` TEXT NOT NULL, `duration` INTEGER NOT NULL, `fileLastModified` INTEGER NOT NULL, `marks` TEXT NOT NULL, `bookId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "file", + "columnName": "file", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileLastModified", + "columnName": "fileLastModified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markData", + "columnName": "marks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_chapters_bookId", + "unique": false, + "columnNames": [ + "bookId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookId` ON `${TABLE_NAME}` (`bookId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookMetaData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `author` TEXT, `name` TEXT NOT NULL, `root` TEXT NOT NULL, `addedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "root", + "columnName": "root", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAtMillis", + "columnName": "addedAtMillis", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "bookSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `currentFile` TEXT NOT NULL, `positionInChapter` INTEGER NOT NULL, `playbackSpeed` REAL NOT NULL, `loudnessGain` INTEGER NOT NULL, `skipSilence` INTEGER NOT NULL, `active` INTEGER NOT NULL, `lastPlayedAtMillis` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentFile", + "columnName": "currentFile", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "positionInChapter", + "columnName": "positionInChapter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackSpeed", + "columnName": "playbackSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "loudnessGain", + "columnName": "loudnessGain", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "skipSilence", + "columnName": "skipSilence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlayedAtMillis", + "columnName": "lastPlayedAtMillis", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chapters2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `fileLastModified` TEXT NOT NULL, `markData` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileLastModified", + "columnName": "fileLastModified", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markData", + "columnName": "markData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "content2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playbackSpeed` REAL NOT NULL, `skipSilence` INTEGER NOT NULL, `isActive` INTEGER NOT NULL, `lastPlayedAt` TEXT NOT NULL, `author` TEXT, `name` TEXT NOT NULL, `addedAt` TEXT NOT NULL, `chapters` TEXT NOT NULL, `currentChapter` TEXT NOT NULL, `positionInChapter` INTEGER NOT NULL, `cover` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playbackSpeed", + "columnName": "playbackSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "skipSilence", + "columnName": "skipSilence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlayedAt", + "columnName": "lastPlayedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapters", + "columnName": "chapters", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentChapter", + "columnName": "currentChapter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "positionInChapter", + "columnName": "positionInChapter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "bookmark2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `chapterId` TEXT NOT NULL, `title` TEXT, `time` INTEGER NOT NULL, `addedAt` TEXT NOT NULL, `setBySleepTimer` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterId", + "columnName": "chapterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "setBySleepTimer", + "columnName": "setBySleepTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0108f4d676f29cb092d96c03912c9c86')" + ] + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/voice/data/Chapter.kt b/data/src/main/kotlin/voice/data/Chapter.kt index 7e12e0d90e..b105a44bc5 100644 --- a/data/src/main/kotlin/voice/data/Chapter.kt +++ b/data/src/main/kotlin/voice/data/Chapter.kt @@ -19,16 +19,12 @@ import java.time.Instant data class Chapter( @PrimaryKey val id: Id, - val name: String, + val name: String?, val duration: Long, val fileLastModified: Instant, val markData: List, ) : Comparable { - init { - require(name.isNotEmpty()) - } - @Ignore val chapterMarks: List = if (markData.isEmpty()) { listOf(ChapterMark(name, 0L, duration)) diff --git a/data/src/main/kotlin/voice/data/ChapterMark.kt b/data/src/main/kotlin/voice/data/ChapterMark.kt index 50b4e62b96..3691750bf8 100644 --- a/data/src/main/kotlin/voice/data/ChapterMark.kt +++ b/data/src/main/kotlin/voice/data/ChapterMark.kt @@ -13,7 +13,7 @@ data class MarkData( } data class ChapterMark( - val name: String, + val name: String?, val startMs: Long, val endMs: Long, ) diff --git a/data/src/main/kotlin/voice/data/repo/internals/AppDb.kt b/data/src/main/kotlin/voice/data/repo/internals/AppDb.kt index 1e4f43745b..548efddfde 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/AppDb.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/AppDb.kt @@ -27,7 +27,10 @@ import voice.data.repo.internals.dao.LegacyBookDao Bookmark::class, ], version = AppDb.VERSION, - autoMigrations = [AutoMigration(from = 51, to = 52)], + autoMigrations = [ + AutoMigration(from = 51, to = 52), + AutoMigration(from = 52, to = 53), + ], ) @TypeConverters(Converters::class) abstract class AppDb : RoomDatabase() { @@ -38,7 +41,7 @@ abstract class AppDb : RoomDatabase() { abstract fun legacyBookDao(): LegacyBookDao companion object { - const val VERSION = 52 + const val VERSION = 54 const val DATABASE_NAME = "autoBookDB" } } diff --git a/data/src/main/kotlin/voice/data/repo/internals/PersistenceModule.kt b/data/src/main/kotlin/voice/data/repo/internals/PersistenceModule.kt index 7e380adc65..26e29dfddc 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/PersistenceModule.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/PersistenceModule.kt @@ -11,33 +11,6 @@ import voice.data.repo.internals.dao.BookContentDao import voice.data.repo.internals.dao.BookmarkDao import voice.data.repo.internals.dao.ChapterDao import voice.data.repo.internals.dao.LegacyBookDao -import voice.data.repo.internals.migrations.Migration23to24 -import voice.data.repo.internals.migrations.Migration24to25 -import voice.data.repo.internals.migrations.Migration25to26 -import voice.data.repo.internals.migrations.Migration26to27 -import voice.data.repo.internals.migrations.Migration27to28 -import voice.data.repo.internals.migrations.Migration28to29 -import voice.data.repo.internals.migrations.Migration29to30 -import voice.data.repo.internals.migrations.Migration30to31 -import voice.data.repo.internals.migrations.Migration31to32 -import voice.data.repo.internals.migrations.Migration32to34 -import voice.data.repo.internals.migrations.Migration34to35 -import voice.data.repo.internals.migrations.Migration35to36 -import voice.data.repo.internals.migrations.Migration36to37 -import voice.data.repo.internals.migrations.Migration37to38 -import voice.data.repo.internals.migrations.Migration38to39 -import voice.data.repo.internals.migrations.Migration39to40 -import voice.data.repo.internals.migrations.Migration40to41 -import voice.data.repo.internals.migrations.Migration41to42 -import voice.data.repo.internals.migrations.Migration42to43 -import voice.data.repo.internals.migrations.Migration43to44 -import voice.data.repo.internals.migrations.Migration44 -import voice.data.repo.internals.migrations.Migration45 -import voice.data.repo.internals.migrations.Migration46 -import voice.data.repo.internals.migrations.Migration47 -import voice.data.repo.internals.migrations.Migration48 -import voice.data.repo.internals.migrations.Migration49 -import voice.data.repo.internals.migrations.Migration50 import javax.inject.Singleton @Module @@ -60,43 +33,10 @@ object PersistenceModule { @Singleton fun appDb( context: Context, - migrations: Array, + migrations: Set<@JvmSuppressWildcards Migration>, ): AppDb { return Room.databaseBuilder(context, AppDb::class.java, AppDb.DATABASE_NAME) - .addMigrations(*migrations) + .addMigrations(*migrations.toTypedArray()) .build() } - - @Provides - fun migrations(): Array { - return arrayOf( - Migration23to24(), - Migration24to25(), - Migration25to26(), - Migration26to27(), - Migration27to28(), - Migration28to29(), - Migration29to30(), - Migration30to31(), - Migration31to32(), - Migration32to34(), - Migration34to35(), - Migration35to36(), - Migration36to37(), - Migration37to38(), - Migration38to39(), - Migration39to40(), - Migration40to41(), - Migration41to42(), - Migration42to43(), - Migration43to44(), - Migration44(), - Migration45(), - Migration46(), - Migration47(), - Migration48(), - Migration49(), - Migration50(), - ) - } } diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration23to24.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration23to24.kt index 69420fbd0d..45328a456c 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration23to24.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration23to24.kt @@ -1,11 +1,17 @@ package voice.data.repo.internals.migrations +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -/** - * Drops all tables and creates new ones. - */ -class Migration23to24 : IncrementalMigration(23) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration23to24 +@Inject constructor() : IncrementalMigration(23) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("DROP TABLE IF EXISTS TABLE_BOOK") diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration24to25.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration24to25.kt index 86510f2f5f..65c9d13a82 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration24to25.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration24to25.kt @@ -3,22 +3,28 @@ package voice.data.repo.internals.migrations import android.annotation.SuppressLint import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQueryBuilder +import com.squareup.anvil.annotations.ContributesMultibinding import org.json.JSONArray import org.json.JSONException import org.json.JSONObject +import voice.common.AppScope import voice.data.repo.internals.moveToNextLoop import voice.logging.core.Logger import java.io.File import java.io.IOException import java.util.InvalidPropertiesFormatException +import javax.inject.Inject -/** - * Migrate the database so they will be stored as json objects - */ +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) @SuppressLint("Recycle") -class Migration24to25 : IncrementalMigration(24) { +class Migration24to25 +@Inject constructor() : IncrementalMigration(24) { override fun migrate(db: SupportSQLiteDatabase) { val copyBookTableName = "TABLE_BOOK_COPY" @@ -160,14 +166,7 @@ class Migration24to25 : IncrementalMigration(24) { currentTime = 0 } - var speed = 1.0f - try { - speed = java.lang.Float.valueOf(playingInformation.getString(jsonSpeed)) - } catch (e: JSONException) { - e.printStackTrace() - } catch (e: NumberFormatException) { - e.printStackTrace() - } + val speed = playingInformation.getString(jsonSpeed).toFloatOrNull() ?: 1F var name = "" try { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration25to26.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration25to26.kt index 85323b88af..cbd93070fd 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration25to26.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration25to26.kt @@ -1,12 +1,18 @@ package voice.data.repo.internals.migrations +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding import org.json.JSONObject +import voice.common.AppScope +import javax.inject.Inject -/** - * A previous version caused empty books to be added. So we delete them now. - */ -class Migration25to26 : IncrementalMigration(25) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration25to26 +@Inject constructor() : IncrementalMigration(25) { override fun migrate(db: SupportSQLiteDatabase) { // get all books diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration26to27.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration26to27.kt index f110efc4fa..be7a25a82f 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration26to27.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration26to27.kt @@ -3,13 +3,19 @@ package voice.data.repo.internals.migrations import android.annotation.SuppressLint import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.moveToNextLoop +import javax.inject.Inject -/** - * Adds a new column indicating if the book should be actively shown or hidden. - */ -class Migration26to27 : IncrementalMigration(26) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration26to27 +@Inject constructor() : IncrementalMigration(26) { @SuppressLint("Recycle") override fun migrate(db: SupportSQLiteDatabase) { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration27to28.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration27to28.kt index 15c9a42c17..763ab17b60 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration27to28.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration27to28.kt @@ -1,11 +1,17 @@ package voice.data.repo.internals.migrations +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -/** - * Deletes the table if that failed previously due to a bug in [.upgrade26] - */ -class Migration27to28 : IncrementalMigration(27) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration27to28 +@Inject constructor() : IncrementalMigration(27) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("DROP TABLE IF EXISTS TABLE_BOOK_COPY") diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration28to29.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration28to29.kt index 60417ab3f5..a2955f2a28 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration28to29.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration28to29.kt @@ -2,12 +2,21 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding import org.json.JSONObject +import voice.common.AppScope import voice.logging.core.Logger import java.io.File +import javax.inject.Inject -class Migration28to29 : IncrementalMigration(28) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration28to29 +@Inject constructor() : IncrementalMigration(28) { override fun migrate(db: SupportSQLiteDatabase) { db.query("TABLE_BOOK", arrayOf("BOOK_JSON", "BOOK_ID")) diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration29to30.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration29to30.kt index abf7dda033..142cf01631 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration29to30.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration29to30.kt @@ -3,9 +3,13 @@ package voice.data.repo.internals.migrations import android.annotation.SuppressLint import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding import org.json.JSONObject +import voice.common.AppScope import voice.data.repo.internals.moveToNextLoop +import javax.inject.Inject // tables private const val TABLE_BOOK = "tableBooks" @@ -67,8 +71,13 @@ private const val CREATE_TABLE_BOOKMARKS = """ ) """ +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) @SuppressLint("Recycle") -class Migration29to30 : IncrementalMigration(29) { +class Migration29to30 +@Inject constructor() : IncrementalMigration(29) { override fun migrate(db: SupportSQLiteDatabase) { // fetching old contents diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration30to31.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration30to31.kt index 79a904782c..9dd3ff2ec9 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration30to31.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration30to31.kt @@ -1,15 +1,21 @@ package voice.data.repo.internals.migrations import android.annotation.SuppressLint +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQueryBuilder +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.moveToNextLoop +import javax.inject.Inject -/** - * Queries through all books and removes the ones that were added empty by a bug. - */ +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) @SuppressLint("Recycle") -class Migration30to31 : IncrementalMigration(30) { +class Migration30to31 +@Inject constructor() : IncrementalMigration(30) { override fun migrate(db: SupportSQLiteDatabase) { // book keys diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration31to32.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration31to32.kt index 128a81b35e..005430115c 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration31to32.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration31to32.kt @@ -3,15 +3,21 @@ package voice.data.repo.internals.migrations import android.annotation.SuppressLint import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQueryBuilder +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.moveToNextLoop +import javax.inject.Inject -/** - * Corrects media paths that have been falsely set. - */ +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) @SuppressLint("Recycle") -class Migration31to32 : IncrementalMigration(31) { +class Migration31to32 +@Inject constructor() : IncrementalMigration(31) { private val BOOK_ID = "bookId" private val TABLE_BOOK = "tableBooks" diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration32to34.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration32to34.kt index 6ea149486b..1110fdb9aa 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration32to34.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration32to34.kt @@ -6,11 +6,14 @@ import android.database.sqlite.SQLiteDatabase import android.provider.BaseColumns import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.getLong import voice.data.repo.internals.getString import voice.data.repo.internals.mapRows import voice.data.repo.internals.transaction import voice.logging.core.Logger +import javax.inject.Inject private const val BOOKMARK_TABLE_NAME = "tableBookmarks" private const val BM_PATH = "bookmarkPath" @@ -31,7 +34,9 @@ private const val CREATE_TABLE_BOOKMARKS = """ ) """ -class Migration32to34 : Migration(32, 34) { +@ContributesMultibinding(AppScope::class) +class Migration32to34 +@Inject constructor() : Migration(32, 34) { @SuppressLint("Recycle") override fun migrate(db: SupportSQLiteDatabase) { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration34to35.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration34to35.kt index fbaeed5007..07e0716dfb 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration34to35.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration34to35.kt @@ -1,11 +1,17 @@ package voice.data.repo.internals.migrations +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -/** - * Due to a bug negative book ids were inserted - */ -class Migration34to35 : IncrementalMigration(34) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration34to35 +@Inject constructor() : IncrementalMigration(34) { private val TABLE_NAME = "tableBooks" diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration35to36.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration35to36.kt index 6a57f34503..93ec307ddb 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration35to36.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration35to36.kt @@ -2,7 +2,10 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.getFloat import voice.data.repo.internals.getInt import voice.data.repo.internals.getLong @@ -10,6 +13,7 @@ import voice.data.repo.internals.getString import voice.data.repo.internals.getStringOrNull import voice.data.repo.internals.mapRows import voice.data.repo.internals.transaction +import javax.inject.Inject private const val ID = "bookId" private const val NAME = "bookName" @@ -35,7 +39,12 @@ private const val CREATE_TABLE = """ ) """ -class Migration35to36 : IncrementalMigration(35) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration35to36 +@Inject constructor() : IncrementalMigration(35) { override fun migrate(db: SupportSQLiteDatabase) { val entries = db.query(TABLE_NAME) diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration36to37.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration36to37.kt index 605486b3dd..4d40866ddb 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration36to37.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration36to37.kt @@ -2,11 +2,15 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.getLong import voice.data.repo.internals.getString import voice.data.repo.internals.mapRows import voice.data.repo.internals.transaction +import javax.inject.Inject private const val TABLE_NAME = "tableChapters" private const val DURATION = "chapterDuration" @@ -25,10 +29,12 @@ private const val CREATE_TABLE = """ ) """ -/** - * The field LAST_MODIFIED was added to the chapters - */ -class Migration36to37 : IncrementalMigration(36) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration36to37 +@Inject constructor() : IncrementalMigration(36) { override fun migrate(db: SupportSQLiteDatabase) { val data = db.query("SELECT * FROM $TABLE_NAME").mapRows { @@ -51,5 +57,10 @@ class Migration36to37 : IncrementalMigration(36) { } } - private data class Holder(val duration: Long, val name: String, val path: String, val bookId: Long) + private data class Holder( + val duration: Long, + val name: String, + val path: String, + val bookId: Long, + ) } diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration37to38.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration37to38.kt index 5ba7e52331..f15443610b 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration37to38.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration37to38.kt @@ -2,10 +2,19 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.transaction +import javax.inject.Inject -class Migration37to38 : IncrementalMigration(37) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration37to38 +@Inject constructor() : IncrementalMigration(37) { override fun migrate(db: SupportSQLiteDatabase) { db.transaction { // add new chapter mark table diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration38to39.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration38to39.kt index fb25d66233..fdab54f4ea 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration38to39.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration38to39.kt @@ -2,9 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -class Migration38to39 : IncrementalMigration(38) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration38to39 +@Inject constructor() : IncrementalMigration(38) { override fun migrate(db: SupportSQLiteDatabase) { // invalidate modification time stamps so the chapters will be re-scanned val lastModifiedCv = ContentValues().apply { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration39to40.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration39to40.kt index efe1569787..721d4f5daa 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration39to40.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration39to40.kt @@ -2,13 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -/** - * From DB version 39, the position of a book must no longer be negative. So all negative positions - * get set to 0. - */ -class Migration39to40 : IncrementalMigration(39) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration39to40 +@Inject constructor() : IncrementalMigration(39) { private val BOOK_TABLE_NAME = "tableBooks" private val BOOK_TIME = "bookTime" diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration40to41.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration40to41.kt index 196801323f..27fdff4af8 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration40to41.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration40to41.kt @@ -1,11 +1,17 @@ package voice.data.repo.internals.migrations +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -/** - * In 41 the loudness column was introduced. - */ -class Migration40to41 : IncrementalMigration(40) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration40to41 +@Inject constructor() : IncrementalMigration(40) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE tableBooks ADD loudnessGain INTEGER") diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration41to42.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration41to42.kt index 99e1d64576..8bf05a7005 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration41to42.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration41to42.kt @@ -2,9 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -class Migration41to42 : IncrementalMigration(41) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration41to42 +@Inject constructor() : IncrementalMigration(41) { override fun migrate(db: SupportSQLiteDatabase) { // invalidate modification time stamps so the chapters will be re-scanned val lastModifiedCv = ContentValues().apply { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration42to43.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration42to43.kt index 35ffc8c1f9..c18c56902d 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration42to43.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration42to43.kt @@ -2,9 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -class Migration42to43 : IncrementalMigration(42) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration42to43 +@Inject constructor() : IncrementalMigration(42) { override fun migrate(db: SupportSQLiteDatabase) { // invalidate modification time stamps so the chapters will be re-scanned val lastModifiedCv = ContentValues().apply { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration43to44.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration43to44.kt index bcb6989bcc..c05dc6aefb 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration43to44.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration43to44.kt @@ -3,7 +3,10 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase import androidx.core.content.contentValuesOf +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.consumeEach import voice.data.repo.internals.getFloat import voice.data.repo.internals.getInt @@ -13,11 +16,14 @@ import voice.data.repo.internals.getString import voice.data.repo.internals.getStringOrNull import voice.data.repo.internals.moveToNextLoop import java.util.UUID +import javax.inject.Inject -/** - * Initial Room Migration - */ -class Migration43to44 : IncrementalMigration(43) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration43to44 +@Inject constructor() : IncrementalMigration(43) { override fun migrate(db: SupportSQLiteDatabase) { createNewTables(db) diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration44.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration44.kt index 2f8a237e06..30d96c5cbb 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration44.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration44.kt @@ -2,13 +2,19 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.getString +import javax.inject.Inject -/** - * Because of an an issue in synchronization, there are inconsistent books which have bookSettings that point to absent chapters. - */ -class Migration44 : IncrementalMigration(44) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration44 +@Inject constructor() : IncrementalMigration(44) { override fun migrate(db: SupportSQLiteDatabase) { db.query("SELECT * FROM bookSettings").use { bookSettingsCursor -> diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration45.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration45.kt index 18e2bb73b1..da892d5189 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration45.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration45.kt @@ -2,13 +2,19 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.getString +import javax.inject.Inject -/** - * Because of an an issue in synchronization, there are inconsistent books which have bookSettings that point to absent chapters. - */ -class Migration45 : IncrementalMigration(45) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration45 +@Inject constructor() : IncrementalMigration(45) { override fun migrate(db: SupportSQLiteDatabase) { db.query("SELECT * FROM bookSettings").use { bookSettingsCursor -> diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration46.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration46.kt index e55dbf9b96..0d67c46033 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration46.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration46.kt @@ -2,12 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -/** - * The duration parsing changed so we trigger a file rescan. - */ -class Migration46 : IncrementalMigration(46) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration46 +@Inject constructor() : IncrementalMigration(46) { override fun migrate(db: SupportSQLiteDatabase) { // invalidate modification time stamps so the chapters will be re-scanned diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration47.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration47.kt index 399364d075..6c09c45506 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration47.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration47.kt @@ -2,9 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -class Migration47 : IncrementalMigration(47) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration47 +@Inject constructor() : IncrementalMigration(47) { override fun migrate(db: SupportSQLiteDatabase) { // the format of the marks has changed. Write an empty array. Also clear the fileLastModified to trigger a rescan. diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration48.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration48.kt index b3fc0a73d3..d88e66cdec 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration48.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration48.kt @@ -2,9 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -class Migration48 : IncrementalMigration(48) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration48 +@Inject constructor() : IncrementalMigration(48) { override fun migrate(db: SupportSQLiteDatabase) { // there was a bug a in the chapter parsing, trigger a scan. diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration49.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration49.kt index 8230179d94..c155885271 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration49.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration49.kt @@ -2,14 +2,23 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope import voice.data.repo.internals.getInt import voice.data.repo.internals.getString import voice.data.repo.internals.moveToNextLoop import java.time.Instant import java.util.UUID +import javax.inject.Inject -class Migration49 : IncrementalMigration(49) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration49 +@Inject constructor() : IncrementalMigration(49) { override fun migrate(db: SupportSQLiteDatabase) { with(db) { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration50.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration50.kt index 2a16e891dc..a48e1cab74 100644 --- a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration50.kt +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration50.kt @@ -2,9 +2,18 @@ package voice.data.repo.internals.migrations import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import javax.inject.Inject -class Migration50 : IncrementalMigration(50) { +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration50 +@Inject constructor() : IncrementalMigration(50) { override fun migrate(db: SupportSQLiteDatabase) { with(db) { diff --git a/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration53.kt b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration53.kt new file mode 100644 index 0000000000..ea15a00389 --- /dev/null +++ b/data/src/main/kotlin/voice/data/repo/internals/migrations/Migration53.kt @@ -0,0 +1,32 @@ +package voice.data.repo.internals.migrations + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.squareup.anvil.annotations.ContributesMultibinding +import voice.common.AppScope +import java.time.Instant +import javax.inject.Inject + +// clear the fileLastModified to trigger a rescan of the chapters +@ContributesMultibinding( + scope = AppScope::class, + boundType = Migration::class, +) +class Migration53 +@Inject constructor() : IncrementalMigration(53) { + + override fun migrate(db: SupportSQLiteDatabase) { + val lastModifiedCv = ContentValues().apply { + put("fileLastModified", Instant.EPOCH.toString()) + } + db.update( + "chapters2", + SQLiteDatabase.CONFLICT_FAIL, + lastModifiedCv, + null, + null, + ) + } +} diff --git a/data/src/test/kotlin/voice/data/TestComponent.kt b/data/src/test/kotlin/voice/data/TestComponent.kt new file mode 100644 index 0000000000..dc89522eca --- /dev/null +++ b/data/src/test/kotlin/voice/data/TestComponent.kt @@ -0,0 +1,36 @@ +package voice.data + +import androidx.room.migration.Migration +import com.squareup.anvil.annotations.MergeComponent +import dagger.BindsInstance +import de.paulwoitaschek.flowpref.Pref +import de.paulwoitaschek.flowpref.inmemory.InMemoryPref +import voice.common.AppScope +import voice.common.pref.PrefKeys +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +@MergeComponent( + scope = AppScope::class, +) +interface TestComponent { + + val migrations: Set<@JvmSuppressWildcards Migration> + + @dagger.Component.Factory + interface Factory { + + fun create( + @BindsInstance + @Named(PrefKeys.DARK_THEME) + darkThemePref: Pref, + ): TestComponent + } +} + +internal fun allMigrations(): Array { + return DaggerTestComponent.factory() + .create(InMemoryPref(false)) + .migrations.toTypedArray() +} diff --git a/data/src/test/kotlin/voice/data/repo/internals/DataBaseMigratorTest.kt b/data/src/test/kotlin/voice/data/repo/internals/DataBaseMigratorTest.kt index dae283e407..c095dd482e 100644 --- a/data/src/test/kotlin/voice/data/repo/internals/DataBaseMigratorTest.kt +++ b/data/src/test/kotlin/voice/data/repo/internals/DataBaseMigratorTest.kt @@ -9,6 +9,7 @@ import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import voice.data.allMigrations import java.util.UUID import kotlin.random.Random @@ -34,7 +35,7 @@ class DataBaseMigratorTest { dbName, AppDb.VERSION, true, - *PersistenceModule.migrations(), + *allMigrations(), ) } @@ -81,7 +82,7 @@ class DataBaseMigratorTest { dbName, 45, true, - *PersistenceModule.migrations(), + *allMigrations(), ) val migratedBookSettings = migratedDb.query("SELECT * FROM bookSettings").mapRows { @@ -227,7 +228,7 @@ class DataBaseMigratorTest { dbName, 44, true, - *PersistenceModule.migrations(), + *allMigrations(), ) val metaDataCursor = migratedDb.query("SELECT * FROM bookMetaData") diff --git a/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt b/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt index d1b631c93f..d87fa275a5 100644 --- a/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt +++ b/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt @@ -56,7 +56,8 @@ class BookSearchHandler val bookNameMatches = it.content.name.contains(query, ignoreCase = true) val authorMatches = it.content.author?.contains(query, ignoreCase = true) == true val chapterNameMatches = it.chapters.any { chapter -> - chapter.name.contains(query, ignoreCase = true) + val chapterName = chapter.name + chapterName != null && chapterName.contains(query, ignoreCase = true) } bookNameMatches || authorMatches || chapterNameMatches } diff --git a/scanner/src/main/kotlin/voice/app/scanner/BookParser.kt b/scanner/src/main/kotlin/voice/app/scanner/BookParser.kt index bf4d41c55d..e16079b48f 100644 --- a/scanner/src/main/kotlin/voice/app/scanner/BookParser.kt +++ b/scanner/src/main/kotlin/voice/app/scanner/BookParser.kt @@ -1,6 +1,7 @@ package voice.app.scanner import android.app.Application +import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile import voice.common.BookId @@ -23,13 +24,16 @@ class BookParser private val legacyBookDao: LegacyBookDao, private val application: Application, private val bookmarkMigrator: BookmarkMigrator, + private val context: Context, ) { suspend fun parseAndStore(chapters: List, file: DocumentFile): BookContent { val id = BookId(file.uri) return contentRepo.getOrPut(id) { - val analyzed = mediaAnalyzer.analyze(chapters.first().id.toUri()) - + val uri = chapters.first().id.toUri() + val analyzed = DocumentFile.fromSingleUri(context, uri)?.let { + mediaAnalyzer.analyze(it) + } val filePath = file.uri.filePath() val migrationMetaData = filePath?.let { legacyBookDao.bookMetaData() diff --git a/scanner/src/main/kotlin/voice/app/scanner/ChapterParser.kt b/scanner/src/main/kotlin/voice/app/scanner/ChapterParser.kt index c42c4bc550..55d2d71bdc 100644 --- a/scanner/src/main/kotlin/voice/app/scanner/ChapterParser.kt +++ b/scanner/src/main/kotlin/voice/app/scanner/ChapterParser.kt @@ -19,7 +19,7 @@ class ChapterParser if (file.isFile) { val id = Chapter.Id(file.uri) val chapter = chapterRepo.getOrPut(id, Instant.ofEpochMilli(file.lastModified())) { - val metaData = mediaAnalyzer.analyze(file.uri) ?: return@getOrPut null + val metaData = mediaAnalyzer.analyze(file) ?: return@getOrPut null Chapter( id = id, duration = metaData.duration, diff --git a/scanner/src/main/kotlin/voice/app/scanner/FFProbeAnalyze.kt b/scanner/src/main/kotlin/voice/app/scanner/FFProbeAnalyze.kt new file mode 100644 index 0000000000..257a76ca65 --- /dev/null +++ b/scanner/src/main/kotlin/voice/app/scanner/FFProbeAnalyze.kt @@ -0,0 +1,47 @@ +package voice.app.scanner + +import android.content.Context +import androidx.documentfile.provider.DocumentFile +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import voice.ffmpeg.ffprobe +import voice.logging.core.Logger +import javax.inject.Inject + +class FFProbeAnalyze +@Inject constructor( + private val context: Context, +) { + + private val json = Json { + ignoreUnknownKeys = true + allowStructuredMapKeys = true + } + + suspend fun analyze(file: DocumentFile): MetaDataScanResult? { + val result = ffprobe( + input = file.uri, + context = context, + command = listOf( + "-print_format", "json=c=1", + "-show_chapters", + "-loglevel", "quiet", + "-show_entries", "format=duration", + "-show_entries", "format_tags=artist,title,album", + "-show_entries", "stream_tags=artist,title,album", + "-select_streams", "a", // only select the audio stream + ), + ) + if (result == null) { + Logger.w("Unable to parse $file.") + return null + } + + return try { + json.decodeFromString(MetaDataScanResult.serializer(), result) + } catch (e: SerializationException) { + Logger.w(e, "Unable to parse $file") + return null + } + } +} diff --git a/scanner/src/main/kotlin/voice/app/scanner/MediaAnalyzer.kt b/scanner/src/main/kotlin/voice/app/scanner/MediaAnalyzer.kt index 6c11a583c8..44212cce01 100644 --- a/scanner/src/main/kotlin/voice/app/scanner/MediaAnalyzer.kt +++ b/scanner/src/main/kotlin/voice/app/scanner/MediaAnalyzer.kt @@ -1,65 +1,27 @@ package voice.app.scanner -import android.content.Context -import android.net.Uri import androidx.documentfile.provider.DocumentFile -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json import voice.data.MarkData -import voice.ffmpeg.ffprobe import voice.logging.core.Logger import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -/** - * Analyzes media files for meta data and duration. - */ class MediaAnalyzer @Inject constructor( - private val context: Context, + private val ffProbeAnalyze: FFProbeAnalyze, ) { - private val json = Json { - ignoreUnknownKeys = true - allowStructuredMapKeys = true - } - - suspend fun analyze(uri: Uri): Metadata? { - Logger.d("analyze $uri") - - val result = ffprobe( - input = uri, - context = context, - command = listOf( - "-print_format", "json=c=1", - "-show_chapters", - "-loglevel", "quiet", - "-show_entries", "format=duration", - "-show_entries", "format_tags=artist,title,album", - "-show_entries", "stream_tags=artist,title,album", - "-select_streams", "a", // only select the audio stream - ), - ) - if (result == null) { - Logger.w("Unable to parse $uri.") - return null - } - - val parsed = try { - json.decodeFromString(MetaDataScanResult.serializer(), result) - } catch (e: SerializationException) { - Logger.w(e, "Unable to parse $uri") - return null - } - - val duration = parsed.format?.duration + suspend fun analyze(file: DocumentFile): Metadata? { + Logger.d("analyze ${file.uri}") + val result = ffProbeAnalyze.analyze(file) ?: return null + val duration = result.format?.duration return if (duration != null && duration > 0) { Metadata( duration = duration.seconds.inWholeMilliseconds, - chapterName = parsed.findTag(TagType.Title) ?: chapterNameFallback(uri, context), - author = parsed.findTag(TagType.Artist), - bookName = parsed.findTag(TagType.Album), - chapters = parsed.chapters.mapIndexed { index, metaDataChapter -> + chapterName = result.findTag(TagType.Title) ?: file.chapterNameFallback(), + author = result.findTag(TagType.Artist), + bookName = result.findTag(TagType.Album), + chapters = result.chapters.mapIndexed { index, metaDataChapter -> MarkData( startMs = metaDataChapter.start.inWholeMilliseconds, name = metaDataChapter.tags?.find(TagType.Title) ?: (index + 1).toString(), @@ -67,23 +29,22 @@ class MediaAnalyzer }, ) } else { - Logger.w("Unable to parse $uri") + Logger.w("Unable to parse $file") null } } data class Metadata( val duration: Long, - val chapterName: String, + val chapterName: String?, val author: String?, val bookName: String?, val chapters: List, ) } -private fun chapterNameFallback(uri: Uri, context: Context): String { - val file = DocumentFile.fromSingleUri(context, uri) - val name = file?.name ?: "Chapter" +private fun DocumentFile.chapterNameFallback(): String? { + val name = name ?: return null return name.substringBeforeLast(".") .trim() .takeUnless { it.isEmpty() } diff --git a/scanner/src/test/kotlin/voice/app/scanner/MediaAnalyzerTest.kt b/scanner/src/test/kotlin/voice/app/scanner/MediaAnalyzerTest.kt new file mode 100644 index 0000000000..9921c7d8eb --- /dev/null +++ b/scanner/src/test/kotlin/voice/app/scanner/MediaAnalyzerTest.kt @@ -0,0 +1,42 @@ +package voice.app.scanner + +import androidx.documentfile.provider.DocumentFile +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.io.File + +internal class MediaAnalyzerTest { + + private val ffprobe = mockk() + private val analyzer = MediaAnalyzer(ffprobe) + + @Test + fun chapterNameUsed() = runTest { + val file = DocumentFile.fromFile(File("mybook.mp3")) + coEvery { + ffprobe.analyze(any()) + } returns MetaDataScanResult( + format = MetaDataFormat( + tags = mapOf("title" to "MyTitle"), + duration = 123.45, + ), + ) + analyzer.analyze(file)!!.chapterName shouldBe "MyTitle" + } + + @Test + fun chapterFallbackDerivedFromFileName() = runTest { + val file = DocumentFile.fromFile(File("mybook.mp3")) + coEvery { + ffprobe.analyze(any()) + } returns MetaDataScanResult( + format = MetaDataFormat( + duration = 123.45, + ), + ) + analyzer.analyze(file)!!.chapterName shouldBe "mybook" + } +} diff --git a/scanner/src/test/kotlin/voice/app/scanner/MediaScannerTest.kt b/scanner/src/test/kotlin/voice/app/scanner/MediaScannerTest.kt index dfd2540175..0b8fb040ed 100644 --- a/scanner/src/test/kotlin/voice/app/scanner/MediaScannerTest.kt +++ b/scanner/src/test/kotlin/voice/app/scanner/MediaScannerTest.kt @@ -146,6 +146,7 @@ class MediaScannerTest { legacyBookDao = db.legacyBookDao(), bookmarkDao = db.bookmarkDao(), ), + context = ApplicationProvider.getApplicationContext(), ), ) @@ -164,7 +165,7 @@ class MediaScannerTest { check(it.createNewFile()) } .also { - coEvery { mediaAnalyzer.analyze(it.toUri()) } coAnswers { + coEvery { mediaAnalyzer.analyze(any()) } coAnswers { MediaAnalyzer.Metadata( duration = 1000L, author = "Author",