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