From d15055cc29fed30fc97520deecb8bdb8eb04a30a Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Tue, 7 Jun 2022 17:50:11 -0600 Subject: [PATCH] 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". --- .../java/org/oxycblt/auxio/music/MusicUtil.kt | 16 +- .../auxio/music/backend/ExoPlayerBackend.kt | 154 +++++++++++------- 2 files changed, 111 insertions(+), 59 deletions(-) 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 e7f57fb76..bb6a907ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt index 3f2093487..97fce0359 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt @@ -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() + val vorbisTags = mutableMapOf() + // 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) { + // 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) { + // 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 } } /**