diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/format/Vorbis.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/format/Vorbis.kt deleted file mode 100644 index f351150bf..000000000 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/format/Vorbis.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * Vorbis.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.format - -import org.oxycblt.musikr.util.positiveOrNull - -/** - * Parse an ID3v2-style position + total [String] field. These fields consist of a number and an - * (optional) total value delimited by a /. - * - * @return The position value extracted from the string field, or null if: - * - The position could not be parsed - * - The position was zeroed AND the total value was not present/zeroed - * - * @see transformPositionField - */ -internal fun String.parseSlashPositionField() = - split('/', limit = 2).let { - transformPositionField(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull()) - } - -/** - * Parse a vorbis-style position + total field. These fields consist of two fields for the position - * and total numbers. - * - * @param pos The position value, or null if not present. - * @param total The total value, if not present. - * @return The position value extracted from the field, or null if: - * - The position could not be parsed - * - The position was zeroed AND the total value was not present/zeroed - * - * @see transformPositionField - */ -internal fun parseXiphPositionField(pos: String?, total: String?) = - pos?.let { posStr -> - posStr.toIntOrNull()?.let { transformPositionField(it, total?.toIntOrNull()) } - ?: posStr.parseSlashPositionField() - } - -/** - * Transform a raw position + total field into a position a way that tolerates placeholder values. - * - * @param pos The position value, or null if not present. - * @param total The total value, if not present. - * @return The position value extracted from the field, or null if: - * - The position could not be parsed - * - The position was zeroed AND the total value was not present/zeroed - */ -internal fun transformPositionField(pos: Int?, total: Int?) = - if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) { - pos - } else { - null - } diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/format/ID3.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/ID3Genre.kt similarity index 90% rename from musikr/src/main/java/org/oxycblt/musikr/tag/format/ID3.kt rename to musikr/src/main/java/org/oxycblt/musikr/tag/interpret/ID3Genre.kt index 4e9390711..e7049f7fe 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/format/ID3.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/ID3Genre.kt @@ -1,24 +1,5 @@ -/* - * Copyright (c) 2024 Auxio Project - * ID3.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.format +package org.oxycblt.musikr.tag.interpret -/// --- ID3v2 PARSING --- /** * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer @@ -55,7 +36,7 @@ private fun String.parseId3v1Genre(): String? { // try to index the genre table with such. val numeric = toIntOrNull() - // Not a numeric value, try some other fixed values. + // Not a numeric value, try some other fixed values. ?: return when (this) { // CR and RX are not technically ID3v1, but are formatted similarly to a plain // number. diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpreter.kt index 658942bf7..f162e897c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpreter.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/Interpreter.kt @@ -26,7 +26,6 @@ import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.Placeholder import org.oxycblt.musikr.tag.ReleaseType import org.oxycblt.musikr.tag.ReplayGainAdjustment -import org.oxycblt.musikr.tag.format.parseId3GenreNames import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.util.toUuidOrNull diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt index 08620d399..778de5380 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagFields.kt @@ -21,9 +21,8 @@ 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.parseSlashPositionField -import org.oxycblt.musikr.tag.format.parseXiphPositionField import org.oxycblt.musikr.util.nonZeroOrNull +import org.oxycblt.musikr.util.positiveOrNull // 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 @@ -46,17 +45,66 @@ internal fun Metadata.sortName() = (xiph["TITLESORT"] ?: mp4["sonm"] ?: id3v2["T // Track. internal fun Metadata.track() = - (parseXiphPositionField( + (parseSeparatedPosition( xiph["TRACKNUMBER"]?.first(), (xiph["TOTALTRACKS"] ?: xiph["TRACKTOTAL"] ?: xiph["TRACKC"])?.first()) - ?: (mp4["trkn"] ?: id3v2["TRCK"])?.run { first().parseSlashPositionField() }) + ?: (mp4["trkn"] ?: id3v2["TRCK"])?.run { first().parseJoinedPosition() }) // Disc and it's subtitle name. internal fun Metadata.disc() = - (parseXiphPositionField( + (parseSeparatedPosition( xiph["DISCNUMBER"]?.first(), (xiph["TOTALDISCS"] ?: xiph["DISCTOTAL"] ?: xiph["DISCC"])?.run { first() }) - ?: (mp4["disk"] ?: id3v2["TPOS"])?.run { first().parseSlashPositionField() }) + ?: (mp4["disk"] ?: id3v2["TPOS"])?.run { first().parseJoinedPosition() }) + +/** + * Parse an ID3v2-style position + total [String] field. These fields consist of a number and an + * (optional) total value delimited by a /. + * + * @return The position value extracted from the string field, or null if: + * - The position could not be parsed + * - The position was zeroed AND the total value was not present/zeroed + * + * @see transformPosition + */ +private fun String.parseJoinedPosition() = + split('/', limit = 2).let { + transformPosition(it[0].toIntOrNull(), it.getOrNull(1)?.toIntOrNull()) + } + +/** + * Parse a vorbis-style position + total field. These fields consist of two fields for the position + * and total numbers. + * + * @param pos The position value, or null if not present. + * @param total The total value, if not present. + * @return The position value extracted from the field, or null if: + * - The position could not be parsed + * - The position was zeroed AND the total value was not present/zeroed + * + * @see transformPosition + */ +private fun parseSeparatedPosition(pos: String?, total: String?) = + pos?.let { posStr -> + posStr.toIntOrNull()?.let { transformPosition(it, total?.toIntOrNull()) } + ?: posStr.parseJoinedPosition() + } + +/** + * Transform a raw position + total field into a position a way that tolerates placeholder values. + * + * @param pos The position value, or null if not present. + * @param total The total value, if not present. + * @return The position value extracted from the field, or null if: + * - The position could not be parsed + * - The position was zeroed AND the total value was not present/zeroed + */ +private fun transformPosition(pos: Int?, total: Int?) = + if (pos != null && (pos > 0 || (total?.positiveOrNull() != null))) { + pos + } else { + null + } internal fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first() @@ -88,6 +136,40 @@ internal fun Metadata.date() = ?: id3v2["TDRL"]) ?.run { Date.from(first()) } ?: parseId3v23Date()) +private fun Metadata.parseId3v23Date(): Date? { + // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY + // is present. + val year = + id3v2["TORY"]?.run { first().toIntOrNull() } + ?: id3v2["TYER"]?.run { first().toIntOrNull() } + ?: return null + + val tdat = id3v2["TDAT"] + return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) { + // TDAT frames consist of a 4-digit string where the first two digits are + // the month and the last two digits are the day. + val mm = tdat.first().substring(0..1).toInt() + val dd = tdat.first().substring(2..3).toInt() + + val time = id3v2["TIME"] + if (time != null && time.first().length == 4 && time.first().isDigitsOnly()) { + // TIME frames consist of a 4-digit string where the first two digits are + // the hour and the last two digits are the minutes. No second value is + // possible. + val hh = time.first().substring(0..1).toInt() + val mi = time.first().substring(2..3).toInt() + // Able to return a full date. + Date.from(year, mm, dd, hh, mi) + } else { + // Unable to parse time, just return a date + Date.from(year, mm, dd) + } + } else { + // Unable to parse month/day, just return a year + return Date.from(year) + } +} + // Album internal fun Metadata.albumMusicBrainzId() = (xiph["MUSICBRAINZ_ALBUMID"] @@ -248,37 +330,3 @@ private fun List.parseReplayGainAdjustment() = * https://github.com/vanilla-music/vanilla */ private val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") } - -private fun Metadata.parseId3v23Date(): Date? { - // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY - // is present. - val year = - id3v2["TORY"]?.run { first().toIntOrNull() } - ?: id3v2["TYER"]?.run { first().toIntOrNull() } - ?: return null - - val tdat = id3v2["TDAT"] - return if (tdat != null && tdat.first().length == 4 && tdat.first().isDigitsOnly()) { - // TDAT frames consist of a 4-digit string where the first two digits are - // the month and the last two digits are the day. - val mm = tdat.first().substring(0..1).toInt() - val dd = tdat.first().substring(2..3).toInt() - - val time = id3v2["TIME"] - if (time != null && time.first().length == 4 && time.first().isDigitsOnly()) { - // TIME frames consist of a 4-digit string where the first two digits are - // the hour and the last two digits are the minutes. No second value is - // possible. - val hh = time.first().substring(0..1).toInt() - val mi = time.first().substring(2..3).toInt() - // Able to return a full date. - Date.from(year, mm, dd, hh, mi) - } else { - // Unable to parse time, just return a date - Date.from(year, mm, dd) - } - } else { - // Unable to parse month/day, just return a year - return Date.from(year) - } -}