music: add support for date-encoding years

Add support for date-encoding years such as "YYYYMMDD".

This is a semi-common timestamp edge-case, it seems, primarily due to
taggers wanting to encode date information in older tag formats.
This commit is contained in:
Alexander Capehart 2023-01-06 13:47:18 -07:00
parent a29875b5bf
commit 3502af33e7
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 28 additions and 8 deletions

View file

@ -182,14 +182,25 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
*/
private val ISO8601_REGEX =
Regex(
"""^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""")
"""^(\d{4})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""")
/**
* Create a [Date] from a year component.
* @param year The year component.
* @return A new [Date] of the given component, or null if the component is invalid.
*/
fun from(year: Int) = fromTokens(listOf(year))
fun from(year: Int) =
if (year in 10000000..100000000) {
// Year is actually more likely to be a separated date timestamp. Interpret
// it as such.
val stringYear = year.toString()
from(
stringYear.substring(0..3).toInt(),
stringYear.substring(4..5).toInt(),
stringYear.substring(6..7).toInt())
} else {
fromTokens(listOf(year))
}
/**
* Create a [Date] from a date component.
@ -222,8 +233,9 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
*/
fun from(timestamp: String): Date? {
val tokens =
// Match the input with the timestamp regex
(ISO8601_REGEX.matchEntire(timestamp) ?: return null)
// Match the input with the timestamp regex. If there is no match, see if we can
// fall back to some kind of year value.
(ISO8601_REGEX.matchEntire(timestamp) ?: return timestamp.toIntOrNull()?.let(::from))
.groupValues
// Filter to the specific tokens we want and convert them to integer tokens.
.mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }
@ -238,7 +250,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
*/
private fun fromTokens(tokens: List<Int>): Date? {
val validated = mutableListOf<Int>()
validateTokens(tokens, validated)
transformTokens(tokens, validated)
if (validated.isEmpty()) {
// No token was valid, return null.
return null
@ -252,7 +264,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
* @param src The input tokens to validate.
* @param dst The destination list to add valid tokens to.
*/
private fun validateTokens(src: List<Int>, dst: MutableList<Int>) {
private fun transformTokens(src: List<Int>, dst: MutableList<Int>) {
dst.add(src.getOrNull(0)?.nonZeroOrNull() ?: return)
dst.add(src.getOrNull(1)?.inRangeOrNull(1..12) ?: return)
dst.add(src.getOrNull(2)?.inRangeOrNull(1..31) ?: return)
@ -260,5 +272,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return)
dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return)
}
private fun transformYearToken(src: List<Int>, dst: MutableList<Int>) {}
}
}

View file

@ -305,7 +305,7 @@ abstract class MediaStoreExtractor(
// MediaStore only exposes the year value of a file. This is actually worse than it
// seems, as it means that it will not read ID3v2 TDRC tags or Vorbis DATE comments.
// This is one of the major weaknesses of using MediaStore, hence the redundancy layers.
raw.date = cursor.getIntOrNull(yearIndex)?.let(Date::from)
raw.date = cursor.getStringOrNull(yearIndex)?.let(Date::from)
// A non-existent album name should theoretically be the name of the folder it contained
// in, but in practice it is more often "0" (as in /storage/emulated/0), even when it the
// file is not actually in the root internal storage directory. We can't do anything to

View file

@ -293,7 +293,7 @@ class Task(context: Context, private val raw: Song.Raw) {
// date tag that android supports, so it must be 15 years old or more!)
(comments["originaldate"]?.run { Date.from(first()) }
?: comments["date"]?.run { Date.from(first()) }
?: comments["year"]?.run { first().toIntOrNull()?.let(Date::from) })
?: comments["year"]?.run { Date.from(first()) })
?.let { raw.date = it }
// Album

View file

@ -124,6 +124,12 @@ class DateTest {
assertEquals(Date.from(0), null)
}
@Test
fun date_fromYearDate() {
assertEquals("2016", Date.from(2016).toString())
assertEquals("2016", Date.from("2016").toString())
}
@Test
fun dateRange_fromDates() {
val range =