music: enable quality tags [#128]

Enable the ExoPlayer parser in the UI.

Now that runtime rescanning is implemented, this feature can also be
finally enabled.

Resolves #128.
This commit is contained in:
OxygenCobalt 2022-07-02 20:35:18 -06:00
parent d6f166a3ee
commit 3bdf7b136e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 79 additions and 58 deletions

View file

@ -3,6 +3,8 @@
## dev ## dev
#### What's New #### What's New
- Added direct metadata parsing, allowing more correct metadata at the cost of longer loading times
- Auxio can now reload music without requiring a restart
- Added a shuffle shortcut - Added a shuffle shortcut
- Widgets now have a more sleek and consistent button layout - Widgets now have a more sleek and consistent button layout
- "Rounded album covers" is now "Round mode" - "Rounded album covers" is now "Round mode"

View file

@ -27,6 +27,8 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend
import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend
import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend
import org.oxycblt.auxio.music.backend.ExoPlayerBackend
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
@ -80,7 +82,7 @@ class Indexer {
return return
} }
synchronized(this) { this.controller = controller } this.controller = controller
} }
/** Unregister a [Controller] with this instance. */ /** Unregister a [Controller] with this instance. */
@ -119,9 +121,8 @@ class Indexer {
this.callback = null this.callback = null
} }
@Synchronized
fun index(context: Context) { fun index(context: Context) {
val generation = ++currentGeneration val generation = synchronized(this) { ++currentGeneration }
val notGranted = val notGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
@ -227,7 +228,15 @@ class Indexer {
else -> Api21MediaStoreBackend() else -> Api21MediaStoreBackend()
} }
val songs = buildSongs(context, mediaStoreBackend, generation) val settings = Settings(context)
val backend =
if (settings.useQualityTags) {
ExoPlayerBackend(mediaStoreBackend)
} else {
mediaStoreBackend
}
val songs = buildSongs(context, backend, generation)
if (songs.isEmpty()) { if (songs.isEmpty()) {
return null return null
} }

View file

@ -27,10 +27,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -43,8 +44,14 @@ import org.oxycblt.auxio.util.logD
* boilerplate you skip is not worth the insanity of androidx. * boilerplate you skip is not worth the insanity of androidx.
* *
* @author OxygenCobalt * @author OxygenCobalt
*
* TODO: Add file observing
*
* TODO: Audit usages of synchronized
*
* TODO: Rework UI flow once again
*/ */
class IndexerService : Service(), Indexer.Controller { class IndexerService : Service(), Indexer.Controller, Settings.Callback {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
@ -53,6 +60,7 @@ class IndexerService : Service(), Indexer.Controller {
private val updateScope = CoroutineScope(serviceJob + Dispatchers.Main) private val updateScope = CoroutineScope(serviceJob + Dispatchers.Main)
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var settings: Settings
private var isForeground = false private var isForeground = false
private lateinit var notification: IndexerNotification private lateinit var notification: IndexerNotification
@ -61,6 +69,7 @@ class IndexerService : Service(), Indexer.Controller {
super.onCreate() super.onCreate()
notification = IndexerNotification(this) notification = IndexerNotification(this)
settings = Settings(this, this)
indexer.registerController(this) indexer.registerController(this)
if (musicStore.library == null && indexer.isIndeterminate) { if (musicStore.library == null && indexer.isIndeterminate) {
@ -83,12 +92,14 @@ class IndexerService : Service(), Indexer.Controller {
indexer.cancelLast() indexer.cancelLast()
indexer.unregisterController(this) indexer.unregisterController(this)
serviceJob.cancel() serviceJob.cancel()
settings.release()
} }
// --- CONTROLLER CALLBACKS ---
override fun onStartIndexing() { override fun onStartIndexing() {
if (indexer.isIndexing) { if (indexer.isIndexing) {
indexer.cancelLast() indexer.cancelLast()
indexScope.cancel()
} }
indexScope.launch { indexer.index(this@IndexerService) } indexScope.launch { indexer.index(this@IndexerService) }
@ -103,9 +114,8 @@ class IndexerService : Service(), Indexer.Controller {
val newLibrary = state.response.library val newLibrary = state.response.library
// Load was completed successfully, so apply the new library if we // Load was completed successfully. However, we still need to do some
// have not already. Only when we are done updating the library will // extra work to update the app's state.
// the service stop it's foreground state.
updateScope.launch { updateScope.launch {
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
@ -116,7 +126,7 @@ class IndexerService : Service(), Indexer.Controller {
PlaybackStateDatabase.getInstance(this@IndexerService), newLibrary) PlaybackStateDatabase.getInstance(this@IndexerService), newLibrary)
} }
withContext(Dispatchers.Main) { musicStore.updateLibrary(newLibrary) } musicStore.updateLibrary(newLibrary)
stopForegroundSession() stopForegroundSession()
} }
@ -152,6 +162,16 @@ class IndexerService : Service(), Indexer.Controller {
} }
} }
// --- SETTING CALLBACKS ---
override fun onSettingChanged(key: String) {
if (key == getString(R.string.set_key_music_dirs) ||
key == getString(R.string.set_key_music_dirs_include) ||
key == getString(R.string.set_key_quality_tags)) {
onStartIndexing()
}
}
private fun stopForegroundSession() { private fun stopForegroundSession() {
if (isForeground) { if (isForeground) {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)

View file

@ -26,7 +26,6 @@ import org.oxycblt.auxio.util.contentResolverSafe
* The main storage for music items. The items themselves are located in a [Library], however this * The main storage for music items. The items themselves are located in a [Library], however this
* might not be available at all times. * might not be available at all times.
* *
* TODO: Add automatic rescanning [major change]
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MusicStore private constructor() { class MusicStore private constructor() {

View file

@ -184,7 +184,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
} }
} }
is VorbisComment -> { is VorbisComment -> {
val id = tag.value.sanitize() val id = tag.key.sanitize()
val value = tag.value.sanitize() val value = tag.value.sanitize()
if (value.isNotEmpty()) { if (value.isNotEmpty()) {
vorbisTags[id] = value vorbisTags[id] = value
@ -244,6 +244,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
} }
private fun populateVorbis(tags: Map<String, String>) { private fun populateVorbis(tags: Map<String, String>) {
logD(tags)
// Title // Title
tags["TITLE"]?.let { audio.title = it } tags["TITLE"]?.let { audio.title = it }
@ -266,7 +267,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
tags["ALBUM"]?.let { audio.album = it } tags["ALBUM"]?.let { audio.album = it }
// Artist // Artist
tags["ARTIST"]?.let { audio.title } tags["ARTIST"]?.let { audio.artist = it }
// Album artist. This actually comes into two flavors: // Album artist. This actually comes into two flavors:
// 1. ALBUMARTIST, which is the most common // 1. ALBUMARTIST, which is the most common

View file

@ -25,16 +25,12 @@ import android.view.LayoutInflater
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.music.IndexerViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -46,12 +42,11 @@ import org.oxycblt.auxio.util.showToast
*/ */
class MusicDirsDialog : class MusicDirsDialog :
ViewBindingDialogFragment<DialogMusicDirsBinding>(), MusicDirAdapter.Listener { ViewBindingDialogFragment<DialogMusicDirsBinding>(), MusicDirAdapter.Listener {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val indexerModel: IndexerViewModel by activityViewModels()
private val dirAdapter = MusicDirAdapter(this) private val dirAdapter = MusicDirAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private var storageManager: StorageManager? = null private val storageManager: StorageManager by lifecycleObject { binding ->
binding.context.getSystemServiceSafe(StorageManager::class)
}
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicDirsBinding.inflate(inflater) DialogMusicDirsBinding.inflate(inflater)
@ -61,7 +56,17 @@ class MusicDirsDialog :
builder builder
.setTitle(R.string.set_dirs) .setTitle(R.string.set_dirs)
.setNeutralButton(R.string.lbl_add, null) .setNeutralButton(R.string.lbl_add, null)
.setPositiveButton(R.string.lbl_save, null) .setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager)
val newDirs =
MusicDirs(
dirs = dirAdapter.data.currentList,
shouldInclude = isInclude(requireBinding()))
if (dirs != newDirs) {
logD("Committing changes")
settings.setMusicDirs(newDirs)
}
}
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
} }
@ -80,19 +85,6 @@ class MusicDirsDialog :
logD("Opening launcher") logD("Opening launcher")
launcher.launch(null) launcher.launch(null)
} }
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
val dirs = settings.getMusicDirs(requireStorageManager())
if (dirs.dirs != dirAdapter.data.currentList ||
dirs.shouldInclude != isInclude(requireBinding())) {
logD("Committing changes")
saveAndDismiss()
} else {
logD("Dropping changes")
dismiss()
}
}
} }
binding.dirsRecycler.apply { binding.dirsRecycler.apply {
@ -100,7 +92,6 @@ class MusicDirsDialog :
itemAnimator = null itemAnimator = null
} }
val storageManager = requireStorageManager()
var dirs = settings.getMusicDirs(storageManager) var dirs = settings.getMusicDirs(storageManager)
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -173,7 +164,7 @@ class MusicDirsDialog :
val treeUri = DocumentsContract.getTreeDocumentId(docUri) val treeUri = DocumentsContract.getTreeDocumentId(docUri)
// Parsing handles the rest // Parsing handles the rest
return Directory.fromDocumentUri(requireStorageManager(), treeUri) return Directory.fromDocumentUri(storageManager, treeUri)
} }
private fun updateMode() { private fun updateMode() {
@ -188,22 +179,6 @@ class MusicDirsDialog :
private fun isInclude(binding: DialogMusicDirsBinding) = private fun isInclude(binding: DialogMusicDirsBinding) =
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
private fun saveAndDismiss() {
settings.setMusicDirs(MusicDirs(dirAdapter.data.currentList, isInclude(requireBinding())))
indexerModel.reindex()
dismiss()
}
private fun requireStorageManager(): StorageManager {
val mgr = storageManager
if (mgr != null) {
return mgr
}
val newMgr = requireContext().getSystemServiceSafe(StorageManager::class)
storageManager = newMgr
return newMgr
}
companion object { companion object {
const val TAG = BuildConfig.APPLICATION_ID + ".tag.EXCLUDED" const val TAG = BuildConfig.APPLICATION_ID + ".tag.EXCLUDED"
const val KEY_PENDING_DIRS = BuildConfig.APPLICATION_ID + ".key.PENDING_DIRS" const val KEY_PENDING_DIRS = BuildConfig.APPLICATION_ID + ".key.PENDING_DIRS"

View file

@ -185,6 +185,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
val pauseOnRepeat: Boolean val pauseOnRepeat: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false) get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false)
/** Whether to parse metadata directly with ExoPlayer. */
val useQualityTags: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false)
/** Get the list of directories that music should be hidden/loaded from. */ /** Get the list of directories that music should be hidden/loaded from. */
fun getMusicDirs(storageManager: StorageManager): MusicDirs { fun getMusicDirs(storageManager: StorageManager): MusicDirs {
val key = context.getString(R.string.set_key_music_dirs) val key = context.getString(R.string.set_key_music_dirs)
@ -214,9 +218,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
musicDirs.dirs.map(Directory::toDocumentUri).toSet()) musicDirs.dirs.map(Directory::toDocumentUri).toSet())
putBoolean( putBoolean(
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude) context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
apply()
// TODO: This is a stopgap measure before automatic rescanning, remove
commit()
} }
} }

View file

@ -83,6 +83,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* extendable. You have been warned. * extendable. You have been warned.
* *
* @author OxygenCobalt (With help from Umano and Hai Zhang) * @author OxygenCobalt (With help from Umano and Hai Zhang)
*
* FIXME: Inconsistent padding across recreates
*/ */
class BottomSheetLayout class BottomSheetLayout
@JvmOverloads @JvmOverloads

View file

@ -27,6 +27,7 @@
<string name="set_key_reindex" translatable="false">auxio_reindex</string> <string name="set_key_reindex" translatable="false">auxio_reindex</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_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_quality_tags">auxio_quality_tags</string>
<string name="set_key_search_filter" translatable="false">KEY_SEARCH_FILTER</string> <string name="set_key_search_filter" translatable="false">KEY_SEARCH_FILTER</string>

View file

@ -132,7 +132,7 @@
<string name="set_save">Save playback state</string> <string name="set_save">Save playback state</string>
<string name="set_save_desc">Save the current playback state now</string> <string name="set_save_desc">Save the current playback state now</string>
<string name="set_reindex">Reload music</string> <string name="set_reindex">Reload music</string>
<string name="set_reindex_desc">Will restart app</string> <string name="set_reindex_desc">May wipe playback state</string>
<string name="set_dirs">Music folders</string> <string name="set_dirs">Music folders</string>
<string name="set_dirs_desc">Manage where music should be loaded from</string> <string name="set_dirs_desc">Manage where music should be loaded from</string>
<string name="set_dirs_mode">Mode</string> <string name="set_dirs_mode">Mode</string>
@ -140,6 +140,8 @@
<string name="set_dirs_mode_exclude_desc">Music will <b>not</b> be loaded from the folders you add.</string> <string name="set_dirs_mode_exclude_desc">Music will <b>not</b> be loaded from the folders you add.</string>
<string name="set_dirs_mode_include">Include</string> <string name="set_dirs_mode_include">Include</string>
<string name="set_dirs_mode_include_desc">Music will <b>only</b> be loaded from the folders you add.</string> <string name="set_dirs_mode_include_desc">Music will <b>only</b> be loaded from the folders you add.</string>
<string name="set_quality_tags">Ignore MediaStore tags</string>
<string name="set_quality_tags_desc">Increases tag quality, but requires longer loading times</string>
<!-- Error Namespace | Error Labels --> <!-- Error Namespace | Error Labels -->
<string name="err_no_music">No music found</string> <string name="err_no_music">No music found</string>

View file

@ -173,5 +173,13 @@
app:summary="@string/set_dirs_desc" app:summary="@string/set_dirs_desc"
app:title="@string/set_dirs" /> app:title="@string/set_dirs" />
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:allowDividerBelow="false"
app:defaultValue="false"
app:iconSpaceReserved="false"
app:key="@string/set_key_quality_tags"
app:summary="@string/set_quality_tags_desc"
app:title="@string/set_quality_tags" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>