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:
Alexander Capehart 2024-12-11 16:55:04 -07:00
parent b53b7a0c6a
commit 42390f4b3f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 49 additions and 32 deletions

View file

@ -18,6 +18,8 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -209,6 +211,7 @@ class MusicRepositoryImpl
@Inject @Inject
constructor( constructor(
private val musikr: Musikr, private val musikr: Musikr,
@ApplicationContext private val context: Context,
private val tagDatabase: TagDatabase, private val tagDatabase: TagDatabase,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
) : MusicRepository { ) : MusicRepository {
@ -358,9 +361,10 @@ constructor(
val storage = val storage =
if (withCache) { if (withCache) {
Storage(TagCache.full(tagDatabase), StoredCovers.buildOn()) Storage(TagCache.full(tagDatabase), StoredCovers.from(context, "covers"))
} else { } else {
Storage(TagCache.writeOnly(tagDatabase), StoredCovers.new()) // TODO: Revisioned covers
Storage(TagCache.writeOnly(tagDatabase), StoredCovers.from(context, "covers"))
} }
val newLibrary = val newLibrary =
musikr.run( musikr.run(

View file

@ -23,6 +23,6 @@ import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.cache.TagCache import org.oxycblt.musikr.tag.cache.TagCache
import org.oxycblt.musikr.tag.interpret.Separators 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) data class Interpretation(val nameFactory: Name.Known.Factory, val separators: Separators)

View file

@ -19,26 +19,28 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
interface CoverFiles { internal interface CoverFiles {
suspend fun read(id: String): InputStream? suspend fun read(id: String): InputStream?
suspend fun write(id: String, data: ByteArray) 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 private class CoverFilesImpl(
@Inject private val context: Context,
constructor( private val dir: File,
@ApplicationContext private val context: Context,
private val coverFormat: CoverFormat private val coverFormat: CoverFormat
) : CoverFiles { ) : CoverFiles {
private val fileMutexes = mutableMapOf<String, Mutex>() private val fileMutexes = mutableMapOf<String, Mutex>()
@ -61,12 +63,12 @@ constructor(
val fileMutex = getMutexForFile(id) val fileMutex = getMutexForFile(id)
fileMutex.withLock { fileMutex.withLock {
val targetFile = File(context.filesDir, getTargetFilePath(id)) val targetFile = File(dir, getTargetFilePath(id))
if (targetFile.exists()) { if (targetFile.exists()) {
return return
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val tempFile = File(context.filesDir, getTempFilePath(id)) val tempFile = File(dir, getTempFilePath(id))
try { try {
tempFile.outputStream().use { coverFormat.transcodeInto(data, it) } tempFile.outputStream().use { coverFormat.transcodeInto(data, it) }

View file

@ -22,15 +22,18 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
import java.io.OutputStream import java.io.OutputStream
import javax.inject.Inject
interface CoverFormat { internal interface CoverFormat {
val extension: String val extension: String
fun transcodeInto(data: ByteArray, output: OutputStream): Boolean 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 val extension = EXTENSION
override fun transcodeInto(data: ByteArray, output: OutputStream) = override fun transcodeInto(data: ByteArray, output: OutputStream) =

View file

@ -19,13 +19,16 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import java.security.MessageDigest import java.security.MessageDigest
import javax.inject.Inject
interface CoverIdentifier { interface CoverIdentifier {
suspend fun identify(data: ByteArray): String suspend fun identify(data: ByteArray): String
companion object {
fun md5(): CoverIdentifier = MD5CoverIdentifier()
}
} }
class CoverIdentifierImpl @Inject constructor() : CoverIdentifier { private class MD5CoverIdentifier() : CoverIdentifier {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override suspend fun identify(data: ByteArray): String { override suspend fun identify(data: ByteArray): String {
val digest = val digest =

View file

@ -22,16 +22,9 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface CoverModule { interface CoverModule {
@Singleton @Binds fun appFiles(impl: CoverFilesImpl): CoverFiles @Binds fun coverParser(impl: CoverParserImpl): CoverParser
@Binds fun coverIdentifier(identifierImpl: CoverIdentifierImpl): CoverIdentifier
@Binds fun coverFormat(coverFormatImpl: CoverFormatImpl): CoverFormat
@Binds fun coverExtractor(coverExtractor: CoverParserImpl): CoverParser
} }

View file

@ -18,18 +18,30 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import android.content.Context
import java.io.InputStream import java.io.InputStream
interface StoredCovers { interface StoredCovers {
suspend fun read(cover: Cover.Single): InputStream? 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 { companion object {
suspend fun buildOn(): Editor = TODO() fun from(context: Context, path: String): StoredCovers =
FileStoredCovers(
fun new(): Editor = TODO() 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)
}
}

View file

@ -68,7 +68,7 @@ constructor(
val metadata = metadataExtractor.extract(node.file) val metadata = metadataExtractor.extract(node.file)
val tags = tagParser.parse(node.file, metadata) val tags = tagParser.parse(node.file, metadata)
val coverData = coverParser.extract(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) ExtractedMusic.Song(node.file, tags, cover)
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)