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

View file

@ -18,11 +18,11 @@
package org.oxycblt.musikr
import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.cache.TagCache
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)

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
* TagDatabase.kt is part of Auxio.
* CacheDatabase.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
@ -16,7 +16,7 @@
* 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 androidx.room.Dao
@ -30,36 +30,37 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.util.correctWhitespace
import org.oxycblt.musikr.tag.util.splitEscaped
@Database(entities = [CachedTags::class], version = 50, exportSchema = false)
abstract class TagDatabase : RoomDatabase() {
internal abstract fun cachedSongsDao(): TagDao
@Database(entities = [CachedInfo::class], version = 50, exportSchema = false)
abstract class CacheDatabase : RoomDatabase() {
internal abstract fun cachedSongsDao(): CacheInfoDao
companion object {
fun from(context: Context) =
Room.databaseBuilder(
context.applicationContext, TagDatabase::class.java, "music_cache.db")
context.applicationContext, CacheDatabase::class.java, "music_cache.db")
.fallbackToDestructiveMigration()
.build()
}
}
@Dao
internal interface TagDao {
@Query("SELECT * FROM CachedTags WHERE uri = :uri AND dateModified = :dateModified")
suspend fun selectTags(uri: String, dateModified: Long): CachedTags?
internal interface CacheInfoDao {
@Query("SELECT * FROM CachedInfo WHERE uri = :uri AND dateModified = :dateModified")
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
@TypeConverters(CachedTags.Converters::class)
internal data class CachedTags(
@TypeConverters(CachedInfo.Converters::class)
internal data class CachedInfo(
/**
* The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black
* box only used for comparison.
@ -108,31 +109,34 @@ internal data class CachedTags(
/** @see AudioFile.albumArtistSortNames */
val albumArtistSortNames: List<String> = listOf(),
/** @see AudioFile.genreNames */
val genreNames: List<String> = listOf()
val genreNames: List<String> = listOf(),
val cover: Cover? = null
) {
fun intoParsedTags() =
ParsedTags(
musicBrainzId = musicBrainzId,
name = name,
sortName = sortName,
durationMs = durationMs,
replayGainTrackAdjustment = replayGainTrackAdjustment,
replayGainAlbumAdjustment = replayGainAlbumAdjustment,
track = track,
disc = disc,
subtitle = subtitle,
date = date,
albumMusicBrainzId = albumMusicBrainzId,
albumName = albumName,
albumSortName = albumSortName,
releaseTypes = releaseTypes,
artistMusicBrainzIds = artistMusicBrainzIds,
artistNames = artistNames,
artistSortNames = artistSortNames,
albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
albumArtistNames = albumArtistNames,
albumArtistSortNames = albumArtistSortNames,
genreNames = genreNames)
fun intoCachedSong() =
CachedSong(
ParsedTags(
musicBrainzId = musicBrainzId,
name = name,
sortName = sortName,
durationMs = durationMs,
replayGainTrackAdjustment = replayGainTrackAdjustment,
replayGainAlbumAdjustment = replayGainAlbumAdjustment,
track = track,
disc = disc,
subtitle = subtitle,
date = date,
albumMusicBrainzId = albumMusicBrainzId,
albumName = albumName,
albumSortName = albumSortName,
releaseTypes = releaseTypes,
artistMusicBrainzIds = artistMusicBrainzIds,
artistNames = artistNames,
artistSortNames = artistSortNames,
albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
albumArtistNames = albumArtistNames,
albumArtistSortNames = albumArtistSortNames,
genreNames = genreNames),
cover)
object Converters {
@TypeConverter
@ -145,33 +149,38 @@ internal data class CachedTags(
@TypeConverter fun fromDate(date: Date?) = date?.toString()
@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 {
fun fromParsedTags(deviceFile: DeviceFile, parsedTags: ParsedTags) =
CachedTags(
fun fromCachedSong(deviceFile: DeviceFile, cachedSong: CachedSong) =
CachedInfo(
uri = deviceFile.uri.toString(),
dateModified = deviceFile.lastModified,
musicBrainzId = parsedTags.musicBrainzId,
name = parsedTags.name,
sortName = parsedTags.sortName,
durationMs = parsedTags.durationMs,
replayGainTrackAdjustment = parsedTags.replayGainTrackAdjustment,
replayGainAlbumAdjustment = parsedTags.replayGainAlbumAdjustment,
track = parsedTags.track,
disc = parsedTags.disc,
subtitle = parsedTags.subtitle,
date = parsedTags.date,
albumMusicBrainzId = parsedTags.albumMusicBrainzId,
albumName = parsedTags.albumName,
albumSortName = parsedTags.albumSortName,
releaseTypes = parsedTags.releaseTypes,
artistMusicBrainzIds = parsedTags.artistMusicBrainzIds,
artistNames = parsedTags.artistNames,
artistSortNames = parsedTags.artistSortNames,
albumArtistMusicBrainzIds = parsedTags.albumArtistMusicBrainzIds,
albumArtistNames = parsedTags.albumArtistNames,
albumArtistSortNames = parsedTags.albumArtistSortNames,
genreNames = parsedTags.genreNames)
musicBrainzId = cachedSong.parsedTags.musicBrainzId,
name = cachedSong.parsedTags.name,
sortName = cachedSong.parsedTags.sortName,
durationMs = cachedSong.parsedTags.durationMs,
replayGainTrackAdjustment = cachedSong.parsedTags.replayGainTrackAdjustment,
replayGainAlbumAdjustment = cachedSong.parsedTags.replayGainAlbumAdjustment,
track = cachedSong.parsedTags.track,
disc = cachedSong.parsedTags.disc,
subtitle = cachedSong.parsedTags.subtitle,
date = cachedSong.parsedTags.date,
albumMusicBrainzId = cachedSong.parsedTags.albumMusicBrainzId,
albumName = cachedSong.parsedTags.albumName,
albumSortName = cachedSong.parsedTags.albumSortName,
releaseTypes = cachedSong.parsedTags.releaseTypes,
artistMusicBrainzIds = cachedSong.parsedTags.artistMusicBrainzIds,
artistNames = cachedSong.parsedTags.artistNames,
artistSortNames = cachedSong.parsedTags.artistSortNames,
albumArtistMusicBrainzIds = cachedSong.parsedTags.albumArtistMusicBrainzIds,
albumArtistNames = cachedSong.parsedTags.albumArtistNames,
albumArtistSortNames = cachedSong.parsedTags.albumArtistSortNames,
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.merge
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.CachedSong
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverParser
import org.oxycblt.musikr.fs.query.DeviceFile
@ -51,14 +52,16 @@ constructor(
nodes
.filterIsInstance<ExploreNode.Audio>()
.map {
val tags = storage.tagCache.read(it.file)
val tags = storage.cache.read(it.file)
MaybeCachedSong(it.file, tags)
}
.flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED)
val (cachedSongs, uncachedSongs) =
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 extractedSongs =
@ -77,7 +80,7 @@ constructor(
val writtenSongs =
merge(*extractedSongs)
.map {
storage.tagCache.write(it.file, it.tags)
storage.cache.write(it.file, CachedSong(it.tags, it.cover))
it
}
.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 {

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