music: re-add configurable covers

This commit is contained in:
Alexander Capehart 2024-12-28 14:00:40 -05:00
parent ff6d2fe228
commit a1cd4f7b26
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 194 additions and 49 deletions

View file

@ -123,10 +123,10 @@ object IntegerTable {
const val ACTION_MODE_SHUFFLE = 0xA11B
/** CoverMode.Off */
const val COVER_MODE_OFF = 0xA11C
/** CoverMode.MediaStore */
const val COVER_MODE_FAST = 0xA11D
/** CoverMode.Balanced */
const val COVER_MODE_BALANCED = 0xA11D
/** CoverMode.Quality */
const val COVER_MODE_QUALITY = 0xA11E
const val COVER_MODE_HIGH_QUALITY = 0xA11E
/** PlaySong.FromAll */
const val PLAY_SONG_FROM_ALL = 0xA11F
/** PlaySong.FromAlbum */
@ -139,4 +139,6 @@ object IntegerTable {
const val PLAY_SONG_FROM_PLAYLIST = 0xA123
/** PlaySong.ByItself */
const val PLAY_SONG_BY_ITSELF = 0xA124
/** CoverMode.SaveSpace */
const val COVER_MODE_SAVE_SPACE = 0xA125
}

View file

@ -26,12 +26,10 @@ import org.oxycblt.auxio.IntegerTable
* @author Alexander Capehart (OxygenCobalt)
*/
enum class CoverMode {
/** Do not load album covers ("Off"). */
OFF,
/** Load covers from the fast, but lower-quality media store database ("Fast"). */
FAST,
/** Load high-quality covers directly from music files ("Quality"). */
QUALITY;
SAVE_SPACE,
BALANCED,
HIGH_QUALITY;
/**
* The integer representation of this instance.
@ -42,8 +40,9 @@ enum class CoverMode {
get() =
when (this) {
OFF -> IntegerTable.COVER_MODE_OFF
FAST -> IntegerTable.COVER_MODE_FAST
QUALITY -> IntegerTable.COVER_MODE_QUALITY
SAVE_SPACE -> IntegerTable.COVER_MODE_SAVE_SPACE
BALANCED -> IntegerTable.COVER_MODE_BALANCED
HIGH_QUALITY -> IntegerTable.COVER_MODE_HIGH_QUALITY
}
companion object {
@ -57,8 +56,9 @@ enum class CoverMode {
fun fromIntCode(intCode: Int) =
when (intCode) {
IntegerTable.COVER_MODE_OFF -> OFF
IntegerTable.COVER_MODE_FAST -> FAST
IntegerTable.COVER_MODE_QUALITY -> QUALITY
IntegerTable.COVER_MODE_SAVE_SPACE -> SAVE_SPACE
IntegerTable.COVER_MODE_BALANCED -> BALANCED
IntegerTable.COVER_MODE_HIGH_QUALITY -> HIGH_QUALITY
else -> null
}
}

View file

@ -49,7 +49,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
get() =
CoverMode.fromIntCode(
sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
?: CoverMode.FAST
?: CoverMode.BALANCED
override val forceSquareCovers: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false)
@ -64,8 +64,8 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
when {
!sharedPreferences.getBoolean(OLD_KEY_SHOW_COVERS, true) -> CoverMode.OFF
!sharedPreferences.getBoolean(OLD_KEY_QUALITY_COVERS, true) ->
CoverMode.FAST
else -> CoverMode.QUALITY
CoverMode.BALANCED
else -> CoverMode.BALANCED
}
sharedPreferences.edit {
@ -74,6 +74,24 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
remove(OLD_KEY_QUALITY_COVERS)
}
}
if (sharedPreferences.contains(OLD_KEY_COVER_MODE)) {
L.d("Migrating cover mode setting")
var mode =
CoverMode.fromIntCode(sharedPreferences.getInt(OLD_KEY_COVER_MODE, Int.MIN_VALUE))
?: CoverMode.BALANCED
if (mode == CoverMode.HIGH_QUALITY) {
// High quality now has space characteristics that could be
// undesirable, clamp to balanced.
mode = CoverMode.BALANCED
}
sharedPreferences.edit {
putInt(getString(R.string.set_key_cover_mode), mode.intCode)
remove(OLD_KEY_COVER_MODE)
}
}
}
override fun onSettingChanged(key: String, listener: ImageSettings.Listener) {
@ -87,5 +105,6 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
private companion object {
const val OLD_KEY_SHOW_COVERS = "KEY_SHOW_COVERS"
const val OLD_KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS"
const val OLD_KEY_COVER_MODE = "auxio_cover_mode"
}
}

View file

@ -0,0 +1,38 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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.cover.CoverIdentifier
@Module
@InstallIn(SingletonComponent::class)
abstract class CoverModule {
@Binds abstract fun configCovers(impl: SettingCoversImpl): SettingCovers
}
@Module
@InstallIn(SingletonComponent::class)
abstract class CoverProvidesModule {
@Provides fun identifier(): CoverIdentifier = CoverIdentifier.md5()
}

View file

@ -1,9 +1,26 @@
/*
* Copyright (c) 2024 Auxio Project
* CoverUtil.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.covers
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun Context.coversDir() = withContext(Dispatchers.IO) {
filesDir.resolve("covers").apply { mkdirs() }
}
suspend fun Context.coversDir() =
withContext(Dispatchers.IO) { filesDir.resolve("covers").apply { mkdirs() } }

View file

@ -1,18 +1,34 @@
/*
* Copyright (c) 2024 Auxio Project
* NullCovers.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.covers
import android.content.Context
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverIdentifier
import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.cover.ObtainResult
import java.io.InputStream
class NullCovers(private val context: Context, private val identifier: CoverIdentifier) : MutableCovers {
class NullCovers(private val context: Context, private val identifier: CoverIdentifier) :
MutableCovers {
override suspend fun obtain(id: String) = ObtainResult.Hit(NullCover(id))
override suspend fun write(data: ByteArray): Cover =
NullCover(identifier.identify(data))
override suspend fun write(data: ByteArray): Cover = NullCover(identifier.identify(data))
override suspend fun cleanup(excluding: Collection<Cover>) {
context.coversDir().listFiles()?.forEach { it.deleteRecursively() }
@ -21,4 +37,4 @@ class NullCovers(private val context: Context, private val identifier: CoverIden
private class NullCover(override val id: String) : Cover {
override suspend fun open() = null
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2024 Auxio Project
* SettingCovers.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.covers
import android.content.Context
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.CoverIdentifier
import org.oxycblt.musikr.cover.CoverParams
import org.oxycblt.musikr.cover.MutableCovers
interface SettingCovers {
suspend fun create(context: Context, revision: UUID): MutableCovers
}
class SettingCoversImpl
@Inject
constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) :
SettingCovers {
override suspend fun create(context: Context, revision: UUID): MutableCovers =
when (imageSettings.coverMode) {
CoverMode.OFF -> NullCovers(context, identifier)
CoverMode.SAVE_SPACE -> siloedCovers(context, revision, CoverParams.of(750, 70))
CoverMode.BALANCED -> siloedCovers(context, revision, CoverParams.of(750, 85))
CoverMode.HIGH_QUALITY -> siloedCovers(context, revision, CoverParams.of(1000, 100))
}
private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams) =
SiloedCovers.from(context, CoverSilo(revision, with), identifier)
}

View file

@ -57,7 +57,11 @@ class SiloedCovers(
}
companion object {
suspend fun from(context: Context, silo: CoverSilo, identifier: CoverIdentifier): SiloedCovers {
suspend fun from(
context: Context,
silo: CoverSilo,
identifier: CoverIdentifier
): SiloedCovers {
val rootDir: File
val revisionDir: File
withContext(Dispatchers.IO) {

View file

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music
import android.content.Context
@ -27,9 +27,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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.image.covers.CoverSilo
import org.oxycblt.auxio.image.covers.SiloedCovers
import org.oxycblt.musikr.IndexingProgress
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Library
@ -40,8 +39,6 @@ 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.cover.CoverIdentifier
import org.oxycblt.musikr.cover.CoverParams
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators
@ -241,19 +238,16 @@ constructor(
@ApplicationContext private val context: Context,
private val storedCache: StoredCache,
private val storedPlaylists: StoredPlaylists,
private val settingCovers: SettingCovers,
private val musicSettings: MusicSettings
) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
@Volatile
private var indexingWorker: MusicRepository.IndexingWorker? = null
@Volatile private var indexingWorker: MusicRepository.IndexingWorker? = null
@Volatile
override var library: MutableLibrary? = null
@Volatile
private var previousCompletedState: IndexingState.Completed? = null
@Volatile
private var currentIndexingState: IndexingState? = null
@Volatile override var library: MutableLibrary? = null
@Volatile private var previousCompletedState: IndexingState.Completed? = null
@Volatile private var currentIndexingState: IndexingState? = null
override val indexingState: IndexingState?
get() = currentIndexingState ?: previousCompletedState
@ -393,11 +387,7 @@ constructor(
val currentRevision = musicSettings.revision
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
val cache = if (withCache) storedCache.visible() else storedCache.invisible()
val covers = SiloedCovers.from(
context,
CoverSilo(newRevision, CoverParams.of(750, 80)),
CoverIdentifier.md5()
)
val covers = settingCovers.create(context, newRevision)
val storage = Storage(cache, covers, storedPlaylists)
val interpretation = Interpretation(nameFactory, separators)
@ -423,9 +413,9 @@ constructor(
// TODO: Remove this once you start work on kindred.
deviceLibraryChanged =
this.library?.songs != newLibrary.songs ||
this.library?.albums != newLibrary.albums ||
this.library?.artists != newLibrary.artists ||
this.library?.genres != newLibrary.genres
this.library?.albums != newLibrary.albums ||
this.library?.artists != newLibrary.artists ||
this.library?.genres != newLibrary.genres
userLibraryChanged = this.library?.playlists != newLibrary.playlists
if (!deviceLibraryChanged && !userLibraryChanged) {
L.d("Library has not changed, skipping update")

View file

@ -18,12 +18,14 @@
package org.oxycblt.auxio.settings.categories
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.settings.BasePreferenceFragment
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.util.navigateSafe
@ -36,6 +38,7 @@ import timber.log.Timber as L
*/
@AndroidEntryPoint
class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) {
private val musicModel: MusicViewModel by viewModels()
@Inject lateinit var imageLoader: ImageLoader
override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
@ -46,9 +49,17 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
}
override fun onSetupPreference(preference: Preference) {
if (preference.key == getString(R.string.set_key_cover_mode) ||
preference.key == getString(R.string.set_key_square_covers)) {
if (preference.key == getString(R.string.set_key_cover_mode)) {
L.d("Configuring cover mode setting")
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
L.d("Cover mode changed, reloading music")
musicModel.refresh()
true
}
}
if (preference.key == getString(R.string.set_key_square_covers)) {
L.d("Configuring square cover setting")
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
L.d("Cover mode changed, resetting image memory cache")

View file

@ -14,7 +14,7 @@
<string name="set_key_rescan" translatable="false">auxio_rescan</string>
<string name="set_key_observing" translatable="false">auxio_observing</string>
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
<string name="set_key_cover_mode" translatable="false">auxio_cover_mode</string>
<string name="set_key_cover_mode" translatable="false">auxio_cover_mode2</string>
<string name="set_key_square_covers" translatable="false">auxio_square_covers</string>
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>