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())) }