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
#### 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
- Widgets now have a more sleek and consistent button layout
- "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.Api29MediaStoreBackend
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.util.logD
import org.oxycblt.auxio.util.logE
@ -80,7 +82,7 @@ class Indexer {
return
}
synchronized(this) { this.controller = controller }
this.controller = controller
}
/** Unregister a [Controller] with this instance. */
@ -119,9 +121,8 @@ class Indexer {
this.callback = null
}
@Synchronized
fun index(context: Context) {
val generation = ++currentGeneration
val generation = synchronized(this) { ++currentGeneration }
val notGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
@ -227,7 +228,15 @@ class Indexer {
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()) {
return null
}

View file

@ -27,10 +27,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
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.
*
* @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 musicStore = MusicStore.getInstance()
@ -53,6 +60,7 @@ class IndexerService : Service(), Indexer.Controller {
private val updateScope = CoroutineScope(serviceJob + Dispatchers.Main)
private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var settings: Settings
private var isForeground = false
private lateinit var notification: IndexerNotification
@ -61,6 +69,7 @@ class IndexerService : Service(), Indexer.Controller {
super.onCreate()
notification = IndexerNotification(this)
settings = Settings(this, this)
indexer.registerController(this)
if (musicStore.library == null && indexer.isIndeterminate) {
@ -83,12 +92,14 @@ class IndexerService : Service(), Indexer.Controller {
indexer.cancelLast()
indexer.unregisterController(this)
serviceJob.cancel()
settings.release()
}
// --- CONTROLLER CALLBACKS ---
override fun onStartIndexing() {
if (indexer.isIndexing) {
indexer.cancelLast()
indexScope.cancel()
}
indexScope.launch { indexer.index(this@IndexerService) }
@ -103,9 +114,8 @@ class IndexerService : Service(), Indexer.Controller {
val newLibrary = state.response.library
// Load was completed successfully, so apply the new library if we
// have not already. Only when we are done updating the library will
// the service stop it's foreground state.
// Load was completed successfully. However, we still need to do some
// extra work to update the app's state.
updateScope.launch {
imageLoader.memoryCache?.clear()
@ -116,7 +126,7 @@ class IndexerService : Service(), Indexer.Controller {
PlaybackStateDatabase.getInstance(this@IndexerService), newLibrary)
}
withContext(Dispatchers.Main) { musicStore.updateLibrary(newLibrary) }
musicStore.updateLibrary(newLibrary)
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() {
if (isForeground) {
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
* might not be available at all times.
*
* TODO: Add automatic rescanning [major change]
* @author OxygenCobalt
*/
class MusicStore private constructor() {

View file

@ -184,7 +184,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
}
}
is VorbisComment -> {
val id = tag.value.sanitize()
val id = tag.key.sanitize()
val value = tag.value.sanitize()
if (value.isNotEmpty()) {
vorbisTags[id] = value
@ -244,6 +244,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
}
private fun populateVorbis(tags: Map<String, String>) {
logD(tags)
// Title
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 }
// Artist
tags["ARTIST"]?.let { audio.title }
tags["ARTIST"]?.let { audio.artist = it }
// Album artist. This actually comes into two flavors:
// 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.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
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.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD
@ -46,12 +42,11 @@ import org.oxycblt.auxio.util.showToast
*/
class MusicDirsDialog :
ViewBindingDialogFragment<DialogMusicDirsBinding>(), MusicDirAdapter.Listener {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val indexerModel: IndexerViewModel by activityViewModels()
private val dirAdapter = MusicDirAdapter(this)
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) =
DialogMusicDirsBinding.inflate(inflater)
@ -61,7 +56,17 @@ class MusicDirsDialog :
builder
.setTitle(R.string.set_dirs)
.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)
}
@ -80,19 +85,6 @@ class MusicDirsDialog :
logD("Opening launcher")
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 {
@ -100,7 +92,6 @@ class MusicDirsDialog :
itemAnimator = null
}
val storageManager = requireStorageManager()
var dirs = settings.getMusicDirs(storageManager)
if (savedInstanceState != null) {
@ -173,7 +164,7 @@ class MusicDirsDialog :
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
// Parsing handles the rest
return Directory.fromDocumentUri(requireStorageManager(), treeUri)
return Directory.fromDocumentUri(storageManager, treeUri)
}
private fun updateMode() {
@ -188,22 +179,6 @@ class MusicDirsDialog :
private fun isInclude(binding: DialogMusicDirsBinding) =
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 {
const val TAG = BuildConfig.APPLICATION_ID + ".tag.EXCLUDED"
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
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. */
fun getMusicDirs(storageManager: StorageManager): MusicDirs {
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())
putBoolean(
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
// TODO: This is a stopgap measure before automatic rescanning, remove
commit()
apply()
}
}

View file

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

View file

@ -27,6 +27,7 @@
<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_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>

View file

@ -132,7 +132,7 @@
<string name="set_save">Save playback state</string>
<string name="set_save_desc">Save the current playback state now</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_desc">Manage where music should be loaded from</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_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_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 -->
<string name="err_no_music">No music found</string>

View file

@ -173,5 +173,13 @@
app:summary="@string/set_dirs_desc"
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>
</PreferenceScreen>