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 1/2] 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"]) + } +} From a34f40c2415f456a9452693044a70bb37a89362a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:44:32 -0600 Subject: [PATCH 2/2] More clear method name --- app/build.gradle.kts | 1 - .../cloudstream3/syncproviders/AuthAPI.kt | 4 +-- .../cloudstream3/utils/AppContextUtils.kt | 9 +++--- .../com/lagradost/cloudstream3/MainAPI.kt | 32 +++++++++---------- ...QueryTest.kt => SplitUrlParametersTest.kt} | 30 ++++++++--------- 5 files changed, 37 insertions(+), 39 deletions(-) rename library/src/commonTest/kotlin/com/lagradost/cloudstream3/{SplitQueryTest.kt => SplitUrlParametersTest.kt} (64%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cdc4e03891b..66a55ae8830 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,6 @@ 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 b2a50764f82..c269f2a12e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.base64Encode -import com.lagradost.cloudstream3.splitQuery +import com.lagradost.cloudstream3.splitUrlParameters import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING import java.security.SecureRandom @@ -165,7 +165,7 @@ abstract class AuthAPI { get() = unixTimeMS fun splitRedirectUrl(redirectUrl: String): Map { - return splitQuery( + return splitUrlParameters( 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 1299047e1d0..1f2050bb9c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -86,7 +86,6 @@ 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 @@ -630,15 +629,15 @@ object AppContextUtils { // Deprecate after next stable /* @Deprecated( - message = "Use Ktor 'Url' based splitQuery instead.", + message = "Use splitUrlParameters instead.", replaceWith = ReplaceWith( - expression = "splitQuery(Url(url.toString()))", - imports = ["com.lagradost.cloudstream3.splitQuery", "io.ktor.http.Url"], + expression = "splitUrlParameters(url.toString())", + imports = ["com.lagradost.cloudstream3.splitUrlParameters"], ), level = DeprecationLevel.WARNING, ) */ fun splitQuery(url: java.net.URL): Map { - return com.lagradost.cloudstream3.splitQuery(Url(url.toString())) + return com.lagradost.cloudstream3.splitUrlParameters(url.toString()) } /**| S1:E2 Hello World diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index c69bd15771c..17c9c7972be 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -762,45 +762,45 @@ fun sortUrls(urls: Set): List { } /** - * Splits the query string of a [Url] into a map of key-value pairs. + * Splits the parameters 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, + * built-in parameters 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. + * Note: if a parameter key appears multiple times (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. + * @param url the [Url] whose parameters should be extracted. + * @return a map of decoded parameter names to their first decoded value. * * @sample - * splitQuery(Url("https://example.com/path?foo=bar&baz=qux")) + * splitUrlParameters(Url("https://example.com/path?foo=bar&baz=qux")) * // returns {"foo": "bar", "baz": "qux"} */ @Prerelease -fun splitQuery(url: Url): Map { +fun splitUrlParameters(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. + * Splits the parameters 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]. + * to [splitUrlParameters]. * - * @param url the URL string whose query parameters should be extracted. - * @return a map of decoded query parameter names to their first decoded value. + * @param url the URL string whose parameters should be extracted. + * @return a map of decoded parameter names to their first decoded value. * * @sample - * splitQuery("https://example.com/path?foo=bar&baz=qux") + * splitUrlParameters("https://example.com/path?foo=bar&baz=qux") * // returns {"foo": "bar", "baz": "qux"} */ @Prerelease -fun splitQuery(url: String): Map { - return splitQuery(Url(url)) +fun splitUrlParameters(url: String): Map { + return splitUrlParameters(Url(url)) } /** diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitQueryTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitUrlParametersTest.kt similarity index 64% rename from library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitQueryTest.kt rename to library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitUrlParametersTest.kt index ebcfd2637bc..7e58c70be84 100644 --- a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitQueryTest.kt +++ b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/SplitUrlParametersTest.kt @@ -5,71 +5,71 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class SplitQueryTest { +class SplitUrlParametersTest { @Test - fun splitsBasicQueryParameters() { + fun splitsBasicParameters() { val url = Url("https://example.com/path?foo=bar&baz=qux") - val result = splitQuery(url) + val result = splitUrlParameters(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) + val result = splitUrlParameters(url) assertEquals(mapOf("na me" to "hello world", "sp&ec" to "a+b"), result) } @Test - fun returnsEmptyMapWhenThereIsNoQueryString() { + fun returnsEmptyMapWhenThereAreNoParameters() { val url = Url("https://example.com/path") - val result = splitQuery(url) + val result = splitUrlParameters(url) assertTrue(result.isEmpty()) } @Test fun keepsOnlyFirstValueForRepeatedKeys() { val url = Url("https://example.com/path?a=1&a=2&a=3") - val result = splitQuery(url) + val result = splitUrlParameters(url) assertEquals(mapOf("a" to "1"), result) } @Test fun handlesParameterWithNoValue() { val url = Url("https://example.com/path?flag&foo=bar") - val result = splitQuery(url) + val result = splitUrlParameters(url) assertEquals("bar", result["foo"]) assertEquals("", result["flag"]) } @Test - fun stringOverloadSplitsBasicQueryParameters() { - val result = splitQuery("https://example.com/path?foo=bar&baz=qux") + fun stringOverloadSplitsBasicParameters() { + val result = splitUrlParameters("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") + val result = splitUrlParameters("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") + fun stringOverloadReturnsEmptyMapWhenThereAreNoParameters() { + val result = splitUrlParameters("https://example.com/path") assertTrue(result.isEmpty()) } @Test fun stringOverloadKeepsOnlyFirstValueForRepeatedKeys() { - val result = splitQuery("https://example.com/path?a=1&a=2&a=3") + val result = splitUrlParameters("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") + val result = splitUrlParameters("https://example.com/path?flag&foo=bar") assertEquals("bar", result["foo"]) assertEquals("", result["flag"]) }