musikr: collapse tag utils
This commit is contained in:
parent
3a429c14be
commit
8339920ce1
4 changed files with 90 additions and 132 deletions
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,24 +1,5 @@
|
||||||
/*
|
package org.oxycblt.musikr.tag.interpret
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.musikr.tag.format
|
|
||||||
|
|
||||||
/// --- ID3v2 PARSING ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer
|
* 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.
|
// try to index the genre table with such.
|
||||||
val numeric =
|
val numeric =
|
||||||
toIntOrNull()
|
toIntOrNull()
|
||||||
// Not a numeric value, try some other fixed values.
|
// Not a numeric value, try some other fixed values.
|
||||||
?: return when (this) {
|
?: return when (this) {
|
||||||
// CR and RX are not technically ID3v1, but are formatted similarly to a plain
|
// CR and RX are not technically ID3v1, but are formatted similarly to a plain
|
||||||
// number.
|
// number.
|
|
@ -26,7 +26,6 @@ import org.oxycblt.musikr.tag.Name
|
||||||
import org.oxycblt.musikr.tag.Placeholder
|
import org.oxycblt.musikr.tag.Placeholder
|
||||||
import org.oxycblt.musikr.tag.ReleaseType
|
import org.oxycblt.musikr.tag.ReleaseType
|
||||||
import org.oxycblt.musikr.tag.ReplayGainAdjustment
|
import org.oxycblt.musikr.tag.ReplayGainAdjustment
|
||||||
import org.oxycblt.musikr.tag.format.parseId3GenreNames
|
|
||||||
import org.oxycblt.musikr.tag.parse.ParsedTags
|
import org.oxycblt.musikr.tag.parse.ParsedTags
|
||||||
import org.oxycblt.musikr.util.toUuidOrNull
|
import org.oxycblt.musikr.util.toUuidOrNull
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,8 @@ package org.oxycblt.musikr.tag.parse
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import org.oxycblt.musikr.metadata.Metadata
|
import org.oxycblt.musikr.metadata.Metadata
|
||||||
import org.oxycblt.musikr.tag.Date
|
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.nonZeroOrNull
|
||||||
|
import org.oxycblt.musikr.util.positiveOrNull
|
||||||
|
|
||||||
// Note: TagLibJNI deliberately uppercases descriptive tags to avoid casing issues,
|
// 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
|
// 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.
|
// Track.
|
||||||
internal fun Metadata.track() =
|
internal fun Metadata.track() =
|
||||||
(parseXiphPositionField(
|
(parseSeparatedPosition(
|
||||||
xiph["TRACKNUMBER"]?.first(),
|
xiph["TRACKNUMBER"]?.first(),
|
||||||
(xiph["TOTALTRACKS"] ?: xiph["TRACKTOTAL"] ?: xiph["TRACKC"])?.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.
|
// Disc and it's subtitle name.
|
||||||
internal fun Metadata.disc() =
|
internal fun Metadata.disc() =
|
||||||
(parseXiphPositionField(
|
(parseSeparatedPosition(
|
||||||
xiph["DISCNUMBER"]?.first(),
|
xiph["DISCNUMBER"]?.first(),
|
||||||
(xiph["TOTALDISCS"] ?: xiph["DISCTOTAL"] ?: xiph["DISCC"])?.run { 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()
|
internal fun Metadata.subtitle() = (xiph["DISCSUBTITLE"] ?: id3v2["TSST"])?.first()
|
||||||
|
|
||||||
|
@ -88,6 +136,40 @@ internal fun Metadata.date() =
|
||||||
?: id3v2["TDRL"])
|
?: id3v2["TDRL"])
|
||||||
?.run { Date.from(first()) } ?: parseId3v23Date())
|
?.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
|
// Album
|
||||||
internal fun Metadata.albumMusicBrainzId() =
|
internal fun Metadata.albumMusicBrainzId() =
|
||||||
(xiph["MUSICBRAINZ_ALBUMID"]
|
(xiph["MUSICBRAINZ_ALBUMID"]
|
||||||
|
@ -248,37 +330,3 @@ private fun List<String>.parseReplayGainAdjustment() =
|
||||||
* https://github.com/vanilla-music/vanilla
|
* https://github.com/vanilla-music/vanilla
|
||||||
*/
|
*/
|
||||||
private val REPLAYGAIN_ADJUSTMENT_FILTER_REGEX by lazy { Regex("[^\\d.-]") }
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue