music: improve exoplayer tag management

Extend the range of tags Auxio is capable of recognizing, removing some
edge-cases in the process.

This is mostly three changes:
1. Auxio no longer tolerates tracks/discs/years that are 0. These are
usually never valid.
2. Auxio now has support for original date/year values. This are niche
tags, but I was inspired to implement it by this HN discussion at
https://news.ycombinator.com/item?id=31659799 simply because it
differentiates Auxio.
3. Auxio can handle the more esoteric tag variations in vorbis, such
as "YEAR" or "ALBUM ARTIST".
This commit is contained in:
OxygenCobalt 2022-06-07 17:50:11 -06:00
parent c929357d76
commit d15055cc29
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
2 changed files with 111 additions and 59 deletions

View file

@ -57,18 +57,26 @@ val Long.albumCoverUri: Uri
/**
* Parse out the number field from an NN/TT string that is typically found in DISC_NUMBER and
* CD_TRACK_NUMBER.
* CD_TRACK_NUMBER. Values of zero will be ignored under the assumption that they are invalid.
*/
val String.no: Int?
get() = split('/', limit = 2).getOrNull(0)?.toIntOrNull()
get() = split('/', limit = 2)[0].toIntOrNull()?.let { if (it > 0) it else null }
/**
* Parse out a plain year from a string. Values of 0 will be ignored under the assumption that they
* are invalid.
*/
val String.year: Int?
get() = toIntOrNull()?.let { if (it > 0) it else null }
/**
* Parse out the year field from a (presumably) ISO-8601-like date. This differs across tag formats
* and has no real consistency, but it's assumed that most will format granular dates as YYYY-MM-DD
* (...) and thus we can parse the year out by splitting at the first -.
* (...) and thus we can parse the year out by splitting at the first -. Values of 0 will be ignored
* under the assumption that they are invalid.
*/
val String.iso8601year: Int?
get() = split('-', limit = 2).getOrNull(0)?.toIntOrNull()
get() = split('-', limit = 2)[0].toIntOrNull()?.let { if (it > 0) it else null }
/**
* Slice a string so that any preceding articles like The/A(n) are truncated. This is hilariously

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.audioUri
import org.oxycblt.auxio.music.id3GenreName
import org.oxycblt.auxio.music.iso8601year
import org.oxycblt.auxio.music.no
import org.oxycblt.auxio.music.year
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
@ -158,72 +159,115 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
}
private fun completeAudio(metadata: Metadata) {
val id3v2Tags = mutableMapOf<String, String>()
val vorbisTags = mutableMapOf<String, String>()
// ExoPlayer only exposes ID3v2 and Vorbis metadata, which constitutes the vast majority
// of audio formats. Some formats (like FLAC) can contain both ID3v2 and vorbis tags, but
// this isn't too big of a deal, as we generally let the "source of truth" for metadata
// be the last instance of a particular tag in a file.
// of audio formats. Load both of these types of tags into separate maps, letting the
// "source of truth" be the last of a particular tag in a file.
for (i in 0 until metadata.length()) {
when (val tag = metadata[i]) {
is TextInformationFrame -> populateWithId3v2(tag)
is VorbisComment -> populateWithVorbis(tag)
is TextInformationFrame -> {
val id = tag.id.sanitize()
val value = tag.value.sanitize()
if (value.isNotEmpty()) {
id3v2Tags[id] = value
}
}
is VorbisComment -> {
val id = tag.value.sanitize()
val value = tag.value.sanitize()
if (value.isNotEmpty()) {
vorbisTags[id] = value
}
}
}
}
when {
vorbisTags.isEmpty() -> populateId3v2(id3v2Tags)
id3v2Tags.isEmpty() -> populateVorbis(vorbisTags)
else -> {
// Some formats (like FLAC) can contain both ID3v2 and Vorbis, so we apply
// them both with priority given to vorbis.
populateId3v2(id3v2Tags)
populateVorbis(vorbisTags)
}
}
}
private fun populateWithId3v2(frame: TextInformationFrame) {
val id = frame.id.sanitize()
val value = frame.value.sanitize()
if (value.isEmpty()) {
return
}
private fun populateId3v2(tags: Map<String, String>) {
// Title
tags["TIT2"]?.let { audio.title = it }
when (id) {
// Title
"TIT2" -> audio.title = value
// Track, as NN/TT
"TRCK" -> value.no?.let { audio.track = it }
// Disc, as NN/TT
"TPOS" -> value.no?.let { audio.disc = it }
// ID3v2.3 year, should be digits
"TYER" -> value.toIntOrNull()?.let { audio.year = it }
// ID3v2.4 year, parse as ISO-8601
"TDRC" -> value.iso8601year?.let { audio.year = it }
// Album
"TALB" -> audio.album = value
// Artist
"TPE1" -> audio.artist = value
// Album artist
"TPE2" -> audio.albumArtist = value
// Genre, with the weird ID3 rules
"TCON" -> audio.genre = value.id3GenreName
}
// Track, as NN/TT
tags["TRCK"]?.no?.let { audio.track = it }
// Disc, as NN/TT
tags["TPOS"]?.no?.let { audio.disc = it }
// Dates are somewhat complicated, as not only did their semantics change from a flat year
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
// date types.
// Our hierarchy for dates is as such:
// 1. ID3v2.4 Original Date, as it resolves the "Released in X, Remastered in Y" issue
// 2. ID3v2.4 Recording Date, as it is the most common date type
// 3. ID3v2.4 Release Date, as it is the second most common date type
// 4. ID3v2.3 Original Date, as it is like #1
// 5. ID3v2.3 Release Year, as it is the most common date type
tags["TYER"]?.year?.let { audio.year = it }
tags["TORY"]?.year?.let { audio.year = it }
tags["TDRL"]?.iso8601year?.let { audio.year = it }
tags["TDRC"]?.iso8601year?.let { audio.year = it }
tags["TDOR"]?.iso8601year?.let { audio.year = it }
// Album
tags["TALB"]?.let { audio.album = it }
// Artist
tags["TPE1"]?.let { audio.artist = it }
// Album artist
tags["TPE2"]?.let { audio.albumArtist = it }
// Genre, with the weird ID3 rules.
tags["TCON"]?.let { audio.genre = it.id3GenreName }
}
private fun populateWithVorbis(comment: VorbisComment) {
val key = comment.key.sanitize()
val value = comment.value.sanitize()
if (value.isEmpty()) {
return
}
private fun populateVorbis(tags: Map<String, String>) {
// Title
tags["TITLE"]?.let { audio.title = it }
when (key) {
// Title
"TITLE" -> audio.title = value
// Track, might be NN/TT
"TRACKNUMBER" -> value.no?.let { audio.track = it }
// Disc, might be NN/TT
"DISCNUMBER" -> value.no?.let { audio.disc = it }
// Date, presumably as ISO-8601
"DATE" -> value.iso8601year?.let { audio.year = it }
// Album
"ALBUM" -> audio.album = value
// Artist
"ARTIST" -> audio.artist = value
// Album artist
"ALBUMARTIST" -> audio.albumArtist = value
// Genre, assumed that ID3 rules do not apply here.
"GENRE" -> audio.genre = value
}
// Track, might be NN/TT, most often though TOTALTRACKS handles T.
tags["TRACKNUMBER"]?.no?.let { audio.track = it }
// Disc, might be NN/TT, most often though TOTALDISCS handles T.
tags["DISCNUMBER"]?.no?.let { audio.disc = it }
// Vorbis dates are less complicated, but there are still several types
// Our hierarchy for dates is as such:
// 1. Original Date, as it solves the "Released in X, Remastered in Y" issue
// 2. Date, as it is the most common date type
// 3. Year, as old vorbis tags tended to use this (I know this because it's the only
// tag that android supports, so it must be 15 years old or more!)
tags["YEAR"]?.year?.let { audio.year = it }
tags["DATE"]?.iso8601year?.let { audio.year = it }
tags["ORIGINALDATE"]?.iso8601year?.let { audio.year = it }
// Album
tags["ALBUM"]?.let { audio.album = it }
// Artist
tags["ARTIST"]?.let { audio.title }
// Album artist. This actually comes into two flavors:
// 1. ALBUMARTIST, which is the most common
// 2. ALBUM ARTIST, which is present on older vorbis tags
tags["ALBUM ARTIST"]?.let { audio.albumArtist = it }
tags["ALBUMARTIST"]?.let { audio.albumArtist = it }
// Genre, no ID3 rules here
tags["GENRE"]?.let { audio.genre = it }
}
/**