From b19b6665bb5a827d1b643c0e40e30bce3f7f8ac0 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 16 Oct 2023 20:51:19 -0600 Subject: [PATCH] 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. --- CHANGELOG.md | 4 +++ .../oxycblt/auxio/music/metadata/TextTags.kt | 13 +++++---- .../auxio/music/metadata/TextTagsTest.kt | 28 ++++++++++++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2509c153..8d72ef4c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt index a3d916b69..737ee6f8c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TextTags.kt @@ -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>() + private val _id3v2 = mutableMapOf>() /** The ID3v2 text identification frames found in the file. Can have more than one value. */ val id3v2: Map> 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 -> { diff --git a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt b/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt index 73c5b926d..9966c16e9 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/metadata/TextTagsTest.kt @@ -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())) }