Revert "musikr: bundle cover resolution with key"

This reverts commit 8cc939b58d.
This commit is contained in:
Alexander Capehart 2024-12-20 15:28:25 -05:00
parent 8cc939b58d
commit 8b69042288
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
17 changed files with 149 additions and 214 deletions

View file

@ -372,7 +372,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
*/
fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) =
bindImpl(Cover.Multi.from(songs.mapNotNull { it.cover }), desc, errorRes)
bindImpl(Cover.multi(songs), desc, errorRes)
private fun bindImpl(cover: Cover?, desc: String, @DrawableRes errorRes: Int) {
val request =

View file

@ -47,7 +47,7 @@ import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.StoredCovers
class CoverKeyer @Inject constructor() : Keyer<Cover> {
override fun key(data: Cover, options: Options) = "${data.id}&${options.size}"
override fun key(data: Cover, options: Options) = "${data.key}&${options.size}"
}
class CoverFetcher
@ -56,14 +56,16 @@ private constructor(
private val cover: Cover,
private val size: Size,
) : Fetcher {
private val storedCovers = StoredCovers.from(context, "covers")
override suspend fun fetch(): FetchResult? {
val streams =
when (val cover = cover) {
is Cover.Single -> listOfNotNull(cover.resolve())
is Cover.Single -> listOfNotNull(storedCovers.read(cover))
is Cover.Multi ->
buildList {
for (single in cover.all) {
single.resolve()?.let { add(it) }
storedCovers.read(single)?.let { add(it) }
if (size == 4) {
break
}

View file

@ -38,7 +38,6 @@ import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.CacheDatabase
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.playlist.db.PlaylistDatabase
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming
@ -369,15 +368,15 @@ constructor(
revision = this.library?.revision ?: musicSettings.revision
storage =
Storage(
Cache.writeOnly(cacheDatabase),
StoredCovers.editor(context, Components.parseUnix("covers_${UUID.randomUUID()}")),
Cache.full(cacheDatabase),
StoredCovers.from(context, "covers_$revision"),
StoredPlaylists.from(playlistDatabase))
} else {
revision = UUID.randomUUID()
storage =
Storage(
Cache.writeOnly(cacheDatabase),
StoredCovers.editor(context, Components.parseUnix("covers_$revision")),
StoredCovers.from(context, "covers_$revision"),
StoredPlaylists.from(playlistDatabase))
}

View file

@ -18,8 +18,6 @@
package org.oxycblt.auxio.music
import org.oxycblt.musikr.Album
import org.oxycblt.musikr.Artist
import java.util.UUID
import org.oxycblt.musikr.Library
import org.oxycblt.musikr.MutableLibrary

View file

@ -26,7 +26,7 @@ import org.oxycblt.musikr.tag.interpret.Separators
data class Storage(
val cache: Cache,
val coverEditor: StoredCovers.Editor,
val storedCovers: StoredCovers,
val storedPlaylists: StoredPlaylists
)

View file

@ -18,12 +18,11 @@
package org.oxycblt.musikr.cache
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong
interface Cache {
suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
suspend fun read(file: DeviceFile): CacheResult
suspend fun write(song: RawSong)
@ -41,9 +40,9 @@ sealed interface CacheResult {
}
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult =
override suspend fun read(file: DeviceFile) =
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let {
CacheResult.Hit(it.intoRawSong(file, storedCovers))
CacheResult.Hit(it.intoRawSong(file))
} ?: CacheResult.Miss(file)
override suspend fun write(song: RawSong) =
@ -51,8 +50,7 @@ private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
}
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
CacheResult.Miss(file)
override suspend fun read(file: DeviceFile) = CacheResult.Miss(file)
override suspend fun write(song: RawSong) =
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))

View file

@ -31,7 +31,6 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong
@ -90,9 +89,9 @@ internal data class CachedSong(
val genreNames: List<String>,
val replayGainTrackAdjustment: Float?,
val replayGainAlbumAdjustment: Float?,
val coverId: String?,
val cover: Cover.Single?,
) {
suspend fun intoRawSong(file: DeviceFile, storedCovers: StoredCovers) =
fun intoRawSong(file: DeviceFile) =
RawSong(
file,
Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
@ -118,7 +117,7 @@ internal data class CachedSong(
genreNames = genreNames,
replayGainTrackAdjustment = replayGainTrackAdjustment,
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
coverId?.let { storedCovers.find(it) })
cover)
object Converters {
@TypeConverter
@ -131,6 +130,10 @@ internal data class CachedSong(
@TypeConverter fun fromDate(date: Date?) = date?.toString()
@TypeConverter fun toDate(string: String?) = string?.let(Date::from)
@TypeConverter fun fromCover(cover: Cover.Single?) = cover?.key
@TypeConverter fun toCover(key: String?) = key?.let { Cover.Single(it) }
}
companion object {
@ -159,9 +162,9 @@ internal data class CachedSong(
genreNames = rawSong.tags.genreNames,
replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
cover = rawSong.cover,
mimeType = rawSong.properties.mimeType,
bitrateHz = rawSong.properties.bitrateKbps,
sampleRateHz = rawSong.properties.sampleRateHz,
coverId = rawSong.cover?.id)
sampleRateHz = rawSong.properties.sampleRateHz)
}
}

View file

@ -1,118 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* AppFiles.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 android.content.Context
import android.util.Log
import java.io.File
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.util.update
import java.io.InputStream
import java.io.OutputStream
import java.security.MessageDigest
import java.util.UUID
internal interface AppFiles {
suspend fun read(path: Components): AppFile?
suspend fun write(path: Components, block: suspend (OutputStream) -> Unit): AppFile?
companion object {
fun from(context: Context): AppFiles =
AppFilesImpl(context.filesDir)
}
}
interface AppFile {
val path: Components
suspend fun open(): InputStream?
}
private class AppFilesImpl(private val rootDir: File) :
AppFiles {
private val tempDir = File(rootDir, "tmp-${UUID.randomUUID()}")
private val fileMutexes = mutableMapOf<String, Mutex>()
private val mapMutex = Mutex()
private suspend fun getMutexForFile(path: String): Mutex {
return mapMutex.withLock { fileMutexes.getOrPut(path) { Mutex() } }
}
override suspend fun read(path: Components): AppFile? =
withContext(Dispatchers.IO) {
val file = rootDir.resolve(path.unixString)
if (file.exists()) {
AppFileImpl(path, file)
} else {
null
}
}
override suspend fun write(path: Components, block: suspend (OutputStream) -> Unit): AppFile? =
withContext(Dispatchers.IO) {
if (!tempDir.exists()) {
tempDir.mkdirs()
}
val parentDir = rootDir.resolve(path.parent().toString())
if (parentDir.isFile) {
parentDir.delete()
}
if (!parentDir.exists()) {
parentDir.mkdirs()
}
val pathString = path.unixString
val fileMutex = getMutexForFile(pathString)
fileMutex.withLock {
val targetFile = rootDir.resolve(pathString)
if (targetFile.exists()) {
return@withLock AppFileImpl(path, targetFile)
}
val tempFile = tempDir.resolve(pathString.sha256())
try {
block(tempFile.outputStream())
tempFile.renameTo(targetFile)
AppFileImpl(path, targetFile)
} catch (e: IOException) {
tempFile.delete()
null
}
}
}
}
class AppFileImpl(
override val path: Components,
private val file: File
) : AppFile {
override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() }
}
@OptIn(ExperimentalStdlibApi::class)
private fun String.sha256() = MessageDigest.getInstance("SHA-256").let {
it.update(this)
it.digest().toHexString()
}

View file

@ -15,31 +15,34 @@
* 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 org.oxycblt.musikr.Song
import java.io.InputStream
sealed interface Cover {
val id: String
val key: String
interface Single : Cover {
suspend fun resolve(): InputStream?
data class Single(override val key: String) : Cover
class Multi(val all: List<Single>) : Cover {
override val key = "multi@${all.hashCode()}"
}
class Multi private constructor(val all: List<Single>) : Cover {
override val id = "multi@${all.hashCode()}"
companion object {
fun nil() = Multi(listOf())
companion object {
fun from(covers: Collection<Single>) =
Multi(
covers
.groupBy { it.id }
.entries
.sortedByDescending { it.key }
.sortedByDescending { it.value.size }
.map { it.value.first() })
}
fun single(key: String) = Single(key)
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
private fun order(songs: Collection<Song>) =
songs
.mapNotNull { it.cover }
.groupBy { it.key }
.entries
.sortedByDescending { it.key }
.sortedByDescending { it.value.size }
.map { it.value.first() }
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2024 Auxio Project
* CoverFiles.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 android.content.Context
import java.io.File
import java.io.IOException
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
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(File(context.filesDir, path).also { it.mkdirs() }, format)
}
}
private class CoverFilesImpl(private val dir: File, private val coverFormat: CoverFormat) :
CoverFiles {
private val fileMutexes = mutableMapOf<String, Mutex>()
private val mapMutex = Mutex()
private suspend fun getMutexForFile(file: String): Mutex {
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
}
override suspend fun read(id: String): InputStream? =
withContext(Dispatchers.IO) {
try {
File(dir, getTargetFilePath(id)).inputStream()
} catch (e: IOException) {
null
}
}
override suspend fun write(id: String, data: ByteArray) {
val fileMutex = getMutexForFile(id)
fileMutex.withLock {
val targetFile = File(dir, getTargetFilePath(id))
if (targetFile.exists()) {
return
}
withContext(Dispatchers.IO) {
val tempFile = File(dir, getTempFilePath(id))
try {
tempFile.outputStream().use { coverFormat.transcodeInto(data, it) }
tempFile.renameTo(targetFile)
} catch (e: IOException) {
tempFile.delete()
}
}
}
}
private fun getTargetFilePath(name: String) = "cover_${name}.${coverFormat.extension}"
private fun getTempFilePath(name: String) = "${getTargetFilePath(name)}.tmp"
}

View file

@ -15,63 +15,33 @@
* 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 android.content.Context
import android.util.Log
import org.oxycblt.musikr.fs.Components
import java.io.File
import java.io.InputStream
interface StoredCovers {
suspend fun find(id: String): Cover.Single?
suspend fun read(cover: Cover.Single): InputStream?
interface Editor : StoredCovers {
suspend fun add(data: ByteArray): Cover.Single?
}
suspend fun write(data: ByteArray): Cover.Single?
companion object {
fun from(context: Context): StoredCovers =
FileStoredCovers(AppFiles.from(context))
fun editor(context: Context, path: Components): Editor =
FileStoredCoversEditor(
path,
AppFiles.from(context),
CoverIdentifier.md5(),
CoverFormat.webp()
)
fun from(context: Context, path: String): StoredCovers =
FileStoredCovers(
CoverIdentifier.md5(), CoverFiles.from(context, path, CoverFormat.webp()))
}
}
private open class FileStoredCovers(
private val appFiles: AppFiles
private class FileStoredCovers(
private val coverIdentifier: CoverIdentifier,
private val coverFiles: CoverFiles
) : StoredCovers {
override suspend fun find(id: String) =
appFiles.read(Components.parseUnix(id))?.let { FileCover(it) }
}
override suspend fun read(cover: Cover.Single) = coverFiles.read(cover.key)
private class FileStoredCoversEditor(
val root: Components,
val appFiles: AppFiles,
val coverIdentifier: CoverIdentifier,
val coverFormat: CoverFormat
) : FileStoredCovers(appFiles), StoredCovers.Editor {
override suspend fun add(data: ByteArray): Cover.Single? {
val id = coverIdentifier.identify(data)
val path = getTargetPath(id)
val file = appFiles.write(path) {
coverFormat.transcodeInto(data, it)
override suspend fun write(data: ByteArray) =
coverIdentifier.identify(data).let { key ->
coverFiles.write(key, data)
Cover.Single(key)
}
return file?.let { FileCover(it) }
}
private fun getTargetPath(id: String) = root.child("$id.${coverFormat.extension}")
}
private class FileCover(private val file: AppFile) : Cover.Single {
override val id: String = file.path.unixString
override suspend fun resolve() = file.open()
}

View file

@ -154,7 +154,7 @@ value class Components private constructor(val components: List<String>) {
fun containing(other: Components) = Components(other.components.drop(components.size))
companion object {
internal companion object {
/**
* Parses a path string into a [Components] instance by the unix path separator (/).
*

View file

@ -56,7 +56,7 @@ internal class AlbumImpl(private val core: AlbumCore) : Album {
override val releaseType = preAlbum.releaseType
override val durationMs = core.songs.sumOf { it.durationMs }
override val dateAdded = core.songs.minOf { it.dateAdded }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover })
override val cover = Cover.multi(core.songs)
override val dates: Date.Range? =
core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) }

View file

@ -55,7 +55,7 @@ internal class ArtistImpl(private val core: ArtistCore) : Artist {
get() = core.resolveGenres().toList()
override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover })
override val cover = Cover.multi(core.songs)
private val hashCode =
31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode()

View file

@ -44,7 +44,7 @@ internal class GenreImpl(private val core: GenreCore) : Genre {
override val songs = core.songs
override val artists = core.artists
override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover })
override val cover = Cover.multi(core.songs)
private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode()

View file

@ -33,7 +33,7 @@ internal class PlaylistImpl(val core: PlaylistCore) : Playlist {
override val uid = core.prePlaylist.handle.uid
override val name: Name.Known = core.prePlaylist.name
override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.Multi.from(core.songs.mapNotNull { it.cover })
override val cover = Cover.multi(core.songs)
override val songs = core.songs
private var hashCode =

View file

@ -19,7 +19,6 @@
package org.oxycblt.musikr.pipeline
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
@ -51,7 +50,7 @@ internal interface ExtractStep {
MetadataExtractor.from(context),
TagParser.new(),
storage.cache,
storage.coverEditor)
storage.storedCovers)
}
}
@ -60,7 +59,7 @@ private class ExtractStepImpl(
private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser,
private val cache: Cache,
private val coverEditor: StoredCovers.Editor
private val storedCovers: StoredCovers
) : ExtractStep {
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val filterFlow =
@ -75,7 +74,7 @@ private class ExtractStepImpl(
val cacheResults =
audioNodes
.map { wrap(it) { cache.read(it, coverEditor) } }
.map { wrap(it, cache::read) }
.flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED)
val cacheFlow =
@ -121,9 +120,7 @@ private class ExtractStepImpl(
metadata
.mapNotNull { fileWith ->
val tags = tagParser.parse(fileWith.file, fileWith.with)
val cover = fileWith.with.cover?.let {
coverEditor.add(it)
}
val cover = fileWith.with.cover?.let { storedCovers.write(it) }
RawSong(fileWith.file, fileWith.with.properties, tags, cover)
}
.flowOn(Dispatchers.IO)