musikr: add cover key to cache

This commit is contained in:
Alexander Capehart 2024-12-11 17:08:35 -07:00
parent 42390f4b3f
commit f13c1e364b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 136 additions and 121 deletions

View file

@ -26,7 +26,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext 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.tag.cache.TagDatabase import org.oxycblt.musikr.cache.CacheDatabase
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -41,5 +41,5 @@ interface MusicModule {
class MusikrShimModule { class MusikrShimModule {
@Singleton @Singleton
@Provides @Provides
fun tagDatabase(@ApplicationContext context: Context) = TagDatabase.from(context) fun tagDatabase(@ApplicationContext context: Context) = CacheDatabase.from(context)
} }

View file

@ -36,10 +36,10 @@ 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.CacheDatabase
import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.cache.TagCache
import org.oxycblt.musikr.tag.cache.TagDatabase
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
import timber.log.Timber as L import timber.log.Timber as L
@ -212,7 +212,7 @@ class MusicRepositoryImpl
constructor( constructor(
private val musikr: Musikr, private val musikr: Musikr,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val tagDatabase: TagDatabase, private val cacheDatabase: CacheDatabase,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
) : MusicRepository { ) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>() private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
@ -361,10 +361,10 @@ constructor(
val storage = val storage =
if (withCache) { if (withCache) {
Storage(TagCache.full(tagDatabase), StoredCovers.from(context, "covers")) Storage(Cache.full(cacheDatabase), StoredCovers.from(context, "covers"))
} else { } else {
// TODO: Revisioned covers // TODO: Revisioned covers
Storage(TagCache.writeOnly(tagDatabase), StoredCovers.from(context, "covers")) Storage(Cache.writeOnly(cacheDatabase), StoredCovers.from(context, "covers"))
} }
val newLibrary = val newLibrary =
musikr.run( musikr.run(

View file

@ -18,11 +18,11 @@
package org.oxycblt.musikr package org.oxycblt.musikr
import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.cache.TagCache
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
data class Storage(val tagCache: TagCache, val storedCovers: StoredCovers) data class Storage(val cache: Cache, val storedCovers: StoredCovers)
data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators) data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators)

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 Auxio Project
* Cache.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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.cache
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.tag.parse.ParsedTags
interface Cache {
suspend fun read(file: DeviceFile): CachedSong?
suspend fun write(file: DeviceFile, song: CachedSong)
companion object {
fun full(db: CacheDatabase): Cache = FullCache(db.cachedSongsDao())
fun writeOnly(db: CacheDatabase): Cache = WriteOnlyCache(db.cachedSongsDao())
}
}
data class CachedSong(val parsedTags: ParsedTags, val cover: Cover?)
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
override suspend fun read(file: DeviceFile) =
cacheInfoDao.selectInfo(file.uri.toString(), file.lastModified)?.intoCachedSong()
override suspend fun write(file: DeviceFile, song: CachedSong) =
cacheInfoDao.updateInfo(CachedInfo.fromCachedSong(file, song))
}
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
override suspend fun read(file: DeviceFile) = null
override suspend fun write(file: DeviceFile, song: CachedSong) =
cacheInfoDao.updateInfo(CachedInfo.fromCachedSong(file, song))
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2023 Auxio Project * Copyright (c) 2023 Auxio Project
* TagDatabase.kt is part of Auxio. * CacheDatabase.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.musikr.tag.cache package org.oxycblt.musikr.cache
import android.content.Context import android.content.Context
import androidx.room.Dao import androidx.room.Dao
@ -30,36 +30,37 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters import androidx.room.TypeConverters
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.util.correctWhitespace import org.oxycblt.musikr.tag.util.correctWhitespace
import org.oxycblt.musikr.tag.util.splitEscaped import org.oxycblt.musikr.tag.util.splitEscaped
@Database(entities = [CachedTags::class], version = 50, exportSchema = false) @Database(entities = [CachedInfo::class], version = 50, exportSchema = false)
abstract class TagDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
internal abstract fun cachedSongsDao(): TagDao internal abstract fun cachedSongsDao(): CacheInfoDao
companion object { companion object {
fun from(context: Context) = fun from(context: Context) =
Room.databaseBuilder( Room.databaseBuilder(
context.applicationContext, TagDatabase::class.java, "music_cache.db") context.applicationContext, CacheDatabase::class.java, "music_cache.db")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
} }
} }
@Dao @Dao
internal interface TagDao { internal interface CacheInfoDao {
@Query("SELECT * FROM CachedTags WHERE uri = :uri AND dateModified = :dateModified") @Query("SELECT * FROM CachedInfo WHERE uri = :uri AND dateModified = :dateModified")
suspend fun selectTags(uri: String, dateModified: Long): CachedTags? suspend fun selectInfo(uri: String, dateModified: Long): CachedInfo?
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateTags(cachedTags: CachedTags) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateInfo(cachedInfo: CachedInfo)
} }
@Entity @Entity
@TypeConverters(CachedTags.Converters::class) @TypeConverters(CachedInfo.Converters::class)
internal data class CachedTags( internal data class CachedInfo(
/** /**
* The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black * The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black
* box only used for comparison. * box only used for comparison.
@ -108,9 +109,11 @@ internal data class CachedTags(
/** @see AudioFile.albumArtistSortNames */ /** @see AudioFile.albumArtistSortNames */
val albumArtistSortNames: List<String> = listOf(), val albumArtistSortNames: List<String> = listOf(),
/** @see AudioFile.genreNames */ /** @see AudioFile.genreNames */
val genreNames: List<String> = listOf() val genreNames: List<String> = listOf(),
val cover: Cover? = null
) { ) {
fun intoParsedTags() = fun intoCachedSong() =
CachedSong(
ParsedTags( ParsedTags(
musicBrainzId = musicBrainzId, musicBrainzId = musicBrainzId,
name = name, name = name,
@ -132,7 +135,8 @@ internal data class CachedTags(
albumArtistMusicBrainzIds = albumArtistMusicBrainzIds, albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
albumArtistNames = albumArtistNames, albumArtistNames = albumArtistNames,
albumArtistSortNames = albumArtistSortNames, albumArtistSortNames = albumArtistSortNames,
genreNames = genreNames) genreNames = genreNames),
cover)
object Converters { object Converters {
@TypeConverter @TypeConverter
@ -145,33 +149,38 @@ internal data class CachedTags(
@TypeConverter fun fromDate(date: Date?) = date?.toString() @TypeConverter fun fromDate(date: Date?) = date?.toString()
@TypeConverter fun toDate(string: String?) = string?.let(Date::from) @TypeConverter fun toDate(string: String?) = string?.let(Date::from)
@TypeConverter fun fromCover(cover: Cover?) = cover?.key
@TypeConverter fun toCover(key: String?) = key?.let { Cover.Single(it) }
} }
companion object { companion object {
fun fromParsedTags(deviceFile: DeviceFile, parsedTags: ParsedTags) = fun fromCachedSong(deviceFile: DeviceFile, cachedSong: CachedSong) =
CachedTags( CachedInfo(
uri = deviceFile.uri.toString(), uri = deviceFile.uri.toString(),
dateModified = deviceFile.lastModified, dateModified = deviceFile.lastModified,
musicBrainzId = parsedTags.musicBrainzId, musicBrainzId = cachedSong.parsedTags.musicBrainzId,
name = parsedTags.name, name = cachedSong.parsedTags.name,
sortName = parsedTags.sortName, sortName = cachedSong.parsedTags.sortName,
durationMs = parsedTags.durationMs, durationMs = cachedSong.parsedTags.durationMs,
replayGainTrackAdjustment = parsedTags.replayGainTrackAdjustment, replayGainTrackAdjustment = cachedSong.parsedTags.replayGainTrackAdjustment,
replayGainAlbumAdjustment = parsedTags.replayGainAlbumAdjustment, replayGainAlbumAdjustment = cachedSong.parsedTags.replayGainAlbumAdjustment,
track = parsedTags.track, track = cachedSong.parsedTags.track,
disc = parsedTags.disc, disc = cachedSong.parsedTags.disc,
subtitle = parsedTags.subtitle, subtitle = cachedSong.parsedTags.subtitle,
date = parsedTags.date, date = cachedSong.parsedTags.date,
albumMusicBrainzId = parsedTags.albumMusicBrainzId, albumMusicBrainzId = cachedSong.parsedTags.albumMusicBrainzId,
albumName = parsedTags.albumName, albumName = cachedSong.parsedTags.albumName,
albumSortName = parsedTags.albumSortName, albumSortName = cachedSong.parsedTags.albumSortName,
releaseTypes = parsedTags.releaseTypes, releaseTypes = cachedSong.parsedTags.releaseTypes,
artistMusicBrainzIds = parsedTags.artistMusicBrainzIds, artistMusicBrainzIds = cachedSong.parsedTags.artistMusicBrainzIds,
artistNames = parsedTags.artistNames, artistNames = cachedSong.parsedTags.artistNames,
artistSortNames = parsedTags.artistSortNames, artistSortNames = cachedSong.parsedTags.artistSortNames,
albumArtistMusicBrainzIds = parsedTags.albumArtistMusicBrainzIds, albumArtistMusicBrainzIds = cachedSong.parsedTags.albumArtistMusicBrainzIds,
albumArtistNames = parsedTags.albumArtistNames, albumArtistNames = cachedSong.parsedTags.albumArtistNames,
albumArtistSortNames = parsedTags.albumArtistSortNames, albumArtistSortNames = cachedSong.parsedTags.albumArtistSortNames,
genreNames = parsedTags.genreNames) genreNames = cachedSong.parsedTags.genreNames,
cover = cachedSong.cover)
} }
} }

View file

@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.CachedSong
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverParser import org.oxycblt.musikr.cover.CoverParser
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
@ -51,14 +52,16 @@ constructor(
nodes nodes
.filterIsInstance<ExploreNode.Audio>() .filterIsInstance<ExploreNode.Audio>()
.map { .map {
val tags = storage.tagCache.read(it.file) val tags = storage.cache.read(it.file)
MaybeCachedSong(it.file, tags) MaybeCachedSong(it.file, tags)
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val (cachedSongs, uncachedSongs) = val (cachedSongs, uncachedSongs) =
cacheResults.mapPartition { cacheResults.mapPartition {
it.tags?.let { tags -> ExtractedMusic.Song(it.file, tags, null) } it.cachedSong?.let { song ->
ExtractedMusic.Song(it.file, song.parsedTags, song.cover)
}
} }
val split = uncachedSongs.distribute(8) val split = uncachedSongs.distribute(8)
val extractedSongs = val extractedSongs =
@ -77,7 +80,7 @@ constructor(
val writtenSongs = val writtenSongs =
merge(*extractedSongs) merge(*extractedSongs)
.map { .map {
storage.tagCache.write(it.file, it.tags) storage.cache.write(it.file, CachedSong(it.tags, it.cover))
it it
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
@ -89,7 +92,7 @@ constructor(
) )
} }
data class MaybeCachedSong(val file: DeviceFile, val tags: ParsedTags?) data class MaybeCachedSong(val file: DeviceFile, val cachedSong: CachedSong?)
} }
sealed interface ExtractedMusic { sealed interface ExtractedMusic {

View file

@ -1,49 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* TagCache.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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.tag.cache
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.tag.parse.ParsedTags
interface TagCache {
suspend fun read(file: DeviceFile): ParsedTags?
suspend fun write(file: DeviceFile, tags: ParsedTags)
companion object {
fun full(db: TagDatabase): TagCache = FullTagCache(db.cachedSongsDao())
fun writeOnly(db: TagDatabase): TagCache = WriteOnlyTagCache(db.cachedSongsDao())
}
}
private class FullTagCache(private val tagDao: TagDao) : TagCache {
override suspend fun read(file: DeviceFile) =
tagDao.selectTags(file.uri.toString(), file.lastModified)?.intoParsedTags()
override suspend fun write(file: DeviceFile, tags: ParsedTags) =
tagDao.updateTags(CachedTags.fromParsedTags(file, tags))
}
private class WriteOnlyTagCache(private val tagDao: TagDao) : TagCache {
override suspend fun read(file: DeviceFile) = null
override suspend fun write(file: DeviceFile, tags: ParsedTags) =
tagDao.updateTags(CachedTags.fromParsedTags(file, tags))
}