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 const val ACTION_MODE_SHUFFLE = 0xA11B
/** CoverMode.Off */ /** CoverMode.Off */
const val COVER_MODE_OFF = 0xA11C const val COVER_MODE_OFF = 0xA11C
/** CoverMode.MediaStore */ /** CoverMode.Balanced */
const val COVER_MODE_FAST = 0xA11D const val COVER_MODE_BALANCED = 0xA11D
/** CoverMode.Quality */ /** CoverMode.Quality */
const val COVER_MODE_QUALITY = 0xA11E const val COVER_MODE_HIGH_QUALITY = 0xA11E
/** PlaySong.FromAll */ /** PlaySong.FromAll */
const val PLAY_SONG_FROM_ALL = 0xA11F const val PLAY_SONG_FROM_ALL = 0xA11F
/** PlaySong.FromAlbum */ /** PlaySong.FromAlbum */
@ -139,4 +139,6 @@ object IntegerTable {
const val PLAY_SONG_FROM_PLAYLIST = 0xA123 const val PLAY_SONG_FROM_PLAYLIST = 0xA123
/** PlaySong.ByItself */ /** PlaySong.ByItself */
const val PLAY_SONG_BY_ITSELF = 0xA124 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) * @author Alexander Capehart (OxygenCobalt)
*/ */
enum class CoverMode { enum class CoverMode {
/** Do not load album covers ("Off"). */
OFF, OFF,
/** Load covers from the fast, but lower-quality media store database ("Fast"). */ SAVE_SPACE,
FAST, BALANCED,
/** Load high-quality covers directly from music files ("Quality"). */ HIGH_QUALITY;
QUALITY;
/** /**
* The integer representation of this instance. * The integer representation of this instance.
@ -42,8 +40,9 @@ enum class CoverMode {
get() = get() =
when (this) { when (this) {
OFF -> IntegerTable.COVER_MODE_OFF OFF -> IntegerTable.COVER_MODE_OFF
FAST -> IntegerTable.COVER_MODE_FAST SAVE_SPACE -> IntegerTable.COVER_MODE_SAVE_SPACE
QUALITY -> IntegerTable.COVER_MODE_QUALITY BALANCED -> IntegerTable.COVER_MODE_BALANCED
HIGH_QUALITY -> IntegerTable.COVER_MODE_HIGH_QUALITY
} }
companion object { companion object {
@ -57,8 +56,9 @@ enum class CoverMode {
fun fromIntCode(intCode: Int) = fun fromIntCode(intCode: Int) =
when (intCode) { when (intCode) {
IntegerTable.COVER_MODE_OFF -> OFF IntegerTable.COVER_MODE_OFF -> OFF
IntegerTable.COVER_MODE_FAST -> FAST IntegerTable.COVER_MODE_SAVE_SPACE -> SAVE_SPACE
IntegerTable.COVER_MODE_QUALITY -> QUALITY IntegerTable.COVER_MODE_BALANCED -> BALANCED
IntegerTable.COVER_MODE_HIGH_QUALITY -> HIGH_QUALITY
else -> null else -> null
} }
} }

View file

@ -49,7 +49,7 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
get() = get() =
CoverMode.fromIntCode( CoverMode.fromIntCode(
sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
?: CoverMode.FAST ?: CoverMode.BALANCED
override val forceSquareCovers: Boolean override val forceSquareCovers: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false) get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false)
@ -64,8 +64,8 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
when { when {
!sharedPreferences.getBoolean(OLD_KEY_SHOW_COVERS, true) -> CoverMode.OFF !sharedPreferences.getBoolean(OLD_KEY_SHOW_COVERS, true) -> CoverMode.OFF
!sharedPreferences.getBoolean(OLD_KEY_QUALITY_COVERS, true) -> !sharedPreferences.getBoolean(OLD_KEY_QUALITY_COVERS, true) ->
CoverMode.FAST CoverMode.BALANCED
else -> CoverMode.QUALITY else -> CoverMode.BALANCED
} }
sharedPreferences.edit { sharedPreferences.edit {
@ -74,6 +74,24 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
remove(OLD_KEY_QUALITY_COVERS) 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) { override fun onSettingChanged(key: String, listener: ImageSettings.Listener) {
@ -87,5 +105,6 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
private companion object { private companion object {
const val OLD_KEY_SHOW_COVERS = "KEY_SHOW_COVERS" const val OLD_KEY_SHOW_COVERS = "KEY_SHOW_COVERS"
const val OLD_KEY_QUALITY_COVERS = "KEY_QUALITY_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 package org.oxycblt.auxio.image.covers
import android.content.Context import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
suspend fun Context.coversDir() = withContext(Dispatchers.IO) { suspend fun Context.coversDir() =
filesDir.resolve("covers").apply { mkdirs() } 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 package org.oxycblt.auxio.image.covers
import android.content.Context import android.content.Context
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverIdentifier import org.oxycblt.musikr.cover.CoverIdentifier
import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.MutableCovers import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.cover.ObtainResult 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 obtain(id: String) = ObtainResult.Hit(NullCover(id))
override suspend fun write(data: ByteArray): Cover = override suspend fun write(data: ByteArray): Cover = NullCover(identifier.identify(data))
NullCover(identifier.identify(data))
override suspend fun cleanup(excluding: Collection<Cover>) { override suspend fun cleanup(excluding: Collection<Cover>) {
context.coversDir().listFiles()?.forEach { it.deleteRecursively() } context.coversDir().listFiles()?.forEach { it.deleteRecursively() }

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 { 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 rootDir: File
val revisionDir: File val revisionDir: File
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View file

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

View file

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