diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt
index 16de62c99..175dbde36 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheModule.kt
@@ -30,20 +30,20 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
-interface CacheModule {
- @Binds fun cacheRepository(cacheRepository: CacheRepositoryImpl): CacheRepository
+interface TagCacheModule {
+ @Binds fun tagCache(cacheRepository: TagCacheImpl): TagCache
}
@Module
@InstallIn(SingletonComponent::class)
-class CacheRoomModule {
+class TagDatabaseModule {
@Singleton
@Provides
fun database(@ApplicationContext context: Context) =
Room.databaseBuilder(
- context.applicationContext, CacheDatabase::class.java, "music_cache.db")
+ context.applicationContext, TagDatabase::class.java, "music_cache.db")
.fallbackToDestructiveMigration()
.build()
- @Provides fun cachedSongsDao(database: CacheDatabase) = database.cachedSongsDao()
+ @Provides fun tagsDao(database: TagDatabase) = database.cachedSongsDao()
}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt
deleted file mode 100644
index 0621b6ee7..000000000
--- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheRepository.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (c) 2022 Auxio Project
- * CacheRepository.kt is part of Auxio.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.auxio.music.cache
-
-import javax.inject.Inject
-import org.oxycblt.auxio.music.device.RawSong
-import timber.log.Timber as L
-
-/**
- * A repository allowing access to cached metadata obtained in prior music loading operations.
- *
- * @author Alexander Capehart (OxygenCobalt)
- */
-interface CacheRepository {
- /**
- * Read the current [Cache], if it exists.
- *
- * @return The stored [Cache], or null if it could not be obtained.
- */
- suspend fun readCache(): Cache?
-
- /**
- * Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data.
- *
- * @param rawSongs The [rawSongs] to write to the cache.
- */
- suspend fun writeCache(rawSongs: List)
-}
-
-class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: CachedSongsDao) :
- CacheRepository {
- override suspend fun readCache(): Cache? =
- try {
- // Faster to load the whole database into memory than do a query on each
- // populate call.
- val songs = cachedSongsDao.readSongs()
- L.d("Successfully read ${songs.size} songs from cache")
- CacheImpl(songs)
- } catch (e: Exception) {
- L.e("Unable to load cache database.")
- L.e(e.stackTraceToString())
- null
- }
-
- override suspend fun writeCache(rawSongs: List) {
- try {
- // Still write out whatever data was extracted.
- cachedSongsDao.nukeSongs()
- L.d("Successfully deleted old cache")
- cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw))
- L.d("Successfully wrote ${rawSongs.size} songs to cache")
- } catch (e: Exception) {
- L.e("Unable to save cache database.")
- L.e(e.stackTraceToString())
- }
- }
-}
-
-/**
- * A cache of music metadata obtained in prior music loading operations. Obtain an instance with
- * [CacheRepository].
- *
- * @author Alexander Capehart (OxygenCobalt)
- */
-interface Cache {
- /** Whether this cache has encountered a [RawSong] that did not have a cache entry. */
- val invalidated: Boolean
-
- /**
- * Populate a [RawSong] from a cache entry, if it exists.
- *
- * @param rawSong The [RawSong] to populate.
- * @return true if a cache entry could be applied to [rawSong], false otherwise.
- */
- fun populate(rawSong: RawSong): Boolean
-}
-
-private class CacheImpl(cachedSongs: List) : Cache {
- private val cacheMap = buildMap {
- for (cachedSong in cachedSongs) {
- put(cachedSong.mediaStoreId, cachedSong)
- }
- }
-
- override var invalidated = false
-
- override fun populate(rawSong: RawSong): Boolean {
- // For a cached raw song to be used, it must exist within the cache and have matching
- // addition and modification timestamps. Technically the addition timestamp doesn't
- // exist, but to safeguard against possible OEM-specific timestamp incoherence, we
- // check for it anyway.
- val cachedSong = cacheMap[rawSong.mediaStoreId]
- if (cachedSong != null &&
- cachedSong.dateAdded == rawSong.dateAdded &&
- cachedSong.dateModified == rawSong.dateModified) {
- cachedSong.copyToRaw(rawSong)
- return true
- }
-
- // We could not populate this song. This means our cache is stale and should be
- // re-written with newly-loaded music.
- invalidated = true
- return false
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/TagCache.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/TagCache.kt
index ec222b1aa..2dad7b98f 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/cache/TagCache.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/cache/TagCache.kt
@@ -1,2 +1,36 @@
package org.oxycblt.auxio.music.cache
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.transform
+import org.oxycblt.auxio.music.device.RawSong
+import org.oxycblt.auxio.music.fs.DeviceFile
+import org.oxycblt.auxio.music.metadata.TagResult
+import javax.inject.Inject
+
+interface TagCache {
+ fun read(files: Flow): Flow
+ suspend fun write(rawSongs: Flow)
+}
+
+class TagCacheImpl @Inject constructor(
+ private val tagDao: TagDao
+) : TagCache {
+ override fun read(files: Flow) =
+ files.transform { file ->
+ val tags = tagDao.selectTags(file.uri.toString(), file.lastModified)
+ if (tags != null) {
+ val rawSong = RawSong(file = file)
+ tags.copyToRaw(rawSong)
+ TagResult.Hit(rawSong)
+ } else {
+ TagResult.Miss(file)
+ }
+ }
+
+ override suspend fun write(rawSongs: Flow) {
+ rawSongs.collect { rawSong ->
+ tagDao.updateTags(Tags.fromRaw(rawSong))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/cache/TagDatabase.kt
similarity index 82%
rename from app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt
rename to app/src/main/java/org/oxycblt/auxio/music/cache/TagDatabase.kt
index 5ea7c8ebf..1d62adb3e 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/cache/CacheDatabase.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/cache/TagDatabase.kt
@@ -22,6 +22,7 @@ import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.Insert
+import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RoomDatabase
@@ -32,36 +33,32 @@ import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.metadata.correctWhitespace
import org.oxycblt.auxio.music.metadata.splitEscaped
-@Database(entities = [CachedSong::class], version = 49, exportSchema = false)
-abstract class CacheDatabase : RoomDatabase() {
- abstract fun cachedSongsDao(): CachedSongsDao
+@Database(entities = [Tags::class], version = 50, exportSchema = false)
+abstract class TagDatabase : RoomDatabase() {
+ abstract fun cachedSongsDao(): TagDao
}
@Dao
-interface CachedSongsDao {
- @Query("SELECT * FROM CachedSong") suspend fun readSongs(): List
+interface TagDao {
+ @Query("SELECT * FROM Tags WHERE uri = :uri AND dateModified = :dateModified")
+ suspend fun selectTags(uri: String, dateModified: Long): Tags?
- @Query("DELETE FROM CachedSong") suspend fun nukeSongs()
-
- @Insert suspend fun insertSongs(songs: List)
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun updateTags(tags: Tags)
}
@Entity
-@TypeConverters(CachedSong.Converters::class)
-data class CachedSong(
+@TypeConverters(Tags.Converters::class)
+data class Tags(
/**
- * The ID of the [RawSong]'s audio file, obtained from MediaStore. Note that this ID is highly
- * unstable and should only be used for accessing the audio file.
+ * The Uri of the [RawSong]'s audio file, obtained from SAF.
+ * This should ideally be a black box only used for comparison.
*/
- @PrimaryKey var mediaStoreId: Long,
- /** @see RawSong.dateAdded */
- var dateAdded: Long,
+ @PrimaryKey val uri: String,
/** The latest date the [RawSong]'s audio file was modified, as a unix epoch timestamp. */
- var dateModified: Long,
- /** @see RawSong.size */
- var size: Long? = null,
+ val dateModified: Long,
/** @see RawSong */
- var durationMs: Long,
+ val durationMs: Long,
/** @see RawSong.replayGainTrackAdjustment */
val replayGainTrackAdjustment: Float? = null,
/** @see RawSong.replayGainAlbumAdjustment */
@@ -110,7 +107,6 @@ data class CachedSong(
rawSong.name = name
rawSong.sortName = sortName
- rawSong.size = size
rawSong.durationMs = durationMs
rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment
@@ -154,16 +150,12 @@ data class CachedSong(
companion object {
fun fromRaw(rawSong: RawSong) =
- CachedSong(
- mediaStoreId =
- requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No MediaStore ID" },
- dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" },
- dateModified =
- requireNotNull(rawSong.dateModified) { "Invalid raw: No date modified" },
+ Tags(
+ uri = rawSong.file.uri.toString(),
+ dateModified = rawSong.file.lastModified,
musicBrainzId = rawSong.musicBrainzId,
name = requireNotNull(rawSong.name) { "Invalid raw: No name" },
sortName = rawSong.sortName,
- size = rawSong.size,
durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" },
replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment,
replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment,
diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt
index b1bfc1045..8e2d4df2c 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt
@@ -77,7 +77,7 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
// rather than just being a glorified async.
val lastModified = cursor.getLong(lastModifiedIndex)
val size = cursor.getLong(sizeIndex)
- emit(DeviceFile(childUri, mimeType, path, lastModified))
+ emit(DeviceFile(childUri, mimeType, path, size, lastModified))
}
}
// Hypothetically, we could just emitAll as we recurse into a new directory,
@@ -96,7 +96,7 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_SIZE,
- DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED
)
}
}
diff --git a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt
index 9914dbe5f..43872923e 100644
--- a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt
+++ b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt
@@ -38,7 +38,7 @@ class CacheRepositoryTest {
@Test
fun cache_read_noInvalidate() {
val dao =
- mockk {
+ mockk {
coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B))
}
val cacheRepository = CacheRepositoryImpl(dao)
@@ -62,7 +62,7 @@ class CacheRepositoryTest {
@Test
fun cache_read_invalidate() {
val dao =
- mockk {
+ mockk {
coEvery { readSongs() }.returnsMany(listOf(CACHED_SONG_A, CACHED_SONG_B))
}
val cacheRepository = CacheRepositoryImpl(dao)
@@ -86,7 +86,7 @@ class CacheRepositoryTest {
@Test
fun cache_read_crashes() {
- val dao = mockk { coEvery { readSongs() } throws IllegalStateException() }
+ val dao = mockk { coEvery { readSongs() } throws IllegalStateException() }
val cacheRepository = CacheRepositoryImpl(dao)
assertEquals(null, runBlocking { cacheRepository.readCache() })
coVerifyAll { dao.readSongs() }
@@ -94,10 +94,10 @@ class CacheRepositoryTest {
@Test
fun cache_write() {
- var currentlyStoredSongs = listOf()
- val insertSongsArg = slot>()
+ var currentlyStoredSongs = listOf()
+ val insertSongsArg = slot>()
val dao =
- mockk {
+ mockk {
coEvery { nukeSongs() } answers { currentlyStoredSongs = listOf() }
coEvery { insertSongs(capture(insertSongsArg)) } answers
@@ -122,7 +122,7 @@ class CacheRepositoryTest {
@Test
fun cache_write_nukeCrashes() {
val dao =
- mockk {
+ mockk {
coEvery { nukeSongs() } throws IllegalStateException()
coEvery { insertSongs(listOf()) } just Runs
}
@@ -134,7 +134,7 @@ class CacheRepositoryTest {
@Test
fun cache_write_insertCrashes() {
val dao =
- mockk {
+ mockk {
coEvery { nukeSongs() } just Runs
coEvery { insertSongs(listOf()) } throws IllegalStateException()
}
@@ -148,7 +148,7 @@ class CacheRepositoryTest {
private companion object {
val CACHED_SONG_A =
- CachedSong(
+ Tags(
mediaStoreId = 0,
dateAdded = 1,
dateModified = 2,
@@ -206,7 +206,7 @@ class CacheRepositoryTest {
)
val CACHED_SONG_B =
- CachedSong(
+ Tags(
mediaStoreId = 9,
dateAdded = 10,
dateModified = 11,