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 e9e6d664c..790217807 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicModule.kt @@ -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) } 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 76398af1b..d98a54c45 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -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() @@ -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( diff --git a/app/src/main/java/org/oxycblt/musikr/Config.kt b/app/src/main/java/org/oxycblt/musikr/Config.kt index 4d6d9728f..741b29cfc 100644 --- a/app/src/main/java/org/oxycblt/musikr/Config.kt +++ b/app/src/main/java/org/oxycblt/musikr/Config.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/app/src/main/java/org/oxycblt/musikr/cache/Cache.kt new file mode 100644 index 000000000..d02557e15 --- /dev/null +++ b/app/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -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 . + */ + +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)) +} diff --git a/app/src/main/java/org/oxycblt/musikr/tag/cache/TagDatabase.kt b/app/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt similarity index 53% rename from app/src/main/java/org/oxycblt/musikr/tag/cache/TagDatabase.kt rename to app/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index a87407e9f..bfd2f40e0 100644 --- a/app/src/main/java/org/oxycblt/musikr/tag/cache/TagDatabase.kt +++ b/app/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -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 . */ -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 = listOf(), /** @see AudioFile.genreNames */ - val genreNames: List = listOf() + val genreNames: List = 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) } } diff --git a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 235ade081..4e8245643 100644 --- a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -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() .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 { diff --git a/app/src/main/java/org/oxycblt/musikr/tag/cache/TagCache.kt b/app/src/main/java/org/oxycblt/musikr/tag/cache/TagCache.kt deleted file mode 100644 index 400cdb3f0..000000000 --- a/app/src/main/java/org/oxycblt/musikr/tag/cache/TagCache.kt +++ /dev/null @@ -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 . - */ - -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)) -}