music: add basic framework for separators
Add a basic framework for user-customizeable separators. This is designed to be extendable if needed, albeit the app will likely only let the user choose between +, ;, /, &, and ,.
This commit is contained in:
parent
574224ff99
commit
4c954e83b0
5 changed files with 76 additions and 33 deletions
|
@ -28,6 +28,7 @@ import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
|||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.audioUri
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
@ -122,6 +123,7 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class Task(context: Context, private val raw: Song.Raw) {
|
||||
private val settings = Settings(context)
|
||||
private val future =
|
||||
MetadataRetriever.retrieveMetadata(
|
||||
context,
|
||||
|
@ -233,18 +235,18 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
tags["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||
|
||||
// (Sort) Artist
|
||||
tags["TPE1"]?.let { raw.artistNames = it.parseMultiValue() }
|
||||
tags["TSOP"]?.let { raw.artistSortNames = it.parseMultiValue() }
|
||||
tags["TPE1"]?.let { raw.artistNames = it.parseMultiValue(settings) }
|
||||
tags["TSOP"]?.let { raw.artistSortNames = it.parseMultiValue(settings) }
|
||||
|
||||
// (Sort) Album artist
|
||||
tags["TPE2"]?.let { raw.albumArtistNames = it.parseMultiValue() }
|
||||
tags["TSO2"]?.let { raw.albumArtistSortNames = it.parseMultiValue() }
|
||||
tags["TPE2"]?.let { raw.albumArtistNames = it.parseMultiValue(settings) }
|
||||
tags["TSO2"]?.let { raw.albumArtistSortNames = it.parseMultiValue(settings) }
|
||||
|
||||
// Genre, with the weird ID3 rules.
|
||||
tags["TCON"]?.let { raw.genreNames = it.parseId3GenreNames() }
|
||||
tags["TCON"]?.let { raw.genreNames = it.parseId3GenreNames(settings) }
|
||||
|
||||
// Release type (GRP1 is sometimes used for this, so fall back to it)
|
||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseReleaseType()?.let {
|
||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseReleaseType(settings)?.let {
|
||||
raw.albumReleaseType = it
|
||||
}
|
||||
}
|
||||
|
@ -297,18 +299,18 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
tags["ALBUMSORT"]?.let { raw.albumSortName = it[0] }
|
||||
|
||||
// (Sort) Artist
|
||||
tags["ARTIST"]?.let { raw.artistNames = it.parseMultiValue() }
|
||||
tags["ARTISTSORT"]?.let { raw.artistSortNames = it.parseMultiValue() }
|
||||
tags["ARTIST"]?.let { raw.artistNames = it.parseMultiValue(settings) }
|
||||
tags["ARTISTSORT"]?.let { raw.artistSortNames = it.parseMultiValue(settings) }
|
||||
|
||||
// (Sort) Album artist
|
||||
tags["ALBUMARTIST"]?.let { raw.albumArtistNames = it.parseMultiValue() }
|
||||
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortNames = it.parseMultiValue() }
|
||||
tags["ALBUMARTIST"]?.let { raw.albumArtistNames = it.parseMultiValue(settings) }
|
||||
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortNames = it.parseMultiValue(settings) }
|
||||
|
||||
// Genre, no ID3 rules here
|
||||
tags["GENRE"]?.let { raw.genreNames = it.parseMultiValue() }
|
||||
tags["GENRE"]?.let { raw.genreNames = it.parseMultiValue(settings) }
|
||||
|
||||
// Release type
|
||||
tags["RELEASETYPE"]?.parseReleaseType()?.let { raw.albumReleaseType = it }
|
||||
tags["RELEASETYPE"]?.parseReleaseType(settings)?.let { raw.albumReleaseType = it }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -91,6 +91,9 @@ import org.oxycblt.auxio.util.logD
|
|||
* I wish I was born in the neolithic.
|
||||
*/
|
||||
|
||||
// TODO: Make context a member var to cache Settings
|
||||
// TODO: Move duration util to MusicUtil
|
||||
|
||||
/**
|
||||
* Represents a [Indexer.Backend] that loads music from the media database ([MediaStore]). This is
|
||||
* not a fully-featured class by itself, and it's API-specific derivatives should be used instead.
|
||||
|
@ -163,6 +166,8 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
cursor: Cursor,
|
||||
emitIndexing: (Indexer.Indexing) -> Unit
|
||||
): List<Song> {
|
||||
val settings = Settings(context)
|
||||
|
||||
val rawSongs = mutableListOf<Song.Raw>()
|
||||
while (cursor.moveToNext()) {
|
||||
rawSongs.add(buildRawSong(context, cursor))
|
||||
|
@ -189,7 +194,8 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
// format a genre was derived from, we have to treat them like they are ID3
|
||||
// genres, even when they might not be.
|
||||
val id = genreCursor.getLong(idIndex)
|
||||
val name = (genreCursor.getStringOrNull(nameIndex) ?: continue).parseId3GenreNames()
|
||||
val name = (genreCursor.getStringOrNull(nameIndex) ?: continue)
|
||||
.parseId3GenreNames(settings)
|
||||
|
||||
context.contentResolverSafe.useQuery(
|
||||
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
|
||||
|
@ -249,6 +255,8 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
* outlined in [projection].
|
||||
*/
|
||||
open fun buildRawSong(context: Context, cursor: Cursor): Song.Raw {
|
||||
val settings = Settings(context)
|
||||
|
||||
// Initialize our cursor indices if we haven't already.
|
||||
if (idIndex == -1) {
|
||||
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||
|
@ -292,14 +300,19 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
// Android does not make a non-existent artist tag null, it instead fills it in
|
||||
// as <unknown>, which makes absolutely no sense given how other fields default
|
||||
// to null if they are not present. If this field is <unknown>, null it so that
|
||||
// it's easier to handle later.
|
||||
// it's easier to handle later. While we can't natively parse multi-value tags,
|
||||
// from MediaStore itself, we can still parse by user-defined separators.
|
||||
raw.artistNames =
|
||||
cursor.getString(artistIndex).run {
|
||||
if (this != MediaStore.UNKNOWN_STRING) listOf(this) else null
|
||||
if (this != MediaStore.UNKNOWN_STRING) {
|
||||
maybeParseSeparators(settings)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// The album artist field is nullable and never has placeholder values.
|
||||
raw.albumArtistNames = cursor.getStringOrNull(albumArtistIndex)?.let { listOf(it) }
|
||||
raw.albumArtistNames = cursor.getStringOrNull(albumArtistIndex)?.maybeParseSeparators(settings)
|
||||
|
||||
return raw
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.oxycblt.auxio.music.system
|
|||
import androidx.core.text.isDigitsOnly
|
||||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.music.ReleaseType
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
||||
/**
|
||||
|
@ -30,8 +31,8 @@ fun String.parseYear() = toIntOrNull()?.let(Date::from)
|
|||
/** Parse an ISO-8601 time-stamp from this field into a [Date]. */
|
||||
fun String.parseTimestamp() = Date.from(this)
|
||||
|
||||
private val SEPARATOR_REGEX = Regex("[^\\\\][\\[,;/+&]")
|
||||
private val ESCAPED_REGEX = Regex("\\\\[\\[,;/+&]")
|
||||
private val SEPARATOR_REGEX_CACHE = mutableMapOf<String, Regex>()
|
||||
private val ESCAPE_REGEX_CACHE = mutableMapOf<String, Regex>()
|
||||
|
||||
/**
|
||||
* Fully parse a multi-value tag.
|
||||
|
@ -41,36 +42,49 @@ private val ESCAPED_REGEX = Regex("\\\\[\\[,;/+&]")
|
|||
*
|
||||
* Alternatively, if there are several tags already, it will be returned without modification.
|
||||
*/
|
||||
fun List<String>.parseMultiValue() =
|
||||
fun List<String>.parseMultiValue(settings: Settings) =
|
||||
if (size == 1) {
|
||||
get(0).parseSeparatorsImpl()
|
||||
get(0).maybeParseSeparators(settings)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a tag into multiple values using a series of generic separators. Will trim whitespace.
|
||||
* Maybe a single tag into multi values with the user-preferred separators. If not enabled,
|
||||
* the plain string will be returned.
|
||||
*/
|
||||
private fun String.parseSeparatorsImpl() =
|
||||
// First split by non-escaped separators (No preceding \), and then split by escaped
|
||||
// separators.
|
||||
SEPARATOR_REGEX.split(this).map {
|
||||
ESCAPED_REGEX.replace(it) { match -> match.value.substring(1) }.trim()
|
||||
fun String.maybeParseSeparators(settings: Settings): List<String> {
|
||||
// Get the separators the user desires. If null, we don't parse any.
|
||||
val separators = settings.separators ?: return listOf(this)
|
||||
|
||||
// Try to cache compiled regexes for particular separator combinations.
|
||||
val regex = synchronized(SEPARATOR_REGEX_CACHE) {
|
||||
SEPARATOR_REGEX_CACHE.getOrPut(separators) { Regex("[^\\\\][$separators]") }
|
||||
}
|
||||
|
||||
val escape = synchronized(ESCAPE_REGEX_CACHE) {
|
||||
ESCAPE_REGEX_CACHE.getOrPut(separators) { Regex("\\\\[$separators]")}
|
||||
}
|
||||
|
||||
return regex.split(this).map { value ->
|
||||
// Convert escaped separators to their correct value
|
||||
escape.replace(value) { match -> match.value.substring(1) }.trim()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a multi-value tag into a [ReleaseType], handling separators in the process.
|
||||
*/
|
||||
fun List<String>.parseReleaseType() = ReleaseType.parse(parseMultiValue())
|
||||
fun List<String>.parseReleaseType(settings: Settings) = ReleaseType.parse(parseMultiValue(settings))
|
||||
|
||||
/**
|
||||
* Parse a multi-value genre name using ID3v2 rules. If there is one value, the ID3v2.3
|
||||
* rules will be used, followed by separator parsing. Otherwise, each value will be iterated
|
||||
* through, and numeric values transformed into string values.
|
||||
*/
|
||||
fun List<String>.parseId3GenreNames() =
|
||||
fun List<String>.parseId3GenreNames(settings: Settings) =
|
||||
if (size == 1) {
|
||||
get(0).parseId3GenreNames()
|
||||
get(0).parseId3GenreNames(settings)
|
||||
} else {
|
||||
map { it.parseId3v1Genre() ?: it }
|
||||
}
|
||||
|
@ -78,10 +92,10 @@ fun List<String>.parseId3GenreNames() =
|
|||
/**
|
||||
* Parse a single genre name using ID3v2.3 rules.
|
||||
*/
|
||||
fun String.parseId3GenreNames() =
|
||||
fun String.parseId3GenreNames(settings: Settings) =
|
||||
parseId3v1Genre()?.let { listOf(it) } ?:
|
||||
parseId3v2Genre() ?:
|
||||
parseSeparatorsImpl()
|
||||
maybeParseSeparators(settings)
|
||||
|
||||
private fun String.parseId3v1Genre(): String? =
|
||||
when {
|
||||
|
|
|
@ -219,13 +219,26 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of separators the user wants to parse by.
|
||||
*/
|
||||
var separators: String?
|
||||
// Differ from convention and store a string of separator characters instead of an int
|
||||
// code. This makes it easier to use in Regexes and makes it more extendable.
|
||||
get() = inner.getString(context.getString(R.string.set_key_separators), null)?.ifEmpty { null }
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putString(context.getString(R.string.set_key_separators), value)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The current filter mode of the search tab */
|
||||
var searchFilterMode: DisplayMode?
|
||||
get() =
|
||||
DisplayMode.fromInt(
|
||||
inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE))
|
||||
set(value) {
|
||||
logD(value)
|
||||
inner.edit {
|
||||
putInt(
|
||||
context.getString(R.string.set_key_search_filter),
|
||||
|
|
|
@ -25,13 +25,14 @@
|
|||
<string name="set_key_keep_shuffle" translatable="false">KEY_KEEP_SHUFFLE</string>
|
||||
<string name="set_key_rewind_prev" translatable="false">KEY_PREV_REWIND</string>
|
||||
<string name="set_key_repeat_pause" translatable="false">KEY_LOOP_PAUSE</string>
|
||||
|
||||
<string name="set_key_save_state" translatable="false">auxio_save_state</string>
|
||||
<string name="set_key_wipe_state" translatable="false">auxio_wipe_state</string>
|
||||
<string name="set_key_restore_state" translatable="false">auxio_restore_state</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_include" translatable="false">auxio_include_dirs</string>
|
||||
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
||||
<string name="set_key_observing" translatable="false">auxio_observing</string>
|
||||
<string name="set_key_quality_tags" translatable="false">auxio_quality_tags</string>
|
||||
|
||||
|
|
Loading…
Reference in a new issue