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
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(

View file

@ -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)

View file

@ -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) }

View file

@ -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) =

View file

@ -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 =

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)