diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d4402368..6ca00a29 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,7 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true } lint { @@ -128,6 +129,8 @@ dependencies { implementation(libs.koin.androidx.compose) ksp(libs.androidx.room3.compiler) + coreLibraryDesugaring(libs.desugar.jdk.libs) + debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/app/src/main/kotlin/com/subtracks/data/source/subsonic/SubsonicXml.kt b/app/src/main/kotlin/com/subtracks/data/source/subsonic/SubsonicXml.kt index c39ac374..7aa980a5 100644 --- a/app/src/main/kotlin/com/subtracks/data/source/subsonic/SubsonicXml.kt +++ b/app/src/main/kotlin/com/subtracks/data/source/subsonic/SubsonicXml.kt @@ -7,6 +7,10 @@ import com.subtracks.data.model.PlaylistSong import com.subtracks.data.model.Song import org.w3c.dom.Document import org.w3c.dom.Element +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeParseException object SubsonicXml { fun artists( @@ -128,46 +132,19 @@ private fun Element.longAttr(name: String): Long? = getAttribute(name).takeIf { private fun Element.dateAttr(name: String): Long? = getAttribute(name).takeIf { it.isNotEmpty() }?.let(IsoDate::parse) internal object IsoDate { - private val pattern = - Regex( - "^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2}):(\\d{2})(?:\\.\\d+)?\\s*(Z|[+-]\\d{2}:?\\d{2})?)?$", - ) + private val timezone = Regex("(Z|[+-]\\d{2}:?\\d{2})$", RegexOption.IGNORE_CASE) fun parse(value: String): Long? { - val match = pattern.matchEntire(value.trim()) ?: return null - val year = match.groupValues[1].toIntOrNull() ?: return null - val month = match.groupValues[2].toIntOrNull() ?: return null - val day = match.groupValues[3].toIntOrNull() ?: return null - val hour = match.groupValues[4].toIntOrNull() ?: 0 - val minute = match.groupValues[5].toIntOrNull() ?: 0 - val second = match.groupValues[6].toIntOrNull() ?: 0 - if (month !in 1..12 || day !in 1..31 || hour !in 0..23 || minute !in 0..59 || second !in 0..59) { - return null + val text = value.trim() + return try { + val zoned = if (timezone.containsMatchIn(text)) text else text + "Z" + OffsetDateTime.parse(zoned).toEpochSecond() + } catch (_: DateTimeParseException) { + try { + LocalDate.parse(text).atStartOfDay(ZoneOffset.UTC).toEpochSecond() + } catch (_: DateTimeParseException) { + null + } } - val days = daysFromCivil(year, month, day) - return days * 86400L + hour * 3600L + minute * 60L + second - offsetSeconds(match.groupValues[7]) - } - - private fun offsetSeconds(zone: String): Long { - if (zone.isEmpty() || zone.equals("Z", ignoreCase = true)) return 0 - val digits = zone.drop(1).replace(":", "") - if (digits.length != 4) return 0 - val hours = digits.take(2).toLongOrNull() ?: return 0 - val minutes = digits.drop(2).toLongOrNull() ?: return 0 - val sign = if (zone[0] == '-') -1L else 1L - return sign * (hours * 3600 + minutes * 60) - } - - private fun daysFromCivil( - year: Int, - month: Int, - day: Int, - ): Long { - val y = if (month <= 2) year - 1 else year - val era = (if (y >= 0) y else y - 399) / 400 - val yearOfEra = y - era * 400 - val dayOfYear = (153 * (if (month > 2) month - 3 else month + 9) + 2) / 5 + day - 1 - val dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear - return era * 146097L + dayOfEra - 719468L } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 02c9272e..3f6ee8b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ coil = "3.6.3" koin = "4.2.2" media3 = "1.11.1" okhttp = "5.5.0" +desugarJdkLibs = "2.1.5" ktlint = "1.8.0" ktlintGradle = "14.2.0" roborazzi = "1.74.0" @@ -63,6 +64,7 @@ koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compos kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs" } okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" } roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" } roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }