From 626acd518d97c1beaab82c77d6efbf507b9fbf85 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:56:44 -0600 Subject: [PATCH] Add new splitQuery API accepting Ktor Url or string --- app/build.gradle.kts | 1 + .../cloudstream3/syncproviders/AuthAPI.kt | 7 +- .../cloudstream3/utils/AppContextUtils.kt | 27 ++++--- .../com/lagradost/cloudstream3/MainAPI.kt | 54 ++++++++++++- .../lagradost/cloudstream3/SplitQueryTest.kt | 76 +++++++++++++++++++ 5 files changed, 142 insertions(+), 23 deletions(-) create mode 100644 library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitQueryTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6c784f3ef8d..92bf8123dc9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -224,6 +224,7 @@ dependencies { implementation(libs.bundles.navigation) implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.serialization.json) // JSON Parser + implementation(libs.ktor.http) // Design & UI implementation(libs.preference.ktx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index 184a9fbcc64..6f371b6ace9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.splitQuery import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed @@ -35,11 +36,9 @@ import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting -import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.txt -import java.net.URL import java.security.SecureRandom import java.util.Date import java.util.concurrent.TimeUnit @@ -184,9 +183,7 @@ abstract class AuthAPI { fun splitRedirectUrl(redirectUrl: String): Map { return splitQuery( - URL( - redirectUrl.replace(APP_STRING, "https").replace("/#", "?") - ) + redirectUrl.replace(APP_STRING, "https").replace("/#", "?") ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 7278fcdd74f..1299047e1d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -86,16 +86,14 @@ import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.downloader.DownloadObjects +import io.ktor.http.Url import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.Cache import java.io.File -import java.net.URL -import java.net.URLDecoder import java.util.concurrent.Executor import java.util.concurrent.Executors - object AppContextUtils { fun RecyclerView.isRecyclerScrollable(): Boolean { val layoutManager = @@ -630,16 +628,17 @@ object AppContextUtils { } } - fun splitQuery(url: URL): Map { - val queryPairs: MutableMap = LinkedHashMap() - val query: String = url.query - val pairs = query.split("&").toTypedArray() - for (pair in pairs) { - val idx = pair.indexOf("=") - queryPairs[URLDecoder.decode(pair.substring(0, idx), "UTF-8")] = - URLDecoder.decode(pair.substring(idx + 1), "UTF-8") - } - return queryPairs + // Deprecate after next stable + /* @Deprecated( + message = "Use Ktor 'Url' based splitQuery instead.", + replaceWith = ReplaceWith( + expression = "splitQuery(Url(url.toString()))", + imports = ["com.lagradost.cloudstream3.splitQuery", "io.ktor.http.Url"], + ), + level = DeprecationLevel.WARNING, + ) */ + fun splitQuery(url: java.net.URL): Map { + return com.lagradost.cloudstream3.splitQuery(Url(url.toString())) } /**| S1:E2 Hello World @@ -896,4 +895,4 @@ object AppContextUtils { } else null return currentAudioFocusRequest } -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 4a9e0b10aac..2bb82bf886d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -755,18 +755,64 @@ fun MainAPI.fixUrl(url: String): String { } } -/** Sort the urls based on quality +/** + * Sort the urls based on quality + * * @param urls Set of [ExtractorLink] - * */ + */ fun sortUrls(urls: Set): List { return urls.sortedBy { t -> -t.quality } } -/** Capitalize the first letter of string. +/** + * Splits the query string of a [Url] into a map of key-value pairs. + * + * Unlike a manual `split("&")` / `split("=")` implementation, this relies on Ktor's + * built-in query parser ([Url.parameters]), which already handles URL-decoding, + * malformed pairs, and parameters without a value. + * + * Note: if a key appears multiple times in the query string (e.g. `?a=1&a=2`), + * only the **first** value is kept, since the return type is `Map`. + * Use [Url.parameters] directly if you need all values for repeated keys. + * + * @param url the [Url] whose query parameters should be extracted. + * @return a map of decoded query parameter names to their first decoded value. + * + * @sample + * splitQuery(Url("https://example.com/path?foo=bar&baz=qux")) + * // returns {"foo": "bar", "baz": "qux"} + */ +@Prerelease +fun splitQuery(url: Url): Map { + return url.parameters.entries().associate { (key, values) -> key to values.firstOrNull().orEmpty() } +} + +/** + * Splits the query portion of a raw URL [String] into a map of key-value pairs. + * + * Convenience overload for callers that have a URL as plain text rather than a parsed + * [Url] instance. Internally parses [url] with Ktor's [Url] constructor and delegates + * to [splitQuery]. + * + * @param url the URL string whose query parameters should be extracted. + * @return a map of decoded query parameter names to their first decoded value. + * + * @sample + * splitQuery("https://example.com/path?foo=bar&baz=qux") + * // returns {"foo": "bar", "baz": "qux"} + */ +@Prerelease +fun splitQuery(url: String): Map { + return splitQuery(Url(url)) +} + +/** + * Capitalize the first letter of string. + * * @param str String to be capitalized * @return non-nullable String * @see capitalizeStringNullable - * */ + */ fun capitalizeString(str: String): String { return capitalizeStringNullable(str) ?: str } diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitQueryTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitQueryTest.kt new file mode 100644 index 00000000000..ebcfd2637bc --- /dev/null +++ b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitQueryTest.kt @@ -0,0 +1,76 @@ +package com.lagradost.cloudstream3 + +import io.ktor.http.Url +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SplitQueryTest { + + @Test + fun splitsBasicQueryParameters() { + val url = Url("https://example.com/path?foo=bar&baz=qux") + val result = splitQuery(url) + assertEquals(mapOf("foo" to "bar", "baz" to "qux"), result) + } + + @Test + fun decodesUrlEncodedKeysAndValues() { + val url = Url("https://example.com/path?na%20me=hello%20world&sp%26ec=a%2Bb") + val result = splitQuery(url) + assertEquals(mapOf("na me" to "hello world", "sp&ec" to "a+b"), result) + } + + @Test + fun returnsEmptyMapWhenThereIsNoQueryString() { + val url = Url("https://example.com/path") + val result = splitQuery(url) + assertTrue(result.isEmpty()) + } + + @Test + fun keepsOnlyFirstValueForRepeatedKeys() { + val url = Url("https://example.com/path?a=1&a=2&a=3") + val result = splitQuery(url) + assertEquals(mapOf("a" to "1"), result) + } + + @Test + fun handlesParameterWithNoValue() { + val url = Url("https://example.com/path?flag&foo=bar") + val result = splitQuery(url) + assertEquals("bar", result["foo"]) + assertEquals("", result["flag"]) + } + + @Test + fun stringOverloadSplitsBasicQueryParameters() { + val result = splitQuery("https://example.com/path?foo=bar&baz=qux") + assertEquals(mapOf("foo" to "bar", "baz" to "qux"), result) + } + + @Test + fun stringOverloadDecodesUrlEncodedKeysAndValues() { + val result = splitQuery("https://example.com/path?na%20me=hello%20world&sp%26ec=a%2Bb") + assertEquals(mapOf("na me" to "hello world", "sp&ec" to "a+b"), result) + } + + @Test + fun stringOverloadReturnsEmptyMapWhenThereIsNoQueryString() { + val result = splitQuery("https://example.com/path") + assertTrue(result.isEmpty()) + } + + @Test + fun stringOverloadKeepsOnlyFirstValueForRepeatedKeys() { + val result = splitQuery("https://example.com/path?a=1&a=2&a=3") + assertEquals(mapOf("a" to "1"), result) + } + + @Test + fun stringOverloadHandlesParameterWithNoValue() { + val result = splitQuery("https://example.com/path?flag&foo=bar") + assertEquals("bar", result["foo"]) + assertEquals("", result["flag"]) + } +}