music: move cover parsing to indexing
This drastically slows music loading, but my hope is that in practice most of the slowdown is actually in ExoPlayer's metadata extractor and if I switch off of that things will actually improve. Maybe.
This commit is contained in:
parent
b53b7a0c6a
commit
42390f4b3f
8 changed files with 49 additions and 32 deletions
|
@ -18,6 +18,8 @@
|
|||
|
||||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -209,6 +211,7 @@ class MusicRepositoryImpl
|
|||
@Inject
|
||||
constructor(
|
||||
private val musikr: Musikr,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val tagDatabase: TagDatabase,
|
||||
private val musicSettings: MusicSettings
|
||||
) : MusicRepository {
|
||||
|
@ -358,9 +361,10 @@ constructor(
|
|||
|
||||
val storage =
|
||||
if (withCache) {
|
||||
Storage(TagCache.full(tagDatabase), StoredCovers.buildOn())
|
||||
Storage(TagCache.full(tagDatabase), StoredCovers.from(context, "covers"))
|
||||
} else {
|
||||
Storage(TagCache.writeOnly(tagDatabase), StoredCovers.new())
|
||||
// TODO: Revisioned covers
|
||||
Storage(TagCache.writeOnly(tagDatabase), StoredCovers.from(context, "covers"))
|
||||
}
|
||||
val newLibrary =
|
||||
musikr.run(
|
||||
|
|
|
@ -23,6 +23,6 @@ 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 coverEditor: StoredCovers.Editor)
|
||||
data class Storage(val tagCache: TagCache, val storedCovers: StoredCovers)
|
||||
|
||||
data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators)
|
||||
|
|
|
@ -19,26 +19,28 @@
|
|||
package org.oxycblt.musikr.cover
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface CoverFiles {
|
||||
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(context, File(context.filesDir, path), format)
|
||||
}
|
||||
}
|
||||
|
||||
class CoverFilesImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private class CoverFilesImpl(
|
||||
private val context: Context,
|
||||
private val dir: File,
|
||||
private val coverFormat: CoverFormat
|
||||
) : CoverFiles {
|
||||
private val fileMutexes = mutableMapOf<String, Mutex>()
|
||||
|
@ -61,12 +63,12 @@ constructor(
|
|||
val fileMutex = getMutexForFile(id)
|
||||
|
||||
fileMutex.withLock {
|
||||
val targetFile = File(context.filesDir, getTargetFilePath(id))
|
||||
val targetFile = File(dir, getTargetFilePath(id))
|
||||
if (targetFile.exists()) {
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val tempFile = File(context.filesDir, getTempFilePath(id))
|
||||
val tempFile = File(dir, getTempFilePath(id))
|
||||
|
||||
try {
|
||||
tempFile.outputStream().use { coverFormat.transcodeInto(data, it) }
|
||||
|
|
|
@ -22,15 +22,18 @@ import android.graphics.Bitmap
|
|||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
interface CoverFormat {
|
||||
internal interface CoverFormat {
|
||||
val extension: String
|
||||
|
||||
fun transcodeInto(data: ByteArray, output: OutputStream): Boolean
|
||||
|
||||
companion object {
|
||||
fun webp(): CoverFormat = WebpCoverFormat()
|
||||
}
|
||||
}
|
||||
|
||||
class CoverFormatImpl @Inject constructor() : CoverFormat {
|
||||
private class WebpCoverFormat() : CoverFormat {
|
||||
override val extension = EXTENSION
|
||||
|
||||
override fun transcodeInto(data: ByteArray, output: OutputStream) =
|
||||
|
|
|
@ -19,13 +19,16 @@
|
|||
package org.oxycblt.musikr.cover
|
||||
|
||||
import java.security.MessageDigest
|
||||
import javax.inject.Inject
|
||||
|
||||
interface CoverIdentifier {
|
||||
suspend fun identify(data: ByteArray): String
|
||||
|
||||
companion object {
|
||||
fun md5(): CoverIdentifier = MD5CoverIdentifier()
|
||||
}
|
||||
}
|
||||
|
||||
class CoverIdentifierImpl @Inject constructor() : CoverIdentifier {
|
||||
private class MD5CoverIdentifier() : CoverIdentifier {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override suspend fun identify(data: ByteArray): String {
|
||||
val digest =
|
||||
|
|
|
@ -22,16 +22,9 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface CoverModule {
|
||||
@Singleton @Binds fun appFiles(impl: CoverFilesImpl): CoverFiles
|
||||
|
||||
@Binds fun coverIdentifier(identifierImpl: CoverIdentifierImpl): CoverIdentifier
|
||||
|
||||
@Binds fun coverFormat(coverFormatImpl: CoverFormatImpl): CoverFormat
|
||||
|
||||
@Binds fun coverExtractor(coverExtractor: CoverParserImpl): CoverParser
|
||||
@Binds fun coverParser(impl: CoverParserImpl): CoverParser
|
||||
}
|
||||
|
|
|
@ -18,18 +18,30 @@
|
|||
|
||||
package org.oxycblt.musikr.cover
|
||||
|
||||
import android.content.Context
|
||||
import java.io.InputStream
|
||||
|
||||
interface StoredCovers {
|
||||
suspend fun read(cover: Cover.Single): InputStream?
|
||||
|
||||
interface Editor {
|
||||
suspend fun write(data: ByteArray): Cover.Single?
|
||||
}
|
||||
suspend fun write(data: ByteArray): Cover.Single?
|
||||
|
||||
companion object {
|
||||
suspend fun buildOn(): Editor = TODO()
|
||||
|
||||
fun new(): Editor = TODO()
|
||||
fun from(context: Context, path: String): StoredCovers =
|
||||
FileStoredCovers(
|
||||
CoverIdentifier.md5(), CoverFiles.from(context, path, CoverFormat.webp()))
|
||||
}
|
||||
}
|
||||
|
||||
private class FileStoredCovers(
|
||||
private val coverIdentifier: CoverIdentifier,
|
||||
private val coverFiles: CoverFiles
|
||||
) : 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ constructor(
|
|||
val metadata = metadataExtractor.extract(node.file)
|
||||
val tags = tagParser.parse(node.file, metadata)
|
||||
val coverData = coverParser.extract(metadata)
|
||||
val cover = coverData?.let { storage.coverEditor.write(it) }
|
||||
val cover = coverData?.let { storage.storedCovers.write(it) }
|
||||
ExtractedMusic.Song(node.file, tags, cover)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
|
Loading…
Reference in a new issue