From 42390f4b3f78f0fe0a0bf87b13222b847425a850 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 11 Dec 2024 16:55:04 -0700 Subject: [PATCH] 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. --- .../oxycblt/auxio/music/MusicRepository.kt | 8 +++++-- .../main/java/org/oxycblt/musikr/Config.kt | 2 +- .../org/oxycblt/musikr/cover/CoverFiles.kt | 20 +++++++++------- .../org/oxycblt/musikr/cover/CoverFormat.kt | 9 ++++--- .../oxycblt/musikr/cover/CoverIdentifier.kt | 7 ++++-- .../org/oxycblt/musikr/cover/CoverModule.kt | 9 +------ .../org/oxycblt/musikr/cover/StoredCovers.kt | 24 ++++++++++++++----- .../oxycblt/musikr/pipeline/ExtractStep.kt | 2 +- 8 files changed, 49 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 7b4fb0ce2..76398af1b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -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( diff --git a/app/src/main/java/org/oxycblt/musikr/Config.kt b/app/src/main/java/org/oxycblt/musikr/Config.kt index 4911bc05c..4d6d9728f 100644 --- a/app/src/main/java/org/oxycblt/musikr/Config.kt +++ b/app/src/main/java/org/oxycblt/musikr/Config.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt index 060cac513..f28c9b94a 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt @@ -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() @@ -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) } diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt index b84bb6f3c..0018d609b 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt @@ -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) = diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt index 0a7ac57d7..ef0917e05 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt @@ -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 = diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt index 5d8b2fe32..976a3233f 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt b/app/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt index f880c96a1..a1340990d 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt @@ -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) + } +} diff --git a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 683a9c54f..235ade081 100644 --- a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -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)