musikr: make subpackages for default impls

This commit is contained in:
Alexander Capehart 2025-01-21 15:58:44 -07:00
parent dbf2dd510c
commit 0919f29085
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
16 changed files with 78 additions and 94 deletions

View file

@ -23,7 +23,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.oxycblt.musikr.cover.CoverIdentifier import org.oxycblt.musikr.cover.fs.CoverIdentifier
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

View file

@ -19,7 +19,7 @@
package org.oxycblt.auxio.image.covers package org.oxycblt.auxio.image.covers
import java.util.UUID import java.util.UUID
import org.oxycblt.musikr.cover.CoverParams import org.oxycblt.musikr.cover.fs.CoverParams
data class CoverSilo(val revision: UUID, val params: CoverParams) { data class CoverSilo(val revision: UUID, val params: CoverParams) {
override fun toString() = "${revision}.${params.resolution}.${params.quality}" override fun toString() = "${revision}.${params.resolution}.${params.quality}"

View file

@ -23,9 +23,9 @@ import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.CoverMode
import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.musikr.cover.CoverIdentifier
import org.oxycblt.musikr.cover.CoverParams
import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.cover.fs.CoverIdentifier
import org.oxycblt.musikr.cover.fs.CoverParams
interface SettingCovers { interface SettingCovers {
suspend fun create(context: Context, revision: UUID): MutableCovers suspend fun create(context: Context, revision: UUID): MutableCovers

View file

@ -23,21 +23,21 @@ 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.CoverFormat
import org.oxycblt.musikr.cover.CoverIdentifier
import org.oxycblt.musikr.cover.Covers 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.MutableFileCovers
import org.oxycblt.musikr.cover.ObtainResult import org.oxycblt.musikr.cover.ObtainResult
import org.oxycblt.musikr.fs.app.AppFiles import org.oxycblt.musikr.cover.fs.CoverFormat
import org.oxycblt.musikr.cover.fs.CoverIdentifier
import org.oxycblt.musikr.cover.fs.FSCovers
import org.oxycblt.musikr.cover.fs.FileCover
import org.oxycblt.musikr.cover.fs.MutableFSCovers
import org.oxycblt.musikr.fs.app.AppFS
open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: FileCovers) : Covers { open class SiloedCovers(private val silo: CoverSilo, private val FSCovers: FSCovers) : Covers {
override suspend fun obtain(id: String): ObtainResult<SiloedCover> { 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 = fileCovers.obtain(coverId.id)) { return when (val result = FSCovers.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()
} }
@ -46,7 +46,7 @@ open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: Fil
companion object { companion object {
suspend fun from(context: Context, silo: CoverSilo): SiloedCovers { suspend fun from(context: Context, silo: CoverSilo): SiloedCovers {
val core = SiloCore.from(context, silo) val core = SiloCore.from(context, silo)
return SiloedCovers(silo, FileCovers(core.files, core.format)) return SiloedCovers(silo, FSCovers(core.files, core.format))
} }
} }
} }
@ -55,7 +55,7 @@ class MutableSiloedCovers
private constructor( private constructor(
private val rootDir: File, private val rootDir: File,
private val silo: CoverSilo, private val silo: CoverSilo,
private val fileCovers: MutableFileCovers private val fileCovers: MutableFSCovers
) : SiloedCovers(silo, fileCovers), MutableCovers { ) : SiloedCovers(silo, fileCovers), MutableCovers {
override suspend fun write(data: ByteArray) = SiloedCover(silo, fileCovers.write(data)) override suspend fun write(data: ByteArray) = SiloedCover(silo, fileCovers.write(data))
@ -77,7 +77,7 @@ private constructor(
): MutableSiloedCovers { ): MutableSiloedCovers {
val core = SiloCore.from(context, silo) val core = SiloCore.from(context, silo)
return MutableSiloedCovers( return MutableSiloedCovers(
core.rootDir, silo, MutableFileCovers(core.files, core.format, coverIdentifier)) core.rootDir, silo, MutableFSCovers(core.files, core.format, coverIdentifier))
} }
} }
} }
@ -101,7 +101,7 @@ data class SiloedCoverId(val silo: CoverSilo, val id: String) {
} }
} }
private data class SiloCore(val rootDir: File, val files: AppFiles, val format: CoverFormat) { private data class SiloCore(val rootDir: File, val files: AppFS, val format: CoverFormat) {
companion object { companion object {
suspend fun from(context: Context, silo: CoverSilo): SiloCore { suspend fun from(context: Context, silo: CoverSilo): SiloCore {
val rootDir: File val rootDir: File
@ -110,7 +110,7 @@ private data class SiloCore(val rootDir: File, val files: AppFiles, val format:
rootDir = context.coversDir() rootDir = context.coversDir()
revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() } revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() }
} }
val files = AppFiles.at(revisionDir) val files = AppFS.at(revisionDir)
val format = CoverFormat.jpeg(silo.params) val format = CoverFormat.jpeg(silo.params)
return SiloCore(rootDir, files, format) return SiloCore(rootDir, files, format)
} }

View file

@ -25,7 +25,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
import org.oxycblt.musikr.cache.DBSongCache import org.oxycblt.musikr.cache.DatabaseSongCache
import org.oxycblt.musikr.cache.SongCache import org.oxycblt.musikr.cache.SongCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
@ -34,7 +34,7 @@ import org.oxycblt.musikr.playlist.db.StoredPlaylists
class MusikrShimModule { class MusikrShimModule {
@Singleton @Singleton
@Provides @Provides
fun songCache(@ApplicationContext context: Context): SongCache = DBSongCache.from(context) fun songCache(@ApplicationContext context: Context): SongCache = DatabaseSongCache.from(context)
@Singleton @Singleton
@Provides @Provides

View file

@ -16,10 +16,13 @@
* 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.cache package org.oxycblt.musikr.cache.db
import android.content.Context import android.content.Context
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cache.CachedSong
import org.oxycblt.musikr.cache.MutableSongCache
import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags

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.cache package org.oxycblt.musikr.cache.db
import android.content.Context import android.content.Context
import androidx.room.Dao import androidx.room.Dao

View file

@ -20,22 +20,6 @@ package org.oxycblt.musikr.cover
import java.io.InputStream import java.io.InputStream
interface Covers {
suspend fun obtain(id: String): ObtainResult<out Cover>
}
interface MutableCovers : Covers {
suspend fun write(data: ByteArray): Cover
suspend fun cleanup(excluding: Collection<Cover>)
}
sealed interface ObtainResult<T : Cover> {
data class Hit<T : Cover>(val cover: T) : ObtainResult<T>
class Miss<T : Cover> : ObtainResult<T>
}
interface Cover { interface Cover {
val id: String val id: String
@ -58,3 +42,19 @@ class CoverCollection private constructor(val covers: List<Cover>) {
.map { it.value.first() }) .map { it.value.first() })
} }
} }
interface Covers {
suspend fun obtain(id: String): ObtainResult<out Cover>
}
interface MutableCovers : Covers {
suspend fun write(data: ByteArray): Cover
suspend fun cleanup(excluding: Collection<Cover>)
}
sealed interface ObtainResult<T : Cover> {
data class Hit<T : Cover>(val cover: T) : ObtainResult<T>
class Miss<T : Cover> : ObtainResult<T>
}

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.cover package org.oxycblt.musikr.cover.fs
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory

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.cover package org.oxycblt.musikr.cover.fs
import java.security.MessageDigest import java.security.MessageDigest

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.cover package org.oxycblt.musikr.cover.fs
class CoverParams private constructor(val resolution: Int, val quality: Int) { class CoverParams private constructor(val resolution: Int, val quality: Int) {
override fun hashCode() = 31 * resolution + quality override fun hashCode() = 31 * resolution + quality

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2025 Auxio Project * Copyright (c) 2025 Auxio Project
* FileCovers.kt is part of Auxio. * FSCovers.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -16,16 +16,19 @@
* 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.cover.fs
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.cover.ObtainResult
import org.oxycblt.musikr.fs.app.AppFS
import org.oxycblt.musikr.fs.app.AppFile import org.oxycblt.musikr.fs.app.AppFile
import org.oxycblt.musikr.fs.app.AppFiles
open class FileCovers(private val appFiles: AppFiles, private val coverFormat: CoverFormat) : open class FSCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) : Covers {
Covers {
override suspend fun obtain(id: String): ObtainResult<FileCover> { override suspend fun obtain(id: String): ObtainResult<FileCover> {
val file = appFiles.find(getFileName(id)) val file = appFS.find(getFileName(id))
return if (file != null) { return if (file != null) {
ObtainResult.Hit(FileCoverImpl(id, file)) ObtainResult.Hit(FileCoverImpl(id, file))
} else { } else {
@ -36,20 +39,20 @@ open class FileCovers(private val appFiles: AppFiles, private val coverFormat: C
protected fun getFileName(id: String) = "$id.${coverFormat.extension}" protected fun getFileName(id: String) = "$id.${coverFormat.extension}"
} }
class MutableFileCovers( class MutableFSCovers(
private val appFiles: AppFiles, private val appFS: AppFS,
private val coverFormat: CoverFormat, private val coverFormat: CoverFormat,
private val coverIdentifier: CoverIdentifier private val coverIdentifier: CoverIdentifier
) : FileCovers(appFiles, coverFormat), MutableCovers { ) : FSCovers(appFS, coverFormat), MutableCovers {
override suspend fun write(data: ByteArray): FileCover { override suspend fun write(data: ByteArray): FileCover {
val id = coverIdentifier.identify(data) val id = coverIdentifier.identify(data)
val file = appFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) } val file = appFS.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
return FileCoverImpl(id, file) return FileCoverImpl(id, file)
} }
override suspend fun cleanup(excluding: Collection<Cover>) { override suspend fun cleanup(excluding: Collection<Cover>) {
val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) } val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) }
appFiles.deleteWhere { it !in used } appFS.deleteWhere { it !in used }
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* AppFiles.kt is part of Auxio. * AppFS.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -28,7 +28,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
interface AppFiles { interface AppFS {
suspend fun find(name: String): AppFile? suspend fun find(name: String): AppFile?
suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile
@ -36,9 +36,9 @@ interface AppFiles {
suspend fun deleteWhere(block: (String) -> Boolean) suspend fun deleteWhere(block: (String) -> Boolean)
companion object { companion object {
suspend fun at(dir: File): AppFiles { suspend fun at(dir: File): AppFS {
withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) } withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) }
return AppFilesImpl(dir) return AppFSImpl(dir)
} }
} }
} }
@ -49,7 +49,7 @@ interface AppFile {
suspend fun open(): InputStream? suspend fun open(): InputStream?
} }
private class AppFilesImpl(private val dir: File) : AppFiles { private class AppFSImpl(private val dir: File) : AppFS {
private val fileMutexes = mutableMapOf<String, Mutex>() private val fileMutexes = mutableMapOf<String, Mutex>()
private val mapMutex = Mutex() private val mapMutex = Mutex()

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * Copyright (c) 2024 Auxio Project
* DeviceFiles.kt is part of Auxio. * DeviceFS.kt is part of Auxio.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -32,16 +32,24 @@ import kotlinx.coroutines.flow.flow
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
internal interface DeviceFiles { internal interface DeviceFS {
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
companion object { companion object {
fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe) fun from(context: Context): DeviceFS = DeviceFSImpl(context.contentResolverSafe)
} }
} }
data class DeviceFile(
val uri: Uri,
val mimeType: String,
val path: Path,
val size: Long,
val modifiedMs: Long
)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles { private class DeviceFSImpl(private val contentResolver: ContentResolver) : DeviceFS {
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> = override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> =
locations.flatMapMerge { location -> locations.flatMapMerge { location ->
exploreImpl( exploreImpl(

View file

@ -1,30 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* DeviceFile.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.fs.device
import android.net.Uri
import org.oxycblt.musikr.fs.Path
data class DeviceFile(
val uri: Uri,
val mimeType: String,
val path: Path,
val size: Long,
val modifiedMs: Long
)

View file

@ -35,7 +35,7 @@ import org.oxycblt.musikr.cache.SongCache
import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.ObtainResult import org.oxycblt.musikr.cover.ObtainResult
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.device.DeviceFiles import org.oxycblt.musikr.fs.device.DeviceFS
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
@ -45,19 +45,19 @@ internal interface ExploreStep {
companion object { companion object {
fun from(context: Context, storage: Storage): ExploreStep = fun from(context: Context, storage: Storage): ExploreStep =
ExploreStepImpl( ExploreStepImpl(
DeviceFiles.from(context), storage.storedPlaylists, storage.cache, storage.covers) DeviceFS.from(context), storage.storedPlaylists, storage.cache, storage.covers)
} }
} }
private class ExploreStepImpl( private class ExploreStepImpl(
private val deviceFiles: DeviceFiles, private val deviceFS: DeviceFS,
private val storedPlaylists: StoredPlaylists, private val storedPlaylists: StoredPlaylists,
private val songCache: SongCache, private val songCache: SongCache,
private val covers: Covers private val covers: Covers
) : ExploreStep { ) : ExploreStep {
override fun explore(locations: List<MusicLocation>): Flow<Explored> { override fun explore(locations: List<MusicLocation>): Flow<Explored> {
val audioFiles = val audioFiles =
deviceFiles deviceFS
.explore(locations.asFlow()) .explore(locations.asFlow())
.filter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE } .filter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)