musikr.tag: parse mp4 fields

This commit is contained in:
Alexander Capehart 2024-12-23 16:46:56 -05:00
parent 6652e351cf
commit b6bc065a4a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 94 additions and 53 deletions

View file

@ -30,7 +30,7 @@ import org.oxycblt.musikr.util.positiveOrNull
*
* @see transformPositionField
*/
internal fun String.parseId3v2PositionField() =
internal fun String.parseSlashPositionField() =
split('/', limit = 2).let {
transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull())
}

View file

@ -21,40 +21,47 @@ package org.oxycblt.musikr.tag.parse
import androidx.core.text.isDigitsOnly
import org.oxycblt.musikr.metadata.Metadata
import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.format.parseId3v2PositionField
import org.oxycblt.musikr.tag.format.parseSlashPositionField
import org.oxycblt.musikr.tag.format.parseXiphPositionField
import org.oxycblt.musikr.util.nonZeroOrNull
// Note: TagLibJNI deliberately uppercases descriptive tags to avoid casing issues,
// hence why the casing here is matched. Note that MP4 atoms are kept in their
// original casing, as they are case-sensitive.
// Song
internal fun Metadata.musicBrainzId() =
(xiph["MUSICBRAINZ_RELEASETRACKID"]
?: xiph["MUSICBRAINZ RELEASE TRACK ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ RELEASE TRACK ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_RELEASETRACKID"])
?: id3v2["TXXX:MUSICBRAINZ RELEASE TRACK ID"]
?: id3v2["TXXX:MUSICBRAINZ_RELEASETRACKID"])
?.first()
?: id3v2["TXXX:MUSICBRAINZ_RELEASETRACKID"]?.first()
internal fun Metadata.name() = (xiph["TITLE"] ?: id3v2["TIT2"])?.first()
internal fun Metadata.name() =
(xiph["TITLE"] ?: mp4["©nam"] ?: mp4["©trk"] ?: id3v2["TIT2"])?.first()
internal fun Metadata.sortName() = (xiph["TITLESORT"] ?: id3v2["TSOT"])?.first()
internal fun Metadata.sortName() = (xiph["TITLESORT"] ?: mp4["sonm"] ?: id3v2["TSOT"])?.first()
// Track.
internal fun Metadata.track() =
(parseXiphPositionField(
xiph["TRACKNUMBER"]?.first(),
(xiph["TOTALTRACKS"] ?: xiph["TRACKTOTAL"] ?: xiph["TRACKC"])?.first())
?: id3v2["TRCK"]?.run { first().parseId3v2PositionField() })
?: (mp4["trkn"] ?: id3v2["TRCK"])?.run { first().parseSlashPositionField() })
// Disc and it's subtitle name.
internal fun Metadata.disc() =
(parseXiphPositionField(
xiph["DISCNUMBER"]?.first(),
(xiph["TOTALDISCS"] ?: xiph["DISCTOTAL"] ?: xiph["DISCC"])?.run { first() })
?: id3v2["TPOS"]?.run { first().parseId3v2PositionField() })
?: (mp4["disk"] ?: id3v2["TPOS"])?.run { first().parseSlashPositionField() })
internal fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first()
// Dates are somewhat complicated, as not only did their semantics change from a flat year
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
internal fun Metadata.date() =
// For ID3v 2, are somewhat complicated, as not only did their semantics change from a flat
// year value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
// date types.
// Our hierarchy for dates is as such:
// 1. ID3v2.4 Original Date, as it resolves the "Released in X, Remastered in Y" issue
@ -62,57 +69,65 @@ internal fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.firs
// 3. ID3v2.4 Release Date, as it is the second most common date type
// 4. ID3v2.3 Original Date, as it is like #1
// 5. ID3v2.3 Release Year, as it is the most common date type
// TODO: Show original and normal dates side-by-side
// TODO: Handle dates that are in "January" because the actual specific release date
// isn't known?
internal fun Metadata.date() =
(xiph["ORIGINALDATE"]?.run { Date.from(first()) }
?: xiph["DATE"]?.run { Date.from(first()) }
?: xiph["YEAR"]?.run { Date.from(first()) }
?:
// xiph dates are less complicated, but there are still several types
// xiph dates aren't complicated, but there are still several types
// Our hierarchy for dates is as such:
// 1. Original Date, as it solves the "Released in X, Remastered in Y" issue
// 2. Date, as it is the most common date type
// 3. Year, as old xiph tags tended to use this (I know this because it's the only
// date tag that android supports, so it must be 15 years old or more!)
id3v2["TDOR"]?.run { Date.from(first()) }
?: id3v2["TDRC"]?.run { Date.from(first()) }
?: id3v2["TDRL"]?.run { Date.from(first()) }
?: parseId3v23Date())
// TODO: Show original and normal dates side-by-side
// TODO: Handle dates that are in "January" because the actual specific release date
// isn't known?
((xiph["ORIGINALDATE"]
?: xiph["DATE"]
?: xiph["YEAR"]
?: mp4["©day"]
?: id3v2["TDOR"]
?: id3v2["TDRC"]
?: id3v2["TDRL"])
?.run { Date.from(first()) } ?: parseId3v23Date())
// Album
internal fun Metadata.albumMusicBrainzId() =
(xiph["MUSICBRAINZ_ALBUMID"]
?: xiph["MUSICBRAINZ ALBUM ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ALBUM ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_ALBUMID"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM ID"]
?: id3v2["TXXX:MUSICBRAINZ_ALBUMID"])
?.first()
internal fun Metadata.albumName() = (xiph["ALBUM"] ?: id3v2["TALB"])?.first()
internal fun Metadata.albumName() = (xiph["ALBUM"] ?: mp4["©alb"] ?: id3v2["TALB"])?.first()
internal fun Metadata.albumSortName() = (xiph["ALBUMSORT"] ?: id3v2["TSOA"])?.first()
internal fun Metadata.albumSortName() = (xiph["ALBUMSORT"] ?: mp4["soal"] ?: id3v2["TSOA"])?.first()
internal fun Metadata.releaseTypes() =
// GRP/GRP1 are assumed to also pertain to release types.
// GRP1 is a non-standard iTunes extension.
(xiph["RELEASETYPE"]
?: xiph["MUSICBRAINZ ALBUM TYPE"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ALBUM TYPE"]
?: mp4["----:COM.APPLE.ITUNES:RELEASETYPE"]
?: mp4["©grp"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM TYPE"]
?: id3v2["TXXX:RELEASETYPE"]
?:
// This is a non-standard iTunes extension
id3v2["GRP1"])
?: id3v2["GRP1"])
// Artist
internal fun Metadata.artistMusicBrainzIds() =
(xiph["MUSICBRAINZ_ARTISTID"]
?: xiph["MUSICBRAINZ ARTIST ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ARTIST ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_ARTISTID"]
?: id3v2["TXXX:MUSICBRAINZ ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ_ARTISTID"])
internal fun Metadata.artistNames() =
(xiph["ARTISTS"]
?: xiph["ARTIST"]
?: mp4["----:COM.APPLE.ITUNES:ARTISTS"]
?: mp4["©ART"]
?: mp4["----:COM.APPLE.ITUNES:ARTIST"]
?: id3v2["TXXX:ARTISTS"]
?: id3v2["TPE1"]
?: id3v2["TXXX:ARTIST"])
@ -123,6 +138,12 @@ internal fun Metadata.artistSortNames() =
?: xiph["ARTISTS SORT"]
?: xiph["ARTISTSORT"]
?: xiph["ARTIST SORT"]
?: mp4["----:COM.APPLE.ITUNES:ARTISTSSORT"]
?: mp4["----:COM.APPLE.ITUNES:ARTISTS_SORT"]
?: mp4["----:COM.APPLE.ITUNES:ARTISTS SORT"]
?: mp4["soar"]
?: mp4["----:COM.APPLE.ITUNES:ARTISTSORT"]
?: mp4["----:COM.APPLE.ITUNES:ARTIST SORT"]
?: id3v2["TXXX:ARTISTSSORT"]
?: id3v2["TXXX:ARTISTS_SORT"]
?: id3v2["TXXX:ARTISTS SORT"]
@ -133,6 +154,8 @@ internal fun Metadata.artistSortNames() =
internal fun Metadata.albumArtistMusicBrainzIds() =
(xiph["MUSICBRAINZ_ALBUMARTISTID"]
?: xiph["MUSICBRAINZ ALBUM ARTIST ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ ALBUM ARTIST ID"]
?: mp4["----:COM.APPLE.ITUNES:MUSICBRAINZ_ALBUMARTISTID"]
?: id3v2["TXXX:MUSICBRAINZ ALBUM ARTIST ID"]
?: id3v2["TXXX:MUSICBRAINZ_ALBUMARTISTID"])
@ -142,6 +165,12 @@ internal fun Metadata.albumArtistNames() =
?: xiph["ALBUM ARTISTS"]
?: xiph["ALBUMARTIST"]
?: xiph["ALBUM ARTIST"]
?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTS"]
?: mp4["----:COM.APPLE.ITUNES:ALBUM_ARTISTS"]
?: mp4["----:COM.APPLE.ITUNES:ALBUM ARTISTS"]
?: mp4["aART"]
?: mp4["----:COM.APPLE.ITUNES:ALBUMARTIST"]
?: mp4["----:COM.APPLE.ITUNES:ALBUM ARTIST"]
?: id3v2["TXXX:ALBUMARTISTS"]
?: id3v2["TXXX:ALBUM_ARTISTS"]
?: id3v2["TXXX:ALBUM ARTISTS"]
@ -150,27 +179,37 @@ internal fun Metadata.albumArtistNames() =
?: id3v2["TXXX:ALBUM ARTIST"])
internal fun Metadata.albumArtistSortNames() =
// TSO2 is a non-standard iTunes extension.
(xiph["ALBUMARTISTSSORT"]
?: xiph["ALBUMARTISTS_SORT"]
?: xiph["ALBUMARTISTS SORT"]
?: xiph["ALBUMARTISTSORT"]
?: xiph["ALBUM ARTIST SORT"]
?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTSSORT"]
?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTS_SORT"]
?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTS SORT"]
?: mp4["----:COM.APPLE.ITUNES:ALBUMARTISTSORT"]
?: mp4["soaa"]
?: mp4["----:COM.APPLE.ITUNES:ALBUM ARTIST SORT"]
?: id3v2["TXXX:ALBUMARTISTSSORT"]
?: id3v2["TXXX:ALBUMARTISTS_SORT"]
?: id3v2["TXXX:ALBUMARTISTS SORT"]
?: id3v2["TXXX:ALBUMARTISTSORT"]
// This is a non-standard iTunes extension
?: id3v2["TSO2"]
?: id3v2["TXXX:ALBUM ARTIST SORT"])
// Genre
internal fun Metadata.genreNames() = xiph["GENRE"] ?: id3v2["TCON"]
internal fun Metadata.genreNames() = xiph["GENRE"] ?: mp4["©gen"] ?: mp4["gnre"] ?: id3v2["TCON"]
// Compilation Flag
internal fun Metadata.isCompilation() =
// TCMP is a non-standard itunes extension
(xiph["COMPILATION"]
?: xiph["ITUNESCOMPILATION"]
?: id3v2["TCMP"] // This is a non-standard itunes extension
?: mp4["cpil"]
?: mp4["----:COM.APPLE.ITUNES:COMPILATION"]
?: mp4["----:COM.APPLE.ITUNES:ITUNESCOMPILATION"]
?: id3v2["TCMP"]
?: id3v2["TXXX:COMPILATION"]
?: id3v2["TXXX:ITUNESCOMPILATION"])
?.let {
@ -182,11 +221,13 @@ internal fun Metadata.isCompilation() =
internal fun Metadata.replayGainTrackAdjustment() =
(xiph["R128_TRACK_GAIN"]?.parseR128Adjustment()
?: xiph["REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment()
?: mp4["----:COM.APPLE.ITUNES:REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:REPLAYGAIN_TRACK_GAIN"]?.parseReplayGainAdjustment())
internal fun Metadata.replayGainAlbumAdjustment() =
(xiph["R128_ALBUM_GAIN"]?.parseR128Adjustment()
?: xiph["REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment()
?: mp4["----:COM.APPLE.ITUNES:REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment()
?: id3v2["TXXX:REPLAYGAIN_ALBUM_GAIN"]?.parseReplayGainAdjustment())
private fun List<String>.parseR128Adjustment() =

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.tag.parse
import org.junit.Assert.assertEquals
import org.junit.Test
import org.oxycblt.musikr.tag.format.parseId3GenreNames
import org.oxycblt.musikr.tag.format.parseId3v2PositionField
import org.oxycblt.musikr.tag.format.parseSlashPositionField
import org.oxycblt.musikr.tag.format.parseXiphPositionField
import org.oxycblt.musikr.util.correctWhitespace
import org.oxycblt.musikr.util.splitEscaped
@ -78,21 +78,21 @@ class TagUtilTest {
@Test
fun parseId3v2PositionField_correct() {
assertEquals(16, "16/32".parseId3v2PositionField())
assertEquals(16, "16".parseId3v2PositionField())
assertEquals(16, "16/32".parseSlashPositionField())
assertEquals(16, "16".parseSlashPositionField())
}
@Test
fun parseId3v2PositionField_zeroed() {
assertEquals(null, "0".parseId3v2PositionField())
assertEquals(0, "0/32".parseId3v2PositionField())
assertEquals(null, "0".parseSlashPositionField())
assertEquals(0, "0/32".parseSlashPositionField())
}
@Test
fun parseId3v2PositionField_wack() {
assertEquals(16, "16/".parseId3v2PositionField())
assertEquals(null, "a".parseId3v2PositionField())
assertEquals(null, "a/b".parseId3v2PositionField())
assertEquals(16, "16/".parseSlashPositionField())
assertEquals(null, "a".parseSlashPositionField())
assertEquals(null, "a/b".parseSlashPositionField())
}
@Test