music: refactor separator parsing

Add separator parsing back, albeit in a much different manner.

Now Auxio will try to parse by , ; / + and &. This will be disabled by
default in the future and available as a setting. When parsing by
separator, whitespace is also trimmed. This occurs nowhere else though,
as there is no demand.
This commit is contained in:
Alexander Capehart 2022-09-07 21:51:47 -06:00
parent 2033e2cb1f
commit 81ca021ce7
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 386 additions and 354 deletions

View file

@ -54,7 +54,14 @@ sealed class Music : Item {
* fast-scrolling.
*/
val sortName: String?
get() = rawSortName ?: rawName?.parseSortName()
get() = rawSortName ?: rawName?.run {
when {
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
else -> this
}
}
/**
* Resolve a name from it's raw form to a form suitable to be shown in a ui. Ex. "unknown" would
@ -79,7 +86,7 @@ sealed class Music : Item {
* external sources, as it can persist across app restarts and does not need to encode useless
* information about the relationships between items.
*
* TODO: MusizBrainz tags
* TODO: MusicBrainz tags
*
* @author OxygenCobalt
*/
@ -186,19 +193,24 @@ class Song constructor(private val raw: Raw) : Music() {
// TODO: Multi-artist support
// private val _artists: MutableList<Artist> = mutableListOf()
private val artistName = raw.artistNames?.joinToString()
private val albumArtistName = raw.albumArtistNames?.joinToString()
private val artistSortName = raw.artistSortNames?.joinToString()
private val albumArtistSortName = raw.albumArtistSortNames?.joinToString()
/**
* The raw artist name for this song in particular. First uses the artist tag, and then falls
* back to the album artist tag (i.e parent artist name). Null if name is unknown.
*/
val individualArtistRawName: String?
get() = raw.artistName ?: album.artist.rawName
get() = artistName ?: album.artist.rawName
/**
* Resolve the artist name for this song in particular. First uses the artist tag, and then
* falls back to the album artist tag (i.e parent artist name)
*/
fun resolveIndividualArtistName(context: Context) =
raw.artistName ?: album.artist.resolveName(context)
artistName ?: album.artist.resolveName(context)
private val _genres: MutableList<Genre> = mutableListOf()
/**
@ -218,10 +230,10 @@ class Song constructor(private val raw: Raw) : Music() {
date = raw.date,
releaseType = raw.albumReleaseType,
rawArtist =
if (raw.albumArtistName != null) {
Artist.Raw(raw.albumArtistName, raw.albumArtistSortName)
if (albumArtistName != null) {
Artist.Raw(albumArtistName, albumArtistSortName)
} else {
Artist.Raw(raw.artistName, raw.artistSortName)
Artist.Raw(artistName, artistSortName)
})
val _rawGenres = raw.genreNames?.map { Genre.Raw(it) } ?: listOf(Genre.Raw(null))
@ -248,8 +260,8 @@ class Song constructor(private val raw: Raw) : Music() {
update(_rawAlbum.name.lowercase())
update(_rawAlbum.date)
update(raw.artistName)
update(raw.albumArtistName)
update(artistName)
update(albumArtistName)
update(track)
update(disc)
@ -275,10 +287,10 @@ class Song constructor(private val raw: Raw) : Music() {
var albumName: String? = null,
var albumSortName: String? = null,
var albumReleaseType: ReleaseType? = null,
var artistName: String? = null,
var artistSortName: String? = null,
var albumArtistName: String? = null,
var albumArtistSortName: String? = null,
var artistNames: List<String>? = null,
var artistSortNames: List<String>? = null,
var albumArtistNames: List<String>? = null,
var albumArtistSortNames: List<String>? = null,
var genreNames: List<String>? = null
)
}
@ -729,7 +741,7 @@ sealed class ReleaseType {
companion object {
fun parse(types: List<String>): ReleaseType {
val primary = types[0].trim()
val primary = types[0]
// Primary types should be the first one in sequence. The spec makes no mention of
// whether primary types are a pre-requisite for secondary types, so we assume that
@ -747,7 +759,7 @@ sealed class ReleaseType {
secondaryIdx: Int,
target: (Refinement?) -> ReleaseType
): ReleaseType {
val secondary = (getOrNull(secondaryIdx) ?: return target(null)).trim()
val secondary = (getOrNull(secondaryIdx) ?: return target(null))
return when {
// Compilation is the only weird secondary release type, as it could

View file

@ -28,6 +28,13 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.nonZeroOrNull
import java.util.UUID
/** Shortcut to resolve a year from a nullable date. Will return "No Date" if it is null. */
fun Date?.resolveYear(context: Context) =
this?.resolveYear(context) ?: context.getString(R.string.def_date)
/** Converts this string to a UUID, or returns null if it is not valid. */
fun String.toUuid() = try { UUID.fromString(this) } catch (e: IllegalArgumentException) { null }
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
fun ContentResolver.queryCursor(
uri: Uri,
@ -58,320 +65,3 @@ val Long.audioUri: Uri
/** Converts a [Long] Album ID into a URI pointing to MediaStore-cached album art. */
val Long.albumCoverUri: Uri
get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this)
fun String.toUuid() = try { UUID.fromString(this) } catch (e: IllegalArgumentException) { null }
/**
* Parse out the track number field as if the given Int is formatted as DTTT, where D Is the disc
* and T is the track number. Values of zero will be ignored under the assumption that they are
* invalid.
*/
fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull()
/**
* Parse out the disc number field as if the given Int is formatted as DTTT, where D Is the disc and
* T is the track number. Values of zero will be ignored under the assumption that they are invalid.
*/
fun Int.unpackDiscNo() = div(1000).nonZeroOrNull()
/**
* Parse out the number field from an NN/TT string that is typically found in DISC_NUMBER and
* CD_TRACK_NUMBER. Values of zero will be ignored under the assumption that they are invalid.
*/
fun String.parsePositionNum() = split('/', limit = 2)[0].toIntOrNull()?.nonZeroOrNull()
/** Parse a plain year from the field into a [Date]. */
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)
/** Shortcut to resolve a year from a nullable date. Will return "No Date" if it is null. */
fun Date?.resolveYear(context: Context) =
this?.resolveYear(context) ?: context.getString(R.string.def_date)
/**
* Slice a string so that any preceding articles like The/A(n) are truncated. This is hilariously
* anglo-centric, but it's also a bit of an expected feature in music players, so we implement it
* anyway.
*/
fun String.parseSortName() =
when {
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
length > 3 && startsWith("a ", ignoreCase = true) -> substring(2)
else -> this
}
/** Shortcut to parse a [ReleaseType] from a list of strings */
fun List<String>.parseReleaseType() = ReleaseType.parse(this)
/**
* Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map
* that Auxio uses.
*/
fun String.parseId3GenreName() = parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: listOf(this)
/**
* Decodes the genre names from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map
* that Auxio uses.
*/
fun List<String>.parseId3GenreName() = flatMap { it.parseId3GenreName() }
private fun String.parseId3v1Genre(): String? =
when {
// ID3v1 genres are a plain integer value without formatting, so in that case
// try to index the genre table with such.
isDigitsOnly() -> GENRE_TABLE.getOrNull(toInt())
// CR and RX are not technically ID3v1, but are formatted similarly to a plain number.
this == "CR" -> "Cover"
this == "RX" -> "Remix"
// Current name is fine.
else -> null
}
private fun String.parseId3v2Genre(): List<String>? {
val groups = (GENRE_RE.matchEntire(this) ?: return null).groupValues
val genres = mutableSetOf<String>()
// ID3v2 genres are far more complex and require string grokking to properly implement.
// You can read the spec for it here: https://id3.org/id3v2.3.0#TCON
// This implementation in particular is based off Mutagen's genre parser.
// Case 1: Genre IDs in the format (INT|RX|CR). If these exist, parse them as
// ID3v1 tags.
val genreIds = groups.getOrNull(1)
if (genreIds != null && genreIds.isNotEmpty()) {
val ids = genreIds.substring(1, genreIds.lastIndex).split(")(")
for (id in ids) {
id.parseId3v1Genre()?.let(genres::add)
}
}
// Case 2: Genre names as a normal string. The only case we have to look out for are
// escaped strings formatted as ((genre).
val genreName = groups.getOrNull(3)
if (genreName != null && genreName.isNotEmpty()) {
if (genreName.startsWith("((")) {
genres.add(genreName.substring(1))
} else {
genres.add(genreName)
}
}
return genres.toList()
}
/** Regex that implements matching for ID3v2's genre format. */
private val GENRE_RE = Regex("((?:\\(([0-9]+|RX|CR)\\))*)(.+)?")
/**
* A complete table of all the constant genre values for ID3(v2), including non-standard extensions.
* Note that we do not translate these, as that greatly increases technical complexity.
*/
private val GENRE_TABLE =
arrayOf(
// ID3 Standard
"Blues",
"Classic Rock",
"Country",
"Dance",
"Disco",
"Funk",
"Grunge",
"Hip-Hop",
"Jazz",
"Metal",
"New Age",
"Oldies",
"Other",
"Pop",
"R&B",
"Rap",
"Reggae",
"Rock",
"Techno",
"Industrial",
"Alternative",
"Ska",
"Death Metal",
"Pranks",
"Soundtrack",
"Euro-Techno",
"Ambient",
"Trip-Hop",
"Vocal",
"Jazz+Funk",
"Fusion",
"Trance",
"Classical",
"Instrumental",
"Acid",
"House",
"Game",
"Sound Clip",
"Gospel",
"Noise",
"AlternRock",
"Bass",
"Soul",
"Punk",
"Space",
"Meditative",
"Instrumental Pop",
"Instrumental Rock",
"Ethnic",
"Gothic",
"Darkwave",
"Techno-Industrial",
"Electronic",
"Pop-Folk",
"Eurodance",
"Dream",
"Southern Rock",
"Comedy",
"Cult",
"Gangsta",
"Top 40",
"Christian Rap",
"Pop/Funk",
"Jungle",
"Native American",
"Cabaret",
"New Wave",
"Psychadelic",
"Rave",
"Showtunes",
"Trailer",
"Lo-Fi",
"Tribal",
"Acid Punk",
"Acid Jazz",
"Polka",
"Retro",
"Musical",
"Rock & Roll",
"Hard Rock",
// Winamp extensions, more or less a de-facto standard
"Folk",
"Folk-Rock",
"National Folk",
"Swing",
"Fast Fusion",
"Bebob",
"Latin",
"Revival",
"Celtic",
"Bluegrass",
"Avantgarde",
"Gothic Rock",
"Progressive Rock",
"Psychedelic Rock",
"Symphonic Rock",
"Slow Rock",
"Big Band",
"Chorus",
"Easy Listening",
"Acoustic",
"Humour",
"Speech",
"Chanson",
"Opera",
"Chamber Music",
"Sonata",
"Symphony",
"Booty Bass",
"Primus",
"Porn Groove",
"Satire",
"Slow Jam",
"Club",
"Tango",
"Samba",
"Folklore",
"Ballad",
"Power Ballad",
"Rhythmic Soul",
"Freestyle",
"Duet",
"Punk Rock",
"Drum Solo",
"A capella",
"Euro-House",
"Dance Hall",
"Goa",
"Drum & Bass",
"Club-House",
"Hardcore",
"Terror",
"Indie",
"Britpop",
"Negerpunk",
"Polsk Punk",
"Beat",
"Christian Gangsta",
"Heavy Metal",
"Black Metal",
"Crossover",
"Contemporary Christian",
"Christian Rock",
"Merengue",
"Salsa",
"Thrash Metal",
"Anime",
"JPop",
"Synthpop",
// Winamp 5.6+ extensions, also used by EasyTAG.
// I only include this because post-rock is a based genre and deserves a slot.
"Abstract",
"Art Rock",
"Baroque",
"Bhangra",
"Big Beat",
"Breakbeat",
"Chillout",
"Downtempo",
"Dub",
"EBM",
"Eclectic",
"Electro",
"Electroclash",
"Emo",
"Experimental",
"Garage",
"Global",
"IDM",
"Illbient",
"Industro-Goth",
"Jam Band",
"Krautrock",
"Leftfield",
"Lounge",
"Math Rock",
"New Romantic",
"Nu-Breakz",
"Post-Punk",
"Post-Rock",
"Psytrance",
"Shoegaze",
"Space Rock",
"Trop Rock",
"World Music",
"Neoclassical",
"Audiobook",
"Audio Theatre",
"Neue Deutsche Welle",
"Podcast",
"Indie Rock",
"G-Funk",
"Dubstep",
"Garage Rock",
"Psybient",
// Auxio's extensions (Future garage is also based and deserves a slot)
"Future Garage")

View file

@ -18,7 +18,11 @@
package org.oxycblt.auxio.music
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.storage.StorageManager
@ -31,6 +35,7 @@ import java.lang.reflect.Method
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedMethod
/** A path to a file. [name] is the stripped file name, [parent] is the parent path. */
data class Path(val name: String, val parent: Directory)

View file

@ -28,11 +28,6 @@ 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.music.parseId3GenreName
import org.oxycblt.auxio.music.parsePositionNum
import org.oxycblt.auxio.music.parseReleaseType
import org.oxycblt.auxio.music.parseTimestamp
import org.oxycblt.auxio.music.parseYear
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
@ -238,15 +233,15 @@ class Task(context: Context, private val raw: Song.Raw) {
tags["TSOA"]?.let { raw.albumSortName = it[0] }
// (Sort) Artist
tags["TPE1"]?.let { raw.artistName = it.joinToString() }
tags["TSOP"]?.let { raw.artistSortName = it.joinToString() }
tags["TPE1"]?.let { raw.artistNames = it.parseMultiValue() }
tags["TSOP"]?.let { raw.artistSortNames = it.parseMultiValue() }
// (Sort) Album artist
tags["TPE2"]?.let { raw.albumArtistName = it.joinToString() }
tags["TSO2"]?.let { raw.albumArtistSortName = it.joinToString() }
tags["TPE2"]?.let { raw.albumArtistNames = it.parseMultiValue() }
tags["TSO2"]?.let { raw.albumArtistSortNames = it.parseMultiValue() }
// Genre, with the weird ID3 rules.
tags["TCON"]?.let { raw.genreNames = it.parseId3GenreName() }
tags["TCON"]?.let { raw.genreNames = it.parseId3GenreNames() }
// Release type (GRP1 is sometimes used for this, so fall back to it)
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseReleaseType()?.let {
@ -302,15 +297,15 @@ class Task(context: Context, private val raw: Song.Raw) {
tags["ALBUMSORT"]?.let { raw.albumSortName = it[0] }
// (Sort) Artist
tags["ARTIST"]?.let { raw.artistName = it.joinToString() }
tags["ARTISTSORT"]?.let { raw.artistSortName = it.joinToString() }
tags["ARTIST"]?.let { raw.artistNames = it.parseMultiValue() }
tags["ARTISTSORT"]?.let { raw.artistSortNames = it.parseMultiValue() }
// (Sort) Album artist
tags["ALBUMARTIST"]?.let { raw.albumArtistName = it.joinToString() }
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortName = it.joinToString() }
tags["ALBUMARTIST"]?.let { raw.albumArtistNames = it.parseMultiValue() }
tags["ALBUMARTISTSORT"]?.let { raw.albumArtistSortNames = it.parseMultiValue() }
// Genre, no ID3 rules here
tags["GENRE"]?.let { raw.genreNames = it }
tags["GENRE"]?.let { raw.genreNames = it.parseMultiValue() }
// Release type
tags["RELEASETYPE"]?.parseReleaseType()?.let { raw.albumReleaseType = it }

View file

@ -189,7 +189,7 @@ 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).parseId3GenreName()
val name = (genreCursor.getStringOrNull(nameIndex) ?: continue).parseId3GenreNames()
context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
@ -293,13 +293,13 @@ abstract class MediaStoreBackend : Indexer.Backend {
// 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.
raw.artistName =
raw.artistNames =
cursor.getString(artistIndex).run {
if (this != MediaStore.UNKNOWN_STRING) this else null
if (this != MediaStore.UNKNOWN_STRING) listOf(this) else null
}
// The album artist field is nullable and never has placeholder values.
raw.albumArtistName = cursor.getStringOrNull(albumArtistIndex)
raw.albumArtistNames = cursor.getStringOrNull(albumArtistIndex)?.let { listOf(it) }
return raw
}

View file

@ -0,0 +1,329 @@
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.util.nonZeroOrNull
/**
* Parse out the track number field as if the given Int is formatted as DTTT, where D Is the disc
* and T is the track number. Values of zero will be ignored under the assumption that they are
* invalid.
*/
fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull()
/**
* Parse out the disc number field as if the given Int is formatted as DTTT, where D Is the disc and
* T is the track number. Values of zero will be ignored under the assumption that they are invalid.
*/
fun Int.unpackDiscNo() = div(1000).nonZeroOrNull()
/**
* Parse out the number field from an NN/TT string that is typically found in DISC_NUMBER and
* CD_TRACK_NUMBER. Values of zero will be ignored under the assumption that they are invalid.
*/
fun String.parsePositionNum() = split('/', limit = 2)[0].toIntOrNull()?.nonZeroOrNull()
/** Parse a plain year from the field into a [Date]. */
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("\\\\[\\[,;/+&]")
fun List<String>.parseMultiValue() =
if (size == 1) {
get(0).parseSeparatorsImpl()
} else {
this
}
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 List<String>.parseReleaseType() =
if (size == 1) {
ReleaseType.parse(get(0).parseSeparatorsImpl())
} else {
ReleaseType.parse(this)
}
fun List<String>.parseId3GenreNames() =
if (size == 1) {
get(0).parseId3GenreNames()
} else {
map { it.parseId3v1Genre() ?: it }
}
fun String.parseId3GenreNames() =
parseId3v1Genre()?.let { listOf(it) } ?:
parseId3v2Genre() ?:
parseSeparatorsImpl()
private fun String.parseId3v1Genre(): String? =
when {
// ID3v1 genres are a plain integer value without formatting, so in that case
// try to index the genre table with such.
isDigitsOnly() -> GENRE_TABLE.getOrNull(toInt())
// CR and RX are not technically ID3v1, but are formatted similarly to a plain number.
this == "CR" -> "Cover"
this == "RX" -> "Remix"
// Current name is fine.
else -> null
}
private fun String.parseId3v2Genre(): List<String>? {
val groups = (GENRE_RE.matchEntire(this) ?: return null).groupValues
val genres = mutableSetOf<String>()
// ID3v2 genres are far more complex and require string grokking to properly implement.
// You can read the spec for it here: https://id3.org/id3v2.3.0#TCON
// This implementation in particular is based off Mutagen's genre parser.
// Case 1: Genre IDs in the format (INT|RX|CR). If these exist, parse them as
// ID3v1 tags.
val genreIds = groups.getOrNull(1)
if (genreIds != null && genreIds.isNotEmpty()) {
val ids = genreIds.substring(1, genreIds.lastIndex).split(")(")
for (id in ids) {
id.parseId3v1Genre()?.let(genres::add)
}
}
// Case 2: Genre names as a normal string. The only case we have to look out for are
// escaped strings formatted as ((genre).
val genreName = groups.getOrNull(3)
if (genreName != null && genreName.isNotEmpty()) {
if (genreName.startsWith("((")) {
genres.add(genreName.substring(1))
} else {
genres.add(genreName)
}
}
// If this parsing task didn't change anything, move on.
if (genres.size == 1 && genres.first() == this) {
return null
}
return genres.toList()
}
/** Regex that implements matching for ID3v2's genre format. */
private val GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?")
/**
* A complete table of all the constant genre values for ID3(v2), including non-standard extensions.
* Note that we do not translate these, as that greatly increases technical complexity.
*/
private val GENRE_TABLE =
arrayOf(
// ID3 Standard
"Blues",
"Classic Rock",
"Country",
"Dance",
"Disco",
"Funk",
"Grunge",
"Hip-Hop",
"Jazz",
"Metal",
"New Age",
"Oldies",
"Other",
"Pop",
"R&B",
"Rap",
"Reggae",
"Rock",
"Techno",
"Industrial",
"Alternative",
"Ska",
"Death Metal",
"Pranks",
"Soundtrack",
"Euro-Techno",
"Ambient",
"Trip-Hop",
"Vocal",
"Jazz+Funk",
"Fusion",
"Trance",
"Classical",
"Instrumental",
"Acid",
"House",
"Game",
"Sound Clip",
"Gospel",
"Noise",
"AlternRock",
"Bass",
"Soul",
"Punk",
"Space",
"Meditative",
"Instrumental Pop",
"Instrumental Rock",
"Ethnic",
"Gothic",
"Darkwave",
"Techno-Industrial",
"Electronic",
"Pop-Folk",
"Eurodance",
"Dream",
"Southern Rock",
"Comedy",
"Cult",
"Gangsta",
"Top 40",
"Christian Rap",
"Pop/Funk",
"Jungle",
"Native American",
"Cabaret",
"New Wave",
"Psychadelic",
"Rave",
"Showtunes",
"Trailer",
"Lo-Fi",
"Tribal",
"Acid Punk",
"Acid Jazz",
"Polka",
"Retro",
"Musical",
"Rock & Roll",
"Hard Rock",
// Winamp extensions, more or less a de-facto standard
"Folk",
"Folk-Rock",
"National Folk",
"Swing",
"Fast Fusion",
"Bebob",
"Latin",
"Revival",
"Celtic",
"Bluegrass",
"Avantgarde",
"Gothic Rock",
"Progressive Rock",
"Psychedelic Rock",
"Symphonic Rock",
"Slow Rock",
"Big Band",
"Chorus",
"Easy Listening",
"Acoustic",
"Humour",
"Speech",
"Chanson",
"Opera",
"Chamber Music",
"Sonata",
"Symphony",
"Booty Bass",
"Primus",
"Porn Groove",
"Satire",
"Slow Jam",
"Club",
"Tango",
"Samba",
"Folklore",
"Ballad",
"Power Ballad",
"Rhythmic Soul",
"Freestyle",
"Duet",
"Punk Rock",
"Drum Solo",
"A capella",
"Euro-House",
"Dance Hall",
"Goa",
"Drum & Bass",
"Club-House",
"Hardcore",
"Terror",
"Indie",
"Britpop",
"Negerpunk",
"Polsk Punk",
"Beat",
"Christian Gangsta",
"Heavy Metal",
"Black Metal",
"Crossover",
"Contemporary Christian",
"Christian Rock",
"Merengue",
"Salsa",
"Thrash Metal",
"Anime",
"JPop",
"Synthpop",
// Winamp 5.6+ extensions, also used by EasyTAG.
// I only include this because post-rock is a based genre and deserves a slot.
"Abstract",
"Art Rock",
"Baroque",
"Bhangra",
"Big Beat",
"Breakbeat",
"Chillout",
"Downtempo",
"Dub",
"EBM",
"Eclectic",
"Electro",
"Electroclash",
"Emo",
"Experimental",
"Garage",
"Global",
"IDM",
"Illbient",
"Industro-Goth",
"Jam Band",
"Krautrock",
"Leftfield",
"Lounge",
"Math Rock",
"New Romantic",
"Nu-Breakz",
"Post-Punk",
"Post-Rock",
"Psytrance",
"Shoegaze",
"Space Rock",
"Trop Rock",
"World Music",
"Neoclassical",
"Audiobook",
"Audio Theatre",
"Neue Deutsche Welle",
"Podcast",
"Indie Rock",
"G-Funk",
"Dubstep",
"Garage Rock",
"Psybient",
// Auxio's extensions (Future garage is also based and deserves a slot)
"Future Garage")

View file

@ -130,7 +130,7 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
// capitalization is consistent before continuing.
is TextInformationFrame -> {
key = entry.description?.uppercase()
value = entry.value
value = entry.values[0]
}
// Vorbis comment. These are nearly always uppercase, so a check for such is
// skipped.

View file

@ -74,7 +74,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
}
else -> {
error("Unexpected menu item selected")
return@musicMenuImpl false
}
}
@ -142,7 +141,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
}
else -> {
error("Unexpected menu item selected")
return@musicMenuImpl false
}
}

View file

@ -24,6 +24,7 @@ import java.lang.reflect.Method
import java.util.concurrent.CancellationException
import kotlin.reflect.KClass
import org.oxycblt.auxio.BuildConfig
import java.util.*
/** Assert that we are on a background thread. */
fun requireBackgroundThread() {
@ -43,8 +44,10 @@ fun <T> unlikelyToBeNull(value: T?) =
value!!
}
/** Returns null if this value is 0. */
fun Int.nonZeroOrNull() = if (this > 0) this else null
/** Returns null if this value is not in [range]. */
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
/** Converts a long in milliseconds to a long in deci-seconds */