diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt index 73e56b29f..0eea209b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/coil/Components.kt @@ -47,7 +47,7 @@ import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.StoredCovers class CoverKeyer @Inject constructor() : Keyer { - override fun key(data: Cover, options: Options) = "${data.key}&${options.size}" + override fun key(data: Cover, options: Options) = "${data.id}&${options.size}" } class CoverFetcher @@ -56,16 +56,14 @@ private constructor( private val cover: Cover, private val size: Size, ) : Fetcher { - private val storedCovers = StoredCovers.from(context, "covers") - override suspend fun fetch(): FetchResult? { val streams = when (val cover = cover) { - is Cover.Single -> listOfNotNull(storedCovers.read(cover)) + is Cover.Single -> listOfNotNull(cover.open()) is Cover.Multi -> buildList { for (single in cover.all) { - storedCovers.read(single)?.let { add(it) } + single.open()?.let { add(it) } if (size == 4) { break } 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 e8564f861..65bd954a4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -30,8 +30,10 @@ import kotlinx.coroutines.yield import org.oxycblt.auxio.music.MusicRepository.IndexingWorker import org.oxycblt.musikr.IndexingProgress import org.oxycblt.musikr.Interpretation +import org.oxycblt.musikr.Library import org.oxycblt.musikr.Music import org.oxycblt.musikr.Musikr +import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song import org.oxycblt.musikr.Storage @@ -57,7 +59,7 @@ import timber.log.Timber as L */ interface MusicRepository { /** The current library */ - val library: RevisionedLibrary? + val library: Library? /** The current state of music loading. Null if no load has occurred yet. */ val indexingState: IndexingState? @@ -222,7 +224,7 @@ constructor( private val indexingListeners = mutableListOf() @Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null - @Volatile override var library: MutableRevisionedLibrary? = null + @Volatile override var library: MutableLibrary? = null @Volatile private var previousCompletedState: IndexingState.Completed? = null @Volatile private var currentIndexingState: IndexingState? = null override val indexingState: IndexingState? @@ -365,18 +367,18 @@ constructor( val revision: UUID val storage: Storage if (withCache) { - revision = this.library?.revision ?: musicSettings.revision + revision = musicSettings.revision storage = Storage( Cache.full(cacheDatabase), - StoredCovers.from(context, "covers_$revision"), + MutableRevisionedStoredCovers(context, revision), StoredPlaylists.from(playlistDatabase)) } else { revision = UUID.randomUUID() storage = Storage( Cache.writeOnly(cacheDatabase), - StoredCovers.from(context, "covers_$revision"), + MutableRevisionedStoredCovers(context, revision), StoredPlaylists.from(playlistDatabase)) } @@ -385,8 +387,6 @@ constructor( val newLibrary = Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress) - val revisionedLibrary = MutableRevisionedLibrary(revision, newLibrary) - emitIndexingCompletion(null) // We want to make sure that all reads and writes are synchronized due to the sheer @@ -408,7 +408,7 @@ constructor( return } - this.library = revisionedLibrary + this.library = newLibrary } // Consumers expect their updates to be on the main thread (notably PlaybackService), @@ -418,9 +418,7 @@ constructor( } // Quietly update the revision if needed (this way we don't disrupt any new loads) - if (!withCache) { - musicSettings.revision = revisionedLibrary.revision - } + musicSettings.revision = revision } private suspend fun emitIndexingProgress(progress: IndexingProgress) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/RevisionedLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/RevisionedLibrary.kt index bd398776d..a3e9f46eb 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/RevisionedLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/RevisionedLibrary.kt @@ -15,33 +15,59 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music +import android.content.Context +import androidx.media3.common.util.Log +import org.oxycblt.auxio.util.unlikelyToBeNull import java.util.UUID -import org.oxycblt.musikr.Library -import org.oxycblt.musikr.MutableLibrary -import org.oxycblt.musikr.Playlist -import org.oxycblt.musikr.Song +import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.cover.MutableStoredCovers +import org.oxycblt.musikr.cover.StoredCovers -interface RevisionedLibrary : Library { - val revision: UUID +open class RevisionedStoredCovers( + private val context: Context, + private val revision: UUID? +) : StoredCovers { + protected val inner = revision?.let { StoredCovers.at(context, "covers_$it") } + + override suspend fun obtain(id: String): RevisionedCover? { + val split = id.split('@', limit = 2) + if (split.size != 2) return null + val (coverId, coverRevisionStr) = split + val coverRevision = coverRevisionStr.toUuidOrNull() ?: return null + Log.d("RevisionedStoredCovers", "$coverId $coverRevision $revision") + if (revision != null) { + if (coverRevision != revision) { + return null + } + val storedCovers = unlikelyToBeNull(inner) + return storedCovers.obtain(coverId)?.let { RevisionedCover(revision, it) } + } else { + val storedCovers = StoredCovers.at(context, "covers_$coverRevision") + return storedCovers.obtain(coverId)?.let { RevisionedCover(coverRevision, it) } + } + } } -class MutableRevisionedLibrary(override val revision: UUID, private val inner: MutableLibrary) : - RevisionedLibrary, Library by inner, MutableLibrary { - override suspend fun createPlaylist(name: String, songs: List) = - MutableRevisionedLibrary(revision, inner.createPlaylist(name, songs)) - - override suspend fun renamePlaylist(playlist: Playlist, name: String) = - MutableRevisionedLibrary(revision, inner.renamePlaylist(playlist, name)) - - override suspend fun addToPlaylist(playlist: Playlist, songs: List) = - MutableRevisionedLibrary(revision, inner.addToPlaylist(playlist, songs)) - - override suspend fun rewritePlaylist(playlist: Playlist, songs: List) = - MutableRevisionedLibrary(revision, inner.rewritePlaylist(playlist, songs)) - - override suspend fun deletePlaylist(playlist: Playlist) = - MutableRevisionedLibrary(revision, inner.deletePlaylist(playlist)) +class MutableRevisionedStoredCovers( + context: Context, + private val revision: UUID +) : RevisionedStoredCovers(context, revision), MutableStoredCovers { + override suspend fun write(data: ByteArray): RevisionedCover? { + return unlikelyToBeNull(inner).write(data)?.let { RevisionedCover(revision, it) } + } } + +class RevisionedCover(private val revision: UUID, val inner: Cover.Single) : Cover.Single by inner { + override val id: String + get() = "${inner.id}@${revision}" +} + +internal fun String.toUuidOrNull(): UUID? = + try { + UUID.fromString(this) + } catch (e: IllegalArgumentException) { + null + } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index 8b2ea8807..a428157e1 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -19,6 +19,7 @@ package org.oxycblt.musikr import org.oxycblt.musikr.cache.Cache +import org.oxycblt.musikr.cover.MutableStoredCovers import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.tag.interpret.Naming @@ -26,7 +27,7 @@ import org.oxycblt.musikr.tag.interpret.Separators data class Storage( val cache: Cache, - val storedCovers: StoredCovers, + val storedCovers: MutableStoredCovers, val storedPlaylists: StoredPlaylists ) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt index 89639a33c..81de567d9 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -18,11 +18,12 @@ package org.oxycblt.musikr.cache +import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.pipeline.RawSong interface Cache { - suspend fun read(file: DeviceFile): CacheResult + suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult suspend fun write(song: RawSong) @@ -40,9 +41,9 @@ sealed interface CacheResult { } private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache { - override suspend fun read(file: DeviceFile) = + override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let { - CacheResult.Hit(it.intoRawSong(file)) + CacheResult.Hit(it.intoRawSong(file, storedCovers)) } ?: CacheResult.Miss(file) override suspend fun write(song: RawSong) = @@ -50,7 +51,7 @@ private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache { } private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache { - override suspend fun read(file: DeviceFile) = CacheResult.Miss(file) + override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = CacheResult.Miss(file) override suspend fun write(song: RawSong) = cacheInfoDao.updateSong(CachedSong.fromRawSong(song)) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index da8721f64..76be6bd87 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -31,6 +31,7 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.pipeline.RawSong @@ -89,9 +90,9 @@ internal data class CachedSong( val genreNames: List, val replayGainTrackAdjustment: Float?, val replayGainAlbumAdjustment: Float?, - val cover: Cover.Single?, + val coverId: String?, ) { - fun intoRawSong(file: DeviceFile) = + suspend fun intoRawSong(file: DeviceFile, storedCovers: StoredCovers) = RawSong( file, Properties(mimeType, durationMs, bitrateHz, sampleRateHz), @@ -117,7 +118,7 @@ internal data class CachedSong( genreNames = genreNames, replayGainTrackAdjustment = replayGainTrackAdjustment, replayGainAlbumAdjustment = replayGainAlbumAdjustment), - cover) + coverId?.let { storedCovers.obtain(it) }) object Converters { @TypeConverter @@ -130,10 +131,6 @@ internal data class CachedSong( @TypeConverter fun fromDate(date: Date?) = date?.toString() @TypeConverter fun toDate(string: String?) = string?.let(Date::from) - - @TypeConverter fun fromCover(cover: Cover.Single?) = cover?.key - - @TypeConverter fun toCover(key: String?) = key?.let { Cover.Single(it) } } companion object { @@ -162,7 +159,7 @@ internal data class CachedSong( genreNames = rawSong.tags.genreNames, replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment, replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment, - cover = rawSong.cover, + coverId = rawSong.cover?.id, mimeType = rawSong.properties.mimeType, bitrateHz = rawSong.properties.bitrateKbps, sampleRateHz = rawSong.properties.sampleRateHz) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt index c206bfd7d..5f7376ec2 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt @@ -19,27 +19,26 @@ package org.oxycblt.musikr.cover import org.oxycblt.musikr.Song +import java.io.InputStream sealed interface Cover { - val key: String + val id: String - data class Single(override val key: String) : Cover + interface Single : Cover { + suspend fun open(): InputStream? + } class Multi(val all: List) : Cover { - override val key = "multi@${all.hashCode()}" + override val id = "multi@${all.hashCode()}" } companion object { - fun nil() = Multi(listOf()) - - fun single(key: String) = Single(key) - fun multi(songs: Collection) = order(songs).run { Multi(this) } private fun order(songs: Collection) = songs .mapNotNull { it.cover } - .groupBy { it.key } + .groupBy { it.id } .entries .sortedByDescending { it.key } .sortedByDescending { it.value.size } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt index e1cb17000..e7cae0b48 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt @@ -15,29 +15,34 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.musikr.cover import android.content.Context +import android.util.Log import java.io.File import java.io.IOException -import java.io.InputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import java.io.InputStream +import java.io.OutputStream -internal interface CoverFiles { - suspend fun read(id: String): InputStream? - - suspend fun write(id: String, data: ByteArray) +interface CoverFiles { + suspend fun find(id: String): CoverFile? + suspend fun write(id: String, data: ByteArray): CoverFile? companion object { - fun from(context: Context, path: String, format: CoverFormat): CoverFiles = - CoverFilesImpl(File(context.filesDir, path).also { it.mkdirs() }, format) + fun at(context: Context, path: String): CoverFiles = + CoverFilesImpl(File(context.filesDir, path).also { it.mkdirs() }, CoverFormat.webp()) } } +interface CoverFile { + suspend fun open(): InputStream? +} + private class CoverFilesImpl(private val dir: File, private val coverFormat: CoverFormat) : CoverFiles { private val fileMutexes = mutableMapOf() @@ -47,32 +52,34 @@ private class CoverFilesImpl(private val dir: File, private val coverFormat: Cov return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } } } - override suspend fun read(id: String): InputStream? = + override suspend fun find(id: String): CoverFile? = withContext(Dispatchers.IO) { try { - File(dir, getTargetFilePath(id)).inputStream() + File(dir, getTargetFilePath(id)).takeIf { it.exists() }?.let { CoverFileImpl(it) } } catch (e: IOException) { null } } - override suspend fun write(id: String, data: ByteArray) { + override suspend fun write(id: String, data: ByteArray): CoverFile? { val fileMutex = getMutexForFile(id) - - fileMutex.withLock { + return fileMutex.withLock { val targetFile = File(dir, getTargetFilePath(id)) - if (targetFile.exists()) { - return - } - withContext(Dispatchers.IO) { - val tempFile = File(dir, getTempFilePath(id)) + if (!targetFile.exists()) { + withContext(Dispatchers.IO) { + val tempFile = File(dir, getTempFilePath(id)) - try { - tempFile.outputStream().use { coverFormat.transcodeInto(data, it) } - tempFile.renameTo(targetFile) - } catch (e: IOException) { - tempFile.delete() + try { + tempFile.outputStream().use { coverFormat.transcodeInto(data, it) } + tempFile.renameTo(targetFile) + CoverFileImpl(targetFile) + } catch (e: IOException) { + tempFile.delete() + null + } } + } else { + CoverFileImpl(targetFile) } } } @@ -81,3 +88,7 @@ private class CoverFilesImpl(private val dir: File, private val coverFormat: Cov private fun getTempFilePath(name: String) = "${getTargetFilePath(name)}.tmp" } + +private class CoverFileImpl(private val file: File) : CoverFile { + override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() } +} \ No newline at end of file diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt index a1340990d..f5fc31ab7 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt @@ -15,33 +15,42 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.musikr.cover import android.content.Context import java.io.InputStream interface StoredCovers { - suspend fun read(cover: Cover.Single): InputStream? - - suspend fun write(data: ByteArray): Cover.Single? + suspend fun obtain(id: String): Cover.Single? companion object { - fun from(context: Context, path: String): StoredCovers = + fun at(context: Context, path: String): MutableStoredCovers = FileStoredCovers( - CoverIdentifier.md5(), CoverFiles.from(context, path, CoverFormat.webp())) + CoverIdentifier.md5(), CoverFiles.at(context, path) + ) } } +interface MutableStoredCovers : StoredCovers { + suspend fun write(data: ByteArray): Cover.Single? +} + private class FileStoredCovers( private val coverIdentifier: CoverIdentifier, private val coverFiles: CoverFiles -) : StoredCovers { - override suspend fun read(cover: Cover.Single) = coverFiles.read(cover.key) +) : StoredCovers, MutableStoredCovers { + override suspend fun obtain(id: String) = coverFiles.find(id)?.let { FileCover(id, it) } override suspend fun write(data: ByteArray) = - coverIdentifier.identify(data).let { key -> - coverFiles.write(key, data) - Cover.Single(key) + coverIdentifier.identify(data).let { id -> + coverFiles.write(id, data)?.let { FileCover(id, it) } } } + +private class FileCover( + override val id: String, + private val coverFile: CoverFile +) : Cover.Single { + override suspend fun open() = coverFile.open() +} \ No newline at end of file diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt index 2ccc992be..ac4123031 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt @@ -18,6 +18,7 @@ package org.oxycblt.musikr.pipeline +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -78,7 +79,10 @@ private class EvaluateStepImpl( val graphBuild = merge( filterFlow.manager, - preSongs.onEach { wrap(it, graphBuilder::add) }, + preSongs.onEach { + Log.d("EvaluateStep", it.toString()) + wrap(it, graphBuilder::add) + }, prePlaylists.onEach { wrap(it, graphBuilder::add) }) graphBuild.collect() val graph = graphBuilder.build() diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 89f98b016..72be20214 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -32,6 +32,7 @@ import org.oxycblt.musikr.Storage import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.CacheResult import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.cover.MutableStoredCovers import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.metadata.MetadataExtractor @@ -59,7 +60,7 @@ private class ExtractStepImpl( private val metadataExtractor: MetadataExtractor, private val tagParser: TagParser, private val cache: Cache, - private val storedCovers: StoredCovers + private val storedCovers: MutableStoredCovers ) : ExtractStep { override fun extract(nodes: Flow): Flow { val filterFlow = @@ -74,7 +75,7 @@ private class ExtractStepImpl( val cacheResults = audioNodes - .map { wrap(it, cache::read) } + .map { wrap(it) { file -> cache.read(file, storedCovers)} } .flowOn(Dispatchers.IO) .buffer(Channel.UNLIMITED) val cacheFlow =