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 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

View file

@ -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)

View file

@ -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))
}

View file

@ -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,

View file

@ -63,6 +63,7 @@ private class ExtractStepImpl(
private val storedCovers: MutableStoredCovers
) : ExtractStep {
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
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 {

View file

@ -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,