From 8cc939b58d291b696f40c5004c149312e4df73d2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 20 Dec 2024 12:31:02 -0500 Subject: [PATCH] musikr: bundle cover resolution with key This is a partial refactor, I'm still trying to find a good approach to a revisionable system. --- .../java/org/oxycblt/auxio/image/CoverView.kt | 2 +- .../oxycblt/auxio/image/coil/Components.kt | 8 +- .../oxycblt/auxio/music/MusicRepository.kt | 7 +- .../oxycblt/auxio/music/RevisionedLibrary.kt | 2 + .../main/java/org/oxycblt/musikr/Config.kt | 2 +- .../java/org/oxycblt/musikr/cache/Cache.kt | 10 +- .../org/oxycblt/musikr/cache/CacheDatabase.kt | 15 +-- .../java/org/oxycblt/musikr/cover/AppFiles.kt | 118 ++++++++++++++++++ .../java/org/oxycblt/musikr/cover/Cover.kt | 37 +++--- .../org/oxycblt/musikr/cover/CoverFiles.kt | 83 ------------ .../org/oxycblt/musikr/cover/StoredCovers.kt | 62 ++++++--- .../main/java/org/oxycblt/musikr/fs/Path.kt | 2 +- .../org/oxycblt/musikr/model/AlbumImpl.kt | 2 +- .../org/oxycblt/musikr/model/ArtistImpl.kt | 2 +- .../org/oxycblt/musikr/model/GenreImpl.kt | 2 +- .../org/oxycblt/musikr/model/PlaylistImpl.kt | 2 +- .../oxycblt/musikr/pipeline/ExtractStep.kt | 11 +- 17 files changed, 216 insertions(+), 151 deletions(-) create mode 100644 musikr/src/main/java/org/oxycblt/musikr/cover/AppFiles.kt delete mode 100644 musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index 704378c11..f26bd8e8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -372,7 +372,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * @param errorRes The resource of the error drawable to use if the cover cannot be loaded. */ fun bind(songs: List, desc: String, @DrawableRes errorRes: Int) = - bindImpl(Cover.multi(songs), desc, errorRes) + bindImpl(Cover.Multi.from(songs.mapNotNull { it.cover }), desc, errorRes) private fun bindImpl(cover: Cover?, desc: String, @DrawableRes errorRes: Int) { val request = 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..dd580cbdf 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.resolve()) is Cover.Multi -> buildList { for (single in cover.all) { - storedCovers.read(single)?.let { add(it) } + single.resolve()?.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..90e2b9844 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -38,6 +38,7 @@ 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.fs.Components import org.oxycblt.musikr.playlist.db.PlaylistDatabase import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.tag.interpret.Naming @@ -368,15 +369,15 @@ constructor( revision = this.library?.revision ?: musicSettings.revision storage = Storage( - Cache.full(cacheDatabase), - StoredCovers.from(context, "covers_$revision"), + Cache.writeOnly(cacheDatabase), + StoredCovers.editor(context, Components.parseUnix("covers_${UUID.randomUUID()}")), StoredPlaylists.from(playlistDatabase)) } else { revision = UUID.randomUUID() storage = Storage( Cache.writeOnly(cacheDatabase), - StoredCovers.from(context, "covers_$revision"), + StoredCovers.editor(context, Components.parseUnix("covers_$revision")), StoredPlaylists.from(playlistDatabase)) } 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..c7ba67933 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/RevisionedLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/RevisionedLibrary.kt @@ -18,6 +18,8 @@ package org.oxycblt.auxio.music +import org.oxycblt.musikr.Album +import org.oxycblt.musikr.Artist import java.util.UUID import org.oxycblt.musikr.Library import org.oxycblt.musikr.MutableLibrary diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index 8b2ea8807..8b5f9ee0d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -26,7 +26,7 @@ import org.oxycblt.musikr.tag.interpret.Separators data class Storage( val cache: Cache, - val storedCovers: StoredCovers, + val coverEditor: StoredCovers.Editor, 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..f7dce3c20 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): CacheResult = 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,8 @@ 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..c4c930acc 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.find(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,9 +159,9 @@ internal data class CachedSong( genreNames = rawSong.tags.genreNames, replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment, replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment, - cover = rawSong.cover, mimeType = rawSong.properties.mimeType, bitrateHz = rawSong.properties.bitrateKbps, - sampleRateHz = rawSong.properties.sampleRateHz) + sampleRateHz = rawSong.properties.sampleRateHz, + coverId = rawSong.cover?.id) } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/AppFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/AppFiles.kt new file mode 100644 index 000000000..4d5e95f6a --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/AppFiles.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 Auxio Project + * AppFiles.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.cover + +import android.content.Context +import android.util.Log +import java.io.File +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.oxycblt.musikr.fs.Components +import org.oxycblt.musikr.util.update +import java.io.InputStream +import java.io.OutputStream +import java.security.MessageDigest +import java.util.UUID + +internal interface AppFiles { + suspend fun read(path: Components): AppFile? + + suspend fun write(path: Components, block: suspend (OutputStream) -> Unit): AppFile? + + companion object { + fun from(context: Context): AppFiles = + AppFilesImpl(context.filesDir) + } +} + +interface AppFile { + val path: Components + suspend fun open(): InputStream? +} + +private class AppFilesImpl(private val rootDir: File) : + AppFiles { + private val tempDir = File(rootDir, "tmp-${UUID.randomUUID()}") + private val fileMutexes = mutableMapOf() + private val mapMutex = Mutex() + + private suspend fun getMutexForFile(path: String): Mutex { + return mapMutex.withLock { fileMutexes.getOrPut(path) { Mutex() } } + } + + override suspend fun read(path: Components): AppFile? = + withContext(Dispatchers.IO) { + val file = rootDir.resolve(path.unixString) + if (file.exists()) { + AppFileImpl(path, file) + } else { + null + } + } + + override suspend fun write(path: Components, block: suspend (OutputStream) -> Unit): AppFile? = + withContext(Dispatchers.IO) { + if (!tempDir.exists()) { + tempDir.mkdirs() + } + val parentDir = rootDir.resolve(path.parent().toString()) + if (parentDir.isFile) { + parentDir.delete() + } + if (!parentDir.exists()) { + parentDir.mkdirs() + } + val pathString = path.unixString + val fileMutex = getMutexForFile(pathString) + + fileMutex.withLock { + val targetFile = rootDir.resolve(pathString) + if (targetFile.exists()) { + return@withLock AppFileImpl(path, targetFile) + } + val tempFile = tempDir.resolve(pathString.sha256()) + + try { + block(tempFile.outputStream()) + tempFile.renameTo(targetFile) + AppFileImpl(path, targetFile) + } catch (e: IOException) { + tempFile.delete() + null + } + } + } +} + +class AppFileImpl( + override val path: Components, + private val file: File +) : AppFile { + override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() } +} + +@OptIn(ExperimentalStdlibApi::class) +private fun String.sha256() = MessageDigest.getInstance("SHA-256").let { + it.update(this) + it.digest().toHexString() +} + 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..9b9b04769 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt @@ -15,34 +15,31 @@ * 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 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 - - class Multi(val all: List) : Cover { - override val key = "multi@${all.hashCode()}" + interface Single : Cover { + suspend fun resolve(): InputStream? } - companion object { - fun nil() = Multi(listOf()) + class Multi private constructor(val all: List) : Cover { + override val id = "multi@${all.hashCode()}" - 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 } - .entries - .sortedByDescending { it.key } - .sortedByDescending { it.value.size } - .map { it.value.first() } + companion object { + fun from(covers: Collection) = + Multi( + covers + .groupBy { it.id } + .entries + .sortedByDescending { it.key } + .sortedByDescending { it.value.size } + .map { it.value.first() }) + } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt deleted file mode 100644 index e1cb17000..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * CoverFiles.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.cover - -import android.content.Context -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 - -internal interface CoverFiles { - suspend fun read(id: String): InputStream? - - suspend fun write(id: String, data: ByteArray) - - companion object { - fun from(context: Context, path: String, format: CoverFormat): CoverFiles = - CoverFilesImpl(File(context.filesDir, path).also { it.mkdirs() }, format) - } -} - -private class CoverFilesImpl(private val dir: File, private val coverFormat: CoverFormat) : - CoverFiles { - private val fileMutexes = mutableMapOf() - private val mapMutex = Mutex() - - private suspend fun getMutexForFile(file: String): Mutex { - return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } } - } - - override suspend fun read(id: String): InputStream? = - withContext(Dispatchers.IO) { - try { - File(dir, getTargetFilePath(id)).inputStream() - } catch (e: IOException) { - null - } - } - - override suspend fun write(id: String, data: ByteArray) { - val fileMutex = getMutexForFile(id) - - fileMutex.withLock { - val targetFile = File(dir, getTargetFilePath(id)) - if (targetFile.exists()) { - return - } - 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() - } - } - } - } - - private fun getTargetFilePath(name: String) = "cover_${name}.${coverFormat.extension}" - - private fun getTempFilePath(name: String) = "${getTargetFilePath(name)}.tmp" -} 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..cd223b07a 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,63 @@ * 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 org.oxycblt.musikr.fs.Components +import java.io.File import java.io.InputStream interface StoredCovers { - suspend fun read(cover: Cover.Single): InputStream? + suspend fun find(id: String): Cover.Single? - suspend fun write(data: ByteArray): Cover.Single? + interface Editor : StoredCovers { + suspend fun add(data: ByteArray): Cover.Single? + } companion object { - fun from(context: Context, path: String): StoredCovers = - FileStoredCovers( - CoverIdentifier.md5(), CoverFiles.from(context, path, CoverFormat.webp())) + fun from(context: Context): StoredCovers = + FileStoredCovers(AppFiles.from(context)) + + fun editor(context: Context, path: Components): Editor = + FileStoredCoversEditor( + path, + AppFiles.from(context), + CoverIdentifier.md5(), + CoverFormat.webp() + ) } } -private class FileStoredCovers( - private val coverIdentifier: CoverIdentifier, - private val coverFiles: CoverFiles +private open class FileStoredCovers( + private val appFiles: AppFiles ) : StoredCovers { - override suspend fun read(cover: Cover.Single) = coverFiles.read(cover.key) - - override suspend fun write(data: ByteArray) = - coverIdentifier.identify(data).let { key -> - coverFiles.write(key, data) - Cover.Single(key) - } + override suspend fun find(id: String) = + appFiles.read(Components.parseUnix(id))?.let { FileCover(it) } } + +private class FileStoredCoversEditor( + val root: Components, + val appFiles: AppFiles, + val coverIdentifier: CoverIdentifier, + val coverFormat: CoverFormat +) : FileStoredCovers(appFiles), StoredCovers.Editor { + override suspend fun add(data: ByteArray): Cover.Single? { + val id = coverIdentifier.identify(data) + val path = getTargetPath(id) + val file = appFiles.write(path) { + coverFormat.transcodeInto(data, it) + } + return file?.let { FileCover(it) } + } + + private fun getTargetPath(id: String) = root.child("$id.${coverFormat.extension}") +} + +private class FileCover(private val file: AppFile) : Cover.Single { + override val id: String = file.path.unixString + + override suspend fun resolve() = file.open() +} \ No newline at end of file diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/Path.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/Path.kt index f889d516b..99630df25 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/Path.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/Path.kt @@ -154,7 +154,7 @@ value class Components private constructor(val components: List) { fun containing(other: Components) = Components(other.components.drop(components.size)) - internal companion object { + companion object { /** * Parses a path string into a [Components] instance by the unix path separator (/). * diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt index 19be4885c..8d8e89840 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt @@ -56,7 +56,7 @@ internal class AlbumImpl(private val core: AlbumCore) : Album { override val releaseType = preAlbum.releaseType override val durationMs = core.songs.sumOf { it.durationMs } override val dateAdded = core.songs.minOf { it.dateAdded } - override val cover = Cover.multi(core.songs) + override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) override val dates: Date.Range? = core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt index 4a2ca3990..9caa70482 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt @@ -55,7 +55,7 @@ internal class ArtistImpl(private val core: ArtistCore) : Artist { get() = core.resolveGenres().toList() override val durationMs = core.songs.sumOf { it.durationMs } - override val cover = Cover.multi(core.songs) + override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) private val hashCode = 31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode() diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt index 203c6c60a..171595622 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt @@ -44,7 +44,7 @@ internal class GenreImpl(private val core: GenreCore) : Genre { override val songs = core.songs override val artists = core.artists override val durationMs = core.songs.sumOf { it.durationMs } - override val cover = Cover.multi(core.songs) + override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode() diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt index 602e10f77..f70d3ddc3 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt @@ -33,7 +33,7 @@ internal class PlaylistImpl(val core: PlaylistCore) : Playlist { override val uid = core.prePlaylist.handle.uid override val name: Name.Known = core.prePlaylist.name override val durationMs = core.songs.sumOf { it.durationMs } - override val cover = Cover.multi(core.songs) + override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) override val songs = core.songs private var hashCode = 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..8523f2bca 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -19,6 +19,7 @@ package org.oxycblt.musikr.pipeline import android.content.Context +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -50,7 +51,7 @@ internal interface ExtractStep { MetadataExtractor.from(context), TagParser.new(), storage.cache, - storage.storedCovers) + storage.coverEditor) } } @@ -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 coverEditor: StoredCovers.Editor ) : 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) { cache.read(it, coverEditor) } } .flowOn(Dispatchers.IO) .buffer(Channel.UNLIMITED) val cacheFlow = @@ -120,7 +121,9 @@ private class ExtractStepImpl( metadata .mapNotNull { fileWith -> val tags = tagParser.parse(fileWith.file, fileWith.with) - val cover = fileWith.with.cover?.let { storedCovers.write(it) } + val cover = fileWith.with.cover?.let { + coverEditor.add(it) + } RawSong(fileWith.file, fileWith.with.properties, tags, cover) } .flowOn(Dispatchers.IO)