music: prepare new cover system

This commit is contained in:
Alexander Capehart 2024-12-10 07:02:37 -07:00
parent 8adda19d1a
commit 45ead8253a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 79 additions and 149 deletions

View file

@ -354,20 +354,10 @@ constructor(private val musikr: Musikr, private val fullTagCache: FullTagCache,
} }
val locations = musicSettings.musicLocations val locations = musicSettings.musicLocations
val fakeCoverEditorTemporary = object : StoredCovers.Editor {
override suspend fun write(data: ByteArray): Cover.Single? {
TODO("Not yet implemented")
}
override suspend fun apply() {
TODO("Not yet implemented")
}
}
val storage = if (withCache) { val storage = if (withCache) {
Storage(fullTagCache, fakeCoverEditorTemporary) Storage(fullTagCache, StoredCovers.buildOn())
} else { } else {
Storage(writeOnlyTagCache, fakeCoverEditorTemporary) Storage(writeOnlyTagCache, StoredCovers.new())
} }
val newLibrary = val newLibrary =
musikr.run(locations, storage, Interpretation(nameFactory, separators), ::emitIndexingProgress) musikr.run(locations, storage, Interpretation(nameFactory, separators), ::emitIndexingProgress)

View file

@ -24,14 +24,7 @@ import org.oxycblt.musikr.Song
sealed interface Cover { sealed interface Cover {
val key: String val key: String
data class Single(val song: Song) : Cover { data class Single(override val key: String) : Cover
override val key: String
get() = "${song.uid}@${song.lastModified}"
val uid = song.uid
val uri = song.uri
val lastModified = song.lastModified
}
class Multi(val all: List<Single>) : Cover { class Multi(val all: List<Single>) : Cover {
override val key = "multi@${all.hashCode()}" override val key = "multi@${all.hashCode()}"
@ -42,7 +35,7 @@ sealed interface Cover {
fun nil() = Multi(listOf()) fun nil() = Multi(listOf())
fun single(song: Song) = Single(song) fun single(key: String) = Single(key)
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) } fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }

View file

@ -1,50 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* CoverCache.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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 java.io.InputStream
import javax.inject.Inject
interface CoverCache {
suspend fun read(cover: Cover.Single): InputStream?
suspend fun write(cover: Cover.Single, data: ByteArray): InputStream?
}
class CoverCacheImpl
@Inject
constructor(
private val coverIdentifier: CoverIdentifier,
private val storedCoversDao: StoredCoversDao,
private val coverFiles: CoverFiles
) : CoverCache {
override suspend fun read(cover: Cover.Single): InputStream? {
val id = storedCoversDao.getStoredCoverId(cover.uid, cover.lastModified) ?: return null
return coverFiles.read(id)
}
override suspend fun write(cover: Cover.Single, data: ByteArray): InputStream? {
val id = coverIdentifier.identify(data)
coverFiles.write(id, data)
storedCoversDao.setStoredCover(
StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id))
return coverFiles.read(id)
}
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* CacheModule.kt is part of Auxio. * CoverModule.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -30,26 +30,12 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface StackModule { interface CoverModule {
@Singleton @Binds fun appFiles(impl: CoverFilesImpl): CoverFiles @Singleton @Binds fun appFiles(impl: CoverFilesImpl): CoverFiles
@Binds fun coverCache(cache: CoverCacheImpl): CoverCache
@Binds fun coverIdentifier(identifierImpl: CoverIdentifierImpl): CoverIdentifier @Binds fun coverIdentifier(identifierImpl: CoverIdentifierImpl): CoverIdentifier
@Binds fun coverFormat(coverFormatImpl: CoverFormatImpl): CoverFormat @Binds fun coverFormat(coverFormatImpl: CoverFormatImpl): CoverFormat
}
@Module @Binds fun coverExtractor(coverExtractor: CoverParserImpl): CoverParser
@InstallIn(SingletonComponent::class)
class StoredCoversDatabaseModule {
@Provides fun storedCoversDao(database: StoredCoversDatabase) = database.storedCoversDao()
@Singleton
@Provides
fun database(@ApplicationContext context: Context) =
Room.databaseBuilder(
context.applicationContext, StoredCoversDatabase::class.java, "stored_covers.db")
.fallbackToDestructiveMigration()
.build()
} }

View file

@ -0,0 +1,53 @@
package org.oxycblt.musikr.cover
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Metadata
import androidx.media3.extractor.metadata.flac.PictureFrame
import androidx.media3.extractor.metadata.id3.ApicFrame
import org.oxycblt.musikr.metadata.AudioMetadata
import javax.inject.Inject
interface CoverParser {
suspend fun extract(metadata: AudioMetadata): ByteArray?
}
class CoverParserImpl @Inject constructor() : CoverParser {
override suspend fun extract(metadata: AudioMetadata): ByteArray? {
val exoPlayerMetadata = metadata.exoPlayerFormat?.metadata
return exoPlayerMetadata?.let(::findCoverDataInMetadata)
?: metadata.mediaMetadataRetriever.embeddedPicture
}
private fun findCoverDataInMetadata(metadata: Metadata): ByteArray? {
var fallbackPic: ByteArray? = null
for (i in 0 until metadata.length()) {
// We can only extract pictures from two tags with this method, ID3v2's APIC or
// Vorbis picture comments.
val pic: ByteArray?
val type: Int
when (val entry = metadata.get(i)) {
is ApicFrame -> {
pic = entry.pictureData
type = entry.pictureType
}
is PictureFrame -> {
pic = entry.pictureData
type = entry.pictureType
}
else -> continue
}
if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) {
return pic
} else if (fallbackPic == null) {
fallbackPic = pic
}
}
return fallbackPic
}
}

View file

@ -7,7 +7,10 @@ interface StoredCovers {
interface Editor { interface Editor {
suspend fun write(data: ByteArray): Cover.Single? suspend fun write(data: ByteArray): Cover.Single?
suspend fun apply()
} }
}
companion object {
suspend fun buildOn(): Editor = TODO()
fun new(): Editor = TODO()
}
}

View file

@ -1,49 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* StoredCoversDatabase.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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 androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import org.oxycblt.musikr.Music
@Database(entities = [StoredCover::class], version = 50, exportSchema = false)
abstract class StoredCoversDatabase : RoomDatabase() {
abstract fun storedCoversDao(): StoredCoversDao
}
@Dao
interface StoredCoversDao {
@Query("SELECT coverId FROM StoredCover WHERE uid = :uid AND lastModified = :lastModified")
@TypeConverters(Music.UID.TypeConverters::class)
suspend fun getStoredCoverId(uid: Music.UID, lastModified: Long): String?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun setStoredCover(storedCover: StoredCover)
}
@Entity
@TypeConverters(Music.UID.TypeConverters::class)
data class StoredCover(@PrimaryKey val uid: Music.UID, val lastModified: Long, val coverId: String)

View file

@ -28,9 +28,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverParser
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.tag.cache.TagCache
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser import org.oxycblt.musikr.tag.parse.TagParser
@ -42,7 +43,8 @@ class ExtractStepImpl
@Inject @Inject
constructor( constructor(
private val metadataExtractor: MetadataExtractor, private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser private val tagParser: TagParser,
private val coverParser: CoverParser
) : ExtractStep { ) : ExtractStep {
override fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic> { override fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val cacheResults = val cacheResults =
@ -56,16 +58,18 @@ constructor(
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val (cachedSongs, uncachedSongs) = val (cachedSongs, uncachedSongs) =
cacheResults.mapPartition { cacheResults.mapPartition {
it.tags?.let { tags -> ExtractedMusic.Song(it.file, tags) } it.tags?.let { tags -> ExtractedMusic.Song(it.file, tags, null) }
} }
val split = uncachedSongs.distribute(8) val split = uncachedSongs.distribute(8)
val extractedSongs = val extractedSongs =
Array(split.hot.size) { i -> Array(split.hot.size) { i ->
split.hot[i] split.hot[i]
.map { .map { node ->
val metadata = metadataExtractor.extract(it.file) val metadata = metadataExtractor.extract(node.file)
val tags = tagParser.parse(it.file, metadata) val tags = tagParser.parse(node.file, metadata)
ExtractedMusic.Song(it.file, tags) val coverData = coverParser.extract(metadata)
val cover = coverData?.let { storage.coverEditor.write(it) }
ExtractedMusic.Song(node.file, tags, cover)
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
@ -73,7 +77,7 @@ constructor(
val writtenSongs = val writtenSongs =
merge(*extractedSongs) merge(*extractedSongs)
.map { .map {
tagCache.write(it.file, it.tags) storage.tagCache.write(it.file, it.tags)
it it
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
@ -89,5 +93,5 @@ constructor(
} }
sealed interface ExtractedMusic { sealed interface ExtractedMusic {
data class Song(val file: DeviceFile, val tags: ParsedTags) : ExtractedMusic data class Song(val file: DeviceFile, val tags: ParsedTags, val cover: Cover?) : ExtractedMusic
} }