Skip to content

Commit a124450

Browse files
authored
Migrate Java date utils in library to kotlinx-datetime (#2798)
1 parent 028a794 commit a124450

8 files changed

Lines changed: 281 additions & 41 deletions

File tree

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
cache-read-only: false
2828

2929
- name: Run Gradle
30-
run: ./gradlew assemblePrereleaseDebug lint
30+
run: ./gradlew assemblePrereleaseDebug lint check
3131

3232
- name: Upload Artifact
3333
uses: actions/upload-artifact@v7

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ kotlinGradlePlugin = "2.3.20"
3030
kotlinxAtomicfu = "0.32.1"
3131
kotlinxCollectionsImmutable = "0.4.0"
3232
kotlinxCoroutinesCore = "1.10.2"
33+
kotlinxDatetime = "0.8.0"
3334
kotlinxSerializationJson = "1.11.0"
3435
lifecycleKtx = "2.10.0"
3536
material = "1.14.0"
@@ -92,6 +93,7 @@ kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.re
9293
kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinxAtomicfu" }
9394
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
9495
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
96+
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
9597
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
9698
lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" }
9799
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" }

library/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ kotlin {
5959
implementation(libs.jackson.module.kotlin) // JSON Parser
6060
implementation(libs.kotlinx.atomicfu)
6161
implementation(libs.kotlinx.coroutines.core)
62+
implementation(libs.kotlinx.datetime)
6263
implementation(libs.kotlinx.serialization.json) // JSON Parser
6364
implementation(libs.jsoup) // HTML Parser
6465
implementation(libs.rhino) // Run JavaScript
@@ -68,6 +69,10 @@ kotlin {
6869
// Deprecated; will be removed once extensions have time to migrate from using it
6970
implementation("me.xdrop:fuzzywuzzy:1.4.0")
7071
}
72+
73+
commonTest.dependencies {
74+
implementation(libs.kotlin.test)
75+
}
7176
}
7277
}
7378

library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,25 @@ import com.lagradost.nicehttp.RequestBodyTypes
2525
import okhttp3.Interceptor
2626
import okhttp3.MediaType.Companion.toMediaTypeOrNull
2727
import okhttp3.RequestBody.Companion.toRequestBody
28+
import kotlinx.datetime.LocalDate
29+
import kotlinx.datetime.LocalTime
30+
import kotlinx.datetime.TimeZone
31+
import kotlinx.datetime.atStartOfDayIn
32+
import kotlinx.datetime.format.DateTimeComponents
33+
import kotlinx.datetime.format.FormatStringsInDatetimeFormats
34+
import kotlinx.datetime.format.byUnicodePattern
35+
import kotlinx.datetime.format.char
36+
import kotlinx.datetime.format.parse
37+
import kotlinx.datetime.toInstant
2838
import java.net.URI
29-
import java.text.SimpleDateFormat
30-
import java.util.Date
3139
import java.util.EnumSet
32-
import java.util.Locale
3340
import kotlinx.serialization.json.Json
3441
import kotlin.io.encoding.Base64
3542
import kotlin.io.encoding.ExperimentalEncodingApi
3643
import kotlin.math.absoluteValue
3744
import kotlin.math.roundToInt
45+
import kotlin.time.Clock
46+
import kotlin.time.Instant
3847

3948
/**
4049
* API available only on prerelease builds.
@@ -88,10 +97,10 @@ val mapper = JsonMapper.builder().addModule(kotlinModule())
8897
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
8998

9099
object APIHolder {
91-
val unixTime: Long
92-
get() = System.currentTimeMillis() / 1000L
93100
val unixTimeMS: Long
94-
get() = System.currentTimeMillis()
101+
get() = Clock.System.now().toEpochMilliseconds()
102+
val unixTime: Long
103+
get() = unixTimeMS / 1000L
95104

96105
val allProviders = atomicListOf<MainAPI>()
97106

@@ -2512,15 +2521,45 @@ constructor(
25122521
get() = score?.toInt(100)
25132522
}
25142523

2524+
@OptIn(FormatStringsInDatetimeFormats::class)
25152525
fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") {
2516-
try {
2517-
this.date = SimpleDateFormat(format, Locale.getDefault()).parse(date ?: return)?.time
2518-
} catch (e: Exception) {
2519-
logError(e)
2520-
}
2526+
if (date == null) return
2527+
this.date = runCatching {
2528+
// First try standard ISO 8601 (e.g. "2026-01-01T12:30:00.000Z", "2026-05-17T14:35+02:00")
2529+
runCatching { Instant.parse(date).toEpochMilliseconds() }
2530+
.getOrElse {
2531+
val fmt = DateTimeComponents.Format { byUnicodePattern(format) }
2532+
val components = DateTimeComponents.parse(date, fmt)
2533+
/**
2534+
* Try multiple conversions in order of precision for non-ISO-8601 formats,
2535+
* since the date string may or may not include time and/or timezone offset:
2536+
* 1. If the custom format produced a UTC offset (e.g. "2026-05-17 14:35+02:00"), use it directly
2537+
* 2. If it has time but no offset (e.g. "2026-05-17 14:35"), fall back to device timezone
2538+
* 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
2539+
*/
2540+
runCatching { components.toInstantUsingOffset().toEpochMilliseconds() }
2541+
.recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
2542+
.getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
2543+
}
2544+
}.onFailure { logError(it) }.getOrNull()
25212545
}
25222546

2523-
fun Episode.addDate(date: Date?) {
2547+
@Prerelease
2548+
fun Episode.addDate(date: LocalDate?) {
2549+
this.date = date?.atStartOfDayIn(TimeZone.currentSystemDefault())?.toEpochMilliseconds()
2550+
}
2551+
2552+
@Prerelease
2553+
fun Episode.addDate(date: Instant?) {
2554+
this.date = date?.toEpochMilliseconds()
2555+
}
2556+
2557+
// Deprecate after next stable
2558+
/* @Deprecated(
2559+
message = "Use addDate with LocalDate, Instant, or String instead.",
2560+
level = DeprecationLevel.WARNING,
2561+
) */
2562+
fun Episode.addDate(date: java.util.Date?) {
25242563
this.date = date?.time
25252564
}
25262565

@@ -2657,6 +2696,27 @@ fun fetchUrls(text: String?): List<String> {
26572696
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
26582697
}
26592698

2699+
@Prerelease
2700+
fun isUpcoming(dateString: String?): Boolean {
2701+
return runCatching {
2702+
val fmt = DateTimeComponents.Format {
2703+
year(); char('-'); monthNumber(); char('-'); day()
2704+
}
2705+
val components = DateTimeComponents.parse(dateString ?: return false, fmt)
2706+
/**
2707+
* Try multiple conversions in order of precision, since the date string format
2708+
* may or may not include time and/or timezone offset information:
2709+
* 1. If the string has a UTC offset (e.g. "2026-05-17T14:35+02:00"), use it directly
2710+
* 2. If it has time but no offset (e.g. "2026-05-17T14:35"), fall back to device timezone
2711+
* 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
2712+
*/
2713+
val instant = runCatching { components.toInstantUsingOffset() }
2714+
.recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()) }
2715+
.getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()) }
2716+
Clock.System.now() < instant
2717+
}.onFailure { logError(it) }.getOrElse { false }
2718+
}
2719+
26602720
@Deprecated(
26612721
"toRatingInt() is deprecated. Use new score API instead.",
26622722
level = DeprecationLevel.ERROR

library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.lagradost.cloudstream3.extractors
22

33
import com.fasterxml.jackson.annotation.JsonProperty
4+
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
45
import com.lagradost.cloudstream3.SubtitleFile
56
import com.lagradost.cloudstream3.app
67
import com.lagradost.cloudstream3.utils.ExtractorApi
@@ -21,7 +22,7 @@ open class Vicloud : ExtractorApi() {
2122
) {
2223
val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
2324
app.get(
24-
"$mainUrl/api/?$id=&_=${System.currentTimeMillis()}",
25+
"$mainUrl/api/?$id=&_=$unixTimeMS",
2526
headers = mapOf(
2627
"X-Requested-With" to "XMLHttpRequest"
2728
),

library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.ShowStatus
1818
import com.lagradost.cloudstream3.TvType
1919
import com.lagradost.cloudstream3.addDate
2020
import com.lagradost.cloudstream3.app
21+
import com.lagradost.cloudstream3.isUpcoming
2122
import com.lagradost.cloudstream3.mainPageOf
2223
import com.lagradost.cloudstream3.mvvm.logError
2324
import com.lagradost.cloudstream3.newEpisode
@@ -30,8 +31,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson
3031
import com.lagradost.cloudstream3.utils.AppUtils.toJson
3132
import okhttp3.Interceptor
3233
import okhttp3.Response
33-
import java.text.SimpleDateFormat
34-
import java.util.Locale
3534

3635
//Reference: https://mydramalist.github.io/MDL-API/
3736
abstract class MyDramaListAPI : MainAPI() {
@@ -192,17 +191,6 @@ abstract class MyDramaListAPI : MainAPI() {
192191
return this
193192
}
194193

195-
private fun isUpcoming(dateString: String?): Boolean {
196-
return try {
197-
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
198-
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
199-
unixTimeMS < dateTime
200-
} catch (t: Throwable) {
201-
logError(t)
202-
false
203-
}
204-
}
205-
206194
private fun getStatus(status: String?): ShowStatus? {
207195
return when (status) {
208196
"Airing" -> ShowStatus.Ongoing
@@ -451,4 +439,4 @@ abstract class MyDramaListAPI : MainAPI() {
451439
@JsonProperty("date") val date: String? = null,
452440
@JsonProperty("airedDate") val airedDate: String? = null,
453441
)
454-
}
442+
}

library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.ShowStatus
2222
import com.lagradost.cloudstream3.TvType
2323
import com.lagradost.cloudstream3.addDate
2424
import com.lagradost.cloudstream3.app
25+
import com.lagradost.cloudstream3.isUpcoming
2526
import com.lagradost.cloudstream3.mainPageOf
2627
import com.lagradost.cloudstream3.mvvm.logError
2728
import com.lagradost.cloudstream3.newEpisode
@@ -33,8 +34,6 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse
3334
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
3435
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
3536
import com.lagradost.cloudstream3.utils.AppUtils.toJson
36-
import java.text.SimpleDateFormat
37-
import java.util.Locale
3837

3938
open class TraktProvider : MainAPI() {
4039
override var name = "Trakt"
@@ -292,17 +291,6 @@ open class TraktProvider : MainAPI() {
292291
).text
293292
}
294293

295-
private fun isUpcoming(dateString: String?): Boolean {
296-
return try {
297-
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
298-
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
299-
unixTimeMS < dateTime
300-
} catch (t: Throwable) {
301-
logError(t)
302-
false
303-
}
304-
}
305-
306294
private fun getStatus(t: String?): ShowStatus {
307295
return when (t) {
308296
"returning series" -> ShowStatus.Ongoing

0 commit comments

Comments
 (0)