musikr: make cover files more concrete

This should allow me to implement a solid ContentProvider.
This commit is contained in:
Alexander Capehart 2025-01-01 13:58:52 -07:00
parent 2401f9031f
commit 75455b1b90
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 128 additions and 112 deletions

View file

@ -35,6 +35,6 @@ class NullCovers(private val context: Context, private val identifier: CoverIden
}
}
private class NullCover(override val id: String) : Cover {
class NullCover(override val id: String) : Cover {
override suspend fun open() = null
}

View file

@ -23,24 +23,26 @@ import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverFiles
import org.oxycblt.musikr.fs.app.AppFiles
import org.oxycblt.musikr.cover.CoverFormat
import org.oxycblt.musikr.cover.CoverIdentifier
import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.FileCover
import org.oxycblt.musikr.cover.FileCovers
import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.cover.ObtainResult
class SiloedCovers(
class SiloedCovers
private constructor(
private val rootDir: File,
private val silo: CoverSilo,
private val inner: MutableCovers
private val inner: FileCovers
) : MutableCovers {
override suspend fun obtain(id: String): ObtainResult {
val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss
if (coverId.silo != silo) return ObtainResult.Miss
override suspend fun obtain(id: String): ObtainResult<SiloedCover> {
val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss()
if (coverId.silo != silo) return ObtainResult.Miss()
return when (val result = inner.obtain(coverId.id)) {
is ObtainResult.Hit -> ObtainResult.Hit(SiloedCover(silo, result.cover))
is ObtainResult.Miss -> ObtainResult.Miss
is ObtainResult.Miss -> ObtainResult.Miss()
}
}
@ -68,14 +70,14 @@ class SiloedCovers(
rootDir = context.coversDir()
revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() }
}
val files = CoverFiles.at(revisionDir)
val files = AppFiles.at(revisionDir)
val format = CoverFormat.jpeg(silo.params)
return SiloedCovers(rootDir, silo, Covers.from(files, format, identifier))
return SiloedCovers(rootDir, silo, FileCovers(files, format, identifier))
}
}
}
class SiloedCover(silo: CoverSilo, val innerCover: Cover) : Cover by innerCover {
class SiloedCover(silo: CoverSilo, val innerCover: FileCover) : FileCover by innerCover {
private val innerId = SiloedCoverId(silo, innerCover.id)
override val id = innerId.toString()
}

View file

@ -1,40 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* Cover.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
interface Cover {
val id: String
suspend fun open(): InputStream?
}
class CoverCollection private constructor(val covers: List<Cover>) {
companion object {
fun from(covers: Collection<Cover>) =
CoverCollection(
covers
.groupBy { it.id }
.entries
.sortedByDescending { it.key }
.sortedByDescending { it.value.size }
.map { it.value.first() })
}
}

View file

@ -18,16 +18,10 @@
package org.oxycblt.musikr.cover
interface Covers {
suspend fun obtain(id: String): ObtainResult
import java.io.InputStream
companion object {
fun from(
coverFiles: CoverFiles,
coverFormat: CoverFormat,
identifier: CoverIdentifier = CoverIdentifier.md5()
): MutableCovers = FileCovers(coverFiles, coverFormat, identifier)
}
interface Covers {
suspend fun obtain(id: String): ObtainResult<out Cover>
}
interface MutableCovers : Covers {
@ -36,40 +30,27 @@ interface MutableCovers : Covers {
suspend fun cleanup(excluding: Collection<Cover>)
}
sealed interface ObtainResult {
data class Hit(val cover: Cover) : ObtainResult
interface Cover {
val id: String
data object Miss : ObtainResult
suspend fun open(): InputStream?
}
private class FileCovers(
private val coverFiles: CoverFiles,
private val coverFormat: CoverFormat,
private val coverIdentifier: CoverIdentifier,
) : Covers, MutableCovers {
override suspend fun obtain(id: String): ObtainResult {
val file = coverFiles.find(getFileName(id))
return if (file != null) {
ObtainResult.Hit(FileCover(id, file))
} else {
ObtainResult.Miss
}
class CoverCollection private constructor(val covers: List<Cover>) {
companion object {
fun from(covers: Collection<Cover>) =
CoverCollection(
covers
.groupBy { it.id }
.entries
.sortedByDescending { it.key }
.sortedByDescending { it.value.size }
.map { it.value.first() })
}
override suspend fun write(data: ByteArray): Cover {
val id = coverIdentifier.identify(data)
val file = coverFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
return FileCover(id, file)
}
override suspend fun cleanup(excluding: Collection<Cover>) {
val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) }
coverFiles.deleteWhere { it !in used }
}
private fun getFileName(id: String) = "$id.${coverFormat.extension}"
}
private class FileCover(override val id: String, private val coverFile: CoverFile) : Cover {
override suspend fun open() = coverFile.open()
sealed interface ObtainResult<T : Cover> {
data class Hit<T : Cover>(val cover: T) : ObtainResult<T>
class Miss<T : Cover> : ObtainResult<T>
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2025 Auxio Project
* FileCovers.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.os.ParcelFileDescriptor
import org.oxycblt.musikr.fs.app.AppFile
import org.oxycblt.musikr.fs.app.AppFiles
class FileCovers(
private val appFiles: AppFiles,
private val coverFormat: CoverFormat,
private val coverIdentifier: CoverIdentifier,
) : Covers, MutableCovers {
override suspend fun obtain(id: String): ObtainResult<FileCover> {
val file = appFiles.find(getFileName(id))
return if (file != null) {
ObtainResult.Hit(FileCoverImpl(id, file))
} else {
ObtainResult.Miss()
}
}
override suspend fun write(data: ByteArray): FileCover {
val id = coverIdentifier.identify(data)
val file = appFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
return FileCoverImpl(id, file)
}
override suspend fun cleanup(excluding: Collection<Cover>) {
val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) }
appFiles.deleteWhere { it !in used }
}
private fun getFileName(id: String) = "$id.${coverFormat.extension}"
}
interface FileCover : Cover {
suspend fun fd(): ParcelFileDescriptor?
}
private class FileCoverImpl(override val id: String, private val appFile: AppFile) : FileCover {
override suspend fun fd() = appFile.fd()
override suspend fun open() = appFile.open()
}

View file

@ -23,7 +23,7 @@ import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.musikr.fs.query.contentResolverSafe
import org.oxycblt.musikr.fs.device.contentResolverSafe
import org.oxycblt.musikr.util.splitEscaped
class MusicLocation private constructor(val uri: Uri, val path: Path) {

View file

@ -16,8 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.cover
package org.oxycblt.musikr.fs.app
import android.os.ParcelFileDescriptor
import java.io.File
import java.io.IOException
import java.io.InputStream
@ -27,26 +28,28 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
interface CoverFiles {
suspend fun find(name: String): CoverFile?
interface AppFiles {
suspend fun find(name: String): AppFile?
suspend fun write(name: String, block: suspend (OutputStream) -> Unit): CoverFile
suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile
suspend fun deleteWhere(block: (String) -> Boolean)
companion object {
suspend fun at(dir: File): CoverFiles {
suspend fun at(dir: File): AppFiles {
withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) }
return CoverFilesImpl(dir)
return AppFilesImpl(dir)
}
}
}
interface CoverFile {
interface AppFile {
suspend fun fd(): ParcelFileDescriptor?
suspend fun open(): InputStream?
}
private class CoverFilesImpl(private val dir: File) : CoverFiles {
private class AppFilesImpl(private val dir: File) : AppFiles {
private val fileMutexes = mutableMapOf<String, Mutex>()
private val mapMutex = Mutex()
@ -54,16 +57,16 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
}
override suspend fun find(name: String): CoverFile? =
override suspend fun find(name: String): AppFile? =
withContext(Dispatchers.IO) {
try {
File(dir, name).takeIf { it.exists() }?.let { CoverFileImpl(it) }
File(dir, name).takeIf { it.exists() }?.let { AppFileImpl(it) }
} catch (e: IOException) {
null
}
}
override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): CoverFile {
override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile {
val fileMutex = getMutexForFile(name)
return fileMutex.withLock {
val targetFile = File(dir, name)
@ -74,14 +77,14 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
try {
tempFile.outputStream().use { block(it) }
tempFile.renameTo(targetFile)
CoverFileImpl(targetFile)
AppFileImpl(targetFile)
} catch (e: IOException) {
tempFile.delete()
throw e
}
}
} else {
CoverFileImpl(targetFile)
AppFileImpl(targetFile)
}
}
}
@ -93,6 +96,15 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
}
}
private class CoverFileImpl(private val file: File) : CoverFile {
private class AppFileImpl(private val file: File) : AppFile {
override suspend fun fd() =
withContext(Dispatchers.IO) {
try {
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
} catch (e: IOException) {
null
}
}
override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() }
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.fs.query
package org.oxycblt.musikr.fs.device
import android.content.ContentResolver
import android.content.Context

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.fs.query
package org.oxycblt.musikr.fs.device
import android.content.ContentResolver
import android.content.Context

View file

@ -26,8 +26,8 @@ import java.io.File
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.Volume
import org.oxycblt.musikr.fs.query.contentResolverSafe
import org.oxycblt.musikr.fs.query.useQuery
import org.oxycblt.musikr.fs.device.contentResolverSafe
import org.oxycblt.musikr.fs.device.useQuery
/**
* A factory for parsing the reverse-engineered format of the URIs obtained from document picker.

View file

@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.query.DeviceFiles
import org.oxycblt.musikr.fs.device.DeviceFiles
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.m3u.M3U

View file

@ -24,7 +24,7 @@ import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.musikr.fs.query.contentResolverSafe
import org.oxycblt.musikr.fs.device.contentResolverSafe
import org.oxycblt.musikr.playlist.m3u.M3U
/**