From 0d0a20d760f704bcbf7f9af88791fa999bdf35cf Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 3 Mar 2025 17:00:39 -0700 Subject: [PATCH 01/49] 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/49] 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/49] 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/49] 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" />

Changelog | Wiki | Donate

+ + Get it on Accrescent + +

+

Translation status

From afa094d75390112d7f8fc1aec73d038566a1cdf0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 5 Mar 2025 16:43:28 -0700 Subject: [PATCH 07/49] info: increase badge size --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75a3dc29d..24fccbd16 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@

Changelog | Wiki | Donate

- + - Get it on Accrescent + Get it on Accrescent

From 9a70ae1c4e3bae002d2fa9a2364ad8b2f2880647 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 7 Mar 2025 11:27:38 -0700 Subject: [PATCH 08/49] musikr: cleanup --- .../org/oxycblt/musikr/fs/device/DeviceFS.kt | 74 ++++++++----------- .../oxycblt/musikr/pipeline/EvaluateStep.kt | 2 +- .../org/oxycblt/musikr/pipeline/FlowUtil.kt | 49 +----------- 3 files changed, 35 insertions(+), 90 deletions(-) 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 18685bcbc..bb580008c 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 @@ -75,7 +75,8 @@ private class DeviceFSImpl( location.uri, DocumentsContract.getTreeDocumentId(location.uri), location.path, - null) + null + ) } private fun exploreDirectoryImpl( @@ -99,52 +100,38 @@ private class DeviceFSImpl( val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE) val lastModifiedIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED) - contentResolver.useQuery( - DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId), - PROJECTION - ) { cursor -> - val childUriIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) - val displayNameIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) - val mimeTypeIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) - val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE) - val lastModifiedIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED) - while (cursor.moveToNext()) { - val childId = cursor.getString(childUriIndex) - val displayName = cursor.getString(displayNameIndex) + while (cursor.moveToNext()) { + val childId = cursor.getString(childUriIndex) + val displayName = cursor.getString(displayNameIndex) - // Skip hidden files/directories if ignoreHidden is true - if (!withHidden && displayName.startsWith(".")) { - continue - } + // Skip hidden files/directories if ignoreHidden is true + if (!withHidden && displayName.startsWith(".")) { + continue + } - val newPath = relativePath.file(displayName) - val mimeType = cursor.getString(mimeTypeIndex) - val lastModified = cursor.getLong(lastModifiedIndex) + val newPath = relativePath.file(displayName) + val mimeType = cursor.getString(mimeTypeIndex) + val lastModified = cursor.getLong(lastModifiedIndex) - if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { - recursive.add( - exploreDirectoryImpl(rootUri, childId, newPath, directoryDeferred) + if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { + recursive.add( + exploreDirectoryImpl(rootUri, childId, newPath, directoryDeferred) + ) + } else { + val size = cursor.getLong(sizeIndex) + val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId) + val file = + DeviceFile( + uri = childUri, + mimeType = mimeType, + path = newPath, + size = size, + modifiedMs = lastModified, + parent = directoryDeferred ) - } else { - val size = cursor.getLong(sizeIndex) - val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId) - val file = - DeviceFile( - uri = childUri, - mimeType = mimeType, - path = newPath, - size = size, - modifiedMs = lastModified, - parent = directoryDeferred - ) - children.add(file) - emit(file) - } + children.add(file) + emit(file) } } directoryDeferred.complete(DeviceDirectory(uri, relativePath, parent, children)) @@ -159,6 +146,7 @@ private class DeviceFSImpl( DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_LAST_MODIFIED) + DocumentsContract.Document.COLUMN_LAST_MODIFIED + ) } } 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 a8b519734..a28fdab8c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt @@ -52,7 +52,7 @@ private class EvaluateStepImpl( override suspend fun evaluate(extractedMusic: Flow): MutableLibrary = extractedMusic .filterIsInstance() - .fold(MusicGraph.builder()) { graphBuilder, extracted -> + .tryFold(MusicGraph.builder()) { graphBuilder, extracted -> when (extracted) { is RawSong -> graphBuilder.add(tagInterpreter.interpret(extracted)) is RawPlaylist -> 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 6609e78dd..e5cd78a23 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt @@ -26,39 +26,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.withIndex -internal sealed interface Divert { - data class Left(val value: L) : Divert - - data class Right(val value: R) : Divert -} - -internal class DivertedFlow( - val manager: Flow, - val left: Flow, - val right: Flow -) - -internal inline fun Flow.divert( - crossinline predicate: (T) -> Divert -): DivertedFlow { - val leftChannel = Channel(Channel.UNLIMITED) - val rightChannel = Channel(Channel.UNLIMITED) - val managedFlow = - flow { - collect { - when (val result = predicate(it)) { - is Divert.Left -> leftChannel.send(result.value) - is Divert.Right -> rightChannel.send(result.value) - } - } - leftChannel.close() - rightChannel.close() - } - return DivertedFlow(managedFlow, leftChannel.receiveAsFlow(), rightChannel.receiveAsFlow()) -} - -internal class DistributedFlow(val manager: Flow, val flows: Flow>) - /** * Equally "distributes" the values of some flow across n new flows. * @@ -95,24 +62,14 @@ internal fun Flow.tryMap(transform: suspend (T) -> R): Flow = flow } } -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 { +internal suspend fun Flow.tryFold(initial: A, operation: suspend (A, T) -> A): A { var accumulator = initial collect { value -> try { accumulator = operation(accumulator, value) - emit(accumulator) } catch (e: Exception) { throw PipelineException(value, e) } } -} + return accumulator +} \ No newline at end of file From e2d28f98f4d466fdbfaeb0b6c022148acd701fe3 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 7 Mar 2025 12:01:00 -0700 Subject: [PATCH 09/49] musikr: internalcovers -> embeddedcovers --- .../oxycblt/auxio/image/covers/CoverModule.kt | 2 +- .../oxycblt/auxio/image/covers/CoverSilo.kt | 2 +- .../auxio/image/covers/SettingCovers.kt | 6 ++--- .../auxio/image/covers/SiloedCovers.kt | 24 +++++++++---------- .../{internal => embedded}/CoverFormat.kt | 2 +- .../{internal => embedded}/CoverIdentifier.kt | 2 +- .../{internal => embedded}/CoverParams.kt | 2 +- .../EmbeddedCovers.kt} | 8 +++---- 8 files changed, 24 insertions(+), 24 deletions(-) rename musikr/src/main/java/org/oxycblt/musikr/covers/{internal => embedded}/CoverFormat.kt (98%) rename musikr/src/main/java/org/oxycblt/musikr/covers/{internal => embedded}/CoverIdentifier.kt (96%) rename musikr/src/main/java/org/oxycblt/musikr/covers/{internal => embedded}/CoverParams.kt (96%) rename musikr/src/main/java/org/oxycblt/musikr/covers/{internal/InternalCovers.kt => embedded/EmbeddedCovers.kt} (92%) 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 67ed6b406..714dd1994 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.covers.internal.CoverIdentifier +import org.oxycblt.musikr.covers.embedded.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 5501766c1..a5383621b 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.covers.internal.CoverParams +import org.oxycblt.musikr.covers.embedded.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/SettingCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt index 72c97e4d0..2f4629451 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 @@ -28,9 +28,9 @@ 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.internal.FileCover +import org.oxycblt.musikr.covers.embedded.CoverIdentifier +import org.oxycblt.musikr.covers.embedded.CoverParams +import org.oxycblt.musikr.covers.embedded.FileCover interface SettingCovers { suspend fun mutate(context: Context, revision: UUID): MutableCovers 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 c8f5288aa..e3dd33f84 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 @@ -26,11 +26,11 @@ 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.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.internal.MutableInternalCovers +import org.oxycblt.musikr.covers.embedded.CoverFormat +import org.oxycblt.musikr.covers.embedded.CoverIdentifier +import org.oxycblt.musikr.covers.embedded.FileCover +import org.oxycblt.musikr.covers.embedded.EmbeddedCovers +import org.oxycblt.musikr.covers.embedded.MutableEmbeddedCovers 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 internalCovers = InternalCovers(core.files, core.format) - return when (val result = internalCovers.obtain(siloedId.id)) { + val embeddedCovers = EmbeddedCovers(core.files, core.format) + return when (val result = embeddedCovers.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 internalCovers: InternalCovers) : +open class SiloedCovers(private val silo: CoverSilo, private val embeddedCovers: EmbeddedCovers) : 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 = internalCovers.obtain(coverId.id)) { + return when (val result = embeddedCovers.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 internalCovers: companion object { suspend fun from(context: Context, silo: CoverSilo): SiloedCovers { val core = SiloCore.from(context, silo) - return SiloedCovers(silo, InternalCovers(core.files, core.format)) + return SiloedCovers(silo, EmbeddedCovers(core.files, core.format)) } } } @@ -70,7 +70,7 @@ class MutableSiloedCovers private constructor( private val rootDir: File, private val silo: CoverSilo, - private val fileCovers: MutableInternalCovers + private val fileCovers: MutableEmbeddedCovers ) : SiloedCovers(silo, fileCovers), MutableCovers { override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult = when (val result = fileCovers.create(file, metadata)) { @@ -96,7 +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, MutableEmbeddedCovers(core.files, core.format, coverIdentifier)) } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt similarity index 98% rename from musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt index d64bc5f49..6ed9bce02 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverFormat.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.covers.internal +package org.oxycblt.musikr.covers.embedded import android.graphics.Bitmap import android.graphics.BitmapFactory diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverIdentifier.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverIdentifier.kt similarity index 96% rename from musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverIdentifier.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverIdentifier.kt index a187d0d9e..d842e813d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverIdentifier.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverIdentifier.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.covers.internal +package org.oxycblt.musikr.covers.embedded import java.security.MessageDigest diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverParams.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt similarity index 96% rename from musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverParams.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt index c7bc9a98e..e4ae6e8cf 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/CoverParams.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.covers.internal +package org.oxycblt.musikr.covers.embedded 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/covers/internal/InternalCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt similarity index 92% rename from musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt index 4d2f47bfc..33fec914f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/internal/InternalCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.covers.internal +package org.oxycblt.musikr.covers.embedded import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.covers.CoverResult @@ -28,7 +28,7 @@ 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) : +open class EmbeddedCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) : Covers { override suspend fun obtain(id: String): CoverResult { val file = appFS.find(getFileName(id)) @@ -42,11 +42,11 @@ open class InternalCovers(private val appFS: AppFS, private val coverFormat: Cov protected fun getFileName(id: String) = "$id.${coverFormat.extension}" } -class MutableInternalCovers( +class MutableEmbeddedCovers( private val appFS: AppFS, private val coverFormat: CoverFormat, private val coverIdentifier: CoverIdentifier -) : InternalCovers(appFS, coverFormat), MutableCovers { +) : EmbeddedCovers(appFS, coverFormat), MutableCovers { override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { val data = metadata.cover ?: return CoverResult.Miss() val id = coverIdentifier.identify(data) From cd535eda2e124362a76cd7d69b09124cdc9d4917 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Mar 2025 11:03:35 -0700 Subject: [PATCH 10/49] all: fix merge issues --- .../auxio/image/covers/SettingCovers.kt | 8 ++++---- .../oxycblt/auxio/image/covers/SiloedCovers.kt | 18 +++++++++--------- .../org/oxycblt/auxio/music/MusicRepository.kt | 6 +++--- .../auxio/music/shim/MusikrShimModule.kt | 3 ++- .../musikr/covers/embedded/EmbeddedCovers.kt | 2 +- .../org/oxycblt/musikr/covers/fs/FSCovers.kt | 7 +------ .../org/oxycblt/musikr/fs/device/DeviceFS.kt | 14 +++++--------- .../oxycblt/musikr/pipeline/EvaluateStep.kt | 1 - .../org/oxycblt/musikr/pipeline/ExploreStep.kt | 4 +++- .../org/oxycblt/musikr/pipeline/FlowUtil.kt | 2 +- 10 files changed, 29 insertions(+), 36 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 2f4629451..774eeb65c 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 @@ -25,18 +25,18 @@ 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.FDCover 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.embedded.CoverIdentifier import org.oxycblt.musikr.covers.embedded.CoverParams -import org.oxycblt.musikr.covers.embedded.FileCover +import org.oxycblt.musikr.covers.fs.FSCovers +import org.oxycblt.musikr.covers.fs.MutableFSCovers interface SettingCovers { suspend fun mutate(context: Context, revision: UUID): MutableCovers companion object { - fun immutable(context: Context): Covers = + fun immutable(context: Context): Covers = Covers.chain(BaseSiloedCovers(context), FSCovers(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 e3dd33f84..e1bc4d622 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 @@ -25,18 +25,18 @@ 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.embedded.CoverFormat import org.oxycblt.musikr.covers.embedded.CoverIdentifier -import org.oxycblt.musikr.covers.embedded.FileCover import org.oxycblt.musikr.covers.embedded.EmbeddedCovers import org.oxycblt.musikr.covers.embedded.MutableEmbeddedCovers import org.oxycblt.musikr.fs.app.AppFS import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata -class BaseSiloedCovers(private val context: Context) : Covers { - override suspend fun obtain(id: String): CoverResult { +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 embeddedCovers = EmbeddedCovers(core.files, core.format) @@ -48,8 +48,8 @@ class BaseSiloedCovers(private val context: Context) : Covers { } open class SiloedCovers(private val silo: CoverSilo, private val embeddedCovers: EmbeddedCovers) : - Covers { - override suspend fun obtain(id: String): CoverResult { + 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 = embeddedCovers.obtain(coverId.id)) { @@ -71,8 +71,8 @@ private constructor( private val rootDir: File, private val silo: CoverSilo, private val fileCovers: MutableEmbeddedCovers -) : SiloedCovers(silo, fileCovers), MutableCovers { - override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult = +) : SiloedCovers(silo, fileCovers), MutableCovers { + override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult = when (val result = fileCovers.create(file, metadata)) { is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover)) is CoverResult.Miss -> CoverResult.Miss() @@ -101,8 +101,8 @@ private constructor( } } -data class SiloedCover(private val silo: CoverSilo, val innerCover: FileCover) : - FileCover by innerCover { +data class SiloedCover(private val silo: CoverSilo, val innerCover: FDCover) : + FDCover by innerCover { private val innerId = SiloedCoverId(silo, innerCover.id) override val id = innerId.toString() } 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 59a09e91d..f1e1688d0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -39,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.db.MutableDBCache +import org.oxycblt.musikr.cache.MutableCache import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators @@ -237,7 +237,7 @@ class MusicRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, - private val dbCache: MutableDBCache, + private val cache: MutableCache, private val storedPlaylists: StoredPlaylists, private val settingCovers: SettingCovers, private val musicSettings: MusicSettings @@ -389,7 +389,7 @@ constructor( val currentRevision = musicSettings.revision val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() - val cache = if (withCache) dbCache else WriteOnlyMutableCache(dbCache) + val cache = if (withCache) cache else WriteOnlyMutableCache(cache) val covers = settingCovers.mutate(context, newRevision) val storage = Storage(cache, covers, storedPlaylists) val interpretation = Interpretation(nameFactory, separators, withHidden) 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 ee0702450..827ac75c9 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,6 +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.MutableCache import org.oxycblt.musikr.cache.db.MutableDBCache import org.oxycblt.musikr.playlist.db.StoredPlaylists @@ -33,7 +34,7 @@ import org.oxycblt.musikr.playlist.db.StoredPlaylists class MusikrShimModule { @Singleton @Provides - fun cache(@ApplicationContext context: Context) = MutableDBCache.from(context) + fun cache(@ApplicationContext context: Context): MutableCache = MutableDBCache.from(context) @Singleton @Provides diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt index 33fec914f..e1aa5f24f 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2025 Auxio Project - * InternalCovers.kt is part of Auxio. + * EmbeddedCovers.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/covers/fs/FSCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/fs/FSCovers.kt index b182c775e..a9ced2062 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 @@ -23,8 +23,6 @@ import android.net.Uri import android.os.ParcelFileDescriptor import java.io.InputStream 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 @@ -76,10 +74,7 @@ class MutableFSCovers(private val context: Context) : FSCovers(context), Mutable private fun findCoverInDirectory(directory: DeviceDirectory): DeviceFile? { return directory.children.firstNotNullOfOrNull { node -> - if (node is DeviceFile && isCoverArtFile( - node - ) - ) node else null + if (node is DeviceFile && isCoverArtFile(node)) node else null } } 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 bb580008c..44a5ff652 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 @@ -15,7 +15,7 @@ * 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.content.ContentResolver @@ -75,8 +75,7 @@ private class DeviceFSImpl( location.uri, DocumentsContract.getTreeDocumentId(location.uri), location.path, - null - ) + null) } private fun exploreDirectoryImpl( @@ -116,8 +115,7 @@ private class DeviceFSImpl( if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { recursive.add( - exploreDirectoryImpl(rootUri, childId, newPath, directoryDeferred) - ) + exploreDirectoryImpl(rootUri, childId, newPath, directoryDeferred)) } else { val size = cursor.getLong(sizeIndex) val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId) @@ -128,8 +126,7 @@ private class DeviceFSImpl( path = newPath, size = size, modifiedMs = lastModified, - parent = directoryDeferred - ) + parent = directoryDeferred) children.add(file) emit(file) } @@ -146,7 +143,6 @@ private class DeviceFSImpl( DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_LAST_MODIFIED - ) + DocumentsContract.Document.COLUMN_LAST_MODIFIED) } } 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 a28fdab8c..59e4c9438 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/EvaluateStep.kt @@ -20,7 +20,6 @@ package org.oxycblt.musikr.pipeline import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.fold import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.Storage 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 5a7779c13..12762e22a 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -67,7 +67,9 @@ private class ExploreStepImpl( val addingMs = System.currentTimeMillis() return merge( deviceFS - .explore(locations.asFlow(),) + .explore( + locations.asFlow(), + ) .filter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE } .distribute(8) .distributedMap { file -> 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 e5cd78a23..29eef2735 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt @@ -72,4 +72,4 @@ internal suspend fun Flow.tryFold(initial: A, operation: suspend (A, T } } return accumulator -} \ No newline at end of file +} From 879caf17db24b608edfca59cde924a94e4cdde21 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Mar 2025 14:53:43 -0700 Subject: [PATCH 11/49] musikr: revamp cover system Retains the stateless attributes of the older system but massively simplifies it compared to prior. --- .../oxycblt/auxio/image/covers/CoverModule.kt | 38 ----- .../oxycblt/auxio/image/covers/CoverSilo.kt | 22 --- .../oxycblt/auxio/image/covers/NullCovers.kt | 6 +- ...{CoverUtil.kt => RevisionedTranscoding.kt} | 14 +- .../auxio/image/covers/SettingCovers.kt | 43 ++++-- .../auxio/image/covers/SiloedCovers.kt | 137 ------------------ .../java/org/oxycblt/musikr/covers/Covers.kt | 4 + .../musikr/covers/embedded/CoverParams.kt | 34 ----- .../musikr/covers/embedded/EmbeddedCovers.kt | 55 +++---- .../stored/CoverStorage.kt} | 53 ++++--- .../musikr/covers/stored/StoredCovers.kt | 64 ++++++++ .../CoverFormat.kt => stored/Transcoding.kt} | 54 +++---- 12 files changed, 178 insertions(+), 346 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt rename app/src/main/java/org/oxycblt/auxio/image/covers/{CoverUtil.kt => RevisionedTranscoding.kt} (69%) delete mode 100644 app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt delete mode 100644 musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt rename musikr/src/main/java/org/oxycblt/musikr/{fs/app/AppFS.kt => covers/stored/CoverStorage.kt} (68%) create mode 100644 musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt rename musikr/src/main/java/org/oxycblt/musikr/covers/{embedded/CoverFormat.kt => stored/Transcoding.kt} (61%) 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 deleted file mode 100644 index 714dd1994..000000000 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * CoverModule.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.image.covers - -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import org.oxycblt.musikr.covers.embedded.CoverIdentifier - -@Module -@InstallIn(SingletonComponent::class) -interface CoverModule { - @Binds fun configCovers(impl: SettingCoversImpl): SettingCovers -} - -@Module -@InstallIn(SingletonComponent::class) -class CoverProvidesModule { - @Provides fun identifier(): CoverIdentifier = CoverIdentifier.md5() -} 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 a5383621b..735dd1cda 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,28 +19,6 @@ package org.oxycblt.auxio.image.covers import java.util.UUID -import org.oxycblt.musikr.covers.embedded.CoverParams - -data class CoverSilo(val revision: UUID, val params: CoverParams?) { - override fun toString() = - "${revision}${params?.let { ".${params.resolution}.${params.quality}" } ?: "" }" - - companion object { - fun parse(silo: String): CoverSilo? { - val parts = silo.split('.') - if (parts.size != 1 && parts.size != 3) { - return null - } - val revision = parts[0].toUuidOrNull() ?: return null - if (parts.size > 1) { - val resolution = parts[1].toIntOrNull() ?: return null - val quality = parts[2].toIntOrNull() ?: return null - return CoverSilo(revision, CoverParams.of(resolution, quality)) - } - return CoverSilo(revision, null) - } - } -} private fun String.toUuidOrNull(): UUID? = try { 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 079ed5f6c..df39b5981 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 @@ -18,20 +18,20 @@ package org.oxycblt.auxio.image.covers -import android.content.Context import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.covers.stored.CoverStorage import org.oxycblt.musikr.fs.device.DeviceFile import org.oxycblt.musikr.metadata.Metadata -class NullCovers(private val context: Context) : MutableCovers { +class NullCovers(private val storage: CoverStorage) : MutableCovers { override suspend fun obtain(id: String) = CoverResult.Hit(NullCover) override suspend fun create(file: DeviceFile, metadata: Metadata) = CoverResult.Hit(NullCover) override suspend fun cleanup(excluding: Collection) { - context.coversDir().listFiles()?.forEach { it.deleteRecursively() } + storage.ls(setOf()).map { storage.rm(it) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverUtil.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/RevisionedTranscoding.kt similarity index 69% rename from app/src/main/java/org/oxycblt/auxio/image/covers/CoverUtil.kt rename to app/src/main/java/org/oxycblt/auxio/image/covers/RevisionedTranscoding.kt index 67058e8b6..95980c42d 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/RevisionedTranscoding.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2024 Auxio Project - * CoverUtil.kt is part of Auxio. + * Copyright (c) 2025 Auxio Project + * RevisionedTranscoding.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,9 +18,9 @@ package org.oxycblt.auxio.image.covers -import android.content.Context -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import java.util.UUID +import org.oxycblt.musikr.covers.stored.Transcoding -suspend fun Context.coversDir() = - withContext(Dispatchers.IO) { filesDir.resolve("covers").apply { mkdirs() } } +class RevisionedTranscoding(revision: UUID, private val inner: Transcoding) : Transcoding by inner { + override val tag = "_$revision${inner.tag}" +} 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 774eeb65c..85cc24f1a 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 @@ -19,6 +19,7 @@ package org.oxycblt.auxio.image.covers import android.content.Context +import android.graphics.Bitmap import java.util.UUID import javax.inject.Inject import org.oxycblt.auxio.image.CoverMode @@ -28,16 +29,21 @@ import org.oxycblt.musikr.covers.Covers import org.oxycblt.musikr.covers.FDCover import org.oxycblt.musikr.covers.MutableCovers import org.oxycblt.musikr.covers.embedded.CoverIdentifier -import org.oxycblt.musikr.covers.embedded.CoverParams +import org.oxycblt.musikr.covers.embedded.EmbeddedCovers import org.oxycblt.musikr.covers.fs.FSCovers import org.oxycblt.musikr.covers.fs.MutableFSCovers +import org.oxycblt.musikr.covers.stored.Compress +import org.oxycblt.musikr.covers.stored.CoverStorage +import org.oxycblt.musikr.covers.stored.MutableStoredCovers +import org.oxycblt.musikr.covers.stored.NoTranscoding +import org.oxycblt.musikr.covers.stored.StoredCovers interface SettingCovers { suspend fun mutate(context: Context, revision: UUID): MutableCovers companion object { - fun immutable(context: Context): Covers = - Covers.chain(BaseSiloedCovers(context), FSCovers(context)) + suspend fun immutable(context: Context): Covers = + Covers.chain(StoredCovers(CoverStorage.at(context.coversDir())), FSCovers(context)) } } @@ -45,17 +51,22 @@ class SettingCoversImpl @Inject constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) : SettingCovers { - override suspend fun mutate(context: Context, revision: UUID): MutableCovers = - when (imageSettings.coverMode) { - CoverMode.OFF -> NullCovers(context) - CoverMode.SAVE_SPACE -> siloedCovers(context, revision, CoverParams.of(500, 70)) - CoverMode.BALANCED -> siloedCovers(context, revision, CoverParams.of(750, 85)) - CoverMode.HIGH_QUALITY -> siloedCovers(context, revision, CoverParams.of(1000, 100)) - CoverMode.AS_IS -> siloedCovers(context, revision, null) - } - - private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams?) = - MutableCovers.chain( - MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier), - MutableFSCovers(context)) + override suspend fun mutate(context: Context, revision: UUID): MutableCovers { + val coverStorage = CoverStorage.at(context.coversDir()) + val transcoding = + when (imageSettings.coverMode) { + CoverMode.OFF -> return NullCovers(coverStorage) + CoverMode.SAVE_SPACE -> Compress(Bitmap.CompressFormat.JPEG, 500, 70) + CoverMode.BALANCED -> Compress(Bitmap.CompressFormat.JPEG, 750, 85) + CoverMode.HIGH_QUALITY -> Compress(Bitmap.CompressFormat.JPEG, 1000, 100) + CoverMode.AS_IS -> NoTranscoding + } + val revisionedTranscoding = RevisionedTranscoding(revision, transcoding) + val storedCovers = + MutableStoredCovers(EmbeddedCovers(identifier), coverStorage, revisionedTranscoding) + val fsCovers = MutableFSCovers(context) + return MutableCovers.chain(storedCovers, fsCovers) + } } + +private fun Context.coversDir() = filesDir.resolve("covers") 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 deleted file mode 100644 index e1bc4d622..000000000 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * SiloedCovers.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.image.covers - -import android.content.Context -import java.io.File -import kotlinx.coroutines.Dispatchers -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.embedded.CoverFormat -import org.oxycblt.musikr.covers.embedded.CoverIdentifier -import org.oxycblt.musikr.covers.embedded.EmbeddedCovers -import org.oxycblt.musikr.covers.embedded.MutableEmbeddedCovers -import org.oxycblt.musikr.fs.app.AppFS -import org.oxycblt.musikr.fs.device.DeviceFile -import org.oxycblt.musikr.metadata.Metadata - -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 embeddedCovers = EmbeddedCovers(core.files, core.format) - return when (val result = embeddedCovers.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 embeddedCovers: EmbeddedCovers) : - 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 = embeddedCovers.obtain(coverId.id)) { - is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover)) - is CoverResult.Miss -> CoverResult.Miss() - } - } - - companion object { - suspend fun from(context: Context, silo: CoverSilo): SiloedCovers { - val core = SiloCore.from(context, silo) - return SiloedCovers(silo, EmbeddedCovers(core.files, core.format)) - } - } -} - -class MutableSiloedCovers -private constructor( - private val rootDir: File, - private val silo: CoverSilo, - private val fileCovers: MutableEmbeddedCovers -) : SiloedCovers(silo, fileCovers), MutableCovers { - override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult = - when (val result = fileCovers.create(file, metadata)) { - is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover)) - is CoverResult.Miss -> CoverResult.Miss() - } - - override suspend fun cleanup(excluding: Collection) { - fileCovers.cleanup(excluding.filterIsInstance().map { it.innerCover }) - - // Destroy old revisions no longer being used. - withContext(Dispatchers.IO) { - val exclude = silo.toString() - rootDir.listFiles { file -> file.name != exclude }?.forEach { it.deleteRecursively() } - } - } - - companion object { - suspend fun from( - context: Context, - silo: CoverSilo, - coverIdentifier: CoverIdentifier - ): MutableSiloedCovers { - val core = SiloCore.from(context, silo) - return MutableSiloedCovers( - core.rootDir, silo, MutableEmbeddedCovers(core.files, core.format, coverIdentifier)) - } - } -} - -data class SiloedCover(private val silo: CoverSilo, val innerCover: FDCover) : - FDCover by innerCover { - private val innerId = SiloedCoverId(silo, innerCover.id) - override val id = innerId.toString() -} - -data class SiloedCoverId(val silo: CoverSilo, val id: String) { - override fun toString() = "$id@$silo" - - companion object { - fun parse(id: String): SiloedCoverId? { - val parts = id.split('@') - if (parts.size != 2) return null - val silo = CoverSilo.parse(parts[1]) ?: return null - return SiloedCoverId(silo, parts[0]) - } - } -} - -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 - val revisionDir: File - withContext(Dispatchers.IO) { - rootDir = context.coversDir() - revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() } - } - 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/covers/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt index adef4cf44..3a81665ec 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt @@ -99,6 +99,10 @@ interface FDCover : Cover { suspend fun fd(): ParcelFileDescriptor? } +interface MemoryCover : Cover { + fun data(): ByteArray +} + class CoverCollection private constructor(val covers: List) { override fun hashCode() = covers.hashCode() diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt deleted file mode 100644 index e4ae6e8cf..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * CoverParams.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.embedded - -class CoverParams private constructor(val resolution: Int, val quality: Int) { - override fun hashCode() = 31 * resolution + quality - - override fun equals(other: Any?) = - other is CoverParams && other.resolution == resolution && other.quality == quality - - companion object { - fun of(resolution: Int, quality: Int): CoverParams { - check(resolution > 0) { "Resolution must be positive" } - check(quality in 0..100) { "Quality must be between 0 and 100" } - return CoverParams(resolution, quality) - } - } -} diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt index e1aa5f24f..4607ef444 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt @@ -18,51 +18,36 @@ package org.oxycblt.musikr.covers.embedded +import java.io.ByteArrayInputStream 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.MemoryCover import org.oxycblt.musikr.covers.MutableCovers -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 EmbeddedCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) : - Covers { - override suspend fun obtain(id: String): CoverResult { - val file = appFS.find(getFileName(id)) - return if (file != null) { - CoverResult.Hit(InternalCoverImpl(id, file)) - } else { - CoverResult.Miss() - } - } +class EmbeddedCovers(private val coverIdentifier: CoverIdentifier) : MutableCovers { + override suspend fun obtain(id: String): CoverResult = CoverResult.Miss() - protected fun getFileName(id: String) = "$id.${coverFormat.extension}" -} - -class MutableEmbeddedCovers( - private val appFS: AppFS, - private val coverFormat: CoverFormat, - private val coverIdentifier: CoverIdentifier -) : EmbeddedCovers(appFS, coverFormat), MutableCovers { - override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { + 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(InternalCoverImpl(id, coverFile)) + return CoverResult.Hit(EmbeddedCover(id, data)) } - override suspend fun cleanup(excluding: Collection) { - val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) } - appFS.deleteWhere { it !in used } + override suspend fun cleanup(excluding: Collection) {} +} + +private class EmbeddedCover(override val id: String, private val data: ByteArray) : MemoryCover { + override suspend fun open() = ByteArrayInputStream(data) + + override fun data() = data + + override fun hashCode(): Int = id.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is EmbeddedCover) return false + return id == other.id } } - -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/covers/stored/CoverStorage.kt similarity index 68% rename from musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt index 50b423c08..11382225d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2024 Auxio Project - * AppFS.kt is part of Auxio. + * CoverStorage.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,40 +16,36 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.fs.app +package org.oxycblt.musikr.covers.stored import android.os.ParcelFileDescriptor import java.io.File import java.io.IOException -import java.io.InputStream import java.io.OutputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.oxycblt.musikr.covers.FDCover -interface AppFS { - suspend fun find(name: String): AppFile? +interface CoverStorage { + suspend fun find(name: String): FDCover? - suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile + suspend fun write(name: String, block: suspend (OutputStream) -> Unit): FDCover - suspend fun deleteWhere(block: (String) -> Boolean) + suspend fun ls(exclude: Set): List + + suspend fun rm(file: String) companion object { - suspend fun at(dir: File): AppFS { + suspend fun at(dir: File): CoverStorage { withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) } - return AppFSImpl(dir) + return CoverStorageImpl(dir) } } } -interface AppFile { - suspend fun fd(): ParcelFileDescriptor? - - suspend fun open(): InputStream? -} - -private class AppFSImpl(private val dir: File) : AppFS { +private class CoverStorageImpl(private val dir: File) : CoverStorage { private val fileMutexes = mutableMapOf() private val mapMutex = Mutex() @@ -57,16 +53,16 @@ private class AppFSImpl(private val dir: File) : AppFS { return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } } } - override suspend fun find(name: String): AppFile? = + override suspend fun find(name: String): FDCover? = withContext(Dispatchers.IO) { try { - File(dir, name).takeIf { it.exists() }?.let { AppFileImpl(it) } + File(dir, name).takeIf { it.exists() }?.let { StoredCover(it) } } catch (e: IOException) { null } } - override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile { + override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): FDCover { val fileMutex = getMutexForFile(name) return fileMutex.withLock { val targetFile = File(dir, name) @@ -77,26 +73,31 @@ private class AppFSImpl(private val dir: File) : AppFS { try { tempFile.outputStream().use { block(it) } tempFile.renameTo(targetFile) - AppFileImpl(targetFile) + StoredCover(targetFile) } catch (e: IOException) { tempFile.delete() throw e } } } else { - AppFileImpl(targetFile) + StoredCover(targetFile) } } } - override suspend fun deleteWhere(block: (String) -> Boolean) { + override suspend fun ls(exclude: Set): List = withContext(Dispatchers.IO) { - dir.listFiles { file -> block(file.name) }?.forEach { it.deleteRecursively() } + dir.listFiles()?.map { it.name }?.filter { exclude.contains(it) } ?: emptyList() } + + override suspend fun rm(file: String) { + withContext(Dispatchers.IO) { File(dir, file).delete() } } } -private data class AppFileImpl(private val file: File) : AppFile { +private data class StoredCover(private val file: File) : FDCover { + override val id: String = file.name + override suspend fun fd() = withContext(Dispatchers.IO) { try { @@ -107,4 +108,8 @@ private data class AppFileImpl(private val file: File) : AppFile { } override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() } + + override fun equals(other: Any?) = other is StoredCover && file == other.file + + override fun hashCode() = file.hashCode() } diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt new file mode 100644 index 000000000..4bf88b0d4 --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Auxio Project + * StoredCovers.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.stored + +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.MemoryCover +import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.fs.device.DeviceFile +import org.oxycblt.musikr.metadata.Metadata + +class StoredCovers(private val coverStorage: CoverStorage) : Covers { + override suspend fun obtain(id: String): CoverResult { + val cover = coverStorage.find(id) ?: return CoverResult.Miss() + return CoverResult.Hit(cover) + } +} + +class MutableStoredCovers( + private val src: MutableCovers, + private val coverStorage: CoverStorage, + private val transcoding: Transcoding +) : MutableCovers { + private val base = StoredCovers(coverStorage) + + override suspend fun obtain(id: String): CoverResult = base.obtain(id) + + override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { + val cover = + when (val cover = src.create(file, metadata)) { + is CoverResult.Hit -> cover.cover + is CoverResult.Miss -> return CoverResult.Miss() + } + val coverFile = coverStorage.write(cover.id + transcoding.tag) { it.write(cover.data()) } + return CoverResult.Hit(coverFile) + } + + override suspend fun cleanup(excluding: Collection) { + src.cleanup(excluding) + val used = excluding.mapTo(mutableSetOf()) { it.id } + val unused = coverStorage.ls(exclude = used).filter { it !in used } + for (file in unused) { + coverStorage.rm(file) + } + } +} diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/Transcoding.kt similarity index 61% rename from musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt rename to musikr/src/main/java/org/oxycblt/musikr/covers/stored/Transcoding.kt index 6ed9bce02..d2e347cc7 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/Transcoding.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2025 Auxio Project - * CoverFormat.kt is part of Auxio. + * Transcoding.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,40 +16,47 @@ * along with this program. If not, see . */ -package org.oxycblt.musikr.covers.embedded +package org.oxycblt.musikr.covers.stored import android.graphics.Bitmap import android.graphics.BitmapFactory import java.io.OutputStream -abstract class CoverFormat { - internal abstract val extension: String +interface Transcoding { + val tag: String - internal abstract fun transcodeInto(data: ByteArray, output: OutputStream): Boolean + fun transcodeInto(data: ByteArray, output: OutputStream) +} - companion object { - fun jpeg(params: CoverParams): CoverFormat = - CompressingCoverFormat("jpg", params, Bitmap.CompressFormat.JPEG) +object NoTranscoding : Transcoding { + override val tag = ".img" - fun asIs(): CoverFormat = AsIsCoverFormat() + override fun transcodeInto(data: ByteArray, output: OutputStream) { + output.write(data) } } -private class CompressingCoverFormat( - override val extension: String, - private val params: CoverParams, +class Compress( private val format: Bitmap.CompressFormat, -) : CoverFormat() { - override fun transcodeInto(data: ByteArray, output: OutputStream) = + private val resolution: Int, + private val quality: Int, +) : Transcoding { + override val tag = "_${resolution}x${quality}.${format.name.lowercase()}" + + override fun transcodeInto(data: ByteArray, output: OutputStream) { BitmapFactory.Options().run { inJustDecodeBounds = true BitmapFactory.decodeByteArray(data, 0, data.size, this) - inSampleSize = calculateInSampleSize(params.resolution) + inSampleSize = calculateInSampleSize(resolution) inJustDecodeBounds = false - val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, this) ?: return@run false - bitmap.compress(format, params.quality, output) + val bitmap = + requireNotNull(BitmapFactory.decodeByteArray(data, 0, data.size, this)) { + "Failed to decode bitmap" + } + bitmap.compress(format, quality, output) true } + } private fun BitmapFactory.Options.calculateInSampleSize(size: Int): Int { var inSampleSize = 1 @@ -65,16 +72,3 @@ private class CompressingCoverFormat( return inSampleSize } } - -private class AsIsCoverFormat : CoverFormat() { - override val extension: String = "bin" - - override fun transcodeInto(data: ByteArray, output: OutputStream): Boolean { - return try { - output.write(data) - true - } catch (e: Exception) { - false - } - } -} From 219d26b4dc611f6894459916eb6d0357ec9729ca Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 8 Mar 2025 23:01:08 +0100 Subject: [PATCH 12/49] Translations update from Hosted Weblate (#1014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Ukrainian) Currently translated at 100.0% (315 of 315 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/uk/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (315 of 315 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/zh_Hans/ * Translated using Weblate (Czech) Currently translated at 100.0% (315 of 315 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/cs/ * Translated using Weblate (Arabic) Currently translated at 100.0% (315 of 315 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ar/ * Translated using Weblate (Estonian) Currently translated at 100.0% (315 of 315 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/et/ * Translated using Weblate (Finnish) Currently translated at 100.0% (315 of 315 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/fi/ * Translated using Weblate (Finnish) Currently translated at 100.0% (56 of 56 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/fi/ * Translated using Weblate (German) Currently translated at 100.0% (315 of 315 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/de/ * Translated using Weblate (German) Currently translated at 100.0% (56 of 56 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/de/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/uk/ --------- Co-authored-by: Максим Горпиніч Co-authored-by: 大王叫我来巡山 Co-authored-by: Fjuro Co-authored-by: jonnysemon Co-authored-by: Priit Jõerüüt Co-authored-by: Ricky Tigg Co-authored-by: qwerty287 Co-authored-by: santiago046 Co-authored-by: Alexander Capehart --- app/src/main/res/values-ar/strings.xml | 2 ++ app/src/main/res/values-cs/strings.xml | 4 ++- app/src/main/res/values-de/strings.xml | 4 ++- app/src/main/res/values-et/strings.xml | 2 ++ app/src/main/res/values-fi/strings.xml | 4 ++- app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 5 ++- app/src/main/res/values-zh-rCN/strings.xml | 4 ++- .../metadata/android/de/full_description.txt | 5 ++- .../metadata/android/fi/full_description.txt | 33 ++++++++++--------- 10 files changed, 40 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 9d6db7b73..290891ca8 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -334,4 +334,6 @@ MPEG-4 تحتوي على %s Apple Lossless Audio Codec (ALAC) + تجاهل الملفات المخفية + تخطي الملفات والمجلدات المخفية (على سبيل المثال: الذاكرة المخبّأة) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 386ec94e2..a1c0dc2a7 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -341,4 +341,6 @@ Vaši umělci se zobrazí zde. Vaše seznamy skladeb se zobrazí zde. Vaše žánry se zobrazí zde. - \ No newline at end of file + Přeskočit skryté soubory a složky (např. .cache) + Ignorovat skryté soubory + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6eaa0d219..521ba22d8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -332,4 +332,6 @@ Deine Lieder werden hier angezeigt. Deine Genres werden hier angezeigt. Deine Künstler werden hier angezeigt. - \ No newline at end of file + Versteckte Dateien ignorieren + Überspringt versteckte Dateien und Ordner (z. B. .cache) + diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index af1438fb0..02a0da0ae 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -324,4 +324,6 @@ Sinu žanrid saavad olema nähtavad siin. Sinu albumid saavad olema nähtavad siin. Sinu esitusloendid saavad olema nähtavad siin. + Eira peidetud faile + Jäta vahele peidetud failid ja kaustad (nt. .cache) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index c10ce2e6c..49e81dacc 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -70,7 +70,7 @@ Toista tyylilajista Moniarvoerottimet Ohita äänitiedostot, jotka eivät ole musiikkia, kuten podcastit - Ampersand (&) + Ampersand & Pilkku (,) Plus (+) Puolipiste (;) @@ -322,4 +322,6 @@ Tyylilajisi tulevat näkymään tässä. Apple Lossless Audio Codec (ALAC) Säätä tilaa + Ohita piilotetut tiedostot + Ohita piilotetut tiedostot ja kansiot (esim. .cache) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e714bc71f..a9f823aba 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -334,6 +334,7 @@ Os seus gêneros aparecerão aqui. Economizar espaço Nova pasta + Qualidade original Ignorar arquivos e pastas que estão ocultos (por exemplo, .cache) Ignorar arquivos ocultos diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0d889edf5..ab74ded49 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -336,4 +336,7 @@ Тут відображатимуться ваші списки відтворення. Тут відображатимуться ваші жанри. Тут з’являться ваші виконавці. - \ No newline at end of file + Ігнорувати приховані файли + Пропускати приховані файли та папки (наприклад, .cache) + Як є + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9c56b4c75..052bc3413 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -330,4 +330,6 @@ 流派会出现在此处。 专辑会出现在此处。 播放列表会出现在此处。 - \ No newline at end of file + 跳过隐藏的文件和文件夹(排除 .cache) + 忽略隐藏的文件 + diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt index 97df9a418..cdd4fc070 100644 --- a/fastlane/metadata/android/de/full_description.txt +++ b/fastlane/metadata/android/de/full_description.txt @@ -6,14 +6,13 @@ Auxio ist ein lokaler Musik-Player mit einer schnellen, verlässlichen UI/UX, ab - elegante, am Material Design orientierte UI - Überzeugende UX, die eine einfache Bedienung über Grenzfälle stellt - Anpassbares Verhalten -- Erweiterter Medien-Indexer, der korrekte Metadaten bevorzugt - Unterstützung für CD-Nummer, mehrere Künstler, Releasetypen, präzises/originales Datum, Tags-Sortierung und Release-Typ werden unterstützt (Experimentell) - erweitertes Künstlersystem, das Künstler und Albumkünstler unterstützt - untersützt SD-Karten - verlässliche Wiedergabelisten-Verwaltung -- verlässliches Speichern des Wiedergabezustands -- automatische lückenlose Wiedergabe +- Speichern des Wiedergabezustands - Unterstützung für Android Auto +- automatische lückenlose Wiedergabe - Vollständiger ReplayGain-Support (für MP3-, FLAC-, OGG-, OPUS- und MP4-Dateien) - Externer Equalizerunterstützung (z.B. Wavelet) - Edge-to-Edge diff --git a/fastlane/metadata/android/fi/full_description.txt b/fastlane/metadata/android/fi/full_description.txt index a1b92b47a..8368c2856 100644 --- a/fastlane/metadata/android/fi/full_description.txt +++ b/fastlane/metadata/android/fi/full_description.txt @@ -1,23 +1,24 @@ -Auxio on paikallisen musiikin soitin nopealla ja luotettavalla käyttöliittymällä ilman useita turhia ominaisuuksia kuten muissa soittimissa. Perustuen moderneihin mediantoistokirjastoihin, Auxiolla on parempi tuki kirjastoille ja äänenlaatu verrattuna muihin sovelluksiin, jotka käyttävät vanhentuneita android toimintoja. Lyhyesti, Se soittaa musiikkia. +Auxio on paikallinen musiikkisoitin, jossa on nopea ja luotettava käyttöliittymä/UX ilman monia hyödyttömiä ominaisuuksia, joita muissa musiikkisoittimissa on. Nykyaikaisista mediatoistokirjastoista rakennetussa Auxiossa on ylivertainen kirjastotuki ja kuuntelulaatu verrattuna muihin sovelluksiin, jotka käyttävät vanhentuneita Android-toimintoja. Lyhyesti sanottuna Se soittaa musiikkia. Ominaisuudet - Toisto perustuu Media3 ExoPlayeriin -- Tyylikäs käyttöliittymä johdettu viimeisimmistä Material Design ohjeista -- Omapäinen käyttökokemus joka asettaa helppokäyttöisyyden etusijalle reunatapauksien sijaan -- Mukautettava käyttäytyminen -- Tukee levyjen numeroita, moninkertaisia artisteja, julkaisutyyppejä, tarkkoja/alkuperäisiä päivämääriä, tagijärjestystä ynnä muuta -- Edistynyt esittäjäjärjestelmä, joka yhdistää artistit ja albumiartistit +- Näppärä käyttöliittymä, joka on johdettu uusimmista materiaalisuunnittelun ohjeista +- Mielipiteen mukainen UX, joka asettaa helppokäyttöisyyden etusijalle reunakoteloiden sijaan +- Muokattava käyttäytyminen +- Tuki levynumeroille, useille esittäjille, julkaisutyypeille, tarkille/alkuperäisille päivämäärille, lajittelutunnisteille ja muille +- Kehittynyt artistijärjestelmä, joka yhdistää artistit ja albumin artistit - SD-korttitietoinen kansionhallinta -- Varmatoimiset soittolistatoiminnot -- Android auto-tuki -- Automaattinen katkoton toisto -- Täysi ReplayGain tuki (MP3, FLAC, OGG, OPUS ja MP4 tiedostoissa) -- Tuki ulkoiselle taajuuskorjaimelle (esim. Wavelet) +- Luotettava soittolistatoiminto +- Toistotilan pysyvyys +- Android automaattinen tuki +- Automaattinen välitön toisto +- Täysi ReplayGain-tuki (MP3-, FLAC-, OGG-, OPUS- ja MP4-tiedostoissa) +- Ulkoisen taajuuskorjaimen tuki (esim. Wavelet) - Reunasta reunaan -- Tukee kappaleen sisältämiä kansikuvia +- Upotettu kansituki - Hakutoiminto -- Automaattinen toisto kuulokkeilla -- Tyylikkäitä vimpaimia, jotka mukautuvat automaattisesti kokoonsa -- Täysin yksityinen ja verkkoyhteydetön -- Pyöristämättömät albumin kansikuvat (oletusarvoisesti) +- Kuulokkeiden automaattinen toisto +- Tyylikkäät vimpaimet, jotka mukautuvat automaattisesti kokoonsa +- Täysin yksityinen ja ei-verkkotilassa +- Ei pyöristettyjä albumin kansia (jos haluat ne) From f125e37e956e751daf15b7b5327fe89371e06e54 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 8 Mar 2025 15:07:10 -0700 Subject: [PATCH 13/49] musikr: fix build issue --- .../covers/{CoverSilo.kt => CoversModule.kt} | 20 ++++++++++--------- .../auxio/image/covers/SettingCovers.kt | 7 +++---- 2 files changed, 14 insertions(+), 13 deletions(-) rename app/src/main/java/org/oxycblt/auxio/image/covers/{CoverSilo.kt => CoversModule.kt} (67%) diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CoversModule.kt similarity index 67% rename from app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt rename to app/src/main/java/org/oxycblt/auxio/image/covers/CoversModule.kt index 735dd1cda..96a08f6db 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CoversModule.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2024 Auxio Project - * CoverSilo.kt is part of Auxio. + * Copyright (c) 2023 Auxio Project + * CoversModule.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,11 +18,13 @@ package org.oxycblt.auxio.image.covers -import java.util.UUID +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent -private fun String.toUuidOrNull(): UUID? = - try { - UUID.fromString(this) - } catch (e: IllegalArgumentException) { - null - } +@Module +@InstallIn(SingletonComponent::class) +interface CoilModule { + @Binds fun settingCovers(imageSettings: SettingCoversImpl): SettingCovers +} 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 85cc24f1a..b0271456b 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 @@ -47,9 +47,7 @@ interface SettingCovers { } } -class SettingCoversImpl -@Inject -constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) : +class SettingCoversImpl @Inject constructor(private val imageSettings: ImageSettings) : SettingCovers { override suspend fun mutate(context: Context, revision: UUID): MutableCovers { val coverStorage = CoverStorage.at(context.coversDir()) @@ -63,7 +61,8 @@ constructor(private val imageSettings: ImageSettings, private val identifier: Co } val revisionedTranscoding = RevisionedTranscoding(revision, transcoding) val storedCovers = - MutableStoredCovers(EmbeddedCovers(identifier), coverStorage, revisionedTranscoding) + MutableStoredCovers( + EmbeddedCovers(CoverIdentifier.md5()), coverStorage, revisionedTranscoding) val fsCovers = MutableFSCovers(context) return MutableCovers.chain(storedCovers, fsCovers) } From 513fd980470135aa7e834952bf0966c158c64178 Mon Sep 17 00:00:00 2001 From: happilyretired23 Date: Thu, 13 Mar 2025 08:56:16 -0700 Subject: [PATCH 14/49] Fix typo in README.md Typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24fccbd16..abc3b0326 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ precise/original dates, sort tags, and more - Storage (`READ_MEDIA_AUDIO`, `READ_EXTERNAL_STORAGE`) to read and play your music files - Services (`FOREGROUND_SERVICE`, `WAKE_LOCK`) to keep the music playing in the background -- Notifcations (`POST_NOTIFICATION`) to indicate ongoing playback and music loading +- Notifications (`POST_NOTIFICATION`) to indicate ongoing playback and music loading ## Donate From 05e864e7b5f1562578d2f346023a85b02c19f6f6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 17:27:06 -0600 Subject: [PATCH 15/49] app: remove storage perms No longer needed, unsure why I didn't remove these. --- app/src/main/AndroidManifest.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d0499edd..ecdfc44e2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,9 +2,6 @@ - - - From 436ef8de916d69eb7c517316741543705a230bc4 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 17:36:34 -0600 Subject: [PATCH 16/49] music: force listener trigger on location change Otherwise it just won't actually update normally. Only for this setting though. The others work just fine for some reason. --- app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt | 5 ++++- app/src/main/java/org/oxycblt/auxio/settings/Settings.kt | 6 +----- 2 files changed, 5 insertions(+), 6 deletions(-) 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 8ae954e5a..193d1e827 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -85,7 +85,10 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont sharedPreferences.edit { putString( getString(R.string.set_key_music_locations), MusicLocation.toString(value)) - this@edit.apply() + commit() + // Sometimes changing this setting just won't actually trigger the listener. + // Only this one. No idea why. + listener?.onMusicLocationsChanged() } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 9f77a3c8f..2128d6db4 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -62,17 +62,13 @@ interface Settings { */ abstract class Impl(private val context: Context) : Settings, SharedPreferences.OnSharedPreferenceChangeListener { - init { - L.d(this::class.simpleName) - } - protected val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) /** @see [Context.getString] */ protected fun getString(@StringRes stringRes: Int) = context.getString(stringRes) - private var listener: Listener? = null + protected var listener: Listener? = null override fun registerListener(listener: Listener) { if (this.listener == null) { From b573fd226072accc963cacd96ab0c3252faac915 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 17:55:59 -0600 Subject: [PATCH 17/49] musikr: make wildcard artists display covers Resolves #1048 --- musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e2820a0bb..bcf46f684 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt @@ -55,7 +55,7 @@ internal class ArtistImpl(private val core: ArtistCore) : Artist { get() = core.resolveGenres().toList() override val durationMs = core.songs.sumOf { it.durationMs } - override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }) + override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }.ifEmpty { core.albums.flatMap { it.covers.covers } }) private val hashCode = 31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode() From b8733a180c1c261f793fc4cb85080fa0514caa2c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 18:00:11 -0600 Subject: [PATCH 18/49] musikr: reformat --- musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 bcf46f684..c9bee50b0 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/ArtistImpl.kt @@ -55,7 +55,9 @@ internal class ArtistImpl(private val core: ArtistCore) : Artist { get() = core.resolveGenres().toList() override val durationMs = core.songs.sumOf { it.durationMs } - override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover }.ifEmpty { core.albums.flatMap { it.covers.covers } }) + override val covers = + CoverCollection.from( + core.songs.mapNotNull { it.cover }.ifEmpty { core.albums.flatMap { it.covers.covers } }) private val hashCode = 31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode() From 2f5b78dd845fa7f1841c2c0c39dd3aad40f7fad5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 21:34:40 -0600 Subject: [PATCH 19/49] musikr: fix cover storage dir check --- .../java/org/oxycblt/musikr/covers/stored/CoverStorage.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt index 11382225d..4370a8a91 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt @@ -39,7 +39,9 @@ interface CoverStorage { companion object { suspend fun at(dir: File): CoverStorage { - withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) } + withContext(Dispatchers.IO) { + if (dir.exists()) check(dir.isDirectory) { "Not a directory" } else check(dir.mkdirs()) { "Cannot create directory" } + } return CoverStorageImpl(dir) } } From 652f0891fc011803c8d4f5bd6e7b92d5c12956eb Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 21:35:12 -0600 Subject: [PATCH 20/49] musikr: use transcoding in storedcovers --- .../main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt index 4bf88b0d4..368d4202d 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt @@ -49,7 +49,7 @@ class MutableStoredCovers( is CoverResult.Hit -> cover.cover is CoverResult.Miss -> return CoverResult.Miss() } - val coverFile = coverStorage.write(cover.id + transcoding.tag) { it.write(cover.data()) } + val coverFile = coverStorage.write(cover.id + transcoding.tag) { transcoding.transcodeInto(cover.data(), it) } return CoverResult.Hit(coverFile) } From b3c66d9b556f485585403364f0288a0df3069e73 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 21:36:16 -0600 Subject: [PATCH 21/49] musikr: reformat --- .../java/org/oxycblt/musikr/covers/stored/CoverStorage.kt | 3 ++- .../java/org/oxycblt/musikr/covers/stored/StoredCovers.kt | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt index 4370a8a91..d54d0bf83 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt @@ -40,7 +40,8 @@ interface CoverStorage { companion object { suspend fun at(dir: File): CoverStorage { withContext(Dispatchers.IO) { - if (dir.exists()) check(dir.isDirectory) { "Not a directory" } else check(dir.mkdirs()) { "Cannot create directory" } + if (dir.exists()) check(dir.isDirectory) { "Not a directory" } + else check(dir.mkdirs()) { "Cannot create directory" } } return CoverStorageImpl(dir) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt index 368d4202d..e36f85b05 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt @@ -49,7 +49,10 @@ class MutableStoredCovers( is CoverResult.Hit -> cover.cover is CoverResult.Miss -> return CoverResult.Miss() } - val coverFile = coverStorage.write(cover.id + transcoding.tag) { transcoding.transcodeInto(cover.data(), it) } + val coverFile = + coverStorage.write(cover.id + transcoding.tag) { + transcoding.transcodeInto(cover.data(), it) + } return CoverResult.Hit(coverFile) } From c2dcbd61f83ef5283609e98afbef1c4dc4dc3836 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 22:25:44 -0600 Subject: [PATCH 22/49] musikr: steamline loading pipeline My hope is that overall this is more efficient and also easier to under stand long-term. --- .../oxycblt/musikr/pipeline/ExploreStep.kt | 69 +++++++++------ .../oxycblt/musikr/pipeline/ExtractStep.kt | 85 ++++++++++++++----- .../org/oxycblt/musikr/pipeline/FlowUtil.kt | 24 ++++-- 3 files changed, 123 insertions(+), 55 deletions(-) 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 12762e22a..d4c830f6a 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -21,12 +21,12 @@ 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.asFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -35,6 +35,7 @@ import org.oxycblt.musikr.Interpretation 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.covers.Cover import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.Covers @@ -71,38 +72,54 @@ private class ExploreStepImpl( locations.asFlow(), ) .filter { 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) + .distributedMap(n = 8, on = Dispatchers.IO, buffer = Channel.UNLIMITED) { file -> + when (val cacheResult = cache.read(file)) { + is CacheResult.Hit -> NeedsCover(cacheResult.song) + is CacheResult.Stale -> + Finalized(NewSong(cacheResult.file, cacheResult.addedMs)) + is CacheResult.Miss -> Finalized(NewSong(cacheResult.file, addingMs)) + } + } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + .distributedMap(n = 8, on = Dispatchers.IO, buffer = Channel.UNLIMITED) { + when (it) { + is Finalized -> it + is NeedsCover -> { + when (val coverResult = it.song.coverId?.let { covers.obtain(it) }) { + is CoverResult.Hit -> + Finalized( + RawSong( + it.song.file, + it.song.properties, + it.song.tags, + coverResult.cover, + it.song.addedMs)) + null -> + Finalized( + RawSong( + it.song.file, + it.song.properties, + it.song.tags, + null, + it.song.addedMs)) + else -> Finalized(NewSong(it.song.file, it.song.addedMs)) } } - RawSong( - cachedSong.file, - cachedSong.properties, - cachedSong.tags, - cover, - cachedSong.addedMs) + } } - .flattenMerge() + .map { it.explored } .flowOn(Dispatchers.IO) - .buffer(), + .buffer(Channel.UNLIMITED), flow { emitAll(storedPlaylists.read().asFlow()) } .map { RawPlaylist(it) } .flowOn(Dispatchers.IO) .buffer()) } + + private sealed interface InternalExploreItem + + private data class NeedsCover(val song: CachedSong) : InternalExploreItem + + private data class Finalized(val explored: Explored) : InternalExploreItem } 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 c6d347577..d6a2cb214 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -19,9 +19,12 @@ package org.oxycblt.musikr.pipeline import android.content.Context -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flattenMerge +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import org.oxycblt.musikr.Storage import org.oxycblt.musikr.cache.CachedSong @@ -29,6 +32,7 @@ import org.oxycblt.musikr.cache.MutableCache import org.oxycblt.musikr.covers.Cover import org.oxycblt.musikr.covers.CoverResult import org.oxycblt.musikr.covers.MutableCovers +import org.oxycblt.musikr.metadata.Metadata import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.tag.parse.TagParser @@ -48,34 +52,71 @@ private class ExtractStepImpl( private val cache: MutableCache, private val covers: MutableCovers ) : ExtractStep { - @OptIn(ExperimentalCoroutinesApi::class) override fun extract(nodes: Flow): Flow { val exclude = mutableListOf() return nodes - .distribute(8) - .distributedMap { + // Cover art is huge, so we have to kneecap the concurrency here to avoid excessive + // GCs. We still reap the concurrency benefits here, just not as much as we could. + .distributedMap(on = Dispatchers.IO, n = 8, buffer = Channel.RENDEZVOUS) { when (it) { - is RawSong -> it - is RawPlaylist -> it + is RawSong -> Finalized(it) + is RawPlaylist -> Finalized(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 - } - 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 metadata = metadataExtractor.extract(it.file) + if (metadata != null) NeedsParsing(it, metadata) else Finalized(InvalidSong) } } } - .flattenMerge() + .flowOn(Dispatchers.IO) + .buffer(Channel.RENDEZVOUS) + .distributedMap(on = Dispatchers.IO, n = 8, buffer = Channel.UNLIMITED) { + when (it) { + is Finalized -> it + is NeedsParsing -> { + val tags = tagParser.parse(it.metadata) + val cover = + when (val result = covers.create(it.song.file, it.metadata)) { + is CoverResult.Hit -> result.cover + else -> null + } + NeedsCaching( + RawSong( + it.song.file, it.metadata.properties, tags, cover, it.song.addedMs)) + } + } + } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) + .distributedMap(on = Dispatchers.IO, n = 8, buffer = Channel.UNLIMITED) { + when (it) { + is Finalized -> it + is NeedsCaching -> { + val cachedSong = + CachedSong( + it.song.file, + it.song.properties, + it.song.tags, + it.song.cover?.id, + it.song.addedMs) + cache.write(cachedSong) + exclude.add(cachedSong) + Finalized(it.song) + } + } + } + .map { it.extracted } + .flowOn(Dispatchers.IO) + .buffer(Channel.UNLIMITED) .onCompletion { cache.cleanup(exclude) } } + + private sealed interface ParsedExtractItem + + private data class NeedsParsing(val song: NewSong, val metadata: Metadata) : ParsedExtractItem + + private sealed interface ParsedCachingItem + + private data class NeedsCaching(val song: RawSong) : ParsedCachingItem + + private data class Finalized(val extracted: Extracted) : ParsedExtractItem, ParsedCachingItem } 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 29eef2735..4ca845246 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/FlowUtil.kt @@ -18,10 +18,16 @@ package org.oxycblt.musikr.pipeline +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.withIndex @@ -32,7 +38,13 @@ import kotlinx.coroutines.flow.withIndex * Note that this function requires the "manager" flow to be consumed alongside the split flows in * order to function. Without this, all of the newly split flows will simply block. */ -internal fun Flow.distribute(n: Int): Flow> { +@OptIn(ExperimentalCoroutinesApi::class) +internal fun Flow.distributedMap( + n: Int, + on: CoroutineContext = Dispatchers.Main, + buffer: Int = Channel.UNLIMITED, + block: suspend (T) -> R, +): Flow { val posChannels = List(n) { Channel(Channel.UNLIMITED) } val managerFlow = flow { @@ -44,14 +56,12 @@ internal fun Flow.distribute(n: Int): Flow> { channel.close() } } - return (posChannels.map { it.receiveAsFlow() } + managerFlow).asFlow() + return (posChannels.map { it.receiveAsFlow() } + managerFlow) + .asFlow() + .map { it.tryMap(block).flowOn(on).buffer(buffer) } + .flattenMerge() } -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 { From 8023d2c037c8925453b094884b3abf23359c6e15 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 22:27:17 -0600 Subject: [PATCH 23/49] musikr: remove useless optin --- musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt | 1 - 1 file changed, 1 deletion(-) 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 d4c830f6a..4300e7052 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -63,7 +63,6 @@ private class ExploreStepImpl( private val covers: Covers, private val storedPlaylists: StoredPlaylists ) : ExploreStep { - @OptIn(ExperimentalCoroutinesApi::class) override fun explore(locations: List): Flow { val addingMs = System.currentTimeMillis() return merge( From daf16874262ba9522598d2d87619fb3953921d31 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 15 Mar 2025 22:30:14 -0600 Subject: [PATCH 24/49] musikr: reformat --- musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt | 1 - 1 file changed, 1 deletion(-) 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 4300e7052..8f9f4deb7 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -20,7 +20,6 @@ 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.asFlow From 73b2b921809d3b4538b42f2baaba4cc36b4ddf57 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 17 Mar 2025 13:11:39 +0100 Subject: [PATCH 25/49] Translations update from Hosted Weblate (#1034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/ * Translated using Weblate (Czech) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/cs/ * Translated using Weblate (Italian) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/it/ * Translated using Weblate (Spanish) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/es/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/uk/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/zh_Hans/ * Translated using Weblate (Estonian) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/et/ * Translated using Weblate (German) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/de/ * Translated using Weblate (Lithuanian) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/lt/ * Translated using Weblate (Lithuanian) Currently translated at 100.0% (57 of 57 strings) Translation: Auxio/Metadata Translate-URL: https://hosted.weblate.org/projects/auxio/metadata/lt/ * Translated using Weblate (Spanish) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (316 of 316 strings) Translation: Auxio/Strings Translate-URL: https://hosted.weblate.org/projects/auxio/strings/es/ --------- Co-authored-by: Fjuro Co-authored-by: lorenzoch02 Co-authored-by: Eskuero <3skuero@gmail.com> Co-authored-by: Максим Горпиніч Co-authored-by: 大王叫我来巡山 Co-authored-by: Priit Jõerüüt Co-authored-by: qwerty287 Co-authored-by: Vaclovas Intas Co-authored-by: Adolfo Jayme Barrientos --- app/src/main/res/values-ar/strings.xml | 2 - app/src/main/res/values-cs/strings.xml | 5 +- app/src/main/res/values-de/strings.xml | 5 +- app/src/main/res/values-es/strings.xml | 79 ++++++++++--------- app/src/main/res/values-et/strings.xml | 5 +- app/src/main/res/values-fi/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 7 +- app/src/main/res/values-lt/strings.xml | 5 +- app/src/main/res/values-uk/strings.xml | 4 +- app/src/main/res/values-zh-rCN/strings.xml | 5 +- .../metadata/android/lt/full_description.txt | 7 +- 11 files changed, 67 insertions(+), 59 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 290891ca8..9d6db7b73 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -334,6 +334,4 @@ MPEG-4 تحتوي على %s Apple Lossless Audio Codec (ALAC) - تجاهل الملفات المخفية - تخطي الملفات والمجلدات المخفية (على سبيل المثال: الذاكرة المخبّأة) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a1c0dc2a7..5fdaef113 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -341,6 +341,7 @@ Vaši umělci se zobrazí zde. Vaše seznamy skladeb se zobrazí zde. Vaše žánry se zobrazí zde. - Přeskočit skryté soubory a složky (např. .cache) - Ignorovat skryté soubory + Zahrnout skryté soubory + Zahrnout skryté zvukové soubory (např. .cache) + Původní diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 521ba22d8..e210168ec 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -332,6 +332,7 @@ Deine Lieder werden hier angezeigt. Deine Genres werden hier angezeigt. Deine Künstler werden hier angezeigt. - Versteckte Dateien ignorieren - Überspringt versteckte Dateien und Ordner (z. B. .cache) + Unverändert + Audio-Dateien einbeziehen, die versteckt sind (z. B. .cache) + Versteckte Dateien einbeziehen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 99573f8c6..fb1fd34e4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -22,17 +22,17 @@ Ascendente En reproducción Reproducir - Mezcla + Modo aleatorio Reproducir todo Reproducir por álbum Reproducir por artista Cola Reproducir siguiente - Agregar a la cola - Agregado a la cola + Añadir a la cola + Añadido a la cola Ir al artista Ir al álbum - Agregar + Añadir Guardar Sin carpetas Acerca de @@ -41,19 +41,19 @@ Licencias Desarrollado por Alexander Capehart - Ajustes - Aspecto y Comportamiento + Configuración + Aspecto y comportamiento Tema Automático Claro Oscuro - Esquema de color + Combinación de colores Tema negro Usar un tema completamente negro Pantalla Pestañas de la biblioteca Cambiar visibilidad y orden de las pestañas de la biblioteca - Carátulas redondeadas + Modo redondeado Habilitar las esquinas redondeadas en los elementos adicionales de la interfaz del usuario (requiere que las portadas de los álbumes estén redondeadas) Usar acciones de notificación personalizadas Sonido @@ -77,9 +77,9 @@ Falló la carga de música Auxio necesita permiso para leer su biblioteca de música No se encontró ninguna aplicación que pueda manejar esta tarea - Directorio no soportado + No se admite esta carpeta - Buscar en la biblioteca… + Buscar en la fonoteca… Pista %d Reproducir o pausar @@ -142,13 +142,13 @@ Ver propiedades Mezclar Mezclar todo - De acuerdo + Aceptar Estadísticas de la biblioteca Ajuste sin etiquetas Advertencia: Cambiar el pre-amp a un valor alto puede resultar en picos en algunas pistas de audio. Reproducir desde el elemento que se muestra - Audio matroska - Free Losless Audio Codec (FLAC) + Audio Matroska + Códec de audio sin pérdida libre (FLAC) Advanced Audio Coding (AAC) Cargando tu biblioteca de música… (%1$d/%2$d) Artistas cargados: %d @@ -157,15 +157,15 @@ Número de canciones Recarga automática Recargar la biblioteca musical cada vez que cambie (requiere una notificación persistente) - Cargando tu librería de música… + Cargando la fonoteca… Cargando música - Monitorizando la librería de música - Monitorizando cambios en tu librería de música… - Audio ogg + Monitorizando la fonoteca + Monitorizando cambios en la fonoteca… + Audio Ogg Cuando se reproduce desde los detalles - Fecha de añadido + Fecha de adición Propiedades de la canción - Frecuencia de muestreo + Tasa de muestreo Cancelar Reproducción automática con auriculares Carpetas de música @@ -176,9 +176,9 @@ EP Sencillos Sencillo - Pista de audio - Álbum remix - Pistas de audio + Banda sonora + Álbum de remezclas + Bandas sonoras Mixtapes (recopilación de canciones) Mixtape (recopilación de canciones) Remezclas @@ -191,7 +191,7 @@ Álbumes cargados: %d Duración total: %s Álbum en directo - Single en directo + Sencillo en directo Compilación En directo Audio MPEG-1 @@ -199,9 +199,9 @@ %d kbps -%.1f dB EP en directo - Single remix + Sencillo remezclado Compilaciones - EP de remixes + EP de remezclas Abrir la cola Género Separadores de varios valores @@ -216,7 +216,7 @@ Mezcla de DJ Ecualizador Portadas de álbumes - Apagado + Desactivadas Modo de repetición Más (+) Signo et @@ -260,17 +260,17 @@ Ignorar palabras como \"the\" al ordenar por nombre (funciona mejor con música en inglés) Nueva lista de reproducción Lista de reproducción %d - Agregar a la lista de reproducción + Añadir a la lista de reproducción Agregado a la lista de reproducción Lista de reproducción creada No hay canciones - Borrar - Cambiar el nombre - Cambiar el nombre de la lista de reproducción + Eliminar + Cambiar nombre + Cambiar nombre de lista de reproducción Lista de reproducción renombrada - Lista de reproducción borrada + Lista de reproducción eliminada ¿Borrar %s\? Esto no se puede deshacer. - ¿Borrar la lista de reproducción\? + ¿Quiere eliminar la lista de reproducción? Editar Editando %s Aparece en @@ -328,12 +328,15 @@ Álbum desconocido Desconocido El MPEG-4 contiene %s - Apple Lossless Audio Codec (ALAC) - Tus álbumes aparecerán aquí. - Tus artistas aparecerán aquí. - Tus géneros aparecerán aquí. + Códec de audio sin pérdida de Apple (ALAC) + Sus álbumes aparecerán aquí. + Sus artistas aparecerán aquí. + Sus géneros aparecerán aquí. Nueva carpeta - Tus listas de reproducción aparecerán aquí. - Tus canciones aparecerán aquí. + Sus listas de reproducción aparecerán aquí. + Sus canciones aparecerán aquí. Ahorra espacio + Original + Incluir ficheros de audio ocultos (ej. .cache) + Incluir ficheros ocultos diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 02a0da0ae..a981480f8 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -324,6 +324,7 @@ Sinu žanrid saavad olema nähtavad siin. Sinu albumid saavad olema nähtavad siin. Sinu esitusloendid saavad olema nähtavad siin. - Eira peidetud faile - Jäta vahele peidetud failid ja kaustad (nt. .cache) + Kaasa peidetud failid + Kaasa peidetud helifailid (nt. .cache) + Sellisena, nagu ta on diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 49e81dacc..0eba9a21f 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -322,6 +322,4 @@ Tyylilajisi tulevat näkymään tässä. Apple Lossless Audio Codec (ALAC) Säätä tilaa - Ohita piilotetut tiedostot - Ohita piilotetut tiedostot ja kansiot (esim. .cache) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7d2fe3d74..85ceeb9a5 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -42,7 +42,7 @@ Alexander Capehart Statistiche della libreria - Opzioni + Impostazioni Aspetto Tema Sistema @@ -330,8 +330,11 @@ Le tue playlist appariranno qui. Espandi Nuova cartella - Libera spazio + Bassa qualità MPEG-4 contenente %s Apple Lossless Audio Codec (ALAC) Sconosciuto + Qualità originale + Include file audio nascosti (per esempio in .cache) + Includi file nascosti diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index c7e5d0f38..f8819ce54 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -330,4 +330,7 @@ „Apple“ be nuostolių garso kodekas (ALAC) Nežinomas Išsaugoti vietos - \ No newline at end of file + Kaip yra + Įtraukti paslėptus failus + Įtraukite garso failus, kurie paslėpti (pvz., .cache) + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ab74ded49..042163509 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -336,7 +336,7 @@ Тут відображатимуться ваші списки відтворення. Тут відображатимуться ваші жанри. Тут з’являться ваші виконавці. - Ігнорувати приховані файли - Пропускати приховані файли та папки (наприклад, .cache) Як є + Включати приховані файли + Включати приховані аудіофайли (наприклад, .cache) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 052bc3413..a0e66b11f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -330,6 +330,7 @@ 流派会出现在此处。 专辑会出现在此处。 播放列表会出现在此处。 - 跳过隐藏的文件和文件夹(排除 .cache) - 忽略隐藏的文件 + 保持原样 + 包括隐藏的文件 + 包括隐藏的音频文件(排除 .cache) diff --git a/fastlane/metadata/android/lt/full_description.txt b/fastlane/metadata/android/lt/full_description.txt index 0cb30af0c..c75d9166b 100644 --- a/fastlane/metadata/android/lt/full_description.txt +++ b/fastlane/metadata/android/lt/full_description.txt @@ -1,4 +1,4 @@ -„Auxio“ – tai vietinis muzikos leistuvė su greita, patikima naudotojo sąsaja ir potyris be daugybės nenaudingų funkcijų, esančių kituose muzikos leistuvėse. Sukurta remiantis iš šiuolaikinių medijos įrašo perklausos bibliotekų, „Auxio“ turi geresnį bibliotekos palaikymą ir klausymo kokybę, palyginti su kitomis programeles, kurios naudoja pasenusias „Android“ funkcijas. Trumpai tariant, jame leidžiama muziką. +„Auxio“ – tai vietinė muzikos leistuvė su sparčia, patikima NS / NP be daugybės nenaudingų funkcijų, esančių kituose muzikos leistuvėse. Sukurta remiantis iš šiuolaikinių medijos įrašo perklausos bibliotekų, „Auxio“ turi geresnį bibliotekos palaikymą ir klausymo kokybę, palyginti su kitomis programeles, kurios naudoja pasenusias „Android“ funkcijas. Trumpai tariant, jame leidžiama muziką. Funkcijos @@ -6,8 +6,7 @@ - Sparti naudotojo sąsaja, sukurta pagal naujausias „Material Design“ gaires - Nuomonę turintis naudotojo potyris, kuriame prioritetas teikiamas naudojimo paprastumui, o ne kraštutiniam atvejui - Pasirinktas elgesys -- Palaikomas diskų numerių, kelių atlikėjų, leidinių tipų palaikymas, -tikslias / originalias datas, rūšiavimo žymes ir dar daugiau +- Palaikomas diskų numerių, kelių atlikėjų, leidinių tipų palaikymas, tikslias / originalias datas, rūšiavimo žymes ir dar daugiau - Išplėstinė atlikėjų sistema, kuri suvienija atlikėjus ir albumų atlikėjus - SD kortelių aplankų valdymas - Patikima grojaraščių sudarymo funkcija @@ -22,4 +21,4 @@ tikslias / originalias datas, rūšiavimo žymes ir dar daugiau - Automatinis ausinių leidimas - Stilingi valdikliai, kurie automatiškai prisitaiko prie savo dydžio - Visiškai privati ir neprisijungę -- Jokių suapvalintų albumų viršelių (pagal numatytuosius nustatymus) +- Jokių suapvalintų albumų viršelių (jei jų norite) From 63227a1f1fce2523d811fb2ed9e930f8b570ab2a Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 17 Mar 2025 06:37:36 -0600 Subject: [PATCH 26/49] musikr: fix incorrect cache cleanups --- .../org/oxycblt/musikr/pipeline/ExtractStep.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 d6a2cb214..8ca2fa5ad 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -99,12 +99,22 @@ private class ExtractStepImpl( it.song.cover?.id, it.song.addedMs) cache.write(cachedSong) - exclude.add(cachedSong) Finalized(it.song) } } } - .map { it.extracted } + .map { + if (it.extracted is RawSong) { + exclude.add( + CachedSong( + it.extracted.file, + it.extracted.properties, + it.extracted.tags, + it.extracted.cover?.id, + it.extracted.addedMs)) + } + it.extracted + } .flowOn(Dispatchers.IO) .buffer(Channel.UNLIMITED) .onCompletion { cache.cleanup(exclude) } From 90282f0f746f14461bfa3aa0f3a016cd7e4ee355 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 17 Mar 2025 06:49:56 -0600 Subject: [PATCH 27/49] musikr: clean up data translation --- .../oxycblt/musikr/pipeline/ExploreStep.kt | 28 ++++++--------- .../oxycblt/musikr/pipeline/ExtractStep.kt | 34 ++++++++----------- 2 files changed, 24 insertions(+), 38 deletions(-) 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 8f9f4deb7..0bede6678 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -84,24 +84,13 @@ private class ExploreStepImpl( when (it) { is Finalized -> it is NeedsCover -> { - when (val coverResult = it.song.coverId?.let { covers.obtain(it) }) { + when (val coverResult = + it.cachedSong.coverId?.let { id -> covers.obtain(id) }) { is CoverResult.Hit -> - Finalized( - RawSong( - it.song.file, - it.song.properties, - it.song.tags, - coverResult.cover, - it.song.addedMs)) - null -> - Finalized( - RawSong( - it.song.file, - it.song.properties, - it.song.tags, - null, - it.song.addedMs)) - else -> Finalized(NewSong(it.song.file, it.song.addedMs)) + Finalized(it.cachedSong.toRawSong(coverResult.cover)) + null -> Finalized(it.cachedSong.toRawSong(null)) + else -> + Finalized(NewSong(it.cachedSong.file, it.cachedSong.addedMs)) } } } @@ -117,7 +106,10 @@ private class ExploreStepImpl( private sealed interface InternalExploreItem - private data class NeedsCover(val song: CachedSong) : InternalExploreItem + private data class NeedsCover(val cachedSong: CachedSong) : InternalExploreItem private data class Finalized(val explored: Explored) : InternalExploreItem + + private fun CachedSong.toRawSong(cover: Cover?) = + RawSong(file, properties, tags, cover, addedMs) } 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 8ca2fa5ad..12e17e03e 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -75,13 +75,17 @@ private class ExtractStepImpl( is NeedsParsing -> { val tags = tagParser.parse(it.metadata) val cover = - when (val result = covers.create(it.song.file, it.metadata)) { + when (val result = covers.create(it.newSong.file, it.metadata)) { is CoverResult.Hit -> result.cover else -> null } NeedsCaching( RawSong( - it.song.file, it.metadata.properties, tags, cover, it.song.addedMs)) + it.newSong.file, + it.metadata.properties, + tags, + cover, + it.newSong.addedMs)) } } } @@ -91,27 +95,14 @@ private class ExtractStepImpl( when (it) { is Finalized -> it is NeedsCaching -> { - val cachedSong = - CachedSong( - it.song.file, - it.song.properties, - it.song.tags, - it.song.cover?.id, - it.song.addedMs) - cache.write(cachedSong) - Finalized(it.song) + cache.write(it.rawSong.toCachedSong()) + Finalized(it.rawSong) } } } .map { if (it.extracted is RawSong) { - exclude.add( - CachedSong( - it.extracted.file, - it.extracted.properties, - it.extracted.tags, - it.extracted.cover?.id, - it.extracted.addedMs)) + exclude.add(it.extracted.toCachedSong()) } it.extracted } @@ -122,11 +113,14 @@ private class ExtractStepImpl( private sealed interface ParsedExtractItem - private data class NeedsParsing(val song: NewSong, val metadata: Metadata) : ParsedExtractItem + private data class NeedsParsing(val newSong: NewSong, val metadata: Metadata) : + ParsedExtractItem private sealed interface ParsedCachingItem - private data class NeedsCaching(val song: RawSong) : ParsedCachingItem + private data class NeedsCaching(val rawSong: RawSong) : ParsedCachingItem private data class Finalized(val extracted: Extracted) : ParsedExtractItem, ParsedCachingItem + + private fun RawSong.toCachedSong() = CachedSong(file, properties, tags, cover?.id, addedMs) } From 343856ac690dd0092b624e0754f58b247d6a28e5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 17 Mar 2025 07:39:39 -0600 Subject: [PATCH 28/49] musikr: bump cache db version --- .../src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 9475556c0..ed3f7c891 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/db/CacheDatabase.kt @@ -36,7 +36,7 @@ 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) +@Database(entities = [CachedSongData::class], version = 62, exportSchema = false) internal abstract class CacheDatabase : RoomDatabase() { abstract fun readDao(): CacheReadDao From aac6d8ef4dc9d72137cd8f97e339d53f86f52ec1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 17 Mar 2025 08:12:39 -0600 Subject: [PATCH 29/49] app: cleanup --- app/build.gradle | 4 +- .../BackportBottomSheetBehavior.java | 1 - .../java/org/oxycblt/auxio/MainFragment.kt | 3 - .../decision/DetailDecisionViewModel.kt | 2 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 10 --- .../org/oxycblt/auxio/image/CoverProvider.kt | 2 +- .../java/org/oxycblt/auxio/image/CoverView.kt | 3 +- .../image/coil/CoverCollectionFetcher.kt | 5 +- .../oxycblt/auxio/image/coil/CoverFetcher.kt | 67 +---------------- .../image/coil/RoundedRectTransformation.kt | 4 +- .../image/coil/SquareCropTransformation.kt | 3 +- .../list/recycler/FastScrollRecyclerView.kt | 5 +- .../oxycblt/auxio/music/MusicRepository.kt | 10 +-- .../java/org/oxycblt/auxio/music/MusicType.kt | 5 -- .../music/locations/MusicSourcesDialog.kt | 3 +- .../locations/NewLocationFooterAdapter.kt | 2 +- .../oxycblt/auxio/playback/PlaybackUtil.kt | 7 -- .../playback/service/BetterShuffleOrder.kt | 3 + .../service/ExoPlaybackStateHolder.kt | 3 + .../auxio/playback/service/SystemModule.kt | 3 + .../oxycblt/auxio/settings/AboutFragment.kt | 4 +- .../java/org/oxycblt/auxio/ui/Animations.kt | 26 +++---- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 8 +- .../java/org/oxycblt/auxio/util/StateUtil.kt | 75 ------------------- .../widgets/WidgetBitmapTransformation.kt | 3 +- .../org/oxycblt/auxio/widgets/WidgetUtil.kt | 6 -- .../res/layout-h360dp/fragment_detail.xml | 3 +- .../res/layout-h480dp/fragment_detail.xml | 3 +- .../res/layout-sw600dp/fragment_detail.xml | 3 +- .../res/layout-w600dp/fragment_detail.xml | 3 +- .../main/res/layout/fragment_home_list.xml | 1 + app/src/main/res/layout/view_scroll_thumb.xml | 3 +- app/src/main/res/layout/widget_wafer_thin.xml | 2 +- app/src/main/res/layout/widget_wafer_wide.xml | 3 +- app/src/main/res/values-sq/strings.xml | 2 +- app/src/main/res/values/themes_black.xml | 7 +- .../oxycblt/musikr/cache/db/CacheDatabase.kt | 3 +- .../main/java/org/oxycblt/musikr/tag/Name.kt | 4 + 38 files changed, 76 insertions(+), 228 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d444f70c9..458c17774 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId namespace - versionName "4.0.2" - versionCode 61 + versionName "4.0.3" + versionCode 62 minSdk min_sdk targetSdk target_sdk diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java index 50ef29e94..c8c5ead47 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java @@ -1309,7 +1309,6 @@ public class BackportBottomSheetBehavior extends CoordinatorLayo + " should not be set externally."); } if (!hideable && state == STATE_HIDDEN) { - Log.w(TAG, "Cannot set state: " + state); return; } final int finalState; diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 911cd4db2..10342f1d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio -import android.animation.ValueAnimator import android.os.Bundle import android.view.LayoutInflater import android.view.ViewTreeObserver @@ -514,8 +513,6 @@ class MainFragment : } } - private var scrimAnimator: ValueAnimator? = null - private fun updateSpeedDial(open: Boolean) { requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" } .invalidateEnabled(open) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt index d57d18042..b3286983c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt @@ -98,7 +98,7 @@ sealed interface ArtistShowChoices { val uid: Music.UID /** The current [Artist] choices. */ val choices: List - /** Sanitize this instance with a [DeviceLibrary]. */ + /** Sanitize this instance with a [Library]. */ fun sanitize(newLibrary: Library): ArtistShowChoices? /** Backing implementation of [ArtistShowChoices] that is based on a [Song]. */ diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index a2a2495b8..adc2ea512 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -37,12 +37,10 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import java.lang.reflect.Field -import java.lang.reflect.Method import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeBinding @@ -68,7 +66,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.lazyReflectedField -import org.oxycblt.auxio.util.lazyReflectedMethod import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.showToast import org.oxycblt.musikr.IndexingProgress @@ -94,7 +91,6 @@ class HomeFragment : private var storagePermissionLauncher: ActivityResultLauncher? = null private var getContentLauncher: ActivityResultLauncher? = null private var pendingImportTarget: Playlist? = null - private var lastUpdateTime = -1L override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -512,11 +508,5 @@ class HomeFragment : private companion object { val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView") val RV_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop") - val FAB_HIDE_FROM_USER_FIELD: Method by - lazyReflectedMethod( - FloatingActionButton::class, - "hide", - FloatingActionButton.OnVisibilityChangedListener::class, - Boolean::class) } } 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 160267804..286d820a7 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverProvider.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.image.covers.SettingCovers import org.oxycblt.musikr.covers.CoverResult -class CoverProvider() : ContentProvider() { +class CoverProvider : ContentProvider() { override fun onCreate(): Boolean = true override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { 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 bab90cbe6..53362e0c1 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -37,6 +37,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.Px import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.children +import androidx.core.view.isEmpty import androidx.core.view.updateMarginsRelative import androidx.core.widget.ImageViewCompat import coil3.ImageLoader @@ -172,7 +173,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr super.onFinishInflate() // The image isn't added if other children have populated the body. This is by design. - if (childCount == 0) { + if (isEmpty()) { addView(image) } 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 a6bc9475d..286e59a93 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 @@ -19,9 +19,9 @@ package org.oxycblt.auxio.image.coil import android.content.Context -import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas +import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.toDrawable import coil3.ImageLoader import coil3.asImage @@ -90,8 +90,7 @@ private constructor( val mosaicFrameSize = Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)) - val mosaicBitmap = - Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888) + val mosaicBitmap = createBitmap(mosaicSize.width, mosaicSize.height) val canvas = Canvas(mosaicBitmap) var x = 0 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 4c0ed0c5c..bc6284ee5 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 @@ -18,32 +18,20 @@ package org.oxycblt.auxio.image.coil -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import androidx.core.graphics.drawable.toDrawable import coil3.ImageLoader -import coil3.asImage import coil3.decode.DataSource import coil3.decode.ImageSource import coil3.fetch.FetchResult import coil3.fetch.Fetcher -import coil3.fetch.ImageFetchResult import coil3.fetch.SourceFetchResult import coil3.request.Options -import coil3.size.Dimension -import coil3.size.Size -import coil3.size.pxOrElse -import java.io.InputStream import javax.inject.Inject import okio.FileSystem import okio.buffer import okio.source import org.oxycblt.musikr.covers.Cover -class CoverFetcher private constructor(private val context: Context, private val cover: Cover) : - Fetcher { +class CoverFetcher private constructor(private val cover: Cover) : Fetcher { override suspend fun fetch(): FetchResult? { val stream = cover.open() ?: return null return SourceFetchResult( @@ -52,59 +40,8 @@ class CoverFetcher private constructor(private val context: Context, private val dataSource = DataSource.DISK) } - /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ - private suspend fun createMosaic(streams: List, size: Size): FetchResult { - // Use whatever size coil gives us to create the mosaic. - val mosaicSize = android.util.Size(size.width.mosaicSize(), size.height.mosaicSize()) - val mosaicFrameSize = - Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)) - - val mosaicBitmap = - Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(mosaicBitmap) - - var x = 0 - var y = 0 - - // For each stream, create a bitmap scaled to 1/4th of the mosaics combined size - // and place it on a corner of the canvas. - for (stream in streams) { - if (y == mosaicSize.height) { - break - } - - // Crop the bitmap down to a square so it leaves no empty space - // TODO: Work around this - val bitmap = - SquareCropTransformation.INSTANCE.transform( - BitmapFactory.decodeStream(stream), mosaicFrameSize) - canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) - - x += bitmap.width - if (x == mosaicSize.width) { - x = 0 - y += bitmap.height - } - } - - // It's way easier to map this into a drawable then try to serialize it into an - // BufferedSource. Just make sure we mark it as "sampled" so Coil doesn't try to - // load low-res mosaics into high-res ImageViews. - return ImageFetchResult( - image = mosaicBitmap.toDrawable(context.resources).asImage(), - isSampled = true, - dataSource = DataSource.DISK) - } - - private fun Dimension.mosaicSize(): Int { - // Since we want the mosaic to be perfectly divisible into two, we need to round any - // odd image sizes upwards to prevent the mosaic creation from failing. - val size = pxOrElse { 512 } - return if (size.mod(2) > 0) size + 1 else size - } - class Factory @Inject constructor() : Fetcher.Factory { override fun create(data: Cover, options: Options, imageLoader: ImageLoader) = - CoverFetcher(options.context, data) + CoverFetcher(data) } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/RoundedRectTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/RoundedRectTransformation.kt index 214797ed3..6220f3bd7 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/coil/RoundedRectTransformation.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/coil/RoundedRectTransformation.kt @@ -38,8 +38,8 @@ import coil3.transform.Transformation import kotlin.math.roundToInt /** - * A vendoring of [coil.transform.RoundedCornersTransformation] that can handle non-1:1 aspect ratio - * images without cropping them. + * A vendoring of coil's RoundedCornersTransformation that can handle non-1:1 aspect ratio images + * without cropping them. * * @author Coil Team, Alexander Capehart (OxygenCobalt) */ diff --git a/app/src/main/java/org/oxycblt/auxio/image/coil/SquareCropTransformation.kt b/app/src/main/java/org/oxycblt/auxio/image/coil/SquareCropTransformation.kt index a7c780c92..cb06caffa 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/coil/SquareCropTransformation.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/coil/SquareCropTransformation.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.image.coil import android.graphics.Bitmap +import androidx.core.graphics.scale import coil3.size.Size import coil3.size.pxOrElse import coil3.transform.Transformation @@ -46,7 +47,7 @@ class SquareCropTransformation : Transformation() { val desiredHeight = size.height.pxOrElse { dstSize } if (dstSize != desiredWidth || dstSize != desiredHeight) { // Image is not the desired size, upscale it. - return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true) + return dst.scale(desiredWidth, desiredHeight) } return dst } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index 4fe661b6d..04014c18b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -34,6 +34,7 @@ import android.view.ViewGroup import android.view.WindowInsets import android.widget.FrameLayout import androidx.annotation.AttrRes +import androidx.core.view.isEmpty import androidx.core.view.isInvisible import androidx.core.view.updatePaddingRelative import androidx.core.widget.TextViewCompat @@ -91,7 +92,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private var thumbAnimator: Animator? = null private val thumbView = - context.inflater.inflate(R.layout.view_scroll_thumb, null).apply { + context.inflater.inflate(R.layout.view_scroll_thumb, this).apply { thumbSlider.jumpOut(this) } private val thumbPadding = Rect(0, 0, 0, 0) @@ -339,7 +340,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // [proportion of scroll position to scroll range] * [total thumb range] // This is somewhat adapted from the androidx RecyclerView FastScroller implementation. val offsetY = computeVerticalScrollOffset() - if (computeVerticalScrollRange() < height || childCount == 0) { + if (computeVerticalScrollRange() < height || isEmpty()) { fastScrollingPossible = false hideThumb() hidePopup() 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 f1e1688d0..6b478c520 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -188,8 +188,8 @@ interface MusicRepository { /** * Flags indicating which kinds of music information changed. * - * @param deviceLibrary Whether the current [DeviceLibrary] has changed. - * @param library Whether the current [Playlist]s have changed. + * @param deviceLibrary Whether the current songs/albums/artists/genres has changed. + * @param userLibrary Whether the current playlists have changed. */ data class Changes(val deviceLibrary: Boolean, val userLibrary: Boolean) @@ -244,7 +244,7 @@ constructor( ) : MusicRepository { private val updateListeners = mutableListOf() private val indexingListeners = mutableListOf() - @Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null + @Volatile private var indexingWorker: IndexingWorker? = null @Volatile override var library: MutableLibrary? = null @Volatile private var previousCompletedState: IndexingState.Completed? = null @@ -283,7 +283,7 @@ constructor( } @Synchronized - override fun registerWorker(worker: MusicRepository.IndexingWorker) { + override fun registerWorker(worker: IndexingWorker) { if (indexingWorker != null) { L.w("Worker is already registered") return @@ -293,7 +293,7 @@ constructor( } @Synchronized - override fun unregisterWorker(worker: MusicRepository.IndexingWorker) { + override fun unregisterWorker(worker: IndexingWorker) { if (indexingWorker !== worker) { L.w("Given worker did not match current worker") return diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicType.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicType.kt index 572280f8e..516dd1aa9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicType.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicType.kt @@ -27,15 +27,10 @@ import org.oxycblt.auxio.R * @author Alexander Capehart (OxygenCobalt) */ enum class MusicType { - /** @see Song */ SONGS, - /** @see Album */ ALBUMS, - /** @see Artist */ ARTISTS, - /** @see Genre */ GENRES, - /** @see Playlist */ PLAYLISTS; /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt index 3ca75b8f6..88a4132d5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/locations/MusicSourcesDialog.kt @@ -25,6 +25,7 @@ import android.view.LayoutInflater import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri import androidx.recyclerview.widget.ConcatAdapter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -80,7 +81,7 @@ class MusicSourcesDialog : val locations = savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.mapNotNull { - MusicLocation.existing(requireContext(), Uri.parse(it)) + MusicLocation.existing(requireContext(), it.toUri()) } ?: musicSettings.musicLocations locationAdapter.addAll(locations) diff --git a/app/src/main/java/org/oxycblt/auxio/music/locations/NewLocationFooterAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/locations/NewLocationFooterAdapter.kt index 156c4c049..4623db510 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/locations/NewLocationFooterAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/locations/NewLocationFooterAdapter.kt @@ -51,7 +51,7 @@ class NewLocationFooterAdapter(private val listener: Listener) : } /** - * A [RecyclerView.ViewHolder] that displays a "New Playlist" choice in [NewPlaylistFooterAdapter]. + * A [RecyclerView.ViewHolder] that displays a "New Playlist" choice in [NewLocationFooterAdapter]. * Use [from] to create an instance. * * @author Alexander Capehart (OxygenCobalt) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt index 17b4fcc12..5ec328f91 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackUtil.kt @@ -48,13 +48,6 @@ fun Long.dsToMs() = times(100) */ fun Long.dsToSecs() = floorDiv(10) -/** - * Convert seconds into milliseconds. - * - * @return A converted millisecond value. - */ -fun Long.secsToMs() = times(1000) - /** * Convert a millisecond value into a string duration. * diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/BetterShuffleOrder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/BetterShuffleOrder.kt index 0f037a75a..0b95e4c4b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/BetterShuffleOrder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/BetterShuffleOrder.kt @@ -18,7 +18,9 @@ package org.oxycblt.auxio.playback.service +import androidx.annotation.OptIn import androidx.media3.common.C +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.source.ShuffleOrder /** @@ -28,6 +30,7 @@ import androidx.media3.exoplayer.source.ShuffleOrder * * @author media3 team, Alexander Capehart (OxygenCobalt) */ +@OptIn(UnstableApi::class) class BetterShuffleOrder(private val shuffled: IntArray) : ShuffleOrder { private val indexInShuffled: IntArray = IntArray(shuffled.size) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index b2d89d5c6..491ae281c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -22,11 +22,13 @@ import android.content.Context import android.content.Intent import android.media.audiofx.AudioEffect import android.provider.OpenableColumns +import androidx.annotation.OptIn import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi import androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.RenderersFactory @@ -62,6 +64,7 @@ import org.oxycblt.musikr.MusicParent import org.oxycblt.musikr.Song import timber.log.Timber as L +@OptIn(UnstableApi::class) class ExoPlaybackStateHolder( private val context: Context, private val player: ExoPlayer, diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt index cda467cfb..2854d4873 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt @@ -19,6 +19,8 @@ package org.oxycblt.auxio.playback.service import android.content.Context +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.ContentDataSource import androidx.media3.datasource.DataSource import androidx.media3.exoplayer.source.MediaSource @@ -41,6 +43,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) +@OptIn(UnstableApi::class) class SystemModule { @Provides fun mediaSourceFactory( diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index 11f4b5175..7f278df5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -20,9 +20,9 @@ package org.oxycblt.auxio.settings import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater +import androidx.core.net.toUri import androidx.core.view.updatePadding import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController @@ -102,7 +102,7 @@ class AboutFragment : ViewBindingFragment() { } private fun Context.sendEmail(recipient: String) { - val intent = Intent(Intent.ACTION_SENDTO).apply { data = Uri.parse("mailto:$recipient") } + val intent = Intent(Intent.ACTION_SENDTO).apply { data = "mailto:$recipient".toUri() } startIntent(intent) } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt index edbaf01ef..986893cc3 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -46,25 +46,25 @@ class AnimConfig( companion object { val STANDARD = MR.attr.motionEasingStandardInterpolator - val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator + // val EMPHASIZED = MR.attr.motionEasingEmphasizedInterpolator val EMPHASIZED_ACCELERATE = MR.attr.motionEasingEmphasizedAccelerateInterpolator val EMPHASIZED_DECELERATE = MR.attr.motionEasingEmphasizedDecelerateInterpolator val SHORT1 = MR.attr.motionDurationShort1 to 50 - val SHORT2 = MR.attr.motionDurationShort2 to 100 + // val SHORT2 = MR.attr.motionDurationShort2 to 100 val SHORT3 = MR.attr.motionDurationShort3 to 150 - val SHORT4 = MR.attr.motionDurationShort4 to 200 + // val SHORT4 = MR.attr.motionDurationShort4 to 200 val MEDIUM1 = MR.attr.motionDurationMedium1 to 250 val MEDIUM2 = MR.attr.motionDurationMedium2 to 300 val MEDIUM3 = MR.attr.motionDurationMedium3 to 350 - val MEDIUM4 = MR.attr.motionDurationMedium4 to 400 - val LONG1 = MR.attr.motionDurationLong1 to 450 - val LONG2 = MR.attr.motionDurationLong2 to 500 - val LONG3 = MR.attr.motionDurationLong3 to 550 - val LONG4 = MR.attr.motionDurationLong4 to 600 - val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700 - val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800 - val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900 - val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000 + // val MEDIUM4 = MR.attr.motionDurationMedium4 to 400 + // val LONG1 = MR.attr.motionDurationLong1 to 450 + // val LONG2 = MR.attr.motionDurationLong2 to 500 + // val LONG3 = MR.attr.motionDurationLong3 to 550 + // val LONG4 = MR.attr.motionDurationLong4 to 600 + // val EXTRA_LONG1 = MR.attr.motionDurationExtraLong1 to 700 + // val EXTRA_LONG2 = MR.attr.motionDurationExtraLong2 to 800 + // val EXTRA_LONG3 = MR.attr.motionDurationExtraLong3 to 900 + // val EXTRA_LONG4 = MR.attr.motionDurationExtraLong4 to 1000 fun of(context: Context, @AttrRes interpolator: Int, duration: Pair) = AnimConfig(context, interpolator, duration.first, duration.second) @@ -122,7 +122,7 @@ private constructor( } } - fun jumpToFadeIn(view: View) { + private fun jumpToFadeIn(view: View) { view.apply { alpha = 1f scaleX = 1.0f diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index ec6091863..2322a196e 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -24,7 +24,6 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.PointF -import android.graphics.drawable.Drawable import android.os.Build import android.view.View import android.view.WindowInsets @@ -36,7 +35,6 @@ import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.ShareCompat import androidx.core.graphics.Insets -import androidx.core.graphics.drawable.DrawableCompat import androidx.core.net.toUri import androidx.core.view.children import androidx.navigation.NavController @@ -106,10 +104,6 @@ private fun isUnderImpl( val View.isRtl: Boolean get() = layoutDirection == View.LAYOUT_DIRECTION_RTL -/** Whether this [Drawable] is using an RTL layout direction. */ -val Drawable.isRtl: Boolean - get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL - /** Get a [Context] from a [ViewBinding]'s root [View]. */ val ViewBinding.context: Context get() = root.context @@ -357,7 +351,7 @@ fun Context.startIntent(intent: Intent) { // No app installed to open the link showToast(R.string.err_no_app) } - } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + } else { // On older versions of android, opening links from an ACTION_VIEW intent might // not work in all cases, especially when no default app was set. If that is the // case, we will try to manually handle these cases before we try to launch the diff --git a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt index 7f9f4e9a5..eb74d8f15 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/StateUtil.kt @@ -22,18 +22,11 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import java.util.concurrent.TimeoutException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout -import org.oxycblt.auxio.BuildConfig -import timber.log.Timber as L /** * A wrapper around [StateFlow] exposing a one-time consumable event. @@ -153,71 +146,3 @@ private fun Fragment.launch( ) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) } } - -const val DEFAULT_TIMEOUT = 60000L - -/** - * Wraps [SendChannel.send] with a specified timeout. - * - * @param element The element to send. - * @param timeout The timeout in milliseconds. Defaults to 10 seconds. - * @throws TimeoutException If the timeout is reached, provides context on what element - * specifically. - */ -suspend fun SendChannel.sendWithTimeout(element: E, timeout: Long = DEFAULT_TIMEOUT) { - try { - withTimeout(timeout) { send(element) } - } catch (e: TimeoutCancellationException) { - L.e("Failed to send element to channel $e in ${timeout}ms.") - if (BuildConfig.DEBUG) { - throw TimeoutException("Timed out sending element to channel: $e") - } else { - L.e(e.stackTraceToString()) - send(element) - } - } -} - -/** - * Wraps a [ReceiveChannel] consumption with a specified timeout. Note that the timeout will only - * start on the first element received, as to prevent initialization of dependent coroutines being - * interpreted as a timeout. - * - * @param action The action to perform on each element received. - * @param timeout The timeout in milliseconds. Defaults to 10 seconds. - * @throws TimeoutException If the timeout is reached, provides context on what element - * specifically. - */ -suspend fun ReceiveChannel.forEachWithTimeout( - timeout: Long = DEFAULT_TIMEOUT, - action: suspend (E) -> Unit -) { - var exhausted = false - var subsequent = false - val handler: suspend () -> Unit = { - val value = receiveCatching() - if (value.isClosed && value.exceptionOrNull() == null) { - exhausted = true - } else { - action(value.getOrThrow()) - } - } - while (!exhausted) { - try { - if (subsequent) { - withTimeout(timeout) { handler() } - } else { - handler() - subsequent = true - } - } catch (e: TimeoutCancellationException) { - L.e("Failed to send element to channel $e in ${timeout}ms.") - if (BuildConfig.DEBUG) { - throw TimeoutException("Timed out sending element to channel: $e") - } else { - L.e(e.stackTraceToString()) - handler() - } - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt index d844026af..7ad05b026 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetBitmapTransformation.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.widgets import android.content.res.Resources import android.graphics.Bitmap +import androidx.core.graphics.scale import coil3.size.Size import coil3.transform.Transformation import kotlin.math.sqrt @@ -49,7 +50,7 @@ class WidgetBitmapTransformation(reduce: Float) : Transformation() { val scale = sqrt(maxBitmapArea / inputArea.toDouble()) val newWidth = (input.width * scale).toInt() val newHeight = (input.height * scale).toInt() - return Bitmap.createScaledBitmap(input, newWidth, newHeight, true) + return input.scale(newWidth, newHeight) } return input } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index 049531c88..a5e32881b 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.widgets import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.content.Context import android.os.Build @@ -66,11 +65,6 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) { setInt(viewId, "setLayoutDirection", layoutDirection) } -fun AppWidgetManager.setWidgetPreviewCompat(component: ComponentName, remoteViews: RemoteViews) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - setWidgetPreview(component, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, remoteViews) - } -} /** * Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with an * adaptive layout, in a version-compatible manner. diff --git a/app/src/main/res/layout-h360dp/fragment_detail.xml b/app/src/main/res/layout-h360dp/fragment_detail.xml index 12bb1598b..8a980b1c0 100644 --- a/app/src/main/res/layout-h360dp/fragment_detail.xml +++ b/app/src/main/res/layout-h360dp/fragment_detail.xml @@ -119,7 +119,8 @@ app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/detail_cover" /> + app:layout_constraintTop_toBottomOf="@+id/detail_cover" + tools:ignore="RtlSymmetry" /> + app:layout_constraintTop_toBottomOf="@+id/detail_info" + tools:ignore="RtlSymmetry"/> + app:layout_constraintTop_toTopOf="@+id/detail_play_button" + tools:ignore="RtlSymmetry" /> + app:layout_constraintTop_toTopOf="@+id/detail_play_button" + tools:ignore="RtlSymmetry" /> + xmlns:tools="http://schemas.android.com/tools"> \ No newline at end of file diff --git a/app/src/main/res/layout/widget_wafer_thin.xml b/app/src/main/res/layout/widget_wafer_thin.xml index 385a39fe7..86f1581ca 100644 --- a/app/src/main/res/layout/widget_wafer_thin.xml +++ b/app/src/main/res/layout/widget_wafer_thin.xml @@ -22,7 +22,7 @@ android:scaleType="centerCrop" android:background="@drawable/ui_widget_bg_round" android:clipToOutline="true" - tools:ignore="ContentDescription" /> + tools:ignore="ContentDescription,UnusedAttribute" /> + tools:ignore="ContentDescription,UnusedAttribute" /> Kafe -%.1f dB %d kbps - Duke ngarkuar bibliotekën tuaj muzikore... (%1$d/%2$d) + Duke ngarkuar bibliotekën tuaj muzikore… (%1$d/%2$d) Këngët e ngarkuara: %d Albumet e ngarkuara: %d Artistët e ngarkuar: %d diff --git a/app/src/main/res/values/themes_black.xml b/app/src/main/res/values/themes_black.xml index 75b8f6d24..ce5b0f676 100644 --- a/app/src/main/res/values/themes_black.xml +++ b/app/src/main/res/values/themes_black.xml @@ -1,6 +1,9 @@ - - - - - -