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