From 4f920e922d713166b13dd2da96e56159aafeec94 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 26 Dec 2024 10:33:50 -0500 Subject: [PATCH] musikr: add date added support w/cache This allows me to replicate something resembling date added support while reducing query load. --- .../org/oxycblt/auxio/music/MusicModule.kt | 3 +- .../oxycblt/auxio/music/MusicRepository.kt | 7 ++-- .../java/org/oxycblt/musikr/cache/Cache.kt | 36 ++++++++++++++----- .../org/oxycblt/musikr/cache/CacheDatabase.kt | 12 +++++-- .../oxycblt/musikr/pipeline/ExtractStep.kt | 6 ++-- .../musikr/tag/interpret/TagInterpreter.kt | 3 +- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt index c276ca414..5d524b1fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt @@ -27,6 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton import org.oxycblt.musikr.cache.Cache +import org.oxycblt.musikr.cache.StoredCache import org.oxycblt.musikr.playlist.db.StoredPlaylists @Module @@ -40,7 +41,7 @@ interface MusicModule { @Module @InstallIn(SingletonComponent::class) class MusikrShimModule { - @Singleton @Provides fun cache(@ApplicationContext context: Context) = Cache.from(context) + @Singleton @Provides fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context) @Singleton @Provides diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 159a31ca8..25b8f65a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -37,8 +37,7 @@ import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song import org.oxycblt.musikr.Storage -import org.oxycblt.musikr.cache.Cache -import org.oxycblt.musikr.cache.WriteOnlyCache +import org.oxycblt.musikr.cache.StoredCache import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators @@ -214,7 +213,7 @@ class MusicRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, - private val cache: Cache, + private val storedCache: StoredCache, private val storedPlaylists: StoredPlaylists, private val musicSettings: MusicSettings ) : MusicRepository { @@ -363,7 +362,7 @@ constructor( val currentRevision = musicSettings.revision val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() - val cache = if (withCache) cache else WriteOnlyCache(cache) + val cache = if (withCache) storedCache.full() else storedCache.writeOnly() val covers = MutableRevisionedStoredCovers(context, newRevision) val storage = Storage(cache, covers, storedPlaylists) val interpretation = Interpretation(nameFactory, separators) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt index adb640b6d..758640a16 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -27,30 +27,48 @@ abstract class Cache { internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult internal abstract suspend fun write(song: RawSong) +} + +interface StoredCache { + fun full(): Cache + + fun writeOnly(): Cache companion object { - fun from(context: Context): Cache = CacheImpl(CacheDatabase.from(context).cachedSongsDao()) + fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context)) } } internal sealed interface CacheResult { data class Hit(val song: RawSong) : CacheResult - data class Miss(val file: DeviceFile) : CacheResult + data class Miss(val file: DeviceFile, val dateAdded: Long?) : CacheResult +} + +private class StoredCacheImpl(private val database: CacheDatabase) : StoredCache { + override fun full() = CacheImpl(database.cachedSongsDao()) + + override fun writeOnly() = WriteOnlyCacheImpl(database.cachedSongsDao()) } private class CacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() { - override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = - cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let { - CacheResult.Hit(it.intoRawSong(file, storedCovers)) - } ?: CacheResult.Miss(file) + override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult { + val song = cacheInfoDao.selectSong(file.uri.toString()) ?: + return CacheResult.Miss(file, null) + if (song.dateModified != file.lastModified) { + return CacheResult.Miss(file, song.dateAdded) + } + return CacheResult.Hit(song.intoRawSong(file, storedCovers)) + } override suspend fun write(song: RawSong) = cacheInfoDao.updateSong(CachedSong.fromRawSong(song)) } -class WriteOnlyCache(private val inner: Cache) : Cache() { - override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = CacheResult.Miss(file) +private class WriteOnlyCacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() { + override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = + CacheResult.Miss(file, cacheInfoDao.selectDateAdded(file.uri.toString())) - override suspend fun write(song: RawSong) = inner.write(song) + override suspend fun write(song: RawSong) = + cacheInfoDao.updateSong(CachedSong.fromRawSong(song)) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index 81d233160..45f5dcf97 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -54,8 +54,11 @@ internal abstract class CacheDatabase : RoomDatabase() { @Dao internal interface CacheInfoDao { - @Query("SELECT * FROM CachedSong WHERE uri = :uri AND dateModified = :dateModified") - suspend fun selectSong(uri: String, dateModified: Long): CachedSong? + @Query("SELECT * FROM CachedSong WHERE uri = :uri") + suspend fun selectSong(uri: String): CachedSong? + + @Query("SELECT dateAdded FROM CachedSong WHERE uri = :uri") + suspend fun selectDateAdded(uri: String): Long? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong) } @@ -65,6 +68,7 @@ internal interface CacheInfoDao { internal data class CachedSong( @PrimaryKey val uri: String, val dateModified: Long, + val dateAdded: Long, val mimeType: String, val durationMs: Long, val bitrateHz: Int, @@ -117,7 +121,8 @@ internal data class CachedSong( genreNames = genreNames, replayGainTrackAdjustment = replayGainTrackAdjustment, replayGainAlbumAdjustment = replayGainAlbumAdjustment), - coverId?.let { storedCovers.obtain(it) }) + coverId?.let { storedCovers.obtain(it) }, + dateAdded = dateAdded) object Converters { @TypeConverter @@ -137,6 +142,7 @@ internal data class CachedSong( CachedSong( uri = rawSong.file.uri.toString(), dateModified = rawSong.file.lastModified, + dateAdded = rawSong.dateAdded, musicBrainzId = rawSong.tags.musicBrainzId, name = rawSong.tags.name, sortName = rawSong.tags.sortName, diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 042469261..25e90344e 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -63,6 +63,7 @@ private class ExtractStepImpl( private val storedCovers: MutableStoredCovers ) : ExtractStep { override fun extract(nodes: Flow): Flow { + val startTime = System.currentTimeMillis() val filterFlow = nodes.divert { when (it) { @@ -127,7 +128,7 @@ private class ExtractStepImpl( .mapNotNull { fileWith -> val tags = tagParser.parse(fileWith.file, fileWith.with) val cover = fileWith.with.cover?.let { storedCovers.write(it) } - RawSong(fileWith.file, fileWith.with.properties, tags, cover) + RawSong(fileWith.file, fileWith.with.properties, tags, cover, startTime) } .flowOn(Dispatchers.IO) .buffer(Channel.UNLIMITED) @@ -163,7 +164,8 @@ internal data class RawSong( val file: DeviceFile, val properties: Properties, val tags: ParsedTags, - val cover: Cover? + val cover: Cover?, + val dateAdded: Long ) internal sealed interface ExtractedMusic { diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt index b6f07df30..618272daa 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -65,8 +65,7 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T size = song.file.size, format = Format.infer(song.file.mimeType, song.properties.mimeType), lastModified = song.file.lastModified, - // TODO: Figure out what to do with date added - dateAdded = song.file.lastModified, + dateAdded = song.dateAdded, musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(), name = interpretation.naming.name(song.tags.name, song.tags.sortName), rawName = song.tags.name,