Revert "musikr: bundle cover resolution with key"

This reverts commit 8cc939b58d.
This commit is contained in:
Alexander Capehart 2024-12-20 15:28:25 -05:00
parent 8cc939b58d
commit 8b69042288
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
17 changed files with 149 additions and 214 deletions

View file

@ -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. * @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
*/ */
fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) = fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) =
bindImpl(Cover.Multi.from(songs.mapNotNull { it.cover }), desc, errorRes) bindImpl(Cover.multi(songs), desc, errorRes)
private fun bindImpl(cover: Cover?, desc: String, @DrawableRes errorRes: Int) { private fun bindImpl(cover: Cover?, desc: String, @DrawableRes errorRes: Int) {
val request = val request =

View file

@ -47,7 +47,7 @@ import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.cover.StoredCovers
class CoverKeyer @Inject constructor() : Keyer<Cover> { class CoverKeyer @Inject constructor() : Keyer<Cover> {
override fun key(data: Cover, options: Options) = "${data.id}&${options.size}" override fun key(data: Cover, options: Options) = "${data.key}&${options.size}"
} }
class CoverFetcher class CoverFetcher
@ -56,14 +56,16 @@ private constructor(
private val cover: Cover, private val cover: Cover,
private val size: Size, private val size: Size,
) : Fetcher { ) : Fetcher {
private val storedCovers = StoredCovers.from(context, "covers")
override suspend fun fetch(): FetchResult? { override suspend fun fetch(): FetchResult? {
val streams = val streams =
when (val cover = cover) { when (val cover = cover) {
is Cover.Single -> listOfNotNull(cover.resolve()) is Cover.Single -> listOfNotNull(storedCovers.read(cover))
is Cover.Multi -> is Cover.Multi ->
buildList { buildList {
for (single in cover.all) { for (single in cover.all) {
single.resolve()?.let { add(it) } storedCovers.read(single)?.let { add(it) }
if (size == 4) { if (size == 4) {
break break
} }

View file

@ -38,7 +38,6 @@ import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.CacheDatabase import org.oxycblt.musikr.cache.CacheDatabase
import org.oxycblt.musikr.cover.StoredCovers 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.PlaylistDatabase
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Naming
@ -369,15 +368,15 @@ constructor(
revision = this.library?.revision ?: musicSettings.revision revision = this.library?.revision ?: musicSettings.revision
storage = storage =
Storage( Storage(
Cache.writeOnly(cacheDatabase), Cache.full(cacheDatabase),
StoredCovers.editor(context, Components.parseUnix("covers_${UUID.randomUUID()}")), StoredCovers.from(context, "covers_$revision"),
StoredPlaylists.from(playlistDatabase)) StoredPlaylists.from(playlistDatabase))
} else { } else {
revision = UUID.randomUUID() revision = UUID.randomUUID()
storage = storage =
Storage( Storage(
Cache.writeOnly(cacheDatabase), Cache.writeOnly(cacheDatabase),
StoredCovers.editor(context, Components.parseUnix("covers_$revision")), StoredCovers.from(context, "covers_$revision"),
StoredPlaylists.from(playlistDatabase)) StoredPlaylists.from(playlistDatabase))
} }

View file

@ -18,8 +18,6 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import org.oxycblt.musikr.Album
import org.oxycblt.musikr.Artist
import java.util.UUID import java.util.UUID
import org.oxycblt.musikr.Library import org.oxycblt.musikr.Library
import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.MutableLibrary

View file

@ -26,7 +26,7 @@ import org.oxycblt.musikr.tag.interpret.Separators
data class Storage( data class Storage(
val cache: Cache, val cache: Cache,
val coverEditor: StoredCovers.Editor, val storedCovers: StoredCovers,
val storedPlaylists: StoredPlaylists val storedPlaylists: StoredPlaylists
) )

View file

@ -18,12 +18,11 @@
package org.oxycblt.musikr.cache package org.oxycblt.musikr.cache
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
interface Cache { interface Cache {
suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult suspend fun read(file: DeviceFile): CacheResult
suspend fun write(song: RawSong) suspend fun write(song: RawSong)
@ -41,9 +40,9 @@ sealed interface CacheResult {
} }
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache { private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult = override suspend fun read(file: DeviceFile) =
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let { cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let {
CacheResult.Hit(it.intoRawSong(file, storedCovers)) CacheResult.Hit(it.intoRawSong(file))
} ?: CacheResult.Miss(file) } ?: CacheResult.Miss(file)
override suspend fun write(song: RawSong) = override suspend fun write(song: RawSong) =
@ -51,8 +50,7 @@ private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
} }
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache { private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = override suspend fun read(file: DeviceFile) = CacheResult.Miss(file)
CacheResult.Miss(file)
override suspend fun write(song: RawSong) = override suspend fun write(song: RawSong) =
cacheInfoDao.updateSong(CachedSong.fromRawSong(song)) cacheInfoDao.updateSong(CachedSong.fromRawSong(song))

View file

@ -31,7 +31,6 @@ 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.cover.Cover
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
@ -90,9 +89,9 @@ internal data class CachedSong(
val genreNames: List<String>, val genreNames: List<String>,
val replayGainTrackAdjustment: Float?, val replayGainTrackAdjustment: Float?,
val replayGainAlbumAdjustment: Float?, val replayGainAlbumAdjustment: Float?,
val coverId: String?, val cover: Cover.Single?,
) { ) {
suspend fun intoRawSong(file: DeviceFile, storedCovers: StoredCovers) = fun intoRawSong(file: DeviceFile) =
RawSong( RawSong(
file, file,
Properties(mimeType, durationMs, bitrateHz, sampleRateHz), Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
@ -118,7 +117,7 @@ internal data class CachedSong(
genreNames = genreNames, genreNames = genreNames,
replayGainTrackAdjustment = replayGainTrackAdjustment, replayGainTrackAdjustment = replayGainTrackAdjustment,
replayGainAlbumAdjustment = replayGainAlbumAdjustment), replayGainAlbumAdjustment = replayGainAlbumAdjustment),
coverId?.let { storedCovers.find(it) }) cover)
object Converters { object Converters {
@TypeConverter @TypeConverter
@ -131,6 +130,10 @@ internal data class CachedSong(
@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.Single?) = cover?.key
@TypeConverter fun toCover(key: String?) = key?.let { Cover.Single(it) }
} }
companion object { companion object {
@ -159,9 +162,9 @@ internal data class CachedSong(
genreNames = rawSong.tags.genreNames, genreNames = rawSong.tags.genreNames,
replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment, replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment, replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
cover = rawSong.cover,
mimeType = rawSong.properties.mimeType, mimeType = rawSong.properties.mimeType,
bitrateHz = rawSong.properties.bitrateKbps, bitrateHz = rawSong.properties.bitrateKbps,
sampleRateHz = rawSong.properties.sampleRateHz, sampleRateHz = rawSong.properties.sampleRateHz)
coverId = rawSong.cover?.id)
} }
} }

View file

@ -1,118 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<String, Mutex>()
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()
}

View file

@ -19,27 +19,30 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import java.io.InputStream
sealed interface Cover { sealed interface Cover {
val id: String val key: String
interface Single : Cover { data class Single(override val key: String) : Cover
suspend fun resolve(): InputStream?
class Multi(val all: List<Single>) : Cover {
override val key = "multi@${all.hashCode()}"
} }
class Multi private constructor(val all: List<Single>) : Cover { companion object {
override val id = "multi@${all.hashCode()}" fun nil() = Multi(listOf())
companion object { fun single(key: String) = Single(key)
fun from(covers: Collection<Single>) =
Multi( fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
covers
.groupBy { it.id } private fun order(songs: Collection<Song>) =
.entries songs
.sortedByDescending { it.key } .mapNotNull { it.cover }
.sortedByDescending { it.value.size } .groupBy { it.key }
.map { it.value.first() }) .entries
} .sortedByDescending { it.key }
.sortedByDescending { it.value.size }
.map { it.value.first() }
} }
} }

View file

@ -0,0 +1,83 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<String, Mutex>()
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"
}

View file

@ -19,59 +19,29 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import android.content.Context import android.content.Context
import android.util.Log
import org.oxycblt.musikr.fs.Components
import java.io.File
import java.io.InputStream import java.io.InputStream
interface StoredCovers { interface StoredCovers {
suspend fun find(id: String): Cover.Single? suspend fun read(cover: Cover.Single): InputStream?
interface Editor : StoredCovers { suspend fun write(data: ByteArray): Cover.Single?
suspend fun add(data: ByteArray): Cover.Single?
}
companion object { companion object {
fun from(context: Context): StoredCovers = fun from(context: Context, path: String): StoredCovers =
FileStoredCovers(AppFiles.from(context)) FileStoredCovers(
CoverIdentifier.md5(), CoverFiles.from(context, path, CoverFormat.webp()))
fun editor(context: Context, path: Components): Editor =
FileStoredCoversEditor(
path,
AppFiles.from(context),
CoverIdentifier.md5(),
CoverFormat.webp()
)
} }
} }
private open class FileStoredCovers( private class FileStoredCovers(
private val appFiles: AppFiles private val coverIdentifier: CoverIdentifier,
private val coverFiles: CoverFiles
) : StoredCovers { ) : StoredCovers {
override suspend fun find(id: String) = override suspend fun read(cover: Cover.Single) = coverFiles.read(cover.key)
appFiles.read(Components.parseUnix(id))?.let { FileCover(it) }
}
private class FileStoredCoversEditor( override suspend fun write(data: ByteArray) =
val root: Components, coverIdentifier.identify(data).let { key ->
val appFiles: AppFiles, coverFiles.write(key, data)
val coverIdentifier: CoverIdentifier, Cover.Single(key)
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()
} }

View file

@ -154,7 +154,7 @@ value class Components private constructor(val components: List<String>) {
fun containing(other: Components) = Components(other.components.drop(components.size)) fun containing(other: Components) = Components(other.components.drop(components.size))
companion object { internal companion object {
/** /**
* Parses a path string into a [Components] instance by the unix path separator (/). * Parses a path string into a [Components] instance by the unix path separator (/).
* *

View file

@ -56,7 +56,7 @@ internal class AlbumImpl(private val core: AlbumCore) : Album {
override val releaseType = preAlbum.releaseType override val releaseType = preAlbum.releaseType
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val dateAdded = core.songs.minOf { it.dateAdded } override val dateAdded = core.songs.minOf { it.dateAdded }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) override val cover = Cover.multi(core.songs)
override val dates: Date.Range? = override val dates: Date.Range? =
core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) } core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) }

View file

@ -55,7 +55,7 @@ internal class ArtistImpl(private val core: ArtistCore) : Artist {
get() = core.resolveGenres().toList() get() = core.resolveGenres().toList()
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) override val cover = Cover.multi(core.songs)
private val hashCode = private val hashCode =
31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode() 31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode()

View file

@ -44,7 +44,7 @@ internal class GenreImpl(private val core: GenreCore) : Genre {
override val songs = core.songs override val songs = core.songs
override val artists = core.artists override val artists = core.artists
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) override val cover = Cover.multi(core.songs)
private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode() private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode()

View file

@ -33,7 +33,7 @@ internal class PlaylistImpl(val core: PlaylistCore) : Playlist {
override val uid = core.prePlaylist.handle.uid override val uid = core.prePlaylist.handle.uid
override val name: Name.Known = core.prePlaylist.name override val name: Name.Known = core.prePlaylist.name
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover }) override val cover = Cover.multi(core.songs)
override val songs = core.songs override val songs = core.songs
private var hashCode = private var hashCode =

View file

@ -19,7 +19,6 @@
package org.oxycblt.musikr.pipeline package org.oxycblt.musikr.pipeline
import android.content.Context import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -51,7 +50,7 @@ internal interface ExtractStep {
MetadataExtractor.from(context), MetadataExtractor.from(context),
TagParser.new(), TagParser.new(),
storage.cache, storage.cache,
storage.coverEditor) storage.storedCovers)
} }
} }
@ -60,7 +59,7 @@ private class ExtractStepImpl(
private val metadataExtractor: MetadataExtractor, private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser, private val tagParser: TagParser,
private val cache: Cache, private val cache: Cache,
private val coverEditor: StoredCovers.Editor private val storedCovers: StoredCovers
) : ExtractStep { ) : ExtractStep {
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> { override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val filterFlow = val filterFlow =
@ -75,7 +74,7 @@ private class ExtractStepImpl(
val cacheResults = val cacheResults =
audioNodes audioNodes
.map { wrap(it) { cache.read(it, coverEditor) } } .map { wrap(it, cache::read) }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val cacheFlow = val cacheFlow =
@ -121,9 +120,7 @@ private class ExtractStepImpl(
metadata metadata
.mapNotNull { fileWith -> .mapNotNull { fileWith ->
val tags = tagParser.parse(fileWith.file, fileWith.with) val tags = tagParser.parse(fileWith.file, fileWith.with)
val cover = fileWith.with.cover?.let { val cover = fileWith.with.cover?.let { storedCovers.write(it) }
coverEditor.add(it)
}
RawSong(fileWith.file, fileWith.with.properties, tags, cover) RawSong(fileWith.file, fileWith.with.properties, tags, cover)
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)