diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 675ce3b2f77..8f5c6286636 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -27,7 +27,7 @@ jobs:
         cache-read-only: false
 
     - name: Run Gradle
-      run: ./gradlew assemblePrereleaseDebug lint
+      run: ./gradlew assemblePrereleaseDebug lint check
 
     - name: Upload Artifact
       uses: actions/upload-artifact@v7
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 32e417e62c6..7b63a5ddcae 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -30,6 +30,7 @@ kotlinGradlePlugin = "2.3.20"
 kotlinxAtomicfu = "0.32.1"
 kotlinxCollectionsImmutable = "0.4.0"
 kotlinxCoroutinesCore = "1.10.2"
+kotlinxDatetime = "0.8.0"
 kotlinxSerializationJson = "1.11.0"
 lifecycleKtx = "2.10.0"
 material = "1.14.0"
@@ -92,6 +93,7 @@ kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.re
 kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinxAtomicfu" }
 kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
 kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
+kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
 kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
 lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" }
 lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" }
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
index 0abd6f3de45..ead73eec2c4 100644
--- a/library/build.gradle.kts
+++ b/library/build.gradle.kts
@@ -59,6 +59,7 @@ kotlin {
             implementation(libs.jackson.module.kotlin) // JSON Parser
             implementation(libs.kotlinx.atomicfu)
             implementation(libs.kotlinx.coroutines.core)
+            implementation(libs.kotlinx.datetime)
             implementation(libs.kotlinx.serialization.json) // JSON Parser
             implementation(libs.jsoup) // HTML Parser
             implementation(libs.rhino) // Run JavaScript
@@ -68,6 +69,10 @@ kotlin {
             // Deprecated; will be removed once extensions have time to migrate from using it
             implementation("me.xdrop:fuzzywuzzy:1.4.0")
         }
+
+        commonTest.dependencies {
+            implementation(libs.kotlin.test)
+        }
     }
 }
 
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
index 5de3d3e384a..5d4deba2432 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
@@ -25,16 +25,25 @@ import com.lagradost.nicehttp.RequestBodyTypes
 import okhttp3.Interceptor
 import okhttp3.MediaType.Companion.toMediaTypeOrNull
 import okhttp3.RequestBody.Companion.toRequestBody
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.atStartOfDayIn
+import kotlinx.datetime.format.DateTimeComponents
+import kotlinx.datetime.format.FormatStringsInDatetimeFormats
+import kotlinx.datetime.format.byUnicodePattern
+import kotlinx.datetime.format.char
+import kotlinx.datetime.format.parse
+import kotlinx.datetime.toInstant
 import java.net.URI
-import java.text.SimpleDateFormat
-import java.util.Date
 import java.util.EnumSet
-import java.util.Locale
 import kotlinx.serialization.json.Json
 import kotlin.io.encoding.Base64
 import kotlin.io.encoding.ExperimentalEncodingApi
 import kotlin.math.absoluteValue
 import kotlin.math.roundToInt
+import kotlin.time.Clock
+import kotlin.time.Instant
 
 /**
  * API available only on prerelease builds.
@@ -88,10 +97,10 @@ val mapper = JsonMapper.builder().addModule(kotlinModule())
     .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
 
 object APIHolder {
-    val unixTime: Long
-        get() = System.currentTimeMillis() / 1000L
     val unixTimeMS: Long
-        get() = System.currentTimeMillis()
+        get() = Clock.System.now().toEpochMilliseconds()
+    val unixTime: Long
+        get() = unixTimeMS / 1000L
 
     val allProviders = atomicListOf<MainAPI>()
 
@@ -2512,15 +2521,45 @@ constructor(
         get() = score?.toInt(100)
 }
 
+@OptIn(FormatStringsInDatetimeFormats::class)
 fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") {
-    try {
-        this.date = SimpleDateFormat(format, Locale.getDefault()).parse(date ?: return)?.time
-    } catch (e: Exception) {
-        logError(e)
-    }
+    if (date == null) return
+    this.date = runCatching {
+        // First try standard ISO 8601 (e.g. "2026-01-01T12:30:00.000Z", "2026-05-17T14:35+02:00")
+        runCatching { Instant.parse(date).toEpochMilliseconds() }
+            .getOrElse {
+                val fmt = DateTimeComponents.Format { byUnicodePattern(format) }
+                val components = DateTimeComponents.parse(date, fmt)
+                /**
+                 * Try multiple conversions in order of precision for non-ISO-8601 formats,
+                 * since the date string may or may not include time and/or timezone offset:
+                 * 1. If the custom format produced a UTC offset (e.g. "2026-05-17 14:35+02:00"), use it directly
+                 * 2. If it has time but no offset (e.g. "2026-05-17 14:35"), fall back to device timezone
+                 * 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
+                 */
+                runCatching { components.toInstantUsingOffset().toEpochMilliseconds() }
+                    .recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
+                    .getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
+            }
+    }.onFailure { logError(it) }.getOrNull()
 }
 
-fun Episode.addDate(date: Date?) {
+@Prerelease
+fun Episode.addDate(date: LocalDate?) {
+    this.date = date?.atStartOfDayIn(TimeZone.currentSystemDefault())?.toEpochMilliseconds()
+}
+
+@Prerelease
+fun Episode.addDate(date: Instant?) {
+    this.date = date?.toEpochMilliseconds()
+}
+
+// Deprecate after next stable
+/* @Deprecated(
+    message = "Use addDate with LocalDate, Instant, or String instead.",
+    level = DeprecationLevel.WARNING,
+) */
+fun Episode.addDate(date: java.util.Date?) {
     this.date = date?.time
 }
 
@@ -2657,6 +2696,27 @@ fun fetchUrls(text: String?): List<String> {
     return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
 }
 
+@Prerelease
+fun isUpcoming(dateString: String?): Boolean {
+    return runCatching {
+        val fmt = DateTimeComponents.Format {
+            year(); char('-'); monthNumber(); char('-'); day()
+        }
+        val components = DateTimeComponents.parse(dateString ?: return false, fmt)
+        /**
+         * Try multiple conversions in order of precision, since the date string format
+         * may or may not include time and/or timezone offset information:
+         * 1. If the string has a UTC offset (e.g. "2026-05-17T14:35+02:00"), use it directly
+         * 2. If it has time but no offset (e.g. "2026-05-17T14:35"), fall back to device timezone
+         * 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
+         */
+        val instant = runCatching { components.toInstantUsingOffset() }
+            .recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()) }
+            .getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()) }
+        Clock.System.now() < instant
+    }.onFailure { logError(it) }.getOrElse { false }
+}
+
 @Deprecated(
     "toRatingInt() is deprecated. Use new score API instead.",
     level = DeprecationLevel.ERROR
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt
index 974549fcbbb..6b82ee454e8 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt
@@ -1,6 +1,7 @@
 package com.lagradost.cloudstream3.extractors
 
 import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.APIHolder.unixTimeMS
 import com.lagradost.cloudstream3.SubtitleFile
 import com.lagradost.cloudstream3.app
 import com.lagradost.cloudstream3.utils.ExtractorApi
@@ -21,7 +22,7 @@ open class Vicloud : ExtractorApi() {
     ) {
         val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
         app.get(
-            "$mainUrl/api/?$id=&_=${System.currentTimeMillis()}",
+            "$mainUrl/api/?$id=&_=$unixTimeMS",
             headers = mapOf(
                 "X-Requested-With" to "XMLHttpRequest"
             ),
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt
index cf3e28a8de0..e7e1175f032 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt
@@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.ShowStatus
 import com.lagradost.cloudstream3.TvType
 import com.lagradost.cloudstream3.addDate
 import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.isUpcoming
 import com.lagradost.cloudstream3.mainPageOf
 import com.lagradost.cloudstream3.mvvm.logError
 import com.lagradost.cloudstream3.newEpisode
@@ -30,8 +31,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson
 import com.lagradost.cloudstream3.utils.AppUtils.toJson
 import okhttp3.Interceptor
 import okhttp3.Response
-import java.text.SimpleDateFormat
-import java.util.Locale
 
 //Reference: https://mydramalist.github.io/MDL-API/
 abstract class MyDramaListAPI : MainAPI() {
@@ -192,17 +191,6 @@ abstract class MyDramaListAPI : MainAPI() {
         return this
     }
 
-    private fun isUpcoming(dateString: String?): Boolean {
-        return try {
-            val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
-            val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
-            unixTimeMS < dateTime
-        } catch (t: Throwable) {
-            logError(t)
-            false
-        }
-    }
-
     private fun getStatus(status: String?): ShowStatus? {
         return when (status) {
             "Airing" -> ShowStatus.Ongoing
@@ -451,4 +439,4 @@ abstract class MyDramaListAPI : MainAPI() {
         @JsonProperty("date") val date: String? = null,
         @JsonProperty("airedDate") val airedDate: String? = null,
     )
-}
\ No newline at end of file
+}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
index 8f921d75cea..0e5aeb339da 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
@@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.ShowStatus
 import com.lagradost.cloudstream3.TvType
 import com.lagradost.cloudstream3.addDate
 import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.isUpcoming
 import com.lagradost.cloudstream3.mainPageOf
 import com.lagradost.cloudstream3.mvvm.logError
 import com.lagradost.cloudstream3.newEpisode
@@ -33,8 +34,6 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse
 import com.lagradost.cloudstream3.newTvSeriesSearchResponse
 import com.lagradost.cloudstream3.utils.AppUtils.parseJson
 import com.lagradost.cloudstream3.utils.AppUtils.toJson
-import java.text.SimpleDateFormat
-import java.util.Locale
 
 open class TraktProvider : MainAPI() {
     override var name = "Trakt"
@@ -292,17 +291,6 @@ open class TraktProvider : MainAPI() {
         ).text
     }
 
-    private fun isUpcoming(dateString: String?): Boolean {
-        return try {
-            val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
-            val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
-            unixTimeMS < dateTime
-        } catch (t: Throwable) {
-            logError(t)
-            false
-        }
-    }
-
     private fun getStatus(t: String?): ShowStatus {
         return when (t) {
             "returning series" -> ShowStatus.Ongoing
diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt
new file mode 100644
index 00000000000..4dc56978e88
--- /dev/null
+++ b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt
@@ -0,0 +1,196 @@
+package com.lagradost.cloudstream3
+
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.atStartOfDayIn
+import kotlinx.datetime.toInstant
+import kotlin.time.Instant
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class EpisodeDateTest {
+
+    private val api = object : MainAPI() {
+        override var name = "Test"
+        override var mainUrl = "https://test.com"
+    }
+
+    private fun episode() = api.newEpisode("")
+
+    @Test
+    fun addDateDefaultFormatParsesIsoDate() {
+        val ep = episode()
+        ep.addDate("2026-05-17")
+        val expected = LocalDate(2026, 5, 17)
+            .atStartOfDayIn(TimeZone.currentSystemDefault())
+            .toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+
+    @Test
+    fun addDateNullDoesNotSetDate() {
+        val ep = episode()
+        ep.addDate(null as String?)
+        assertNull(ep.date)
+    }
+
+    @Test
+    fun addDateInvalidStringLeavesDateNull() {
+        val ep = episode()
+        ep.addDate("not-a-date")
+        assertNull(ep.date)
+    }
+
+    @Test
+    fun addDateCustomFormatParsesSlashDate() {
+        val ep = episode()
+        ep.addDate("17/05/2026", "dd/MM/yyyy")
+        val expected = LocalDate(2026, 5, 17)
+            .atStartOfDayIn(TimeZone.currentSystemDefault())
+            .toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+
+    @Test
+    fun addDateIsoDateTimeWithOffsetUsesExactInstant() {
+        val ep = episode()
+        ep.addDate("2026-05-17T10:30:00.000+05:00", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
+        val expected = Instant.parse("2026-05-17T10:30:00.000+05:00").toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+
+    @Test
+    fun addDateUtcDateTimeUsesExactInstant() {
+        val ep = episode()
+        ep.addDate("2026-05-17T10:30:00.000Z", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
+        val expected = Instant.parse("2026-05-17T10:30:00.000Z").toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+
+    @Test
+    fun addDateDateTimeNoOffsetUsesSystemTimezone() {
+        val ep = episode()
+        ep.addDate("2026-05-17T10:30:00", "yyyy-MM-dd'T'HH:mm:ss")
+        val expected = LocalDateTime(2026, 5, 17, 10, 30, 0)
+            .toInstant(TimeZone.currentSystemDefault())
+            .toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+
+    @Test
+    fun addDateLocalDateSetsCorrectEpochMillis() {
+        val ep = episode()
+        ep.addDate(LocalDate(2026, 5, 17))
+        val expected = LocalDate(2026, 5, 17)
+            .atStartOfDayIn(TimeZone.currentSystemDefault())
+            .toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+
+    @Test
+    fun addDateNullLocalDateLeavesDateNull() {
+        val ep = episode()
+        ep.addDate(null as LocalDate?)
+        assertNull(ep.date)
+    }
+
+    @Test
+    fun addDateInstantSetsCorrectEpochMillis() {
+        val ep = episode()
+        val instant = Instant.parse("2026-05-17T10:30:00Z")
+        ep.addDate(instant)
+        assertEquals(instant.toEpochMilliseconds(), ep.date)
+    }
+
+    @Test
+    fun addDateNullInstantLeavesDateNull() {
+        val ep = episode()
+        ep.addDate(null as Instant?)
+        assertNull(ep.date)
+    }
+
+    @Test
+    fun addDateIsoWithMillisAndZUsesExactInstant() {
+        val ep = episode()
+        ep.addDate("2026-01-01T12:30:00.000Z")
+        assertEquals(1767270600000L, ep.date)
+    }
+
+    @Test
+    fun addDateIsoWithZNoMillisUsesExactInstant() {
+        val ep = episode()
+        ep.addDate("2026-01-01T12:30:00Z")
+        assertEquals(1767270600000L, ep.date)
+    }
+
+    @Test
+    fun addDateIsoWithPositiveOffsetUsesExactInstant() {
+        val ep = episode()
+        ep.addDate("2026-05-17T14:35:00+02:00")
+        // 14:35 +02:00 = 12:35 UTC = 2026-05-17T12:35:00Z
+        assertEquals(1779021300000L, ep.date)
+    }
+
+    @Test
+    fun addDateIsoWithNegativeOffsetUsesExactInstant() {
+        val ep = episode()
+        ep.addDate("2026-05-17T09:35:00-05:00")
+        // 09:35 -05:00 = 14:35 UTC = 2026-05-17T14:35:00Z
+        assertEquals(1779028500000L, ep.date)
+    }
+
+    @Test
+    fun addDateCustomFormatWithOffsetUsesExactInstant() {
+        val ep = episode()
+        ep.addDate("17/05/2026 14:35+02:00", "dd/MM/yyyy HH:mmXXX")
+        // 14:35 +02:00 = 12:35 UTC = 2026-05-17T12:35:00Z
+        assertEquals(1779021300000L, ep.date)
+    }
+
+    @Test
+    fun addDateCustomFormatDateTimeNoOffsetUsesSystemTimezone() {
+        val ep = episode()
+        ep.addDate("17/05/2026 14:35", "dd/MM/yyyy HH:mm")
+        val expected = LocalDateTime(2026, 5, 17, 14, 35, 0)
+            .toInstant(TimeZone.currentSystemDefault())
+            .toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+
+    @Test
+    fun addDateCustomFormatDateOnlyUsesStartOfDay() {
+        val ep = episode()
+        ep.addDate("17/05/2026", "dd/MM/yyyy")
+        val expected = LocalDate(2026, 5, 17)
+            .atStartOfDayIn(TimeZone.currentSystemDefault())
+            .toEpochMilliseconds()
+        assertEquals(expected, ep.date)
+    }
+}
+
+class IsUpcomingTest {
+
+    @Test
+    fun isUpcomingFutureDate() {
+        assertTrue(isUpcoming("2099-01-01"))
+    }
+
+    @Test
+    fun isUpcomingPastDate() {
+        assertFalse(isUpcoming("2000-01-01"))
+    }
+
+    @Test
+    fun isUpcomingNullReturnsFalse() {
+        assertFalse(isUpcoming(null))
+    }
+
+    @Test
+    fun isUpcomingInvalidStringReturnsFalse() {
+        assertFalse(isUpcoming("not-a-date"))
+    }
+}
