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 42abb56da..aa3078d40 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -354,20 +354,10 @@ constructor(private val musikr: Musikr, private val fullTagCache: FullTagCache, } 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) { - Storage(fullTagCache, fakeCoverEditorTemporary) + Storage(fullTagCache, StoredCovers.buildOn()) } else { - Storage(writeOnlyTagCache, fakeCoverEditorTemporary) + Storage(writeOnlyTagCache, StoredCovers.new()) } val newLibrary = musikr.run(locations, storage, Interpretation(nameFactory, separators), ::emitIndexingProgress) diff --git a/app/src/main/java/org/oxycblt/musikr/cover/Cover.kt b/app/src/main/java/org/oxycblt/musikr/cover/Cover.kt index 7babc0a95..068bed6e6 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/Cover.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/Cover.kt @@ -24,14 +24,7 @@ import org.oxycblt.musikr.Song sealed interface Cover { val key: String - data class Single(val song: Song) : Cover { - override val key: String - get() = "${song.uid}@${song.lastModified}" - - val uid = song.uid - val uri = song.uri - val lastModified = song.lastModified - } + data class Single(override val key: String) : Cover class Multi(val all: List) : Cover { override val key = "multi@${all.hashCode()}" @@ -42,7 +35,7 @@ sealed interface Cover { fun nil() = Multi(listOf()) - fun single(song: Song) = Single(song) + fun single(key: String) = Single(key) fun multi(songs: Collection) = order(songs).run { Multi(this) } diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverCache.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverCache.kt deleted file mode 100644 index 709f671bb..000000000 --- a/app/src/main/java/org/oxycblt/musikr/cover/CoverCache.kt +++ /dev/null @@ -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 . - */ - -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) - } -} diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CacheModule.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt similarity index 68% rename from app/src/main/java/org/oxycblt/musikr/cover/CacheModule.kt rename to app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt index 16bd1a07f..a22e05fe3 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/CacheModule.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/CoverModule.kt @@ -1,6 +1,6 @@ /* * 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 * it under the terms of the GNU General Public License as published by @@ -30,26 +30,12 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -interface StackModule { +interface CoverModule { @Singleton @Binds fun appFiles(impl: CoverFilesImpl): CoverFiles - @Binds fun coverCache(cache: CoverCacheImpl): CoverCache - @Binds fun coverIdentifier(identifierImpl: CoverIdentifierImpl): CoverIdentifier @Binds fun coverFormat(coverFormatImpl: CoverFormatImpl): CoverFormat -} -@Module -@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() + @Binds fun coverExtractor(coverExtractor: CoverParserImpl): CoverParser } diff --git a/app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt b/app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt new file mode 100644 index 000000000..dcf22827b --- /dev/null +++ b/app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt @@ -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 + } +} \ No newline at end of file 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 773bd751d..fe57a0d2d 100644 --- a/app/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt +++ b/app/src/main/java/org/oxycblt/musikr/cover/StoredCovers.kt @@ -7,7 +7,10 @@ interface StoredCovers { interface Editor { suspend fun write(data: ByteArray): Cover.Single? - - suspend fun apply() } -} \ No newline at end of file + + companion object { + suspend fun buildOn(): Editor = TODO() + fun new(): Editor = TODO() + } +} diff --git a/app/src/main/java/org/oxycblt/musikr/cover/StoredCoversDatabase.kt b/app/src/main/java/org/oxycblt/musikr/cover/StoredCoversDatabase.kt deleted file mode 100644 index cde40884f..000000000 --- a/app/src/main/java/org/oxycblt/musikr/cover/StoredCoversDatabase.kt +++ /dev/null @@ -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 . - */ - -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) 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 60f20e1a8..683a9c54f 100644 --- a/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/app/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -28,9 +28,10 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge 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.metadata.MetadataExtractor -import org.oxycblt.musikr.tag.cache.TagCache import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.TagParser @@ -42,7 +43,8 @@ class ExtractStepImpl @Inject constructor( private val metadataExtractor: MetadataExtractor, - private val tagParser: TagParser + private val tagParser: TagParser, + private val coverParser: CoverParser ) : ExtractStep { override fun extract(storage: Storage, nodes: Flow): Flow { val cacheResults = @@ -56,16 +58,18 @@ constructor( .buffer(Channel.UNLIMITED) val (cachedSongs, uncachedSongs) = 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 extractedSongs = Array(split.hot.size) { i -> split.hot[i] - .map { - val metadata = metadataExtractor.extract(it.file) - val tags = tagParser.parse(it.file, metadata) - ExtractedMusic.Song(it.file, tags) + .map { node -> + 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) } + ExtractedMusic.Song(node.file, tags, cover) } .flowOn(Dispatchers.IO) .buffer(Channel.UNLIMITED) @@ -73,7 +77,7 @@ constructor( val writtenSongs = merge(*extractedSongs) .map { - tagCache.write(it.file, it.tags) + storage.tagCache.write(it.file, it.tags) it } .flowOn(Dispatchers.IO) @@ -89,5 +93,5 @@ constructor( } 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 }