Revert "musikr: bundle cover resolution with key"
This reverts commit 8cc939b58d
.
This commit is contained in:
parent
8cc939b58d
commit
8b69042288
17 changed files with 149 additions and 214 deletions
|
@ -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.
|
* @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) =
|
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) {
|
private fun bindImpl(cover: Cover?, desc: String, @DrawableRes errorRes: Int) {
|
||||||
val request =
|
val request =
|
||||||
|
|
|
@ -47,7 +47,7 @@ import org.oxycblt.musikr.cover.Cover
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
|
|
||||||
class CoverKeyer @Inject constructor() : Keyer<Cover> {
|
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
|
class CoverFetcher
|
||||||
|
@ -56,14 +56,16 @@ private constructor(
|
||||||
private val cover: Cover,
|
private val cover: Cover,
|
||||||
private val size: Size,
|
private val size: Size,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
private val storedCovers = StoredCovers.from(context, "covers")
|
||||||
|
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
val streams =
|
val streams =
|
||||||
when (val cover = cover) {
|
when (val cover = cover) {
|
||||||
is Cover.Single -> listOfNotNull(cover.resolve())
|
is Cover.Single -> listOfNotNull(storedCovers.read(cover))
|
||||||
is Cover.Multi ->
|
is Cover.Multi ->
|
||||||
buildList {
|
buildList {
|
||||||
for (single in cover.all) {
|
for (single in cover.all) {
|
||||||
single.resolve()?.let { add(it) }
|
storedCovers.read(single)?.let { add(it) }
|
||||||
if (size == 4) {
|
if (size == 4) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@ import org.oxycblt.musikr.Storage
|
||||||
import org.oxycblt.musikr.cache.Cache
|
import org.oxycblt.musikr.cache.Cache
|
||||||
import org.oxycblt.musikr.cache.CacheDatabase
|
import org.oxycblt.musikr.cache.CacheDatabase
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
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.PlaylistDatabase
|
||||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||||
import org.oxycblt.musikr.tag.interpret.Naming
|
import org.oxycblt.musikr.tag.interpret.Naming
|
||||||
|
@ -369,15 +368,15 @@ constructor(
|
||||||
revision = this.library?.revision ?: musicSettings.revision
|
revision = this.library?.revision ?: musicSettings.revision
|
||||||
storage =
|
storage =
|
||||||
Storage(
|
Storage(
|
||||||
Cache.writeOnly(cacheDatabase),
|
Cache.full(cacheDatabase),
|
||||||
StoredCovers.editor(context, Components.parseUnix("covers_${UUID.randomUUID()}")),
|
StoredCovers.from(context, "covers_$revision"),
|
||||||
StoredPlaylists.from(playlistDatabase))
|
StoredPlaylists.from(playlistDatabase))
|
||||||
} else {
|
} else {
|
||||||
revision = UUID.randomUUID()
|
revision = UUID.randomUUID()
|
||||||
storage =
|
storage =
|
||||||
Storage(
|
Storage(
|
||||||
Cache.writeOnly(cacheDatabase),
|
Cache.writeOnly(cacheDatabase),
|
||||||
StoredCovers.editor(context, Components.parseUnix("covers_$revision")),
|
StoredCovers.from(context, "covers_$revision"),
|
||||||
StoredPlaylists.from(playlistDatabase))
|
StoredPlaylists.from(playlistDatabase))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import org.oxycblt.musikr.Album
|
|
||||||
import org.oxycblt.musikr.Artist
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.oxycblt.musikr.Library
|
import org.oxycblt.musikr.Library
|
||||||
import org.oxycblt.musikr.MutableLibrary
|
import org.oxycblt.musikr.MutableLibrary
|
||||||
|
|
|
@ -26,7 +26,7 @@ import org.oxycblt.musikr.tag.interpret.Separators
|
||||||
|
|
||||||
data class Storage(
|
data class Storage(
|
||||||
val cache: Cache,
|
val cache: Cache,
|
||||||
val coverEditor: StoredCovers.Editor,
|
val storedCovers: StoredCovers,
|
||||||
val storedPlaylists: StoredPlaylists
|
val storedPlaylists: StoredPlaylists
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,11 @@
|
||||||
|
|
||||||
package org.oxycblt.musikr.cache
|
package org.oxycblt.musikr.cache
|
||||||
|
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
import org.oxycblt.musikr.pipeline.RawSong
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
|
|
||||||
interface Cache {
|
interface Cache {
|
||||||
suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
|
suspend fun read(file: DeviceFile): CacheResult
|
||||||
|
|
||||||
suspend fun write(song: RawSong)
|
suspend fun write(song: RawSong)
|
||||||
|
|
||||||
|
@ -41,9 +40,9 @@ sealed interface CacheResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FullCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
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 {
|
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let {
|
||||||
CacheResult.Hit(it.intoRawSong(file, storedCovers))
|
CacheResult.Hit(it.intoRawSong(file))
|
||||||
} ?: CacheResult.Miss(file)
|
} ?: CacheResult.Miss(file)
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) =
|
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 {
|
private class WriteOnlyCache(private val cacheInfoDao: CacheInfoDao) : Cache {
|
||||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
|
override suspend fun read(file: DeviceFile) = CacheResult.Miss(file)
|
||||||
CacheResult.Miss(file)
|
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) =
|
override suspend fun write(song: RawSong) =
|
||||||
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
|
|
|
@ -31,7 +31,6 @@ import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import org.oxycblt.musikr.cover.Cover
|
import org.oxycblt.musikr.cover.Cover
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.Properties
|
import org.oxycblt.musikr.metadata.Properties
|
||||||
import org.oxycblt.musikr.pipeline.RawSong
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
|
@ -90,9 +89,9 @@ internal data class CachedSong(
|
||||||
val genreNames: List<String>,
|
val genreNames: List<String>,
|
||||||
val replayGainTrackAdjustment: Float?,
|
val replayGainTrackAdjustment: Float?,
|
||||||
val replayGainAlbumAdjustment: Float?,
|
val replayGainAlbumAdjustment: Float?,
|
||||||
val coverId: String?,
|
val cover: Cover.Single?,
|
||||||
) {
|
) {
|
||||||
suspend fun intoRawSong(file: DeviceFile, storedCovers: StoredCovers) =
|
fun intoRawSong(file: DeviceFile) =
|
||||||
RawSong(
|
RawSong(
|
||||||
file,
|
file,
|
||||||
Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
|
Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
|
||||||
|
@ -118,7 +117,7 @@ internal data class CachedSong(
|
||||||
genreNames = genreNames,
|
genreNames = genreNames,
|
||||||
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
||||||
coverId?.let { storedCovers.find(it) })
|
cover)
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -131,6 +130,10 @@ internal data class CachedSong(
|
||||||
@TypeConverter fun fromDate(date: Date?) = date?.toString()
|
@TypeConverter fun fromDate(date: Date?) = date?.toString()
|
||||||
|
|
||||||
@TypeConverter fun toDate(string: String?) = string?.let(Date::from)
|
@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 {
|
companion object {
|
||||||
|
@ -159,9 +162,9 @@ internal data class CachedSong(
|
||||||
genreNames = rawSong.tags.genreNames,
|
genreNames = rawSong.tags.genreNames,
|
||||||
replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
|
replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
|
replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
|
||||||
|
cover = rawSong.cover,
|
||||||
mimeType = rawSong.properties.mimeType,
|
mimeType = rawSong.properties.mimeType,
|
||||||
bitrateHz = rawSong.properties.bitrateKbps,
|
bitrateHz = rawSong.properties.bitrateKbps,
|
||||||
sampleRateHz = rawSong.properties.sampleRateHz,
|
sampleRateHz = rawSong.properties.sampleRateHz)
|
||||||
coverId = rawSong.cover?.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,27 +19,30 @@
|
||||||
package org.oxycblt.musikr.cover
|
package org.oxycblt.musikr.cover
|
||||||
|
|
||||||
import org.oxycblt.musikr.Song
|
import org.oxycblt.musikr.Song
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
sealed interface Cover {
|
sealed interface Cover {
|
||||||
val id: String
|
val key: String
|
||||||
|
|
||||||
interface Single : Cover {
|
data class Single(override val key: String) : Cover
|
||||||
suspend fun resolve(): InputStream?
|
|
||||||
|
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 {
|
companion object {
|
||||||
fun from(covers: Collection<Single>) =
|
fun nil() = Multi(listOf())
|
||||||
Multi(
|
|
||||||
covers
|
fun single(key: String) = Single(key)
|
||||||
.groupBy { it.id }
|
|
||||||
|
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
|
||||||
|
|
||||||
|
private fun order(songs: Collection<Song>) =
|
||||||
|
songs
|
||||||
|
.mapNotNull { it.cover }
|
||||||
|
.groupBy { it.key }
|
||||||
.entries
|
.entries
|
||||||
.sortedByDescending { it.key }
|
.sortedByDescending { it.key }
|
||||||
.sortedByDescending { it.value.size }
|
.sortedByDescending { it.value.size }
|
||||||
.map { it.value.first() })
|
.map { it.value.first() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
83
musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt
Normal file
83
musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt
Normal 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"
|
||||||
|
}
|
|
@ -19,59 +19,29 @@
|
||||||
package org.oxycblt.musikr.cover
|
package org.oxycblt.musikr.cover
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import org.oxycblt.musikr.fs.Components
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
interface StoredCovers {
|
interface StoredCovers {
|
||||||
suspend fun find(id: String): Cover.Single?
|
suspend fun read(cover: Cover.Single): InputStream?
|
||||||
|
|
||||||
interface Editor : StoredCovers {
|
suspend fun write(data: ByteArray): Cover.Single?
|
||||||
suspend fun add(data: ByteArray): Cover.Single?
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(context: Context): StoredCovers =
|
fun from(context: Context, path: String): StoredCovers =
|
||||||
FileStoredCovers(AppFiles.from(context))
|
FileStoredCovers(
|
||||||
|
CoverIdentifier.md5(), CoverFiles.from(context, path, CoverFormat.webp()))
|
||||||
fun editor(context: Context, path: Components): Editor =
|
|
||||||
FileStoredCoversEditor(
|
|
||||||
path,
|
|
||||||
AppFiles.from(context),
|
|
||||||
CoverIdentifier.md5(),
|
|
||||||
CoverFormat.webp()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private open class FileStoredCovers(
|
private class FileStoredCovers(
|
||||||
private val appFiles: AppFiles
|
private val coverIdentifier: CoverIdentifier,
|
||||||
|
private val coverFiles: CoverFiles
|
||||||
) : StoredCovers {
|
) : StoredCovers {
|
||||||
override suspend fun find(id: String) =
|
override suspend fun read(cover: Cover.Single) = coverFiles.read(cover.key)
|
||||||
appFiles.read(Components.parseUnix(id))?.let { FileCover(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FileStoredCoversEditor(
|
override suspend fun write(data: ByteArray) =
|
||||||
val root: Components,
|
coverIdentifier.identify(data).let { key ->
|
||||||
val appFiles: AppFiles,
|
coverFiles.write(key, data)
|
||||||
val coverIdentifier: CoverIdentifier,
|
Cover.Single(key)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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()
|
|
||||||
}
|
}
|
|
@ -154,7 +154,7 @@ value class Components private constructor(val components: List<String>) {
|
||||||
|
|
||||||
fun containing(other: Components) = Components(other.components.drop(components.size))
|
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 (/).
|
* Parses a path string into a [Components] instance by the unix path separator (/).
|
||||||
*
|
*
|
||||||
|
|
|
@ -56,7 +56,7 @@ internal class AlbumImpl(private val core: AlbumCore) : Album {
|
||||||
override val releaseType = preAlbum.releaseType
|
override val releaseType = preAlbum.releaseType
|
||||||
override val durationMs = core.songs.sumOf { it.durationMs }
|
override val durationMs = core.songs.sumOf { it.durationMs }
|
||||||
override val dateAdded = core.songs.minOf { it.dateAdded }
|
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? =
|
override val dates: Date.Range? =
|
||||||
core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) }
|
core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) }
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ internal class ArtistImpl(private val core: ArtistCore) : Artist {
|
||||||
get() = core.resolveGenres().toList()
|
get() = core.resolveGenres().toList()
|
||||||
|
|
||||||
override val durationMs = core.songs.sumOf { it.durationMs }
|
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 =
|
private val hashCode =
|
||||||
31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode()
|
31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode()
|
||||||
|
|
|
@ -44,7 +44,7 @@ internal class GenreImpl(private val core: GenreCore) : Genre {
|
||||||
override val songs = core.songs
|
override val songs = core.songs
|
||||||
override val artists = core.artists
|
override val artists = core.artists
|
||||||
override val durationMs = core.songs.sumOf { it.durationMs }
|
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()
|
private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode()
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ internal class PlaylistImpl(val core: PlaylistCore) : Playlist {
|
||||||
override val uid = core.prePlaylist.handle.uid
|
override val uid = core.prePlaylist.handle.uid
|
||||||
override val name: Name.Known = core.prePlaylist.name
|
override val name: Name.Known = core.prePlaylist.name
|
||||||
override val durationMs = core.songs.sumOf { it.durationMs }
|
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
|
override val songs = core.songs
|
||||||
|
|
||||||
private var hashCode =
|
private var hashCode =
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
package org.oxycblt.musikr.pipeline
|
package org.oxycblt.musikr.pipeline
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -51,7 +50,7 @@ internal interface ExtractStep {
|
||||||
MetadataExtractor.from(context),
|
MetadataExtractor.from(context),
|
||||||
TagParser.new(),
|
TagParser.new(),
|
||||||
storage.cache,
|
storage.cache,
|
||||||
storage.coverEditor)
|
storage.storedCovers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ private class ExtractStepImpl(
|
||||||
private val metadataExtractor: MetadataExtractor,
|
private val metadataExtractor: MetadataExtractor,
|
||||||
private val tagParser: TagParser,
|
private val tagParser: TagParser,
|
||||||
private val cache: Cache,
|
private val cache: Cache,
|
||||||
private val coverEditor: StoredCovers.Editor
|
private val storedCovers: StoredCovers
|
||||||
) : ExtractStep {
|
) : ExtractStep {
|
||||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||||
val filterFlow =
|
val filterFlow =
|
||||||
|
@ -75,7 +74,7 @@ private class ExtractStepImpl(
|
||||||
|
|
||||||
val cacheResults =
|
val cacheResults =
|
||||||
audioNodes
|
audioNodes
|
||||||
.map { wrap(it) { cache.read(it, coverEditor) } }
|
.map { wrap(it, cache::read) }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
val cacheFlow =
|
val cacheFlow =
|
||||||
|
@ -121,9 +120,7 @@ private class ExtractStepImpl(
|
||||||
metadata
|
metadata
|
||||||
.mapNotNull { fileWith ->
|
.mapNotNull { fileWith ->
|
||||||
val tags = tagParser.parse(fileWith.file, fileWith.with)
|
val tags = tagParser.parse(fileWith.file, fileWith.with)
|
||||||
val cover = fileWith.with.cover?.let {
|
val cover = fileWith.with.cover?.let { storedCovers.write(it) }
|
||||||
coverEditor.add(it)
|
|
||||||
}
|
|
||||||
RawSong(fileWith.file, fileWith.with.properties, tags, cover)
|
RawSong(fileWith.file, fileWith.with.properties, tags, cover)
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
Loading…
Reference in a new issue