diff --git a/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagFieldsTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagFieldsTest.kt
new file mode 100644
index 000000000..9409f32e7
--- /dev/null
+++ b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagFieldsTest.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2024 Auxio Project
+ * TagFieldsTest.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.musikr.tag.parse
+
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.oxycblt.musikr.metadata.Metadata
+import org.oxycblt.musikr.metadata.Properties
+import org.oxycblt.musikr.tag.Date
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [30])
+class TagFieldsTest {
+
+ @Test
+ fun metadata_name_formats() {
+ // Test ID3v2 format
+ var metadata = createTestMetadata(id3v2Tags = mapOf("TIT2" to listOf("ID3 Title")))
+ assertEquals("ID3 Title", metadata.name())
+
+ // Test MP4 format
+ metadata = createTestMetadata(mp4Tags = mapOf("©nam" to listOf("MP4 Title")))
+ assertEquals("MP4 Title", metadata.name())
+
+ // Test Xiph format
+ metadata = createTestMetadata(xiphTags = mapOf("TITLE" to listOf("Xiph Title")))
+ assertEquals("Xiph Title", metadata.name())
+
+ // Test priority (xiph > mp4 > id3v2)
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf("TIT2" to listOf("ID3 Title")),
+ mp4Tags = mapOf("©nam" to listOf("MP4 Title")),
+ xiphTags = mapOf("TITLE" to listOf("Xiph Title"))
+ )
+ assertEquals("Xiph Title", metadata.name())
+ }
+
+ @Test
+ fun metadata_track_formats() {
+ // Test ID3v2 format
+ var metadata = createTestMetadata(id3v2Tags = mapOf("TRCK" to listOf("5/10")))
+ assertEquals(5, metadata.track())
+
+ // Test ID3v2 format without total
+ metadata = createTestMetadata(id3v2Tags = mapOf("TRCK" to listOf("5")))
+ assertEquals(5, metadata.track())
+
+ // Test MP4 format
+ metadata = createTestMetadata(mp4Tags = mapOf("trkn" to listOf("7/12")))
+ assertEquals(7, metadata.track())
+
+ // Test Xiph format
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "TRACKNUMBER" to listOf("9"),
+ "TOTALTRACKS" to listOf("15")
+ )
+ )
+ assertEquals(9, metadata.track())
+
+ // Test Xiph alternative total tracks
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "TRACKNUMBER" to listOf("8"),
+ "TRACKTOTAL" to listOf("16")
+ )
+ )
+ assertEquals(8, metadata.track())
+ }
+
+ @Test
+ fun metadata_disc_formats() {
+ // Test ID3v2 format
+ var metadata = createTestMetadata(id3v2Tags = mapOf("TPOS" to listOf("2/3")))
+ assertEquals(2, metadata.disc())
+
+ // Test MP4 format
+ metadata = createTestMetadata(mp4Tags = mapOf("disk" to listOf("1/2")))
+ assertEquals(1, metadata.disc())
+
+ // Test Xiph format
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "DISCNUMBER" to listOf("3"),
+ "TOTALDISCS" to listOf("4")
+ )
+ )
+ assertEquals(3, metadata.disc())
+
+ // Test Xiph alternative total discs
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "DISCNUMBER" to listOf("2"),
+ "DISCTOTAL" to listOf("5")
+ )
+ )
+ assertEquals(2, metadata.disc())
+ }
+
+ @Test
+ fun metadata_subtitle() {
+ var metadata = createTestMetadata(id3v2Tags = mapOf("TSST" to listOf("Bonus Disc")))
+ assertEquals("Bonus Disc", metadata.subtitle())
+
+ metadata = createTestMetadata(xiphTags = mapOf("DISCSUBTITLE" to listOf("Rarities")))
+ assertEquals("Rarities", metadata.subtitle())
+
+ // Test priority
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf("TSST" to listOf("ID3 Subtitle")),
+ xiphTags = mapOf("DISCSUBTITLE" to listOf("Xiph Subtitle"))
+ )
+ assertEquals("Xiph Subtitle", metadata.subtitle())
+ }
+
+ @Test
+ fun metadata_date_formats() {
+ // Test simple year
+ var metadata = createTestMetadata(id3v2Tags = mapOf("TDRC" to listOf("2022")))
+ assertEquals("2022", metadata.date().toString())
+
+ // Test ISO-8601 date
+ metadata = createTestMetadata(id3v2Tags = mapOf("TDRC" to listOf("2022-05-17")))
+ assertEquals("2022-05-17", metadata.date().toString())
+
+ // Test ISO-8601 datetime
+ metadata = createTestMetadata(id3v2Tags = mapOf("TDRC" to listOf("2022-05-17T14:30:00")))
+ assertEquals("2022-05-17T14:30:00Z", metadata.date().toString())
+
+ // Test Xiph date
+ metadata = createTestMetadata(xiphTags = mapOf("DATE" to listOf("2021-12-25")))
+ assertEquals("2021-12-25", metadata.date().toString())
+
+ // Test date priority (ORIGINALDATE > DATE)
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "DATE" to listOf("2023-01-01"),
+ "ORIGINALDATE" to listOf("2021-01-01")
+ )
+ )
+ assertEquals("2021-01-01", metadata.date().toString())
+
+ // Test MP4 date
+ metadata = createTestMetadata(mp4Tags = mapOf("©day" to listOf("2020-11-30")))
+ assertEquals("2020-11-30", metadata.date().toString())
+ }
+
+ @Test
+ fun metadata_id3v23Date() {
+ // Test ID3v2.3 date components
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TYER" to listOf("2019"),
+ "TDAT" to listOf("0523"), // May 23
+ "TIME" to listOf("2145") // 21:45
+ )
+ )
+ val date = metadata.date()
+ assertEquals(2019, date?.year)
+ assertEquals(5, date?.month)
+ assertEquals(23, date?.day)
+ assertEquals(21, date?.hour)
+ assertEquals(45, date?.minute)
+ }
+
+ @Test
+ fun metadata_albumInfo() {
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TALB" to listOf("Album Name"),
+ "TSOA" to listOf("Sort Album Name"),
+ "TXXX:RELEASETYPE" to listOf("compilation")
+ )
+ )
+
+ assertEquals("Album Name", metadata.albumName())
+ assertEquals("Sort Album Name", metadata.albumSortName())
+ assertEquals(listOf("compilation"), metadata.releaseTypes())
+ }
+
+ @Test
+ fun metadata_artistInfo() {
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TPE1" to listOf("Artist 1"),
+ "TPE2" to listOf("Album Artist 1"),
+ "TSOP" to listOf("Artist 1 Sort"),
+ "TSO2" to listOf("Album Artist 1 Sort")
+ )
+ )
+
+ assertEquals(listOf("Artist 1"), metadata.artistNames())
+ assertEquals(listOf("Album Artist 1"), metadata.albumArtistNames())
+ assertEquals(listOf("Artist 1 Sort"), metadata.artistSortNames())
+ assertEquals(listOf("Album Artist 1 Sort"), metadata.albumArtistSortNames())
+ }
+
+ @Test
+ fun metadata_musicBrainzIds() {
+ // Test different formats and variants of MusicBrainz identifiers
+ val metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "MUSICBRAINZ_RELEASETRACKID" to listOf("track-mb-id"),
+ "MUSICBRAINZ_ALBUMID" to listOf("album-mb-id")
+ )
+ )
+
+ assertEquals("track-mb-id", metadata.musicBrainzId())
+ assertEquals("album-mb-id", metadata.albumMusicBrainzId())
+
+ // Test Apple iTunes MP4 variants
+ val metadata2 = createTestMetadata(
+ mp4Tags = mapOf(
+ "----:COM.APPLE.ITUNES:MUSICBRAINZ RELEASE TRACK ID" to listOf("track-mb-id-2"),
+ "----:COM.APPLE.ITUNES:MUSICBRAINZ ALBUM ID" to listOf("album-mb-id-2")
+ )
+ )
+
+ assertEquals("track-mb-id-2", metadata2.musicBrainzId())
+ assertEquals("album-mb-id-2", metadata2.albumMusicBrainzId())
+ }
+
+ @Test
+ fun metadata_isCompilation() {
+ // Test various compilation flags
+ var metadata = createTestMetadata(
+ id3v2Tags = mapOf("TCMP" to listOf("1"))
+ )
+ assertTrue(metadata.isCompilation())
+
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf("TCMP" to listOf("0"))
+ )
+ assertFalse(metadata.isCompilation())
+
+ metadata = createTestMetadata(
+ xiphTags = mapOf("COMPILATION" to listOf("1"))
+ )
+ assertTrue(metadata.isCompilation())
+
+ metadata = createTestMetadata(
+ mp4Tags = mapOf("cpil" to listOf("1"))
+ )
+ assertTrue(metadata.isCompilation())
+
+ // Test invalid value
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf("TCMP" to listOf("yes"))
+ )
+ assertFalse(metadata.isCompilation())
+ }
+
+ @Test
+ fun metadata_replayGain() {
+ // Test ReplayGain track adjustment
+ var metadata = createTestMetadata(
+ xiphTags = mapOf("REPLAYGAIN_TRACK_GAIN" to listOf("-5.5 dB"))
+ )
+ assertEquals(-5.5f, metadata.replayGainTrackAdjustment())
+
+ // Test ReplayGain album adjustment
+ metadata = createTestMetadata(
+ xiphTags = mapOf("REPLAYGAIN_ALBUM_GAIN" to listOf("3.2 dB"))
+ )
+ assertEquals(3.2f, metadata.replayGainAlbumAdjustment())
+
+ // Test R128 track gain
+ metadata = createTestMetadata(
+ xiphTags = mapOf("R128_TRACK_GAIN" to listOf("-2560"))
+ )
+ // R128 is converted to match ReplayGain scale (-2560/256 + 5 = -5.0)
+ assertEquals(-5.0f, metadata.replayGainTrackAdjustment())
+
+ // Test zero value
+ metadata = createTestMetadata(
+ xiphTags = mapOf("REPLAYGAIN_TRACK_GAIN" to listOf("0.0 dB"))
+ )
+ assertNull(metadata.replayGainTrackAdjustment())
+ }
+
+ @Test
+ fun metadata_genreNames() {
+ var metadata = createTestMetadata(
+ id3v2Tags = mapOf("TCON" to listOf("Rock", "Electronic"))
+ )
+ assertEquals(listOf("Rock", "Electronic"), metadata.genreNames())
+
+ metadata = createTestMetadata(
+ mp4Tags = mapOf("©gen" to listOf("Hip Hop", "Jazz"))
+ )
+ assertEquals(listOf("Hip Hop", "Jazz"), metadata.genreNames())
+
+ metadata = createTestMetadata(
+ xiphTags = mapOf("GENRE" to listOf("Classical", "Ambient"))
+ )
+ assertEquals(listOf("Classical", "Ambient"), metadata.genreNames())
+ }
+
+ private fun createTestMetadata(
+ id3v2Tags: Map> = emptyMap(),
+ xiphTags: Map> = emptyMap(),
+ mp4Tags: Map> = emptyMap(),
+ cover: ByteArray? = null,
+ durationMs: Long = 1000,
+ bitrateKbps: Int = 320,
+ sampleRateHz: Int = 44100,
+ mimeType: String = "audio/mpeg"
+ ): Metadata {
+ val properties = Properties(mimeType, durationMs, bitrateKbps, sampleRateHz)
+ return Metadata(id3v2Tags, xiphTags, mp4Tags, cover, properties)
+ }
+}
\ No newline at end of file
diff --git a/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagParserTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagParserTest.kt
new file mode 100644
index 000000000..79135683f
--- /dev/null
+++ b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagParserTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2024 Auxio Project
+ * TagParserTest.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.musikr.tag.parse
+
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.oxycblt.musikr.metadata.Metadata
+import org.oxycblt.musikr.metadata.Properties
+import org.oxycblt.musikr.tag.Date
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [30])
+class TagParserTest {
+ private val tagParser = TagParser.new()
+
+ @Test
+ fun tagParser_parse_basic() {
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TIT2" to listOf("Test Song"),
+ "TALB" to listOf("Test Album"),
+ "TPE1" to listOf("Test Artist"),
+ "TPE2" to listOf("Test Album Artist"),
+ "TYER" to listOf("2020"),
+ "TRCK" to listOf("1/10"),
+ "TPOS" to listOf("1/2"),
+ "TCON" to listOf("Rock", "Electronic")
+ )
+ )
+
+ val tags = tagParser.parse(metadata)
+
+ assertEquals("Test Song", tags.name)
+ assertEquals("Test Album", tags.albumName)
+ assertEquals(listOf("Test Artist"), tags.artistNames)
+ assertEquals(listOf("Test Album Artist"), tags.albumArtistNames)
+ assertEquals(2020, tags.date?.year)
+ assertEquals(1, tags.track)
+ assertEquals(1, tags.disc)
+ assertEquals(listOf("Rock", "Electronic"), tags.genreNames)
+ }
+
+ @Test
+ fun tagParser_parse_multipleFormats() {
+ // Test priority handling between different tag formats
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TIT2" to listOf("ID3 Song"),
+ "TALB" to listOf("ID3 Album")
+ ),
+ xiphTags = mapOf(
+ "TITLE" to listOf("Xiph Song"),
+ "ALBUM" to listOf("Xiph Album")
+ ),
+ mp4Tags = mapOf(
+ "©nam" to listOf("MP4 Song"),
+ "©alb" to listOf("MP4 Album")
+ )
+ )
+
+ val tags = tagParser.parse(metadata)
+
+ // Check priority: xiph > mp4 > id3v2
+ assertEquals("Xiph Song", tags.name)
+ assertEquals("Xiph Album", tags.albumName)
+ }
+
+ @Test
+ fun tagParser_parse_compilation() {
+ // Test compilation album behavior
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TCMP" to listOf("1")
+ )
+ )
+
+ val tags = tagParser.parse(metadata)
+
+ assertEquals(listOf("Various Artists"), tags.albumArtistNames)
+ assertEquals(listOf("compilation"), tags.releaseTypes)
+ }
+
+ @Test
+ fun tagParser_parse_compilationWithReleaseType() {
+ // Test compilation album with explicit release type
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TCMP" to listOf("1"),
+ "TXXX:RELEASETYPE" to listOf("soundtrack")
+ )
+ )
+
+ val tags = tagParser.parse(metadata)
+
+ assertEquals(listOf("Various Artists"), tags.albumArtistNames)
+ assertEquals(listOf("soundtrack"), tags.releaseTypes)
+ }
+
+ @Test
+ fun tagParser_parse_empty() {
+ // Test handling of empty metadata
+ val metadata = createTestMetadata()
+ val tags = tagParser.parse(metadata)
+
+ assertNull(tags.name)
+ assertNull(tags.albumName)
+ assertEquals(emptyList(), tags.artistNames)
+ }
+
+ @Test
+ fun tagParser_musicBrainzIds() {
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TXXX:MUSICBRAINZ RELEASE TRACK ID" to listOf("track-id-123"),
+ "TXXX:MUSICBRAINZ ALBUM ID" to listOf("album-id-456"),
+ "TXXX:MUSICBRAINZ ARTIST ID" to listOf("artist-id-789"),
+ "TXXX:MUSICBRAINZ ALBUM ARTIST ID" to listOf("album-artist-id-012")
+ )
+ )
+
+ val tags = tagParser.parse(metadata)
+
+ assertEquals("track-id-123", tags.musicBrainzId)
+ assertEquals("album-id-456", tags.albumMusicBrainzId)
+ assertEquals(listOf("artist-id-789"), tags.artistMusicBrainzIds)
+ assertEquals(listOf("album-artist-id-012"), tags.albumArtistMusicBrainzIds)
+ }
+
+ @Test
+ fun tagParser_replayGain() {
+ val metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "REPLAYGAIN_TRACK_GAIN" to listOf("-3.5 dB"),
+ "REPLAYGAIN_ALBUM_GAIN" to listOf("-2.1 dB")
+ )
+ )
+
+ val tags = tagParser.parse(metadata)
+
+ assertEquals(-3.5f, tags.replayGainTrackAdjustment)
+ assertEquals(-2.1f, tags.replayGainAlbumAdjustment)
+ }
+
+ private fun createTestMetadata(
+ id3v2Tags: Map> = emptyMap(),
+ xiphTags: Map> = emptyMap(),
+ mp4Tags: Map> = emptyMap(),
+ cover: ByteArray? = null,
+ durationMs: Long = 1000,
+ bitrateKbps: Int = 320,
+ sampleRateHz: Int = 44100,
+ mimeType: String = "audio/mpeg"
+ ): Metadata {
+ val properties = Properties(mimeType, durationMs, bitrateKbps, sampleRateHz)
+ return Metadata(id3v2Tags, xiphTags, mp4Tags, cover, properties)
+ }
+}
\ No newline at end of file
diff --git a/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagParsingEdgeCasesTest.kt b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagParsingEdgeCasesTest.kt
new file mode 100644
index 000000000..2442d3422
--- /dev/null
+++ b/musikr/src/test/java/org/oxycblt/musikr/tag/parse/TagParsingEdgeCasesTest.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2024 Auxio Project
+ * TagParsingEdgeCasesTest.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.musikr.tag.parse
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oxycblt.musikr.metadata.Metadata
+import org.oxycblt.musikr.metadata.Properties
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [30])
+class TagParsingEdgeCasesTest {
+ private val tagParser = TagParser.new()
+
+ @Test
+ fun tagParser_emptyTags() {
+ // Test behavior with completely empty tag maps
+ val metadata = createTestMetadata()
+ val parsedTags = tagParser.parse(metadata)
+
+ // Check that all fields have their default values
+ assertNull(parsedTags.name)
+ assertNull(parsedTags.sortName)
+ assertNull(parsedTags.track)
+ assertNull(parsedTags.disc)
+ assertNull(parsedTags.subtitle)
+ assertNull(parsedTags.date)
+ assertNull(parsedTags.albumName)
+ assertNull(parsedTags.albumSortName)
+ assertTrue(parsedTags.releaseTypes.isEmpty())
+ assertTrue(parsedTags.artistNames.isEmpty())
+ assertTrue(parsedTags.artistSortNames.isEmpty())
+ assertTrue(parsedTags.albumArtistNames.isEmpty())
+ assertTrue(parsedTags.albumArtistSortNames.isEmpty())
+ assertTrue(parsedTags.genreNames.isEmpty())
+ }
+
+ @Test
+ fun tagParser_malformedTags() {
+ // Test behavior with malformed tags
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TRCK" to listOf("not-a-number"),
+ "TPOS" to listOf("also-not-a-number"),
+ "TYER" to listOf("invalid-year"),
+ "REPLAYGAIN_TRACK_GAIN" to listOf("not-a-float dB")
+ )
+ )
+ val parsedTags = tagParser.parse(metadata)
+
+ // Verify all numeric fields are null when given invalid values
+ assertNull(parsedTags.track)
+ assertNull(parsedTags.disc)
+ assertNull(parsedTags.date)
+ assertNull(parsedTags.replayGainTrackAdjustment)
+ }
+
+ @Test
+ fun tagParser_priorityHandling() {
+ // Test that field priority is handled correctly when multiple formats have the same field
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TIT2" to listOf("ID3 Title"),
+ "TALB" to listOf("ID3 Album")
+ ),
+ mp4Tags = mapOf(
+ "©nam" to listOf("MP4 Title"),
+ "©alb" to listOf("MP4 Album")
+ ),
+ xiphTags = mapOf(
+ "TITLE" to listOf("Xiph Title"),
+ "ALBUM" to listOf("Xiph Album")
+ )
+ )
+
+ // Individual field extraction
+ assertEquals("Xiph Title", metadata.name())
+ assertEquals("Xiph Album", metadata.albumName())
+
+ // Full tag parsing
+ val parsedTags = tagParser.parse(metadata)
+ assertEquals("Xiph Title", parsedTags.name)
+ assertEquals("Xiph Album", parsedTags.albumName)
+ }
+
+ @Test
+ fun tagParser_fieldBackoffHandling() {
+ // Test that when a format doesn't have a field, it falls back to the next format
+ val metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TIT2" to listOf("ID3 Title"),
+ "TALB" to listOf("ID3 Album")
+ ),
+ mp4Tags = mapOf(
+ "©nam" to listOf("MP4 Title")
+ // No album in MP4
+ ),
+ xiphTags = mapOf(
+ // No title in Xiph
+ "ALBUM" to listOf("Xiph Album")
+ )
+ )
+
+ // Individual field extraction
+ assertEquals("MP4 Title", metadata.name()) // Xiph missing, falls back to MP4
+ assertEquals("Xiph Album", metadata.albumName()) // Xiph has it, no fallback needed
+
+ // Full tag parsing
+ val parsedTags = tagParser.parse(metadata)
+ assertEquals("MP4 Title", parsedTags.name)
+ assertEquals("Xiph Album", parsedTags.albumName)
+ }
+
+ @Test
+ fun tagParser_compilationBehavior() {
+ // Test the special behavior for compilation albums
+
+ // Case 1: Compilation flag only
+ var metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TCMP" to listOf("1")
+ )
+ )
+ var tags = tagParser.parse(metadata)
+ assertEquals(listOf("Various Artists"), tags.albumArtistNames)
+ assertEquals(listOf("compilation"), tags.releaseTypes)
+
+ // Case 2: Compilation flag with explicit album artist
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TCMP" to listOf("1"),
+ "TPE2" to listOf("Custom Album Artist")
+ )
+ )
+ tags = tagParser.parse(metadata)
+ assertEquals(listOf("Custom Album Artist"), tags.albumArtistNames)
+ assertEquals(listOf("compilation"), tags.releaseTypes)
+
+ // Case 3: Compilation flag with explicit release type
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TCMP" to listOf("1"),
+ "TXXX:RELEASETYPE" to listOf("soundtrack")
+ )
+ )
+ tags = tagParser.parse(metadata)
+ assertEquals(listOf("Various Artists"), tags.albumArtistNames)
+ assertEquals(listOf("soundtrack"), tags.releaseTypes)
+ }
+
+ @Test
+ fun tagFields_replayGainEdgeCases() {
+ // Test various ReplayGain edge cases
+
+ // Non-numeric characters filtered out
+ var metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "REPLAYGAIN_TRACK_GAIN" to listOf("+2.5 dB some text")
+ )
+ )
+ assertEquals(2.5f, metadata.replayGainTrackAdjustment())
+
+ // Zero values filtered
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "REPLAYGAIN_ALBUM_GAIN" to listOf("0.0 dB")
+ )
+ )
+ assertNull(metadata.replayGainAlbumAdjustment())
+
+ // R128 value with special parsing
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "R128_TRACK_GAIN" to listOf("-1280")
+ )
+ )
+ assertEquals(0.0f, metadata.replayGainTrackAdjustment()) // -1280/256 + 5 = 0.0
+
+ // Completely invalid value
+ metadata = createTestMetadata(
+ xiphTags = mapOf(
+ "REPLAYGAIN_TRACK_GAIN" to listOf("not a number")
+ )
+ )
+ assertNull(metadata.replayGainTrackAdjustment())
+ }
+
+ @Test
+ fun tagFields_id3v23DateEdgeCases() {
+ // Test ID3v2.3 date edge cases
+
+ // Just year
+ var metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TYER" to listOf("2018")
+ )
+ )
+ assertEquals("2018", metadata.date().toString())
+
+ // Year and date
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TYER" to listOf("2018"),
+ "TDAT" to listOf("0315") // March 15
+ )
+ )
+ assertEquals("2018-03-15", metadata.date().toString())
+
+ // Year, date and time
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TYER" to listOf("2018"),
+ "TDAT" to listOf("0315"), // March 15
+ "TIME" to listOf("1422") // 14:22
+ )
+ )
+ assertEquals("2018-03-15T14:22Z", metadata.date().toString())
+
+ // Original year
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TORY" to listOf("1995")
+ )
+ )
+ assertEquals("1995", metadata.date().toString())
+
+ // Invalid date components
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TYER" to listOf("2018"),
+ "TDAT" to listOf("invalid"),
+ "TIME" to listOf("invalid")
+ )
+ )
+ assertEquals("2018", metadata.date().toString())
+
+ // Non-numeric date components
+ metadata = createTestMetadata(
+ id3v2Tags = mapOf(
+ "TYER" to listOf("year")
+ )
+ )
+ assertNull(metadata.date())
+ }
+
+ private fun createTestMetadata(
+ id3v2Tags: Map> = emptyMap(),
+ xiphTags: Map> = emptyMap(),
+ mp4Tags: Map> = emptyMap(),
+ cover: ByteArray? = null,
+ durationMs: Long = 1000,
+ bitrateKbps: Int = 320,
+ sampleRateHz: Int = 44100,
+ mimeType: String = "audio/mpeg"
+ ): Metadata {
+ val properties = Properties(mimeType, durationMs, bitrateKbps, sampleRateHz)
+ return Metadata(id3v2Tags, xiphTags, mp4Tags, cover, properties)
+ }
+}
\ No newline at end of file