diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 810a89d63..cb6889a93 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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 = 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 = 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? = null, + var artistSortNames: List? = null, + var albumArtistNames: List? = null, + var albumArtistSortNames: List? = null, var genreNames: List? = null ) } @@ -729,7 +741,7 @@ sealed class ReleaseType { companion object { fun parse(types: List): 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt index d51c9f32f..6ccc54a6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -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.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.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? { - val groups = (GENRE_RE.matchEntire(this) ?: return null).groupValues - val genres = mutableSetOf() - - // 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") diff --git a/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt index b29e4beea..bd9d5b60c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt index a20d44233..77fbdcb3a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/ExoPlayerBackend.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt index 5d96d9713..e77cc5e8e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/MediaStoreBackend.kt @@ -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 , which makes absolutely no sense given how other fields default // to null if they are not present. If this field is , 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 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/system/ParsingUtil.kt new file mode 100644 index 000000000..acdb902b8 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/system/ParsingUtil.kt @@ -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.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.parseReleaseType() = + if (size == 1) { + ReleaseType.parse(get(0).parseSeparatorsImpl()) + } else { + ReleaseType.parse(this) + } + +fun List.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? { + val groups = (GENRE_RE.matchEntire(this) ?: return null).groupValues + val genres = mutableSetOf() + + // 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") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 64069c980..cef7527ff 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -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. diff --git a/app/src/main/java/org/oxycblt/auxio/ui/fragment/MenuFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/fragment/MenuFragment.kt index 26e031e7e..e174a3299 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/fragment/MenuFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/fragment/MenuFragment.kt @@ -74,7 +74,6 @@ abstract class MenuFragment : ViewBindingFragment() { } else -> { error("Unexpected menu item selected") - return@musicMenuImpl false } } @@ -142,7 +141,6 @@ abstract class MenuFragment : ViewBindingFragment() { } else -> { error("Unexpected menu item selected") - return@musicMenuImpl false } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index b9a81aa16..202d881f6 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -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 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 */