diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt index d88bbd4ac..dec9e8152 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt index 8a9949835..1c37fa99e 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt @@ -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 { + 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() } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt deleted file mode 100644 index 3be0d33d3..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Cover.kt +++ /dev/null @@ -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 . - */ - -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) { - companion object { - fun from(covers: Collection) = - CoverCollection( - covers - .groupBy { it.id } - .entries - .sortedByDescending { it.key } - .sortedByDescending { it.value.size } - .map { it.value.first() }) - } -} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt index 93c87e2c7..8fe021e2d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt @@ -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 } interface MutableCovers : Covers { @@ -36,40 +30,27 @@ interface MutableCovers : Covers { suspend fun cleanup(excluding: Collection) } -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) { + companion object { + fun from(covers: Collection) = + 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) { - 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 { + data class Hit(val cover: T) : ObtainResult + + class Miss : ObtainResult } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt new file mode 100644 index 000000000..1864fc905 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt @@ -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 . + */ + +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 { + 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) { + 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() +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt index d3ae6a1f7..f45558757 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/MusicLocation.kt @@ -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) { diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt similarity index 73% rename from musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt index 52ab538d1..32313acf6 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt @@ -16,8 +16,9 @@ * along with this program. If not, see . */ -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() 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() } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/query/DeviceFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt similarity index 99% rename from musikr/src/main/java/org/oxycblt/musikr/fs/query/DeviceFiles.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt index 79e41b9eb..2f737995c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/query/DeviceFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.fs.query +package org.oxycblt.musikr.fs.device import android.content.ContentResolver import android.content.Context diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/query/QueryUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/QueryUtil.kt similarity index 98% rename from musikr/src/main/java/org/oxycblt/musikr/fs/query/QueryUtil.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/device/QueryUtil.kt index 14a05edc4..a305108d6 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/query/QueryUtil.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/QueryUtil.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.fs.query +package org.oxycblt.musikr.fs.device import android.content.ContentResolver import android.content.Context diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt index de1cb6a28..92b7f825f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt @@ -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. diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt index 2439288b0..1e77659e1 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -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 diff --git a/musikr/src/main/java/org/oxycblt/musikr/playlist/ExternalPlaylistManager.kt b/musikr/src/main/java/org/oxycblt/musikr/playlist/ExternalPlaylistManager.kt index 0e6ecf179..f437a16ad 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/playlist/ExternalPlaylistManager.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/playlist/ExternalPlaylistManager.kt @@ -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 /**