music: implement revisioned covers
This commit is contained in:
parent
8b69042288
commit
1843986f75
11 changed files with 142 additions and 97 deletions
|
@ -47,7 +47,7 @@ import org.oxycblt.musikr.cover.Cover
|
|||
import org.oxycblt.musikr.cover.StoredCovers
|
||||
|
||||
class CoverKeyer @Inject constructor() : Keyer<Cover> {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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<MusicRepository.IndexingListener>()
|
||||
@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) {
|
||||
|
|
|
@ -15,33 +15,59 @@
|
|||
* 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.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<Song>) =
|
||||
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<Song>) =
|
||||
MutableRevisionedLibrary(revision, inner.addToPlaylist(playlist, songs))
|
||||
|
||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>) =
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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<String>,
|
||||
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)
|
||||
|
|
|
@ -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<Single>) : 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<Song>) = order(songs).run { Multi(this) }
|
||||
|
||||
private fun order(songs: Collection<Song>) =
|
||||
songs
|
||||
.mapNotNull { it.cover }
|
||||
.groupBy { it.key }
|
||||
.groupBy { it.id }
|
||||
.entries
|
||||
.sortedByDescending { it.key }
|
||||
.sortedByDescending { it.value.size }
|
||||
|
|
|
@ -15,29 +15,34 @@
|
|||
* 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 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<String, Mutex>()
|
||||
|
@ -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() }
|
||||
}
|
|
@ -15,33 +15,42 @@
|
|||
* 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.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()
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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<ExploreNode>): Flow<ExtractedMusic> {
|
||||
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 =
|
||||
|
|
Loading…
Reference in a new issue