music: prepare new cover system
This commit is contained in:
parent
8adda19d1a
commit
45ead8253a
8 changed files with 79 additions and 149 deletions
|
@ -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)
|
||||
|
|
|
@ -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<Single>) : 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<Song>) = order(songs).run { Multi(this) }
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
53
app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt
Normal file
53
app/src/main/java/org/oxycblt/musikr/cover/CoverParser.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -7,7 +7,10 @@ interface StoredCovers {
|
|||
|
||||
interface Editor {
|
||||
suspend fun write(data: ByteArray): Cover.Single?
|
||||
|
||||
suspend fun apply()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun buildOn(): Editor = TODO()
|
||||
fun new(): Editor = TODO()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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<ExploreNode>): Flow<ExtractedMusic> {
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue