Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -184,9 +183,7 @@ abstract class AuthAPI {

fun splitRedirectUrl(redirectUrl: String): Map<String, String> {
return splitQuery(
URL(
redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
)
redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -630,16 +628,17 @@ object AppContextUtils {
}
}

fun splitQuery(url: URL): Map<String, String> {
val queryPairs: MutableMap<String, String> = 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<String, String> {
return com.lagradost.cloudstream3.splitQuery(Url(url.toString()))
}

/**| S1:E2 Hello World
Expand Down Expand Up @@ -896,4 +895,4 @@ object AppContextUtils {
} else null
return currentAudioFocusRequest
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExtractorLink>): List<ExtractorLink> {
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<String, String>`.
* 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<String, String> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep the API-surface somewhat dependency-agnostic. There is no need to make the same mistake as splitQuery(url: java.net.URL) again, when we can just expose a string argument instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I was aiming for was to expose a way to accept a direct URL as well as String. By passing a URL it can sometimes be a bit more type safe as well, and also as we shift towards ktor URL more, sometimes a URL may be wanted directly, in which case, I just thought it was better and more straightforward to accept a URL with a separate convenience method accepting a string only as well. I am open to changes though if you think it should be done differently?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am kinda considering whether this should instead be an extension function on String in StringUtils though, in which case it would make a little more sense to simply drop the URL overload and just accept that extension method. I am honestly not 100% sure on the best option here...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Maybe better not in StringUtils (or is it, still haven't decided that) but perhaps it also being called something like splitUrlParameters or getUrlParameters would make more sense and be more clear than splitQuery?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking maybe just name splitQuery, parseUrlParameters instead. It's a more clear naming scheme, and is more consistent with other Url-based naming.

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<String, String> {
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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"])
}
}