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:
parent
2fe0f3e7d8
commit
b19b6665bb
3 changed files with 39 additions and 6 deletions
|
@ -5,9 +5,13 @@
|
||||||
#### What's New
|
#### What's New
|
||||||
- Added ability to rewind/skip tracks by swiping back/forward
|
- 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
|
#### What's Fixed
|
||||||
- Fixed app restart being required when changing intelligent sorting
|
- Fixed app restart being required when changing intelligent sorting
|
||||||
or music separator settings
|
or music separator settings
|
||||||
|
- Fixed widget/notification actions not working on Android 14
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
|
|
|
@ -22,17 +22,16 @@ import androidx.media3.common.Metadata
|
||||||
import androidx.media3.extractor.metadata.id3.InternalFrame
|
import androidx.media3.extractor.metadata.id3.InternalFrame
|
||||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame
|
import androidx.media3.extractor.metadata.id3.TextInformationFrame
|
||||||
import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
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.
|
* Processing wrapper for [Metadata] that allows organized access to text-based audio tags.
|
||||||
*
|
*
|
||||||
* @param metadata The [Metadata] to wrap.
|
* @param metadata The [Metadata] to wrap.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*
|
|
||||||
* TODO: Merge with TagWorker
|
|
||||||
*/
|
*/
|
||||||
class TextTags(metadata: Metadata) {
|
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. */
|
/** The ID3v2 text identification frames found in the file. Can have more than one value. */
|
||||||
val id3v2: Map<String, List<String>>
|
val id3v2: Map<String, List<String>>
|
||||||
get() = _id3v2
|
get() = _id3v2
|
||||||
|
@ -53,7 +52,11 @@ class TextTags(metadata: Metadata) {
|
||||||
?: tag.id.sanitize()
|
?: tag.id.sanitize()
|
||||||
val values = tag.values.map { it.sanitize() }.correctWhitespace()
|
val values = tag.values.map { it.sanitize() }.correctWhitespace()
|
||||||
if (values.isNotEmpty()) {
|
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 -> {
|
is InternalFrame -> {
|
||||||
|
@ -62,7 +65,7 @@ class TextTags(metadata: Metadata) {
|
||||||
val id = "TXXX:${tag.description.sanitize().lowercase()}"
|
val id = "TXXX:${tag.description.sanitize().lowercase()}"
|
||||||
val value = tag.text
|
val value = tag.text
|
||||||
if (value.isNotEmpty()) {
|
if (value.isNotEmpty()) {
|
||||||
_id3v2[id] = listOf(value)
|
_id3v2.getOrPut(id) { mutableListOf() }.add(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is VorbisComment -> {
|
is VorbisComment -> {
|
||||||
|
|
|
@ -56,7 +56,20 @@ class TextTagsTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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))
|
val textTags = TextTags(VORBIS_METADATA.copyWithAppendedEntriesFrom(ID3V2_METADATA))
|
||||||
assertEquals(listOf("Wheel"), textTags.vorbis["title"])
|
assertEquals(listOf("Wheel"), textTags.vorbis["title"])
|
||||||
assertEquals(listOf("Paraglow"), textTags.vorbis["album"])
|
assertEquals(listOf("Paraglow"), textTags.vorbis["album"])
|
||||||
|
@ -95,6 +108,19 @@ class TextTagsTest {
|
||||||
TextInformationFrame("TPE1", null, listOf("Parannoul", "Asian Glow")),
|
TextInformationFrame("TPE1", null, listOf("Parannoul", "Asian Glow")),
|
||||||
TextInformationFrame("TDRC", null, listOf("2022")),
|
TextInformationFrame("TDRC", null, listOf("2022")),
|
||||||
TextInformationFrame("TXXX", "MusicBrainz Album Type", listOf("ep")),
|
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"),
|
InternalFrame("com.apple.iTunes", "replaygain_track_gain", "+2 dB"),
|
||||||
ApicFrame("", "", 0, byteArrayOf()))
|
ApicFrame("", "", 0, byteArrayOf()))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue