musikr: make cover files more concrete
This should allow me to implement a solid ContentProvider.
This commit is contained in:
parent
2401f9031f
commit
75455b1b90
12 changed files with 128 additions and 112 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
61
musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt
Normal file
61
musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt
Normal 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()
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue