From 3502af33e7087e509b648aa0fef1fad9b39390de Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 6 Jan 2023 13:47:18 -0700 Subject: [PATCH] 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. --- .../main/java/org/oxycblt/auxio/music/Date.kt | 26 ++++++++++++++----- .../music/extractor/MediaStoreExtractor.kt | 2 +- .../music/extractor/MetadataExtractor.kt | 2 +- .../java/org/oxycblt/auxio/music/DateTest.kt | 6 +++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/Date.kt index 6faf49d22..ad815a779 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Date.kt @@ -182,14 +182,25 @@ class Date private constructor(private val tokens: List) : Comparable */ 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) : Comparable */ 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) : Comparable */ private fun fromTokens(tokens: List): Date? { val validated = mutableListOf() - 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) : Comparable * @param src The input tokens to validate. * @param dst The destination list to add valid tokens to. */ - private fun validateTokens(src: List, dst: MutableList) { + private fun transformTokens(src: List, dst: MutableList) { 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) : Comparable dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return) dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return) } + + private fun transformYearToken(src: List, dst: MutableList) {} } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index 1ac7b082a..bc811f508 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index 7e353cb91..4299a6d2b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -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 diff --git a/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt b/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt index 6a5fed7ea..d88825eb1 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt @@ -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 =