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:
parent
d6f166a3ee
commit
3bdf7b136e
11 changed files with 79 additions and 58 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue