music: accept native m4a multi-value tags

M4A has it's own multi-value spec that works similarly to vorbis, where
they just repeat the atom several times with multiple values. Since M4A
atoms are remapped to ID3v2 frames, this more or less requires us to
tolerate duplicate ID3v2 frames as well, which is frustratingly a spec
violation. It solves the problem though

Resolves #558.
This commit is contained in:
Alexander Capehart 2023-10-16 20:51:19 -06:00
parent 2fe0f3e7d8
commit b19b6665bb
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 39 additions and 6 deletions

View file

@ -5,9 +5,13 @@
#### What's New
- Added ability to rewind/skip tracks by swiping back/forward
#### What's Improved
- Added support for native M4A multi-value tags based on duplicate atoms
#### What's Fixed
- Fixed app restart being required when changing intelligent sorting
or music separator settings
- Fixed widget/notification actions not working on Android 14
## 3.2.0

View file

@ -22,17 +22,16 @@ import androidx.media3.common.Metadata
import androidx.media3.extractor.metadata.id3.InternalFrame
import androidx.media3.extractor.metadata.id3.TextInformationFrame
import androidx.media3.extractor.metadata.vorbis.VorbisComment
import org.oxycblt.auxio.util.logD
/**
* Processing wrapper for [Metadata] that allows organized access to text-based audio tags.
*
* @param metadata The [Metadata] to wrap.
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Merge with TagWorker
*/
class TextTags(metadata: Metadata) {
private val _id3v2 = mutableMapOf<String, List<String>>()
private val _id3v2 = mutableMapOf<String, MutableList<String>>()
/** The ID3v2 text identification frames found in the file. Can have more than one value. */
val id3v2: Map<String, List<String>>
get() = _id3v2
@ -53,7 +52,11 @@ class TextTags(metadata: Metadata) {
?: tag.id.sanitize()
val values = tag.values.map { it.sanitize() }.correctWhitespace()
if (values.isNotEmpty()) {
_id3v2[id] = values
// Normally, duplicate ID3v2 frames are forbidden. But since MP4 atoms,
// which can also have duplicates, are mapped to ID3v2 frames by ExoPlayer,
// we must drop this invariant and gracefully treat duplicates as if they
// are another way of specfiying multi-value tags.
_id3v2.getOrPut(id) { mutableListOf() }.addAll(values)
}
}
is InternalFrame -> {
@ -62,7 +65,7 @@ class TextTags(metadata: Metadata) {
val id = "TXXX:${tag.description.sanitize().lowercase()}"
val value = tag.text
if (value.isNotEmpty()) {
_id3v2[id] = listOf(value)
_id3v2.getOrPut(id) { mutableListOf() }.add(value)
}
}
is VorbisComment -> {

View file

@ -56,7 +56,20 @@ class TextTagsTest {
}
@Test
fun textTags_combined() {
fun textTags_mp4() {
val textTags = TextTags(MP4_METADATA)
assertTrue(textTags.vorbis.isEmpty())
assertEquals(listOf("Wheel"), textTags.id3v2["TIT2"])
assertEquals(listOf("Paraglow"), textTags.id3v2["TALB"])
assertEquals(listOf("Parannoul", "Asian Glow"), textTags.id3v2["TPE1"])
assertEquals(listOf("2022"), textTags.id3v2["TDRC"])
assertEquals(listOf("ep"), textTags.id3v2["TXXX:musicbrainz album type"])
assertEquals(listOf("+2 dB"), textTags.id3v2["TXXX:replaygain_track_gain"])
assertEquals(null, textTags.id3v2["metadata_block_picture"])
}
@Test
fun textTags_id3v2_vorbis_combined() {
val textTags = TextTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA))
assertEquals(listOf("Wheel"), textTags.vorbis["title"])
assertEquals(listOf("Paraglow"), textTags.vorbis["album"])
@ -95,6 +108,19 @@ class TextTagsTest {
TextInformationFrame("TPE1", null, listOf("Parannoul", "Asian Glow")),
TextInformationFrame("TDRC", null, listOf("2022")),
TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")),
TextInformationFrame("TXXX", "replaygain_track_gain", listOf("+2 dB")),
ApicFrame("", "", 0, byteArrayOf()))
// MP4 atoms are mapped to ID3v2 text information frames by ExoPlayer, but can
// duplicate frames and have ---- mapped to InternalFrame.
private val MP4_METADATA =
Metadata(
TextInformationFrame("TIT2", null, listOf("Wheel")),
TextInformationFrame("TALB", null, listOf("Paraglow")),
TextInformationFrame("TPE1", null, listOf("Parannoul")),
TextInformationFrame("TPE1", null, listOf("Asian Glow")),
TextInformationFrame("TDRC", null, listOf("2022")),
TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")),
InternalFrame("com.apple.iTunes", "replaygain_track_gain", "+2 dB"),
ApicFrame("", "", 0, byteArrayOf()))
}