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:
parent
a29875b5bf
commit
3502af33e7
4 changed files with 28 additions and 8 deletions
|
@ -182,14 +182,25 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
*/
|
*/
|
||||||
private val ISO8601_REGEX =
|
private val ISO8601_REGEX =
|
||||||
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.
|
* Create a [Date] from a year component.
|
||||||
* @param year The year component.
|
* @param year The year component.
|
||||||
* @return A new [Date] of the given component, or null if the component is invalid.
|
* @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.
|
* 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? {
|
fun from(timestamp: String): Date? {
|
||||||
val tokens =
|
val tokens =
|
||||||
// Match the input with the timestamp regex
|
// Match the input with the timestamp regex. If there is no match, see if we can
|
||||||
(ISO8601_REGEX.matchEntire(timestamp) ?: return null)
|
// fall back to some kind of year value.
|
||||||
|
(ISO8601_REGEX.matchEntire(timestamp) ?: return timestamp.toIntOrNull()?.let(::from))
|
||||||
.groupValues
|
.groupValues
|
||||||
// Filter to the specific tokens we want and convert them to integer tokens.
|
// Filter to the specific tokens we want and convert them to integer tokens.
|
||||||
.mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }
|
.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? {
|
private fun fromTokens(tokens: List<Int>): Date? {
|
||||||
val validated = mutableListOf<Int>()
|
val validated = mutableListOf<Int>()
|
||||||
validateTokens(tokens, validated)
|
transformTokens(tokens, validated)
|
||||||
if (validated.isEmpty()) {
|
if (validated.isEmpty()) {
|
||||||
// No token was valid, return null.
|
// No token was valid, return null.
|
||||||
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 src The input tokens to validate.
|
||||||
* @param dst The destination list to add valid tokens to.
|
* @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(0)?.nonZeroOrNull() ?: return)
|
||||||
dst.add(src.getOrNull(1)?.inRangeOrNull(1..12) ?: return)
|
dst.add(src.getOrNull(1)?.inRangeOrNull(1..12) ?: return)
|
||||||
dst.add(src.getOrNull(2)?.inRangeOrNull(1..31) ?: 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(4)?.inRangeOrNull(0..59) ?: return)
|
||||||
dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return)
|
dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun transformYearToken(src: List<Int>, dst: MutableList<Int>) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -305,7 +305,7 @@ abstract class MediaStoreExtractor(
|
||||||
// MediaStore only exposes the year value of a file. This is actually worse than it
|
// 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.
|
// 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.
|
// 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
|
// 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
|
// 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
|
// file is not actually in the root internal storage directory. We can't do anything to
|
||||||
|
|
|
@ -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!)
|
// date tag that android supports, so it must be 15 years old or more!)
|
||||||
(comments["originaldate"]?.run { Date.from(first()) }
|
(comments["originaldate"]?.run { Date.from(first()) }
|
||||||
?: comments["date"]?.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 }
|
?.let { raw.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
|
|
|
@ -124,6 +124,12 @@ class DateTest {
|
||||||
assertEquals(Date.from(0), null)
|
assertEquals(Date.from(0), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun date_fromYearDate() {
|
||||||
|
assertEquals("2016", Date.from(2016).toString())
|
||||||
|
assertEquals("2016", Date.from("2016").toString())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun dateRange_fromDates() {
|
fun dateRange_fromDates() {
|
||||||
val range =
|
val range =
|
||||||
|
|
Loading…
Reference in a new issue