musikr: add date added support w/cache

This allows me to replicate something resembling date added
support while reducing query load.
This commit is contained in:
Alexander Capehart 2024-12-26 10:33:50 -05:00
parent da76a03298
commit 4f920e922d
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 46 additions and 21 deletions

View file

@ -27,6 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.StoredCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
@Module @Module
@ -40,7 +41,7 @@ interface MusicModule {
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class MusikrShimModule { class MusikrShimModule {
@Singleton @Provides fun cache(@ApplicationContext context: Context) = Cache.from(context) @Singleton @Provides fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context)
@Singleton @Singleton
@Provides @Provides

View file

@ -37,8 +37,7 @@ import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.StoredCache
import org.oxycblt.musikr.cache.WriteOnlyCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
@ -214,7 +213,7 @@ class MusicRepositoryImpl
@Inject @Inject
constructor( constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val cache: Cache, private val storedCache: StoredCache,
private val storedPlaylists: StoredPlaylists, private val storedPlaylists: StoredPlaylists,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
) : MusicRepository { ) : MusicRepository {
@ -363,7 +362,7 @@ constructor(
val currentRevision = musicSettings.revision val currentRevision = musicSettings.revision
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() 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 covers = MutableRevisionedStoredCovers(context, newRevision)
val storage = Storage(cache, covers, storedPlaylists) val storage = Storage(cache, covers, storedPlaylists)
val interpretation = Interpretation(nameFactory, separators) val interpretation = Interpretation(nameFactory, separators)

View file

@ -27,30 +27,48 @@ abstract class Cache {
internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
internal abstract suspend fun write(song: RawSong) internal abstract suspend fun write(song: RawSong)
}
interface StoredCache {
fun full(): Cache
fun writeOnly(): Cache
companion object { 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 { internal sealed interface CacheResult {
data class Hit(val song: RawSong) : 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() { private class CacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let { val song = cacheInfoDao.selectSong(file.uri.toString()) ?:
CacheResult.Hit(it.intoRawSong(file, storedCovers)) return CacheResult.Miss(file, null)
} ?: CacheResult.Miss(file) if (song.dateModified != file.lastModified) {
return CacheResult.Miss(file, song.dateAdded)
}
return CacheResult.Hit(song.intoRawSong(file, storedCovers))
}
override suspend fun write(song: RawSong) = override suspend fun write(song: RawSong) =
cacheInfoDao.updateSong(CachedSong.fromRawSong(song)) cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
} }
class WriteOnlyCache(private val inner: Cache) : Cache() { private class WriteOnlyCacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = CacheResult.Miss(file) 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))
} }

View file

@ -54,8 +54,11 @@ internal abstract class CacheDatabase : RoomDatabase() {
@Dao @Dao
internal interface CacheInfoDao { internal interface CacheInfoDao {
@Query("SELECT * FROM CachedSong WHERE uri = :uri AND dateModified = :dateModified") @Query("SELECT * FROM CachedSong WHERE uri = :uri")
suspend fun selectSong(uri: String, dateModified: Long): CachedSong? 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) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
} }
@ -65,6 +68,7 @@ internal interface CacheInfoDao {
internal data class CachedSong( internal data class CachedSong(
@PrimaryKey val uri: String, @PrimaryKey val uri: String,
val dateModified: Long, val dateModified: Long,
val dateAdded: Long,
val mimeType: String, val mimeType: String,
val durationMs: Long, val durationMs: Long,
val bitrateHz: Int, val bitrateHz: Int,
@ -117,7 +121,8 @@ internal data class CachedSong(
genreNames = genreNames, genreNames = genreNames,
replayGainTrackAdjustment = replayGainTrackAdjustment, replayGainTrackAdjustment = replayGainTrackAdjustment,
replayGainAlbumAdjustment = replayGainAlbumAdjustment), replayGainAlbumAdjustment = replayGainAlbumAdjustment),
coverId?.let { storedCovers.obtain(it) }) coverId?.let { storedCovers.obtain(it) },
dateAdded = dateAdded)
object Converters { object Converters {
@TypeConverter @TypeConverter
@ -137,6 +142,7 @@ internal data class CachedSong(
CachedSong( CachedSong(
uri = rawSong.file.uri.toString(), uri = rawSong.file.uri.toString(),
dateModified = rawSong.file.lastModified, dateModified = rawSong.file.lastModified,
dateAdded = rawSong.dateAdded,
musicBrainzId = rawSong.tags.musicBrainzId, musicBrainzId = rawSong.tags.musicBrainzId,
name = rawSong.tags.name, name = rawSong.tags.name,
sortName = rawSong.tags.sortName, sortName = rawSong.tags.sortName,

View file

@ -63,6 +63,7 @@ private class ExtractStepImpl(
private val storedCovers: MutableStoredCovers private val storedCovers: MutableStoredCovers
) : ExtractStep { ) : ExtractStep {
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> { override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val startTime = System.currentTimeMillis()
val filterFlow = val filterFlow =
nodes.divert { nodes.divert {
when (it) { when (it) {
@ -127,7 +128,7 @@ private class ExtractStepImpl(
.mapNotNull { fileWith -> .mapNotNull { fileWith ->
val tags = tagParser.parse(fileWith.file, fileWith.with) val tags = tagParser.parse(fileWith.file, fileWith.with)
val cover = fileWith.with.cover?.let { storedCovers.write(it) } 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) .flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
@ -163,7 +164,8 @@ internal data class RawSong(
val file: DeviceFile, val file: DeviceFile,
val properties: Properties, val properties: Properties,
val tags: ParsedTags, val tags: ParsedTags,
val cover: Cover? val cover: Cover?,
val dateAdded: Long
) )
internal sealed interface ExtractedMusic { internal sealed interface ExtractedMusic {

View file

@ -65,8 +65,7 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
size = song.file.size, size = song.file.size,
format = Format.infer(song.file.mimeType, song.properties.mimeType), format = Format.infer(song.file.mimeType, song.properties.mimeType),
lastModified = song.file.lastModified, lastModified = song.file.lastModified,
// TODO: Figure out what to do with date added dateAdded = song.dateAdded,
dateAdded = song.file.lastModified,
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(), musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
name = interpretation.naming.name(song.tags.name, song.tags.sortName), name = interpretation.naming.name(song.tags.name, song.tags.sortName),
rawName = song.tags.name, rawName = song.tags.name,