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 override suspend fun open() = null
} }

View file

@ -23,24 +23,26 @@ import java.io.File
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.musikr.cover.Cover 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.CoverFormat
import org.oxycblt.musikr.cover.CoverIdentifier 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.MutableCovers
import org.oxycblt.musikr.cover.ObtainResult import org.oxycblt.musikr.cover.ObtainResult
class SiloedCovers( class SiloedCovers
private constructor(
private val rootDir: File, private val rootDir: File,
private val silo: CoverSilo, private val silo: CoverSilo,
private val inner: MutableCovers private val inner: FileCovers
) : MutableCovers { ) : MutableCovers {
override suspend fun obtain(id: String): ObtainResult { override suspend fun obtain(id: String): ObtainResult<SiloedCover> {
val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss()
if (coverId.silo != silo) return ObtainResult.Miss if (coverId.silo != silo) return ObtainResult.Miss()
return when (val result = inner.obtain(coverId.id)) { return when (val result = inner.obtain(coverId.id)) {
is ObtainResult.Hit -> ObtainResult.Hit(SiloedCover(silo, result.cover)) 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() rootDir = context.coversDir()
revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() } revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() }
} }
val files = CoverFiles.at(revisionDir) val files = AppFiles.at(revisionDir)
val format = CoverFormat.jpeg(silo.params) 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) private val innerId = SiloedCoverId(silo, innerCover.id)
override val id = innerId.toString() 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 package org.oxycblt.musikr.cover
interface Covers { import java.io.InputStream
suspend fun obtain(id: String): ObtainResult
companion object { interface Covers {
fun from( suspend fun obtain(id: String): ObtainResult<out Cover>
coverFiles: CoverFiles,
coverFormat: CoverFormat,
identifier: CoverIdentifier = CoverIdentifier.md5()
): MutableCovers = FileCovers(coverFiles, coverFormat, identifier)
}
} }
interface MutableCovers : Covers { interface MutableCovers : Covers {
@ -36,40 +30,27 @@ interface MutableCovers : Covers {
suspend fun cleanup(excluding: Collection<Cover>) suspend fun cleanup(excluding: Collection<Cover>)
} }
sealed interface ObtainResult { interface Cover {
data class Hit(val cover: Cover) : ObtainResult val id: String
data object Miss : ObtainResult suspend fun open(): InputStream?
} }
private class FileCovers( class CoverCollection private constructor(val covers: List<Cover>) {
private val coverFiles: CoverFiles, companion object {
private val coverFormat: CoverFormat, fun from(covers: Collection<Cover>) =
private val coverIdentifier: CoverIdentifier, CoverCollection(
) : Covers, MutableCovers { covers
override suspend fun obtain(id: String): ObtainResult { .groupBy { it.id }
val file = coverFiles.find(getFileName(id)) .entries
return if (file != null) { .sortedByDescending { it.key }
ObtainResult.Hit(FileCover(id, file)) .sortedByDescending { it.value.size }
} else { .map { it.value.first() })
ObtainResult.Miss
}
} }
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 { sealed interface ObtainResult<T : Cover> {
override suspend fun open() = coverFile.open() 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.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import org.oxycblt.musikr.fs.path.DocumentPathFactory 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 import org.oxycblt.musikr.util.splitEscaped
class MusicLocation private constructor(val uri: Uri, val path: Path) { 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/>. * 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.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -27,26 +28,28 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
interface CoverFiles { interface AppFiles {
suspend fun find(name: String): CoverFile? 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) suspend fun deleteWhere(block: (String) -> Boolean)
companion object { companion object {
suspend fun at(dir: File): CoverFiles { suspend fun at(dir: File): AppFiles {
withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) } 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? 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 fileMutexes = mutableMapOf<String, Mutex>()
private val mapMutex = Mutex() private val mapMutex = Mutex()
@ -54,16 +57,16 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } } return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
} }
override suspend fun find(name: String): CoverFile? = override suspend fun find(name: String): AppFile? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
File(dir, name).takeIf { it.exists() }?.let { CoverFileImpl(it) } File(dir, name).takeIf { it.exists() }?.let { AppFileImpl(it) }
} catch (e: IOException) { } catch (e: IOException) {
null 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) val fileMutex = getMutexForFile(name)
return fileMutex.withLock { return fileMutex.withLock {
val targetFile = File(dir, name) val targetFile = File(dir, name)
@ -74,14 +77,14 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
try { try {
tempFile.outputStream().use { block(it) } tempFile.outputStream().use { block(it) }
tempFile.renameTo(targetFile) tempFile.renameTo(targetFile)
CoverFileImpl(targetFile) AppFileImpl(targetFile)
} catch (e: IOException) { } catch (e: IOException) {
tempFile.delete() tempFile.delete()
throw e throw e
} }
} }
} else { } 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() } 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/>. * 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.ContentResolver
import android.content.Context import android.content.Context

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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.ContentResolver
import android.content.Context 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.Components
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.Volume import org.oxycblt.musikr.fs.Volume
import org.oxycblt.musikr.fs.query.contentResolverSafe import org.oxycblt.musikr.fs.device.contentResolverSafe
import org.oxycblt.musikr.fs.query.useQuery import org.oxycblt.musikr.fs.device.useQuery
/** /**
* A factory for parsing the reverse-engineered format of the URIs obtained from document picker. * 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.Storage
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation 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.PlaylistFile
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.m3u.M3U 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.Components
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.path.DocumentPathFactory 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 import org.oxycblt.musikr.playlist.m3u.M3U
/** /**