From 0d0a20d760f704bcbf7f9af88791fa999bdf35cf Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 3 Mar 2025 17:00:39 -0700 Subject: [PATCH 01/28] musikr: simplify pipeline --- .../oxycblt/auxio/music/MusicRepository.kt | 8 +- .../auxio/music/shim/MusikrShimModule.kt | 4 +- .../auxio/music/shim/WriteOnlyMutableCache.kt | 41 ++++ .../main/java/org/oxycblt/musikr/Config.kt | 6 +- .../main/java/org/oxycblt/musikr/Musikr.kt | 2 +- .../java/org/oxycblt/musikr/cache/Cache.kt | 39 ++-- .../org/oxycblt/musikr/cache/CacheDatabase.kt | 209 ------------------ .../org/oxycblt/musikr/cache/StoredCache.kt | 88 -------- .../oxycblt/musikr/cache/db/CacheDatabase.kt | 127 +++++++++++ .../org/oxycblt/musikr/cache/db/DBCache.kt | 120 ++++++++++ .../musikr/metadata/MetadataExtractor.kt | 19 +- .../oxycblt/musikr/pipeline/EvaluateStep.kt | 48 +--- .../oxycblt/musikr/pipeline/ExploreStep.kt | 66 ++++-- .../oxycblt/musikr/pipeline/ExtractStep.kt | 176 +++------------ .../org/oxycblt/musikr/pipeline/FlowUtil.kt | 42 +++- .../musikr/pipeline/PipelineException.kt | 68 +----- .../oxycblt/musikr/pipeline/PipelineItem.kt | 57 +++++ .../oxycblt/musikr/tag/parse/ParsedTags.kt | 2 +- 18 files changed, 521 insertions(+), 601 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyMutableCache.kt delete mode 100644 musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt delete mode 100644 musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt create mode 100644 musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt create mode 100644 musikr/src/main/java/org/oxycblt/musikr/cache/db/DBCache.kt create mode 100644 musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 90aac5c36..20e1c110f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import org.oxycblt.auxio.image.covers.SettingCovers import org.oxycblt.auxio.music.MusicRepository.IndexingWorker +import org.oxycblt.auxio.music.shim.WriteOnlyMutableCache import org.oxycblt.musikr.IndexingProgress import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.Library @@ -38,7 +39,7 @@ import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song import org.oxycblt.musikr.Storage -import org.oxycblt.musikr.cache.StoredCache +import org.oxycblt.musikr.cache.db.MutableDBCache import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators @@ -236,7 +237,7 @@ class MusicRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, - private val storedCache: StoredCache, + private val dbCache: MutableDBCache, private val storedPlaylists: StoredPlaylists, private val settingCovers: SettingCovers, private val musicSettings: MusicSettings @@ -388,11 +389,10 @@ constructor( val currentRevision = musicSettings.revision val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() - val cache = if (withCache) storedCache.visible() else storedCache.invisible() + val cache = if (withCache) dbCache else WriteOnlyMutableCache(dbCache) val covers = settingCovers.mutate(context, newRevision) val storage = Storage(cache, covers, storedPlaylists) val interpretation = Interpretation(nameFactory, separators, ignoreHidden) - val result = Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress) // Music loading completed, update the revision right now so we re-use this work diff --git a/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt b/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt index 977936cd5..ee0702450 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt @@ -25,7 +25,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -import org.oxycblt.musikr.cache.StoredCache +import org.oxycblt.musikr.cache.db.MutableDBCache import org.oxycblt.musikr.playlist.db.StoredPlaylists @Module @@ -33,7 +33,7 @@ import org.oxycblt.musikr.playlist.db.StoredPlaylists class MusikrShimModule { @Singleton @Provides - fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context) + fun cache(@ApplicationContext context: Context) = MutableDBCache.from(context) @Singleton @Provides diff --git a/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyMutableCache.kt b/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyMutableCache.kt new file mode 100644 index 000000000..d4076f965 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyMutableCache.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Auxio Project + * WriteOnlyMutableCache.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.auxio.music.shim + +import org.oxycblt.musikr.cache.CacheResult +import org.oxycblt.musikr.cache.CachedSong +import org.oxycblt.musikr.cache.MutableCache +import org.oxycblt.musikr.fs.device.DeviceFile + +class WriteOnlyMutableCache(private val inner: MutableCache) : MutableCache { + override suspend fun read(file: DeviceFile): CacheResult { + return when (val result = inner.read(file)) { + is CacheResult.Hit -> CacheResult.Stale(file, result.song.addedMs) + else -> result + } + } + + override suspend fun write(cachedSong: CachedSong) { + inner.write(cachedSong) + } + + override suspend fun cleanup(excluding: List) { + inner.cleanup(excluding) + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index d00b3b052..3d699f2d0 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -18,7 +18,7 @@ package org.oxycblt.musikr -import org.oxycblt.musikr.cache.Cache +import org.oxycblt.musikr.cache.MutableCache import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.playlist.db.StoredPlaylists @@ -31,14 +31,14 @@ data class Storage( * A factory producing a repository of cached metadata to read and write from over the course of * music loading. This will only be used during music loading. */ - val cache: Cache.Factory, + val cache: MutableCache, /** * A repository of cover images to for re-use during music loading. Should be kept in lock-step * with the cache for best performance. This will be used during music loading and when * retrieving cover information from the library. */ - val storedCovers: MutableCovers, + val covers: MutableCovers, /** * A repository of user-created playlists that should also be loaded into the library. This will diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt index c18a01684..91ecded79 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt @@ -143,6 +143,6 @@ private class LibraryResultImpl( override val library: MutableLibrary ) : LibraryResult { override suspend fun cleanup() { - storage.storedCovers.cleanup(library.songs.mapNotNull { it.cover }) + storage.covers.cleanup(library.songs.mapNotNull { it.cover }) } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt index 670ee2f31..fe1fbb42c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -18,25 +18,32 @@ package org.oxycblt.musikr.cache -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.fs.device.DeviceFile -import org.oxycblt.musikr.pipeline.RawSong +import org.oxycblt.musikr.metadata.Properties +import org.oxycblt.musikr.tag.parse.ParsedTags -abstract class Cache { - internal abstract suspend fun read(file: DeviceFile, covers: Covers): CacheResult - - internal abstract suspend fun write(song: RawSong) - - internal abstract suspend fun finalize() - - abstract class Factory { - internal abstract fun open(): Cache - } +interface Cache { + suspend fun read(file: DeviceFile): CacheResult } -internal sealed interface CacheResult { - data class Hit(val song: RawSong) : CacheResult +interface MutableCache : Cache { + suspend fun write(cachedSong: CachedSong) - data class Miss(val file: DeviceFile, val addedMs: Long?) : CacheResult + suspend fun cleanup(excluding: List) +} + +data class CachedSong( + val file: DeviceFile, + val properties: Properties, + val tags: ParsedTags, + val coverId: String?, + val addedMs: Long +) + +sealed interface CacheResult { + data class Hit(val song: CachedSong) : CacheResult + + data class Miss(val file: DeviceFile) : CacheResult + + data class Stale(val file: DeviceFile, val addedMs: Long) : CacheResult } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt deleted file mode 100644 index 19ba41ab2..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * CacheDatabase.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.cache - -import android.content.Context -import androidx.room.Dao -import androidx.room.Database -import androidx.room.Entity -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.PrimaryKey -import androidx.room.Query -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.Transaction -import androidx.room.TypeConverter -import androidx.room.TypeConverters -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverResult -import org.oxycblt.musikr.cover.Covers -import org.oxycblt.musikr.fs.device.DeviceFile -import org.oxycblt.musikr.metadata.Properties -import org.oxycblt.musikr.pipeline.RawSong -import org.oxycblt.musikr.tag.Date -import org.oxycblt.musikr.tag.parse.ParsedTags -import org.oxycblt.musikr.util.correctWhitespace -import org.oxycblt.musikr.util.splitEscaped - -@Database(entities = [CachedSong::class], version = 60, exportSchema = false) -internal abstract class CacheDatabase : RoomDatabase() { - abstract fun visibleDao(): VisibleCacheDao - - abstract fun invisibleDao(): InvisibleCacheDao - - abstract fun writeDao(): CacheWriteDao - - companion object { - fun from(context: Context) = - Room.databaseBuilder( - context.applicationContext, CacheDatabase::class.java, "music_cache.db") - .fallbackToDestructiveMigration() - .build() - } -} - -@Dao -internal interface VisibleCacheDao { - @Query("SELECT * FROM CachedSong WHERE uri = :uri") - suspend fun selectSong(uri: String): CachedSong? - - @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri") - suspend fun selectAddedMs(uri: String): Long? - - @Transaction suspend fun touch(uri: String) = updateTouchedNs(uri, System.nanoTime()) - - @Query("UPDATE CachedSong SET touchedNs = :nowNs WHERE uri = :uri") - suspend fun updateTouchedNs(uri: String, nowNs: Long) -} - -@Dao -internal interface InvisibleCacheDao { - @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri") - suspend fun selectAddedMs(uri: String): Long? -} - -@Dao -internal interface CacheWriteDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong) - - @Query("DELETE FROM CachedSong WHERE touchedNs < :now") suspend fun pruneOlderThan(now: Long) -} - -@Entity -@TypeConverters(CachedSong.Converters::class) -internal data class CachedSong( - @PrimaryKey val uri: String, - val modifiedMs: Long, - val addedMs: Long, - val touchedNs: Long, - val mimeType: String, - val durationMs: Long, - val bitrateHz: Int, - val sampleRateHz: Int, - val musicBrainzId: String?, - val name: String?, - val sortName: String?, - val track: Int?, - val disc: Int?, - val subtitle: String?, - val date: Date?, - val albumMusicBrainzId: String?, - val albumName: String?, - val albumSortName: String?, - val releaseTypes: List, - val artistMusicBrainzIds: List, - val artistNames: List, - val artistSortNames: List, - val albumArtistMusicBrainzIds: List, - val albumArtistNames: List, - val albumArtistSortNames: List, - val genreNames: List, - val replayGainTrackAdjustment: Float?, - val replayGainAlbumAdjustment: Float?, - val coverId: String?, -) { - suspend fun intoRawSong(file: DeviceFile, covers: Covers): RawSong? { - val cover = - when (val result = coverId?.let { covers.obtain(it) }) { - // We found the cover. - is CoverResult.Hit -> result.cover - // We actually didn't find the cover, can't safely convert. - is CoverResult.Miss -> return null - // No cover in the first place, can ignore. - null -> null - } - return RawSong( - file, - Properties(mimeType, durationMs, bitrateHz, sampleRateHz), - ParsedTags( - musicBrainzId = musicBrainzId, - name = name, - sortName = sortName, - durationMs = durationMs, - track = track, - disc = disc, - subtitle = subtitle, - date = date, - albumMusicBrainzId = albumMusicBrainzId, - albumName = albumName, - albumSortName = albumSortName, - releaseTypes = releaseTypes, - artistMusicBrainzIds = artistMusicBrainzIds, - artistNames = artistNames, - artistSortNames = artistSortNames, - albumArtistMusicBrainzIds = albumArtistMusicBrainzIds, - albumArtistNames = albumArtistNames, - albumArtistSortNames = albumArtistSortNames, - genreNames = genreNames, - replayGainTrackAdjustment = replayGainTrackAdjustment, - replayGainAlbumAdjustment = replayGainAlbumAdjustment), - cover = cover, - addedMs = addedMs) - } - - object Converters { - @TypeConverter - fun fromMultiValue(values: List) = - values.joinToString(";") { it.replace(";", "\\;") } - - @TypeConverter - fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace() - - @TypeConverter fun fromDate(date: Date?) = date?.toString() - - @TypeConverter fun toDate(string: String?) = string?.let(Date::from) - } - - companion object { - fun fromRawSong(rawSong: RawSong) = - CachedSong( - uri = rawSong.file.uri.toString(), - modifiedMs = rawSong.file.modifiedMs, - addedMs = rawSong.addedMs, - // Should be strictly monotonic so we don't prune this - // by accident later. - touchedNs = System.nanoTime(), - musicBrainzId = rawSong.tags.musicBrainzId, - name = rawSong.tags.name, - sortName = rawSong.tags.sortName, - durationMs = rawSong.tags.durationMs, - track = rawSong.tags.track, - disc = rawSong.tags.disc, - subtitle = rawSong.tags.subtitle, - date = rawSong.tags.date, - albumMusicBrainzId = rawSong.tags.albumMusicBrainzId, - albumName = rawSong.tags.albumName, - albumSortName = rawSong.tags.albumSortName, - releaseTypes = rawSong.tags.releaseTypes, - artistMusicBrainzIds = rawSong.tags.artistMusicBrainzIds, - artistNames = rawSong.tags.artistNames, - artistSortNames = rawSong.tags.artistSortNames, - albumArtistMusicBrainzIds = rawSong.tags.albumArtistMusicBrainzIds, - albumArtistNames = rawSong.tags.albumArtistNames, - albumArtistSortNames = rawSong.tags.albumArtistSortNames, - genreNames = rawSong.tags.genreNames, - replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment, - replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment, - coverId = rawSong.cover?.id, - mimeType = rawSong.properties.mimeType, - bitrateHz = rawSong.properties.bitrateKbps, - sampleRateHz = rawSong.properties.sampleRateHz) - } -} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt deleted file mode 100644 index c4107c3a5..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * StoredCache.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.cache - -import android.content.Context -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.Covers -import org.oxycblt.musikr.fs.device.DeviceFile -import org.oxycblt.musikr.pipeline.RawSong - -interface StoredCache { - fun visible(): Cache.Factory - - fun invisible(): Cache.Factory - - companion object { - fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context)) - } -} - -private class StoredCacheImpl(private val cacheDatabase: CacheDatabase) : StoredCache { - override fun visible(): Cache.Factory = VisibleStoredCache.Factory(cacheDatabase) - - override fun invisible(): Cache.Factory = InvisibleStoredCache.Factory(cacheDatabase) -} - -private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) : Cache() { - private val created = System.nanoTime() - - override suspend fun write(song: RawSong) = writeDao.updateSong(CachedSong.fromRawSong(song)) - - override suspend fun finalize() { - // Anything not create during this cache's use implies that it has not been - // access during this run and should be pruned. - writeDao.pruneOlderThan(created) - } -} - -private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) : - BaseStoredCache(writeDao) { - override suspend fun read(file: DeviceFile, covers: Covers): CacheResult { - val song = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null) - if (song.modifiedMs != file.modifiedMs) { - // We *found* this file earlier, but it's out of date. - // Send back it with the timestamp so it will be re-used. - // The touch timestamp will be updated on write. - return CacheResult.Miss(file, song.addedMs) - } - // Valid file, update the touch time. - visibleDao.touch(file.uri.toString()) - val rawSong = song.intoRawSong(file, covers) ?: return CacheResult.Miss(file, song.addedMs) - return CacheResult.Hit(rawSong) - } - - class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() { - override fun open() = - VisibleStoredCache(cacheDatabase.visibleDao(), cacheDatabase.writeDao()) - } -} - -private class InvisibleStoredCache( - private val invisibleCacheDao: InvisibleCacheDao, - writeDao: CacheWriteDao -) : BaseStoredCache(writeDao) { - override suspend fun read(file: DeviceFile, covers: Covers) = - CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString())) - - class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() { - override fun open() = - InvisibleStoredCache(cacheDatabase.invisibleDao(), cacheDatabase.writeDao()) - } -} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt new file mode 100644 index 000000000..9475556c0 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 Auxio Project + * CacheDatabase.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.cache.db + +import android.content.Context +import android.net.Uri +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.Transaction +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import org.oxycblt.musikr.tag.Date +import org.oxycblt.musikr.util.correctWhitespace +import org.oxycblt.musikr.util.splitEscaped + +@Database(entities = [CachedSongData::class], version = 61, exportSchema = false) +internal abstract class CacheDatabase : RoomDatabase() { + abstract fun readDao(): CacheReadDao + + abstract fun writeDao(): CacheWriteDao + + companion object { + fun from(context: Context) = + Room.databaseBuilder( + context.applicationContext, CacheDatabase::class.java, "music_cache.db") + .fallbackToDestructiveMigration() + .build() + } +} + +@Dao +internal interface CacheReadDao { + @Query("SELECT * FROM CachedSongData WHERE uri = :uri") + suspend fun selectSong(uri: String): CachedSongData? +} + +@Dao +internal interface CacheWriteDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun updateSong(CachedSongData: CachedSongData) + + @Transaction + suspend fun deleteExcludingUris(uris: Set) { + val delete = selectAllUris().toSet() - uris + for (chunk in delete.chunked(999)) { + deleteExcludingUriChunk(chunk) + } + } + + @Query("SELECT uri FROM CachedSongData") suspend fun selectAllUris(): List + + @Query("DELETE FROM CachedSongData WHERE uri IN (:uris)") + suspend fun deleteExcludingUriChunk(uris: List) +} + +@Entity +@TypeConverters(CachedSongData.Converters::class) +internal data class CachedSongData( + @PrimaryKey val uri: Uri, + val modifiedMs: Long, + val addedMs: Long, + val mimeType: String, + val durationMs: Long, + val bitrateKbps: Int, + val sampleRateHz: Int, + val musicBrainzId: String?, + val name: String?, + val sortName: String?, + val track: Int?, + val disc: Int?, + val subtitle: String?, + val date: Date?, + val albumMusicBrainzId: String?, + val albumName: String?, + val albumSortName: String?, + val releaseTypes: List, + val artistMusicBrainzIds: List, + val artistNames: List, + val artistSortNames: List, + val albumArtistMusicBrainzIds: List, + val albumArtistNames: List, + val albumArtistSortNames: List, + val genreNames: List, + val replayGainTrackAdjustment: Float?, + val replayGainAlbumAdjustment: Float?, + val coverId: String?, +) { + object Converters { + @TypeConverter + fun fromMultiValue(values: List) = + values.joinToString(";") { it.replace(";", "\\;") } + + @TypeConverter + fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace() + + @TypeConverter fun fromDate(date: Date?) = date?.toString() + + @TypeConverter fun toDate(string: String?) = string?.let(Date::from) + + @TypeConverter fun toUri(string: String) = Uri.parse(string) + + @TypeConverter fun fromUri(uri: Uri) = uri.toString() + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/db/DBCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/db/DBCache.kt new file mode 100644 index 000000000..310016909 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/db/DBCache.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025 Auxio Project + * DBCache.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.cache.db + +import android.content.Context +import org.oxycblt.musikr.cache.Cache +import org.oxycblt.musikr.cache.CacheResult +import org.oxycblt.musikr.cache.CachedSong +import org.oxycblt.musikr.cache.MutableCache +import org.oxycblt.musikr.fs.device.DeviceFile +import org.oxycblt.musikr.metadata.Properties +import org.oxycblt.musikr.tag.parse.ParsedTags + +open class DBCache internal constructor(private val readDao: CacheReadDao) : Cache { + override suspend fun read(file: DeviceFile): CacheResult { + val dbSong = readDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file) + if (dbSong.modifiedMs != file.modifiedMs) { + return CacheResult.Stale(file, dbSong.addedMs) + } + val song = + CachedSong( + file, + Properties( + dbSong.mimeType, dbSong.durationMs, dbSong.bitrateKbps, dbSong.sampleRateHz), + ParsedTags( + musicBrainzId = dbSong.musicBrainzId, + name = dbSong.name, + sortName = dbSong.sortName, + durationMs = dbSong.durationMs, + track = dbSong.track, + disc = dbSong.disc, + subtitle = dbSong.subtitle, + date = dbSong.date, + albumMusicBrainzId = dbSong.albumMusicBrainzId, + albumName = dbSong.albumName, + albumSortName = dbSong.albumSortName, + releaseTypes = dbSong.releaseTypes, + artistMusicBrainzIds = dbSong.artistMusicBrainzIds, + artistNames = dbSong.artistNames, + artistSortNames = dbSong.artistSortNames, + albumArtistMusicBrainzIds = dbSong.albumArtistMusicBrainzIds, + albumArtistNames = dbSong.albumArtistNames, + albumArtistSortNames = dbSong.albumArtistSortNames, + genreNames = dbSong.genreNames, + replayGainTrackAdjustment = dbSong.replayGainTrackAdjustment, + replayGainAlbumAdjustment = dbSong.replayGainAlbumAdjustment), + coverId = dbSong.coverId, + addedMs = dbSong.addedMs) + return CacheResult.Hit(song) + } + + companion object { + fun from(context: Context) = DBCache(CacheDatabase.from(context).readDao()) + } +} + +class MutableDBCache +private constructor(readDao: CacheReadDao, private val writeDao: CacheWriteDao) : + MutableCache, DBCache(readDao) { + override suspend fun write(cachedSong: CachedSong) { + val dbSong = + CachedSongData( + uri = cachedSong.file.uri, + modifiedMs = cachedSong.file.modifiedMs, + addedMs = cachedSong.addedMs, + mimeType = cachedSong.properties.mimeType, + durationMs = cachedSong.properties.durationMs, + bitrateKbps = cachedSong.properties.bitrateKbps, + sampleRateHz = cachedSong.properties.sampleRateHz, + musicBrainzId = cachedSong.tags.musicBrainzId, + name = cachedSong.tags.name, + sortName = cachedSong.tags.sortName, + track = cachedSong.tags.track, + disc = cachedSong.tags.disc, + subtitle = cachedSong.tags.subtitle, + date = cachedSong.tags.date, + albumMusicBrainzId = cachedSong.tags.albumMusicBrainzId, + albumName = cachedSong.tags.albumName, + albumSortName = cachedSong.tags.albumSortName, + releaseTypes = cachedSong.tags.releaseTypes, + artistMusicBrainzIds = cachedSong.tags.artistMusicBrainzIds, + artistNames = cachedSong.tags.artistNames, + artistSortNames = cachedSong.tags.artistSortNames, + albumArtistMusicBrainzIds = cachedSong.tags.albumArtistMusicBrainzIds, + albumArtistNames = cachedSong.tags.albumArtistNames, + albumArtistSortNames = cachedSong.tags.albumArtistSortNames, + genreNames = cachedSong.tags.genreNames, + replayGainTrackAdjustment = cachedSong.tags.replayGainTrackAdjustment, + replayGainAlbumAdjustment = cachedSong.tags.replayGainAlbumAdjustment, + coverId = cachedSong.coverId) + writeDao.updateSong(dbSong) + } + + override suspend fun cleanup(excluding: List) { + writeDao.deleteExcludingUris(excluding.mapTo(mutableSetOf()) { it.file.uri.toString() }) + } + + companion object { + fun from(context: Context): MutableDBCache { + val db = CacheDatabase.from(context) + return MutableDBCache(db.readDao(), db.writeDao()) + } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt index 7ce64d949..b9f57c16f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt @@ -18,24 +18,29 @@ package org.oxycblt.musikr.metadata -import android.os.ParcelFileDescriptor +import android.content.ContentResolver +import android.content.Context import java.io.FileInputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.musikr.fs.device.DeviceFile internal interface MetadataExtractor { - suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata? + suspend fun extract(deviceFile: DeviceFile): Metadata? companion object { - fun new(): MetadataExtractor = MetadataExtractorImpl + fun from(context: Context): MetadataExtractor = + MetadataExtractorImpl(context.contentResolver) } } -private object MetadataExtractorImpl : MetadataExtractor { - override suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor) = +private class MetadataExtractorImpl(private val contentResolver: ContentResolver) : + MetadataExtractor { + override suspend fun extract(deviceFile: DeviceFile): Metadata? = withContext(Dispatchers.IO) { - val fis = FileInputStream(fd.fileDescriptor) - TagLibJNI.open(deviceFile, fis).also { fis.close() } + contentResolver.openFileDescriptor(deviceFile.uri, "r")?.use { fd -> + val fis = FileInputStream(fd.fileDescriptor) + TagLibJNI.open(deviceFile, fis).also { fis.close() } + } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt index df4f72cb1..a8b519734 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt @@ -18,16 +18,9 @@ package org.oxycblt.musikr.pipeline -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.fold import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.Storage @@ -38,7 +31,7 @@ import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter import org.oxycblt.musikr.tag.interpret.TagInterpreter internal interface EvaluateStep { - suspend fun evaluate(extractedMusic: Flow): MutableLibrary + suspend fun evaluate(extractedMusic: Flow): MutableLibrary companion object { fun new(storage: Storage, interpretation: Interpretation): EvaluateStep = @@ -56,33 +49,16 @@ private class EvaluateStepImpl( private val storedPlaylists: StoredPlaylists, private val libraryFactory: LibraryFactory ) : EvaluateStep { - override suspend fun evaluate(extractedMusic: Flow): MutableLibrary { - val filterFlow = - extractedMusic.filterIsInstance().divert { - when (it) { - is ExtractedMusic.Valid.Song -> Divert.Right(it.song) - is ExtractedMusic.Valid.Playlist -> Divert.Left(it.file) + override suspend fun evaluate(extractedMusic: Flow): MutableLibrary = + extractedMusic + .filterIsInstance() + .fold(MusicGraph.builder()) { graphBuilder, extracted -> + when (extracted) { + is RawSong -> graphBuilder.add(tagInterpreter.interpret(extracted)) + is RawPlaylist -> + graphBuilder.add(playlistInterpreter.interpret(extracted.file)) } + graphBuilder } - val rawSongs = filterFlow.right - val preSongs = - rawSongs - .map { wrap(it, tagInterpreter::interpret) } - .flowOn(Dispatchers.Default) - .buffer(Channel.UNLIMITED) - val prePlaylists = - filterFlow.left - .map { wrap(it, playlistInterpreter::interpret) } - .flowOn(Dispatchers.Default) - .buffer(Channel.UNLIMITED) - val graphBuilder = MusicGraph.builder() - val graphBuild = - merge( - filterFlow.manager, - preSongs.onEach { wrap(it, graphBuilder::add) }, - prePlaylists.onEach { wrap(it, graphBuilder::add) }) - graphBuild.collect() - val graph = graphBuilder.build() - return libraryFactory.create(graph, storedPlaylists, playlistInterpreter) - } + .let { libraryFactory.create(it.build(), storedPlaylists, playlistInterpreter) } } 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 65f742688..5a27017a6 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -31,50 +31,84 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import org.oxycblt.musikr.Storage +import org.oxycblt.musikr.cache.Cache +import org.oxycblt.musikr.cache.CacheResult +import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.cover.CoverResult +import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.device.DeviceDirectory import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFiles import org.oxycblt.musikr.fs.device.DeviceNode -import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.m3u.M3U internal interface ExploreStep { - fun explore(locations: List): Flow + fun explore(locations: List): Flow companion object { fun from(context: Context, storage: Storage): ExploreStep = - ExploreStepImpl(DeviceFiles.from(context), storage.storedPlaylists) + ExploreStepImpl( + DeviceFiles.from(context), storage.cache, storage.covers, storage.storedPlaylists) } } private class ExploreStepImpl( private val deviceFiles: DeviceFiles, + private val cache: Cache, + private val covers: Covers, private val storedPlaylists: StoredPlaylists ) : ExploreStep { - override fun explore(locations: List): Flow { - val audios = + @OptIn(ExperimentalCoroutinesApi::class) + override fun explore(locations: List): Flow { + val addingMs = System.currentTimeMillis() + return merge( deviceFiles .explore(locations.asFlow()) .flattenFilter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE } + .distribute(8) + .distributedMap { file -> + val cachedSong = + when (val cacheResult = cache.read(file)) { + is CacheResult.Hit -> cacheResult.song + is CacheResult.Stale -> + return@distributedMap NewSong(cacheResult.file, cacheResult.addedMs) + is CacheResult.Miss -> + return@distributedMap NewSong(cacheResult.file, addingMs) + } + val cover = + cachedSong.coverId?.let { coverId -> + when (val coverResult = covers.obtain(coverId)) { + is CoverResult.Hit -> coverResult.cover + else -> + return@distributedMap NewSong( + cachedSong.file, cachedSong.addedMs) + } + } + RawSong( + cachedSong.file, + cachedSong.properties, + cachedSong.tags, + cover, + cachedSong.addedMs) + } + .flattenMerge() .flowOn(Dispatchers.IO) - .buffer() - val playlists = + .buffer(), flow { emitAll(storedPlaylists.read().asFlow()) } - .map { ExploreNode.Playlist(it) } + .map { RawPlaylist(it) } .flowOn(Dispatchers.IO) - .buffer() - return merge(audios, playlists) + .buffer()) } @OptIn(ExperimentalCoroutinesApi::class) - private fun Flow.flattenFilter(block: (DeviceFile) -> Boolean): Flow = + private fun Flow.flattenFilter(block: (DeviceFile) -> Boolean): Flow = flow { collect { - val recurse = mutableListOf>() + val recurse = mutableListOf>() when { - it is DeviceFile && block(it) -> emit(ExploreNode.Audio(it)) + it is DeviceFile && block(it) -> emit(it) it is DeviceDirectory -> recurse.add(it.children.flattenFilter(block)) else -> {} } @@ -82,9 +116,3 @@ private class ExploreStepImpl( } } } - -internal sealed interface ExploreNode { - data class Audio(val file: DeviceFile) : ExploreNode - - data class Playlist(val file: PlaylistFile) : ExploreNode -} diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 576980b3a..3d8cc9407 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -19,181 +19,63 @@ package org.oxycblt.musikr.pipeline import android.content.Context -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.flattenMerge -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.withContext import org.oxycblt.musikr.Storage -import org.oxycblt.musikr.cache.Cache -import org.oxycblt.musikr.cache.CacheResult +import org.oxycblt.musikr.cache.CachedSong +import org.oxycblt.musikr.cache.MutableCache import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverResult import org.oxycblt.musikr.cover.MutableCovers -import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.MetadataExtractor -import org.oxycblt.musikr.metadata.Properties -import org.oxycblt.musikr.playlist.PlaylistFile -import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.TagParser internal interface ExtractStep { - fun extract(nodes: Flow): Flow + fun extract(nodes: Flow): Flow companion object { fun from(context: Context, storage: Storage): ExtractStep = ExtractStepImpl( - context, - MetadataExtractor.new(), - TagParser.new(), - storage.cache, - storage.storedCovers) + MetadataExtractor.from(context), TagParser.new(), storage.cache, storage.covers) } } private class ExtractStepImpl( - private val context: Context, private val metadataExtractor: MetadataExtractor, private val tagParser: TagParser, - private val cacheFactory: Cache.Factory, + private val cache: MutableCache, private val covers: MutableCovers ) : ExtractStep { @OptIn(ExperimentalCoroutinesApi::class) - override fun extract(nodes: Flow): Flow { - val cache = cacheFactory.open() - val addingMs = System.currentTimeMillis() - val filterFlow = - nodes.divert { + override fun extract(nodes: Flow): Flow { + val exclude = mutableListOf() + return nodes + .distribute(8) + .distributedMap { when (it) { - is ExploreNode.Audio -> Divert.Right(it.file) - is ExploreNode.Playlist -> Divert.Left(it.file) - } - } - val audioNodes = filterFlow.right - val playlistNodes = filterFlow.left.map { ExtractedMusic.Valid.Playlist(it) } - - // First distribute audio nodes for parallel cache reading - val readDistributedFlow = audioNodes.distribute(8) - val cacheResults = - readDistributedFlow.flows - .map { flow -> - flow - .map { wrap(it) { file -> cache.read(file, covers) } } - .flowOn(Dispatchers.IO) - .buffer(Channel.UNLIMITED) - } - .flattenMerge() - .buffer(Channel.UNLIMITED) - - // Divert cache hits and misses - val cacheFlow = - cacheResults.divert { - when (it) { - is CacheResult.Hit -> Divert.Left(it.song) - is CacheResult.Miss -> Divert.Right(it.file) - } - } - - // Cache hits can be directly converted to valid songs - val cachedSongs = cacheFlow.left.map { ExtractedMusic.Valid.Song(it) } - - // Process uncached files in parallel - val uncachedFiles = cacheFlow.right - val processingDistributedFlow = uncachedFiles.distribute(8) - - // Process each uncached file in parallel flows - val processedSongs = - processingDistributedFlow.flows - .map { flow -> - flow - .mapNotNull { file -> - wrap(file) { f -> - withContext(Dispatchers.IO) { - context.contentResolver.openFileDescriptor(f.uri, "r") - } - ?.use { - val extractedMetadata = metadataExtractor.extract(file, it) - - if (extractedMetadata != null) { - val tags = tagParser.parse(extractedMetadata) - val cover = - when (val result = - covers.create(f, extractedMetadata)) { - is CoverResult.Hit -> result.cover - else -> null - } - val rawSong = - RawSong( - f, - extractedMetadata.properties, - tags, - cover, - addingMs) - cache.write(rawSong) - - ExtractedMusic.Valid.Song(rawSong) - } else { - ExtractedMusic.Invalid - } - } + is RawSong -> it + is RawPlaylist -> it + is NewSong -> { + val metadata = + metadataExtractor.extract(it.file) ?: return@distributedMap InvalidSong + val tags = tagParser.parse(metadata) + val cover = + when (val result = covers.create(it.file, metadata)) { + is CoverResult.Hit -> result.cover + else -> null } - } - .flowOn(Dispatchers.IO) - .buffer(Channel.UNLIMITED) - } - .flattenMerge() - .buffer(Channel.UNLIMITED) - - // Separate valid processed songs from invalid ones - val processedFlow = - processedSongs.divert { - when (it) { - is ExtractedMusic.Valid.Song -> Divert.Left(it) - is ExtractedMusic.Invalid -> Divert.Right(it) - else -> Divert.Right(ExtractedMusic.Invalid) + val cachedSong = + CachedSong(it.file, metadata.properties, tags, cover?.id, it.addedMs) + cache.write(cachedSong) + exclude.add(cachedSong) + val rawSong = RawSong(it.file, metadata.properties, tags, cover, it.addedMs) + rawSong + } } } - - val processedValidSongs = processedFlow.left - val invalidSongs = processedFlow.right - - val merged = - merge( - filterFlow.manager, - readDistributedFlow.manager, - cacheFlow.manager, - processingDistributedFlow.manager, - processedFlow.manager, - cachedSongs, - processedValidSongs, - invalidSongs, - playlistNodes) - - return merged.onCompletion { cache.finalize() } + .flattenMerge() + .onCompletion { cache.cleanup(exclude) } } } - -internal data class RawSong( - val file: DeviceFile, - val properties: Properties, - val tags: ParsedTags, - val cover: Cover?, - val addedMs: Long -) - -internal sealed interface ExtractedMusic { - sealed interface Valid : ExtractedMusic { - data class Song(val song: RawSong) : Valid - - data class Playlist(val file: PlaylistFile) : Valid - } - - data object Invalid : ExtractedMusic -} diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt index 58f2c6eb0..6609e78dd 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt @@ -65,7 +65,7 @@ internal class DistributedFlow(val manager: Flow, val flows: Flow Flow.distribute(n: Int): DistributedFlow { +internal fun Flow.distribute(n: Int): Flow> { val posChannels = List(n) { Channel(Channel.UNLIMITED) } val managerFlow = flow { @@ -77,6 +77,42 @@ internal fun Flow.distribute(n: Int): DistributedFlow { channel.close() } } - val hotFlows = posChannels.asFlow().map { it.receiveAsFlow() } - return DistributedFlow(managerFlow, hotFlows) + return (posChannels.map { it.receiveAsFlow() } + managerFlow).asFlow() +} + +internal fun Flow>.distributedMap(transform: suspend (T) -> R): Flow> = + flow { + collect { innerFlow -> emit(innerFlow.tryMap(transform)) } + } + +internal fun Flow.tryMap(transform: suspend (T) -> R): Flow = flow { + collect { value -> + try { + emit(transform(value)) + } catch (e: Exception) { + throw PipelineException(value, e) + } + } +} + +internal fun Flow.tryMapNotNull(transform: suspend (T) -> R?): Flow = flow { + collect { value -> + try { + transform(value)?.let { emit(it) } + } catch (e: Exception) { + throw PipelineException(value, e) + } + } +} + +internal fun Flow.tryFold(initial: A, operation: suspend (A, T) -> A): Flow = flow { + var accumulator = initial + collect { value -> + try { + accumulator = operation(accumulator, value) + emit(accumulator) + } catch (e: Exception) { + throw PipelineException(value, e) + } + } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt index 65d7525f5..149e57411 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt @@ -18,71 +18,9 @@ package org.oxycblt.musikr.pipeline -import org.oxycblt.musikr.fs.device.DeviceFile -import org.oxycblt.musikr.playlist.PlaylistFile -import org.oxycblt.musikr.playlist.interpret.PrePlaylist -import org.oxycblt.musikr.tag.interpret.PreSong - -class PipelineException(val processing: WhileProcessing, val error: Exception) : Exception() { +class PipelineException(whileProcessing: Any?, val error: Exception) : Exception() { override val cause = error - override val message = "Error while processing ${processing}: ${error.stackTraceToString()}" + override val message = + "Error while processing ${whileProcessing}: ${error.stackTraceToString()}" } - -sealed interface WhileProcessing { - class AFile internal constructor(private val file: DeviceFile) : WhileProcessing { - override fun toString() = "File @ ${file.path}" - } - - class ARawSong internal constructor(private val rawSong: RawSong) : WhileProcessing { - override fun toString() = "Raw Song @ ${rawSong.file.path}" - } - - class APlaylistFile internal constructor(private val playlist: PlaylistFile) : WhileProcessing { - override fun toString() = "Playlist File @ ${playlist.name}" - } - - class APreSong internal constructor(private val preSong: PreSong) : WhileProcessing { - override fun toString() = "Pre Song @ ${preSong.path}" - } - - class APrePlaylist internal constructor(private val prePlaylist: PrePlaylist) : - WhileProcessing { - override fun toString() = "Pre Playlist @ ${prePlaylist.name}" - } -} - -internal suspend fun wrap(file: DeviceFile, block: suspend (DeviceFile) -> R): R = - try { - block(file) - } catch (e: Exception) { - throw PipelineException(WhileProcessing.AFile(file), e) - } - -internal suspend fun wrap(song: RawSong, block: suspend (RawSong) -> R): R = - try { - block(song) - } catch (e: Exception) { - throw PipelineException(WhileProcessing.ARawSong(song), e) - } - -internal suspend fun wrap(file: PlaylistFile, block: suspend (PlaylistFile) -> R): R = - try { - block(file) - } catch (e: Exception) { - throw PipelineException(WhileProcessing.APlaylistFile(file), e) - } - -internal suspend fun wrap(song: PreSong, block: suspend (PreSong) -> R): R = - try { - block(song) - } catch (e: Exception) { - throw PipelineException(WhileProcessing.APreSong(song), e) - } - -internal suspend fun wrap(playlist: PrePlaylist, block: suspend (PrePlaylist) -> R): R = - try { - block(playlist) - } catch (e: Exception) { - throw PipelineException(WhileProcessing.APrePlaylist(playlist), e) - } diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt new file mode 100644 index 000000000..bd4ef2753 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Auxio Project + * PipelineItem.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.pipeline + +import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.fs.device.DeviceFile +import org.oxycblt.musikr.metadata.Properties +import org.oxycblt.musikr.playlist.PlaylistFile +import org.oxycblt.musikr.tag.parse.ParsedTags + +internal sealed interface PipelineItem + +internal sealed interface Incomplete : PipelineItem + +internal sealed interface Complete : PipelineItem + +internal sealed interface Explored : PipelineItem { + sealed interface New : Explored, Incomplete + + sealed interface Known : Explored, Complete +} + +internal data class NewSong(val file: DeviceFile, val addedMs: Long) : Explored.New + +internal sealed interface Extracted : PipelineItem { + sealed interface Valid : Complete, Extracted + + sealed interface Invalid : Extracted +} + +data object InvalidSong : Extracted.Invalid + +internal data class RawPlaylist(val file: PlaylistFile) : Explored.Known, Extracted.Valid + +internal data class RawSong( + val file: DeviceFile, + val properties: Properties, + val tags: ParsedTags, + val cover: Cover?, + val addedMs: Long +) : Explored.Known, Extracted.Valid diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt index 1d7198a56..70ec61959 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt @@ -20,7 +20,7 @@ package org.oxycblt.musikr.tag.parse import org.oxycblt.musikr.tag.Date -internal data class ParsedTags( +data class ParsedTags( val durationMs: Long, val replayGainTrackAdjustment: Float? = null, val replayGainAlbumAdjustment: Float? = null, From 6feee93438df32687caf8d24a9720d509e80eab6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 3 Mar 2025 19:59:11 -0700 Subject: [PATCH 02/28] musikr: streamline package structure --- .../org/oxycblt/auxio/image/CoverProvider.kt | 2 +- .../java/org/oxycblt/auxio/image/CoverView.kt | 2 +- .../image/coil/CoverCollectionFetcher.kt | 2 +- .../oxycblt/auxio/image/coil/CoverFetcher.kt | 2 +- .../org/oxycblt/auxio/image/coil/Keyers.kt | 4 +- .../oxycblt/auxio/image/covers/CoverModule.kt | 2 +- .../oxycblt/auxio/image/covers/CoverSilo.kt | 2 +- .../oxycblt/auxio/image/covers/NullCovers.kt | 6 +-- .../auxio/image/covers/SettingCovers.kt | 21 ++++----- .../auxio/image/covers/SiloedCovers.kt | 39 ++++++++-------- .../main/java/org/oxycblt/musikr/Config.kt | 4 +- .../src/main/java/org/oxycblt/musikr/Music.kt | 4 +- .../musikr/{cover => covers}/Covers.kt | 2 +- .../FolderCovers.kt => covers/fs/FSCovers.kt} | 15 ++++--- .../{cover => covers/internal}/CoverFormat.kt | 20 +-------- .../internal}/CoverIdentifier.kt | 2 +- .../{cover => covers/internal}/CoverParams.kt | 2 +- .../internal/InternalCovers.kt} | 22 ++++++---- .../musikr/fs/app/{AppFiles.kt => AppFS.kt} | 8 ++-- .../fs/device/{DeviceFiles.kt => DeviceFS.kt} | 27 ++++++++++-- .../oxycblt/musikr/fs/device/DeviceFile.kt | 44 ------------------- .../org/oxycblt/musikr/model/AlbumImpl.kt | 2 +- .../org/oxycblt/musikr/model/ArtistImpl.kt | 2 +- .../org/oxycblt/musikr/model/GenreImpl.kt | 2 +- .../org/oxycblt/musikr/model/PlaylistImpl.kt | 2 +- .../oxycblt/musikr/pipeline/ExploreStep.kt | 14 +++--- .../oxycblt/musikr/pipeline/ExtractStep.kt | 6 +-- .../oxycblt/musikr/pipeline/PipelineItem.kt | 4 +- .../oxycblt/musikr/tag/interpret/PreMusic.kt | 2 +- 29 files changed, 118 insertions(+), 148 deletions(-) rename musikr/src/main/java/org/oxycblt/musikr/{cover => covers}/Covers.kt (99%) rename musikr/src/main/java/org/oxycblt/musikr/{cover/FolderCovers.kt => covers/fs/FSCovers.kt} (90%) rename musikr/src/main/java/org/oxycblt/musikr/{cover => covers/internal}/CoverFormat.kt (71%) rename musikr/src/main/java/org/oxycblt/musikr/{cover => covers/internal}/CoverIdentifier.kt (96%) rename musikr/src/main/java/org/oxycblt/musikr/{cover => covers/internal}/CoverParams.kt (96%) rename musikr/src/main/java/org/oxycblt/musikr/{cover/FileCovers.kt => covers/internal/InternalCovers.kt} (75%) rename musikr/src/main/java/org/oxycblt/musikr/fs/app/{AppFiles.kt => AppFS.kt} (95%) rename musikr/src/main/java/org/oxycblt/musikr/fs/device/{DeviceFiles.kt => DeviceFS.kt} (89%) delete mode 100644 musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt index 5c5bd78d3..160267804 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -28,7 +28,7 @@ import android.os.ParcelFileDescriptor import kotlinx.coroutines.runBlocking import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.image.covers.SettingCovers -import org.oxycblt.musikr.cover.CoverResult +import org.oxycblt.musikr.covers.CoverResult class CoverProvider() : ContentProvider() { override fun onCreate(): Boolean = true diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index 03f731618..bab90cbe6 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -64,7 +64,7 @@ import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.CoverCollection /** * Auxio's extension of [ImageView] that enables cover art loading and playing indicator and diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt index 147721fbb..a6bc9475d 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverCollectionFetcher.kt @@ -46,7 +46,7 @@ import kotlinx.coroutines.withContext import okio.FileSystem import okio.buffer import okio.source -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.CoverCollection class CoverCollectionFetcher private constructor( diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt index 29bbb430b..4c0ed0c5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt @@ -40,7 +40,7 @@ import javax.inject.Inject import okio.FileSystem import okio.buffer import okio.source -import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.covers.Cover class CoverFetcher private constructor(private val context: Context, private val cover: Cover) : Fetcher { diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt index 614b1bdf1..cd90be0f8 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt @@ -21,8 +21,8 @@ package org.oxycblt.auxio.image.coil import coil3.key.Keyer import coil3.request.Options import javax.inject.Inject -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.CoverCollection class CoverKeyer @Inject constructor() : Keyer { override fun key(data: Cover, options: Options) = "${data.id}&${options.size}" diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt index 48f5d1052..67ed6b406 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt @@ -23,7 +23,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.oxycblt.musikr.cover.CoverIdentifier +import org.oxycblt.musikr.covers.internal.CoverIdentifier @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt index 9bd93874d..06456f190 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt @@ -19,7 +19,7 @@ package org.oxycblt.auxio.image.covers import java.util.UUID -import org.oxycblt.musikr.cover.CoverParams +import org.oxycblt.musikr.covers.internal.CoverParams data class CoverSilo(val revision: UUID, val params: CoverParams?) { override fun toString() = 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 cffa626d1..079ed5f6c 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 @@ -19,9 +19,9 @@ package org.oxycblt.auxio.image.covers import android.content.Context -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverResult -import org.oxycblt.musikr.cover.MutableCovers +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.CoverResult +import org.oxycblt.musikr.covers.MutableCovers import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt index e00cfdc2f..1deaae968 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt @@ -23,21 +23,21 @@ import java.util.UUID import javax.inject.Inject import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.ImageSettings -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverIdentifier -import org.oxycblt.musikr.cover.CoverParams -import org.oxycblt.musikr.cover.Covers -import org.oxycblt.musikr.cover.FileCover -import org.oxycblt.musikr.cover.FolderCovers -import org.oxycblt.musikr.cover.MutableCovers -import org.oxycblt.musikr.cover.MutableFolderCovers +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.internal.CoverIdentifier +import org.oxycblt.musikr.covers.internal.CoverParams +import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.internal.FileCover +import org.oxycblt.musikr.covers.fs.FSCovers +import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.covers.fs.MutableFSCovers interface SettingCovers { suspend fun mutate(context: Context, revision: UUID): MutableCovers companion object { fun immutable(context: Context): Covers = - Covers.chain(BaseSiloedCovers(context), FolderCovers(context)) + Covers.chain(BaseSiloedCovers(context), FSCovers(context)) } } @@ -57,5 +57,6 @@ constructor(private val imageSettings: ImageSettings, private val identifier: Co private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams?) = MutableCovers.chain( MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier), - MutableFolderCovers(context)) + MutableFSCovers(context) + ) } 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 856239698..08a89f7da 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 @@ -22,16 +22,16 @@ import android.content.Context import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverFormat -import org.oxycblt.musikr.cover.CoverIdentifier -import org.oxycblt.musikr.cover.CoverResult -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.MutableFileCovers -import org.oxycblt.musikr.fs.app.AppFiles +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.internal.CoverFormat +import org.oxycblt.musikr.covers.internal.CoverIdentifier +import org.oxycblt.musikr.covers.CoverResult +import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.internal.FileCover +import org.oxycblt.musikr.covers.internal.InternalCovers +import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.covers.internal.MutableInternalCovers +import org.oxycblt.musikr.fs.app.AppFS import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata @@ -39,20 +39,20 @@ class BaseSiloedCovers(private val context: Context) : Covers { override suspend fun obtain(id: String): CoverResult { val siloedId = SiloedCoverId.parse(id) ?: return CoverResult.Miss() val core = SiloCore.from(context, siloedId.silo) - val fileCovers = FileCovers(core.files, core.format) - return when (val result = fileCovers.obtain(siloedId.id)) { + val internalCovers = InternalCovers(core.files, core.format) + return when (val result = internalCovers.obtain(siloedId.id)) { is CoverResult.Hit -> CoverResult.Hit(SiloedCover(siloedId.silo, result.cover)) is CoverResult.Miss -> CoverResult.Miss() } } } -open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: FileCovers) : +open class SiloedCovers(private val silo: CoverSilo, private val internalCovers: InternalCovers) : Covers { override suspend fun obtain(id: String): CoverResult { val coverId = SiloedCoverId.parse(id) ?: return CoverResult.Miss() if (silo != coverId.silo) return CoverResult.Miss() - return when (val result = fileCovers.obtain(coverId.id)) { + return when (val result = internalCovers.obtain(coverId.id)) { is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover)) is CoverResult.Miss -> CoverResult.Miss() } @@ -61,7 +61,7 @@ open class SiloedCovers(private val silo: CoverSilo, private val fileCovers: Fil companion object { suspend fun from(context: Context, silo: CoverSilo): SiloedCovers { val core = SiloCore.from(context, silo) - return SiloedCovers(silo, FileCovers(core.files, core.format)) + return SiloedCovers(silo, InternalCovers(core.files, core.format)) } } } @@ -70,7 +70,7 @@ class MutableSiloedCovers private constructor( private val rootDir: File, private val silo: CoverSilo, - private val fileCovers: MutableFileCovers + private val fileCovers: MutableInternalCovers ) : SiloedCovers(silo, fileCovers), MutableCovers { override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult = when (val result = fileCovers.create(file, metadata)) { @@ -96,7 +96,8 @@ private constructor( ): MutableSiloedCovers { val core = SiloCore.from(context, silo) return MutableSiloedCovers( - core.rootDir, silo, MutableFileCovers(core.files, core.format, coverIdentifier)) + core.rootDir, silo, MutableInternalCovers(core.files, core.format, coverIdentifier) + ) } } } @@ -120,7 +121,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 { suspend fun from(context: Context, silo: CoverSilo): SiloCore { val rootDir: File @@ -129,7 +130,7 @@ private data class SiloCore(val rootDir: File, val files: AppFiles, val format: rootDir = context.coversDir() revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() } } - val files = AppFiles.at(revisionDir) + val files = AppFS.at(revisionDir) val format = silo.params?.let(CoverFormat::jpeg) ?: CoverFormat.asIs() return SiloCore(rootDir, files, format) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index 3d699f2d0..df51be932 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -19,8 +19,8 @@ package org.oxycblt.musikr import org.oxycblt.musikr.cache.MutableCache -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.MutableCovers +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.MutableCovers import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators diff --git a/musikr/src/main/java/org/oxycblt/musikr/Music.kt b/musikr/src/main/java/org/oxycblt/musikr/Music.kt index 5ae85f2d2..507103898 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Music.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Music.kt @@ -25,8 +25,8 @@ import java.security.MessageDigest import java.util.UUID import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.CoverCollection import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.tag.Date diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt similarity index 99% rename from musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt index 902bce5c5..b1e266673 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.cover +package org.oxycblt.musikr.covers import java.io.InputStream import org.oxycblt.musikr.fs.device.DeviceFile diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt similarity index 90% rename from musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt index ad5383482..229b5b673 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2025 Auxio Project - * FolderCovers.kt is part of Auxio. + * FSCovers.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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.cover +package org.oxycblt.musikr.covers.fs import android.content.Context import android.net.Uri @@ -26,11 +26,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.withContext +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.CoverResult +import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.covers.internal.FileCover import org.oxycblt.musikr.fs.device.DeviceDirectory import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata -open class FolderCovers(private val context: Context) : Covers { +open class FSCovers(private val context: Context) : Covers { override suspend fun obtain(id: String): CoverResult { // Parse the ID to get the directory URI if (!id.startsWith("folder:")) { @@ -60,8 +65,8 @@ open class FolderCovers(private val context: Context) : Covers { } } -class MutableFolderCovers(private val context: Context) : - FolderCovers(context), MutableCovers { +class MutableFSCovers(private val context: Context) : + FSCovers(context), MutableCovers { override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { val parent = file.parent val coverFile = findCoverInDirectory(parent) ?: return CoverResult.Miss() diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt similarity index 71% rename from musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt index 16af47506..292f8ff3e 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverFormat.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt @@ -1,22 +1,4 @@ -/* - * Copyright (c) 2024 Auxio Project - * CoverFormat.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 +package org.oxycblt.musikr.covers.internal import android.graphics.Bitmap import android.graphics.BitmapFactory diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverIdentifier.kt similarity index 96% rename from musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverIdentifier.kt index ef0917e05..a187d0d9e 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverIdentifier.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverIdentifier.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.cover +package org.oxycblt.musikr.covers.internal import java.security.MessageDigest diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverParams.kt similarity index 96% rename from musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverParams.kt index 1b26dc63f..c7bc9a98e 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/CoverParams.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverParams.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.cover +package org.oxycblt.musikr.covers.internal class CoverParams private constructor(val resolution: Int, val quality: Int) { override fun hashCode() = 31 * resolution + quality diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt similarity index 75% rename from musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt index aadafad1d..9d641a1d3 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/FileCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt @@ -16,18 +16,22 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.cover +package org.oxycblt.musikr.covers.internal import android.os.ParcelFileDescriptor +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.CoverResult +import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.MutableCovers import org.oxycblt.musikr.fs.app.AppFile -import org.oxycblt.musikr.fs.app.AppFiles +import org.oxycblt.musikr.fs.app.AppFS import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata -open class FileCovers(private val appFiles: AppFiles, private val coverFormat: CoverFormat) : +open class InternalCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) : Covers { override suspend fun obtain(id: String): CoverResult { - val file = appFiles.find(getFileName(id)) + val file = appFS.find(getFileName(id)) return if (file != null) { CoverResult.Hit(FileCoverImpl(id, file)) } else { @@ -38,21 +42,21 @@ open class FileCovers(private val appFiles: AppFiles, private val coverFormat: C protected fun getFileName(id: String) = "$id.${coverFormat.extension}" } -class MutableFileCovers( - private val appFiles: AppFiles, +class MutableInternalCovers( + private val appFS: AppFS, private val coverFormat: CoverFormat, private val coverIdentifier: CoverIdentifier -) : FileCovers(appFiles, coverFormat), MutableCovers { +) : InternalCovers(appFS, coverFormat), MutableCovers { override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { val data = metadata.cover ?: return CoverResult.Miss() val id = coverIdentifier.identify(data) - val coverFile = appFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) } + val coverFile = appFS.write(getFileName(id)) { coverFormat.transcodeInto(data, it) } return CoverResult.Hit(FileCoverImpl(id, coverFile)) } override suspend fun cleanup(excluding: Collection) { val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) } - appFiles.deleteWhere { it !in used } + appFS.deleteWhere { it !in used } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt similarity index 95% rename from musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt index 9c8f2a407..77de81796 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -interface AppFiles { +interface AppFS { suspend fun find(name: String): AppFile? suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile @@ -36,9 +36,9 @@ interface AppFiles { suspend fun deleteWhere(block: (String) -> Boolean) companion object { - suspend fun at(dir: File): AppFiles { + suspend fun at(dir: File): AppFS { withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) } - return AppFilesImpl(dir) + return AppFSImpl(dir) } } } @@ -49,7 +49,7 @@ interface AppFile { suspend fun open(): InputStream? } -private class AppFilesImpl(private val dir: File) : AppFiles { +private class AppFSImpl(private val dir: File) : AppFS { private val fileMutexes = mutableMapOf() private val mapMutex = Mutex() diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt similarity index 89% rename from musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt rename to musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt index 2ba57558d..99a9bd8f3 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt @@ -30,16 +30,37 @@ import kotlinx.coroutines.flow.flow import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.Path -internal interface DeviceFiles { +internal interface DeviceFS { fun explore(locations: Flow, ignoreHidden: Boolean = true): Flow companion object { - fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe) + fun from(context: Context): DeviceFS = DeviceFSImpl(context.contentResolverSafe) } } +sealed interface DeviceNode { + val uri: Uri + val path: Path +} + +data class DeviceDirectory( + override val uri: Uri, + override val path: Path, + val parent: DeviceDirectory?, + var children: Flow +) : DeviceNode + +data class DeviceFile( + override val uri: Uri, + override val path: Path, + val modifiedMs: Long, + val mimeType: String, + val size: Long, + val parent: DeviceDirectory +) : DeviceNode + @OptIn(ExperimentalCoroutinesApi::class) -private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles { +private class DeviceFSImpl(private val contentResolver: ContentResolver) : DeviceFS { override fun explore(locations: Flow, ignoreHidden: Boolean): Flow = locations.flatMapMerge { location -> // Create a root directory for each location diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt deleted file mode 100644 index 6590491a6..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt +++ /dev/null @@ -1,44 +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 . - */ - -package org.oxycblt.musikr.fs.device - -import android.net.Uri -import kotlinx.coroutines.flow.Flow -import org.oxycblt.musikr.fs.Path - -sealed interface DeviceNode { - val uri: Uri - val path: Path -} - -data class DeviceDirectory( - override val uri: Uri, - override val path: Path, - val parent: DeviceDirectory?, - var children: Flow -) : DeviceNode - -data class DeviceFile( - override val uri: Uri, - override val path: Path, - val modifiedMs: Long, - val mimeType: String, - val size: Long, - val parent: DeviceDirectory -) : DeviceNode diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt index 6031569b8..c4225d9f4 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt @@ -22,7 +22,7 @@ import org.oxycblt.musikr.Album import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Music import org.oxycblt.musikr.Song -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.CoverCollection import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.interpret.PreAlbum import org.oxycblt.musikr.util.update diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt index e05740401..e2820a0bb 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt @@ -23,7 +23,7 @@ import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Music import org.oxycblt.musikr.Song -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.CoverCollection import org.oxycblt.musikr.tag.interpret.PreArtist import org.oxycblt.musikr.util.update diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt index 0805f284b..18d7858d3 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/GenreImpl.kt @@ -22,7 +22,7 @@ import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Music import org.oxycblt.musikr.Song -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.CoverCollection import org.oxycblt.musikr.tag.interpret.PreGenre import org.oxycblt.musikr.util.update diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt index a92197df5..1837f9967 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/PlaylistImpl.kt @@ -20,7 +20,7 @@ package org.oxycblt.musikr.model import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Song -import org.oxycblt.musikr.cover.CoverCollection +import org.oxycblt.musikr.covers.CoverCollection import org.oxycblt.musikr.playlist.interpret.PrePlaylistInfo import org.oxycblt.musikr.tag.Name 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 5a27017a6..7866a6d6f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -33,13 +33,13 @@ import kotlinx.coroutines.flow.merge import org.oxycblt.musikr.Storage import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.CacheResult -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverResult -import org.oxycblt.musikr.cover.Covers +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.CoverResult +import org.oxycblt.musikr.covers.Covers import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.device.DeviceDirectory import org.oxycblt.musikr.fs.device.DeviceFile -import org.oxycblt.musikr.fs.device.DeviceFiles +import org.oxycblt.musikr.fs.device.DeviceFS import org.oxycblt.musikr.fs.device.DeviceNode import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.m3u.M3U @@ -50,12 +50,12 @@ internal interface ExploreStep { companion object { fun from(context: Context, storage: Storage): ExploreStep = ExploreStepImpl( - DeviceFiles.from(context), storage.cache, storage.covers, storage.storedPlaylists) + DeviceFS.from(context), storage.cache, storage.covers, storage.storedPlaylists) } } private class ExploreStepImpl( - private val deviceFiles: DeviceFiles, + private val deviceFS: DeviceFS, private val cache: Cache, private val covers: Covers, private val storedPlaylists: StoredPlaylists @@ -64,7 +64,7 @@ private class ExploreStepImpl( override fun explore(locations: List): Flow { val addingMs = System.currentTimeMillis() return merge( - deviceFiles + deviceFS .explore(locations.asFlow()) .flattenFilter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE } .distribute(8) diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 3d8cc9407..c6d347577 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -26,9 +26,9 @@ import kotlinx.coroutines.flow.onCompletion import org.oxycblt.musikr.Storage import org.oxycblt.musikr.cache.CachedSong import org.oxycblt.musikr.cache.MutableCache -import org.oxycblt.musikr.cover.Cover -import org.oxycblt.musikr.cover.CoverResult -import org.oxycblt.musikr.cover.MutableCovers +import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.CoverResult +import org.oxycblt.musikr.covers.MutableCovers import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.tag.parse.TagParser diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt index bd4ef2753..20cf30221 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt @@ -18,7 +18,7 @@ package org.oxycblt.musikr.pipeline -import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.playlist.PlaylistFile @@ -44,7 +44,7 @@ internal sealed interface Extracted : PipelineItem { sealed interface Invalid : Extracted } -data object InvalidSong : Extracted.Invalid +internal data object InvalidSong : Extracted.Invalid internal data class RawPlaylist(val file: PlaylistFile) : Explored.Known, Extracted.Valid diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt index 3c744d51e..62f6f3689 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt @@ -21,7 +21,7 @@ package org.oxycblt.musikr.tag.interpret import android.net.Uri import java.util.UUID import org.oxycblt.musikr.Music -import org.oxycblt.musikr.cover.Cover +import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.tag.Date From 22249cc95b444f9a4997eb31e9ae9be34a21c086 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 3 Mar 2025 20:13:38 -0700 Subject: [PATCH 03/28] musikr: cleanup --- .../auxio/image/covers/SettingCovers.kt | 11 ++++---- .../auxio/image/covers/SiloedCovers.kt | 9 +++---- .../main/java/org/oxycblt/musikr/Config.kt | 6 ++--- .../java/org/oxycblt/musikr/covers/Covers.kt | 5 ++++ .../org/oxycblt/musikr/covers/fs/FSCovers.kt | 23 +++++----------- .../musikr/covers/internal/CoverFormat.kt | 18 +++++++++++++ .../musikr/covers/internal/InternalCovers.kt | 26 ++++++++----------- .../java/org/oxycblt/musikr/fs/app/AppFS.kt | 2 +- .../org/oxycblt/musikr/fs/device/DeviceFS.kt | 4 +-- .../oxycblt/musikr/pipeline/ExploreStep.kt | 2 +- 10 files changed, 57 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt index 1deaae968..72c97e4d0 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt @@ -24,13 +24,13 @@ import javax.inject.Inject import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.musikr.covers.Cover +import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.covers.fs.FSCovers +import org.oxycblt.musikr.covers.fs.MutableFSCovers import org.oxycblt.musikr.covers.internal.CoverIdentifier import org.oxycblt.musikr.covers.internal.CoverParams -import org.oxycblt.musikr.covers.Covers import org.oxycblt.musikr.covers.internal.FileCover -import org.oxycblt.musikr.covers.fs.FSCovers -import org.oxycblt.musikr.covers.MutableCovers -import org.oxycblt.musikr.covers.fs.MutableFSCovers interface SettingCovers { suspend fun mutate(context: Context, revision: UUID): MutableCovers @@ -57,6 +57,5 @@ constructor(private val imageSettings: ImageSettings, private val identifier: Co private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams?) = MutableCovers.chain( MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier), - MutableFSCovers(context) - ) + MutableFSCovers(context)) } 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 08a89f7da..c8f5288aa 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,13 +23,13 @@ import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.musikr.covers.Cover -import org.oxycblt.musikr.covers.internal.CoverFormat -import org.oxycblt.musikr.covers.internal.CoverIdentifier import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.covers.internal.CoverFormat +import org.oxycblt.musikr.covers.internal.CoverIdentifier import org.oxycblt.musikr.covers.internal.FileCover import org.oxycblt.musikr.covers.internal.InternalCovers -import org.oxycblt.musikr.covers.MutableCovers import org.oxycblt.musikr.covers.internal.MutableInternalCovers import org.oxycblt.musikr.fs.app.AppFS import org.oxycblt.musikr.fs.device.DeviceFile @@ -96,8 +96,7 @@ private constructor( ): MutableSiloedCovers { val core = SiloCore.from(context, silo) return MutableSiloedCovers( - core.rootDir, silo, MutableInternalCovers(core.files, core.format, coverIdentifier) - ) + core.rootDir, silo, MutableInternalCovers(core.files, core.format, coverIdentifier)) } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index df51be932..e07eae2bd 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -28,8 +28,8 @@ import org.oxycblt.musikr.tag.interpret.Separators /** Side-effect laden [Storage] for use during music loading and [MutableLibrary] operation. */ data class Storage( /** - * A factory producing a repository of cached metadata to read and write from over the course of - * music loading. This will only be used during music loading. + * A repository of cached metadata to read and write from over the course of music loading. This + * will only be used during music loading. */ val cache: MutableCache, @@ -57,5 +57,5 @@ data class Interpretation( val separators: Separators, /** Whether to ignore hidden files and directories (those starting with a dot). */ - val ignoreHidden: Boolean = true + val ignoreHidden: Boolean ) diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt index b1e266673..adef4cf44 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt @@ -18,6 +18,7 @@ package org.oxycblt.musikr.covers +import android.os.ParcelFileDescriptor import java.io.InputStream import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata @@ -94,6 +95,10 @@ interface Cover { override fun hashCode(): Int } +interface FDCover : Cover { + suspend fun fd(): ParcelFileDescriptor? +} + class CoverCollection private constructor(val covers: List) { override fun hashCode() = covers.hashCode() diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt index 229b5b673..2ae18666c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt @@ -29,20 +29,19 @@ import kotlinx.coroutines.withContext import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.FDCover import org.oxycblt.musikr.covers.MutableCovers -import org.oxycblt.musikr.covers.internal.FileCover import org.oxycblt.musikr.fs.device.DeviceDirectory import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata -open class FSCovers(private val context: Context) : Covers { - override suspend fun obtain(id: String): CoverResult { +open class FSCovers(private val context: Context) : Covers { + override suspend fun obtain(id: String): CoverResult { // Parse the ID to get the directory URI if (!id.startsWith("folder:")) { return CoverResult.Miss() } - // TODO: Check if the dir actually exists still to avoid stale uris val directoryUri = id.substring("folder:".length) val uri = Uri.parse(directoryUri) @@ -65,9 +64,8 @@ open class FSCovers(private val context: Context) : Covers { } } -class MutableFSCovers(private val context: Context) : - FSCovers(context), MutableCovers { - override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { +class MutableFSCovers(private val context: Context) : FSCovers(context), MutableCovers { + override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { val parent = file.parent val coverFile = findCoverInDirectory(parent) ?: return CoverResult.Miss() return CoverResult.Hit(FolderCoverImpl(context, coverFile.uri)) @@ -88,12 +86,10 @@ class MutableFSCovers(private val context: Context) : val filename = requireNotNull(file.path.name).lowercase() val mimeType = file.mimeType.lowercase() - // Check if the file is an image if (!mimeType.startsWith("image/")) { return false } - // Common cover art filenames val coverNames = listOf( "cover", @@ -103,11 +99,8 @@ class MutableFSCovers(private val context: Context) : "front", "artwork", "art", - "folder", - "cover") + "folder") - // Check if the filename matches any common cover art names - // Also check for case variations (e.g., Cover.jpg, COVER.JPG) val filenameWithoutExt = filename.substringBeforeLast(".") val extension = filename.substringAfterLast(".", "") @@ -120,12 +113,10 @@ class MutableFSCovers(private val context: Context) : } } -interface FolderCover : FileCover - private data class FolderCoverImpl( private val context: Context, private val uri: Uri, -) : FolderCover { +) : FDCover { override val id = "folder:$uri" override suspend fun fd(): ParcelFileDescriptor? = diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt index 292f8ff3e..d64bc5f49 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2025 Auxio Project + * CoverFormat.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.covers.internal import android.graphics.Bitmap diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt index 9d641a1d3..4d2f47bfc 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2025 Auxio Project - * FileCovers.kt is part of Auxio. + * InternalCovers.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 @@ -18,22 +18,22 @@ package org.oxycblt.musikr.covers.internal -import android.os.ParcelFileDescriptor import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.Covers +import org.oxycblt.musikr.covers.FDCover import org.oxycblt.musikr.covers.MutableCovers -import org.oxycblt.musikr.fs.app.AppFile import org.oxycblt.musikr.fs.app.AppFS +import org.oxycblt.musikr.fs.app.AppFile import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata open class InternalCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) : - Covers { - override suspend fun obtain(id: String): CoverResult { + Covers { + override suspend fun obtain(id: String): CoverResult { val file = appFS.find(getFileName(id)) return if (file != null) { - CoverResult.Hit(FileCoverImpl(id, file)) + CoverResult.Hit(InternalCoverImpl(id, file)) } else { CoverResult.Miss() } @@ -46,12 +46,12 @@ class MutableInternalCovers( private val appFS: AppFS, private val coverFormat: CoverFormat, private val coverIdentifier: CoverIdentifier -) : InternalCovers(appFS, coverFormat), MutableCovers { - override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { +) : InternalCovers(appFS, coverFormat), MutableCovers { + override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { val data = metadata.cover ?: return CoverResult.Miss() val id = coverIdentifier.identify(data) val coverFile = appFS.write(getFileName(id)) { coverFormat.transcodeInto(data, it) } - return CoverResult.Hit(FileCoverImpl(id, coverFile)) + return CoverResult.Hit(InternalCoverImpl(id, coverFile)) } override suspend fun cleanup(excluding: Collection) { @@ -60,12 +60,8 @@ class MutableInternalCovers( } } -interface FileCover : Cover { - suspend fun fd(): ParcelFileDescriptor? -} - -private data class FileCoverImpl(override val id: String, private val appFile: AppFile) : - FileCover { +private data class InternalCoverImpl(override val id: String, private val appFile: AppFile) : + FDCover { override suspend fun fd() = appFile.fd() override suspend fun open() = appFile.open() diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt index 77de81796..50b423c08 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt @@ -1,6 +1,6 @@ /* * 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 * it under the terms of the GNU General Public License as published by diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt index 99a9bd8f3..925a38a09 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFS.kt @@ -1,6 +1,6 @@ /* * 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 * it under the terms of the GNU General Public License as published by @@ -31,7 +31,7 @@ import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.Path internal interface DeviceFS { - fun explore(locations: Flow, ignoreHidden: Boolean = true): Flow + fun explore(locations: Flow, ignoreHidden: Boolean): Flow companion object { fun from(context: Context): DeviceFS = DeviceFSImpl(context.contentResolverSafe) 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 7866a6d6f..17367fb5d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -38,8 +38,8 @@ import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.Covers import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.device.DeviceDirectory -import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFS +import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.fs.device.DeviceNode import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.m3u.M3U From e046aeb67170866dba7089a87cf1ba9e3348e2b2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 3 Mar 2025 20:28:49 -0700 Subject: [PATCH 04/28] musikr: invert hidden setting --- .../oxycblt/auxio/music/MusicRepository.kt | 2 +- .../org/oxycblt/auxio/music/MusicSettings.kt | 8 ++++---- .../categories/MusicPreferenceFragment.kt | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 4 ++-- app/src/main/res/values/settings.xml | 2 +- app/src/main/res/values/strings.xml | 4 ++-- app/src/main/res/xml/preferences_music.xml | 6 +++--- .../main/java/org/oxycblt/musikr/Config.kt | 4 ++-- .../main/java/org/oxycblt/musikr/Musikr.kt | 2 +- .../org/oxycblt/musikr/covers/fs/FSCovers.kt | 10 +--------- .../org/oxycblt/musikr/fs/device/DeviceFS.kt | 20 +++++++++++-------- .../oxycblt/musikr/pipeline/ExploreStep.kt | 12 ++++++++--- 12 files changed, 39 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 20e1c110f..0a17bf570 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -385,7 +385,7 @@ constructor( Naming.simple() } val locations = musicSettings.musicLocations - val ignoreHidden = musicSettings.ignoreHidden + val ignoreHidden = musicSettings.withHidden val currentRevision = musicSettings.revision val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index d94d6ec4c..8ae954e5a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -41,7 +41,7 @@ interface MusicSettings : Settings { /** Whether to exclude non-music audio files from the music library. */ val excludeNonMusic: Boolean /** Whether to ignore hidden files and directories during music loading. */ - val ignoreHidden: Boolean + val withHidden: Boolean /** Whether to be actively watching for changes in the music library. */ val shouldBeObserving: Boolean /** A [String] of characters representing the desired characters to denote multi-value tags. */ @@ -92,8 +92,8 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont override val excludeNonMusic: Boolean get() = sharedPreferences.getBoolean(getString(R.string.set_key_exclude_non_music), true) - override val ignoreHidden: Boolean - get() = sharedPreferences.getBoolean(getString(R.string.set_key_ignore_hidden), true) + override val withHidden: Boolean + get() = sharedPreferences.getBoolean(getString(R.string.set_key_with_hidden), true) override val shouldBeObserving: Boolean get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false) @@ -122,7 +122,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont } getString(R.string.set_key_separators), getString(R.string.set_key_auto_sort_names), - getString(R.string.set_key_ignore_hidden), + getString(R.string.set_key_with_hidden), getString(R.string.set_key_exclude_non_music) -> { L.d("Dispatching indexing setting change for $key") listener.onIndexingSettingChanged() diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt index 9707afae0..752dead1a 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt @@ -67,7 +67,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) true } } - if (preference.key == getString(R.string.set_key_ignore_hidden)) { + if (preference.key == getString(R.string.set_key_with_hidden)) { L.d("Configuring ignore hidden files setting") preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e5b67d5b9..e714bc71f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -334,6 +334,6 @@ Os seus gêneros aparecerão aqui. Economizar espaço Nova pasta - Ignorar arquivos e pastas que estão ocultos (por exemplo, .cache) - Ignorar arquivos ocultos + Ignorar arquivos e pastas que estão ocultos (por exemplo, .cache) + Ignorar arquivos ocultos diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 99438b0d6..eec560b35 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -18,7 +18,7 @@ auxio_square_covers auxio_include_dirs auxio_exclude_non_music - auxio_ignore_hidden + auxio_with_hidden auxio_music_locations2 auxio_separators auxio_auto_sort_names diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64bbcfd36..164034bfc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -267,8 +267,8 @@ Reload the music library whenever it changes (requires persistent notification) Exclude non-music Ignore audio files that are not music, such as podcasts - Ignore hidden files - Skip files and folders that are hidden (ex. .cache) + Include hidden files + Include audio files that are hidden (ex. .cache) Multi-value separators Configure characters that denote multiple tag values Comma (,) diff --git a/app/src/main/res/xml/preferences_music.xml b/app/src/main/res/xml/preferences_music.xml index d164aed5c..e899ace74 100644 --- a/app/src/main/res/xml/preferences_music.xml +++ b/app/src/main/res/xml/preferences_music.xml @@ -17,9 +17,9 @@ + app:key="@string/set_key_with_hidden" + app:summary="@string/set_with_hidden_desc" + app:title="@string/set_with_hidden" />