diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 30bedcc1b..b5960d5d9 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -71,7 +71,6 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - uses: actions/checkout@v6 diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml new file mode 100644 index 000000000..e354d657d --- /dev/null +++ b/.github/workflows/issue_action.yml @@ -0,0 +1,98 @@ +name: Issue automatic actions + +on: + issues: + types: [opened] + +permissions: + contents: read + issues: write + +jobs: + issue-moderator: + runs-on: ubuntu-latest + steps: + - name: Generate access token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.GH_APP_ID }} + private_key: ${{ secrets.GH_APP_KEY }} + + - name: Similarity analysis + id: similarity + uses: actions-cool/issues-similarity-analysis@v1 + with: + token: ${{ steps.generate_token.outputs.token }} + filter-threshold: 0.60 + title-excludes: '' + comment-title: | + ### Your issue looks similar to these issues: + Please close if duplicate. + comment-body: '${index}. ${similarity} #${number}' + + - name: Label if possible duplicate + if: steps.similarity.outputs.similar-issues-found =='true' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible duplicate"] + }) + + - uses: actions/checkout@v6 + + - name: Automatically close issues that dont follow the issue template + uses: lucasbento/auto-close-issues@v1.0.2 + with: + github-token: ${{ steps.generate_token.outputs.token }} + issue-close-message: | + @${issue.user.login}: hello! :wave: + This issue is being automatically closed because it does not follow the issue template." + closed-issues-label: "invalid" + + - name: Check if issue mentions a provider + id: provider_check + env: + GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}" + run: | + wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" + pip3 install httpx + RES="$(python3 ./check_issue.py)" + echo "name=${RES}" >> $GITHUB_OUTPUT + + - name: Comment if issue mentions a provider + if: steps.provider_check.outputs.name != 'none' + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ steps.generate_token.outputs.token }} + body: | + Hello ${{ github.event.issue.user.login }}. + Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). + + Found provider name: `${{ steps.provider_check.outputs.name }}` + + - name: Label if mentions provider + if: steps.provider_check.outputs.name != 'none' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible provider issue"] + }) + + - name: Add eyes reaction to all issues + uses: actions-cool/emoji-helper@v1.0.0 + with: + type: 'issue' + token: ${{ steps.generate_token.outputs.token }} + emoji: 'eyes' diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b5b17ba6a..d9a20a04b 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -62,7 +62,6 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Create pre-release diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8f5c62866..675ce3b2f 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 check + run: ./gradlew assemblePrereleaseDebug lint - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02c1f99e8..ae5301929 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { alias(libs.plugins.android.application) alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.serialization) } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) @@ -104,8 +103,8 @@ android { applicationId = "com.lagradost.cloudstream3" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = libs.versions.versionCode.get().toInt() - versionName = libs.versions.versionName.get() + versionCode = 68 + versionName = "4.7.0" manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get() @@ -207,11 +206,9 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.json) androidTestImplementation(libs.core) - androidTestImplementation(libs.espresso.core) + implementation(libs.junit.ktx) androidTestImplementation(libs.ext.junit) - androidTestImplementation(libs.instancio.core) - androidTestImplementation(libs.junit.ktx) - androidTestImplementation(libs.kotlin.test) + androidTestImplementation(libs.espresso.core) // Android Core & Lifecycle implementation(libs.core.ktx) @@ -222,7 +219,6 @@ dependencies { implementation(libs.bundles.lifecycle) implementation(libs.bundles.navigation) implementation(libs.kotlinx.collections.immutable) - implementation(libs.kotlinx.serialization.json) // JSON Parser // Design & UI implementation(libs.preference.ktx) @@ -259,15 +255,13 @@ dependencies { // Extensions & Other Libs implementation(libs.jsoup) // HTML Parser implementation(libs.rhino) // Run JavaScript + implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance implementation(libs.safefile) // To Prevent the URI File Fu*kery coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9 implementation(libs.jackson.module.kotlin) // JSON Parser implementation(libs.zipline) - // Deprecated; will be removed once extensions have time to migrate from using it - implementation("me.xdrop:fuzzywuzzy:1.4.0") - // Torrent Support implementation(libs.torrentserver) @@ -316,7 +310,6 @@ tasks.withType { optIn.addAll( "com.lagradost.cloudstream3.InternalAPI", "com.lagradost.cloudstream3.Prerelease", - "kotlin.uuid.ExperimentalUuidApi", ) } } diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt deleted file mode 100644 index 80c7b49b0..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.lagradost.cloudstream3 - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import dalvik.system.DexFile -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer -import kotlinx.serialization.serializerOrNull -import org.instancio.Instancio -import org.junit.Test -import org.junit.runner.RunWith -import kotlin.reflect.KClass -import kotlin.reflect.jvm.jvmName -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -@RunWith(AndroidJUnit4::class) -class SerializationClassTester { - // Same as app, or using app reference - val jacksonMapper = mapper - val kotlinxMapper = json - - @Test - fun isIdenticalSerialization() { - val serializableClasses = findSerializableClasses("com.lagradost") - println("Number of serializable classes: ${serializableClasses.size}") - - serializableClasses.forEach { kClass -> - val instance = Instancio.create(kClass.java) - - val jacksonJson = jacksonMapper.writeValueAsString(instance) - val kotlinxJson = serializeWithKotlinx(kClass, instance) - - assertEquals( - jacksonJson, - kotlinxJson, - """ - Serialization mismatch for: - ${kClass.qualifiedName} - - Jackson: - $jacksonJson - - Kotlinx: - $kotlinxJson - - """.trimIndent() - ) - println("Identical serialization for: ${kClass.jvmName}") - } - } - - @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) - @Test - fun isIdenticalDeserialization() { - val serializableClasses = findSerializableClasses("com.lagradost") - println("Number of serializable classes: ${serializableClasses.size}") - - serializableClasses.forEach { kClass -> - val instance = Instancio.create(kClass.java) - // Convert to JSON to get example JSON object - // We prefer jackson here because the app may have many jackson JSON strings in local storage - val originalJson = jacksonMapper.writeValueAsString(instance) - - // Create an object from the JSON using kotlinx - val serializer = - kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass) - assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!") - val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson) - - // Create an object from the JSON using jackson - val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java) - - - // Deep inspect both object using the mapper toJson function. - // This deep equality check can be performed using other methods, but this just works. - val jacksonJson = mapperDecoded.toJson() - val kotlinxJson = kotlinxDecoded.toJson() - - assertEquals( - jacksonJson, - kotlinxJson, - """ - Serialization mismatch for: - ${kClass.qualifiedName} - - Jackson: - $jacksonJson - - Kotlinx: - $kotlinxJson - - """.trimIndent() - ) - println("Identical deserialization for: ${kClass.jvmName}") - } - } - - // DEX files are the best solution to read all our classes dynamically. - // classgraph could be used instead, but it only gives results on the JVM, not Android. - @Suppress("DEPRECATION") - private fun findSerializableClasses(packageName: String): List> { - val context = InstrumentationRegistry - .getInstrumentation() - .targetContext - - val dexFile = DexFile(context.packageCodePath) - return dexFile.entries() - .toList() - .filter { it.startsWith(packageName) } - .mapNotNull { - runCatching { Class.forName(it).kotlin }.getOrNull() - }.filter { kClass -> - // Not possible to use .hasAnnotation() on newer Android versions. - kClass.java.annotations.any { - it is Serializable - } - } - } - - @OptIn(InternalSerializationApi::class) - @Suppress("UNCHECKED_CAST") - private fun serializeWithKotlinx( - kClass: KClass<*>, - value: Any - ): String { - val serializer = kClass.serializer() as KSerializer - return kotlinxMapper.encodeToString(serializer, value) - } -} diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt deleted file mode 100644 index 15ad532f8..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import android.net.Uri -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.Serializable -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test - -@OptIn(ExperimentalSerializationApi::class) -@KeepGeneratedSerializer -@Serializable(with = NonEmptyData.Serializer::class) -data class NonEmptyData( - val title: String = "", - val tags: List = emptyList(), - val meta: Map = emptyMap(), - val name: String = "hello", -) { - object Serializer : NonEmptySerializer(NonEmptyData.generatedSerializer()) -} - -@OptIn(ExperimentalSerializationApi::class) -@KeepGeneratedSerializer -@Serializable(with = WriteOnlyData.Serializer::class) -data class WriteOnlyData( - val fieldA: String = "", - val fieldB: String = "", -) { - object Serializer : WriteOnlySerializer( - WriteOnlyData.generatedSerializer(), - setOf("fieldB"), - ) -} - -@OptIn(ExperimentalSerializationApi::class) -@KeepGeneratedSerializer -@Serializable(with = MultiWriteOnly.Serializer::class) -data class MultiWriteOnly( - val fieldA: String = "", - val fieldB: String = "", - val fieldC: String = "", -) { - object Serializer : WriteOnlySerializer( - MultiWriteOnly.generatedSerializer(), - setOf("fieldB", "fieldC"), - ) -} - -@Serializable -data class UriData( - @Serializable(with = UriSerializer::class) - val uri: Uri = Uri.EMPTY, -) - -class SerializerTest { - - @Test - fun nonEmptySerializerOmitsEmptyStrings() { - val data = NonEmptyData(title = "", name = "hello") - val result = data.toJson() - assertFalse(result.contains("title")) - assertTrue(result.contains("name")) - } - - @Test - fun nonEmptySerializerOmitsEmptyLists() { - val data = NonEmptyData(tags = emptyList(), name = "hello") - val result = data.toJson() - assertFalse(result.contains("tags")) - } - - @Test - fun nonEmptySerializerOmitsEmptyMaps() { - val data = NonEmptyData(meta = emptyMap(), name = "hello") - val result = data.toJson() - assertFalse(result.contains("meta")) - } - - @Test - fun nonEmptySerializerKeepsNonEmptyFields() { - val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v")) - val result = data.toJson() - assertTrue(result.contains("title")) - assertTrue(result.contains("tags")) - assertTrue(result.contains("meta")) - } - - @Test - fun nonEmptySerializerDoesNotAffectDeserialization() { - val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}""" - val result = parseJson(input) - assertEquals("hello", result.title) - assertEquals(listOf("a"), result.tags) - assertEquals(mapOf("k" to "v"), result.meta) - assertEquals("world", result.name) - } - - @Test - fun writeOnlySerializerOmitsFieldOnSerialize() { - val data = WriteOnlyData(fieldA = "hello", fieldB = "secret") - val result = data.toJson() - assertTrue(result.contains("fieldA")) - assertFalse(result.contains("fieldB")) - } - - @Test - fun writeOnlySerializerDeserializesNormally() { - val input = """{"fieldA":"hello","fieldB":"secret"}""" - val result = parseJson(input) - assertEquals("hello", result.fieldA) - assertEquals("secret", result.fieldB) - } - - @Test - fun writeOnlySerializerDeserializesMissingAsDefault() { - val input = """{"fieldA":"hello"}""" - val result = parseJson(input) - assertEquals("hello", result.fieldA) - assertEquals("", result.fieldB) - } - - @Test - fun writeOnlySerializerHandlesMultipleKeys() { - val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2") - val result = data.toJson() - assertTrue(result.contains("fieldA")) - assertFalse(result.contains("fieldB")) - assertFalse(result.contains("fieldC")) - } - - @Test - fun uriSerializerSerializesUriToString() { - val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) - val result = data.toJson() - assertTrue(result.contains("https://example.com/path?query=1")) - } - - @Test - fun uriSerializerDeserializesStringToUri() { - val input = """{"uri":"https://example.com/path?query=1"}""" - val result = parseJson(input) - assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri) - } - - @Test - fun uriSerializerRoundtripsCorrectly() { - val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) - val encoded = data.toJson() - val decoded = parseJson(encoded) - assertEquals(data.uri, decoded.uri) - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 4ce09bd44..ed0aaf9b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -579,10 +579,8 @@ object CommonActivity { // TODO: Figure out why removing the check for SearchAutoComplete seems // to break focus on TV as it shouldn't need to be used. - // Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote) - // send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button. @SuppressLint("RestrictedApi") - if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) && + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) ) { showInputMethod(act.currentFocus?.findFocus()) @@ -603,4 +601,4 @@ object CommonActivity { } return null } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 90583011d..8a98bd297 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -408,10 +408,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa return true } - val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull() - if (matchedApi != null) { - loadResult(str, matchedApi.name, "") - return true + synchronized(apis) { + for (api in apis) { + if (str.startsWith(api.mainUrl)) { + loadResult(str, api.name, "") + return true + } + } } } } @@ -806,11 +809,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + private val pluginsLock = Mutex() private fun onAllPluginsLoaded(success: Boolean = false) { ioSafe { pluginsLock.withLock { - allProviders.withLock { + synchronized(allProviders) { // Load cloned sites after plugins have been loaded since clones depend on plugins. try { getKey>(USER_PROVIDER_API)?.let { list -> @@ -1653,7 +1657,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ioSafe { initAll() // No duplicates (which can happen by registerMainAPI) - apis = allProviders.distinctBy { it } + apis = synchronized(allProviders) { + allProviders.distinctBy { it } + } } // val navView: BottomNavigationView = findViewById(R.id.nav_view) @@ -1961,7 +1967,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (BuildConfig.DEBUG) { var providersAndroidManifestString = "Current androidmanifest should be:\n" - allProviders.withLock { + synchronized(allProviders) { for (api in allProviders) { providersAndroidManifestString += "() )*/ - - if (video.tvType.isEpisodeBased()) { - video.season?.let { intent.putExtra("introdb_season", it) } - video.episode.let { intent.putExtra("introdb_episode", it) } - } - - val position = getViewPos(video.id)?.position - if (position != null) - putExtra("position", position.toInt()) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - val position = intent?.getIntExtra("position", -1) ?: -1 - val duration = intent?.getIntExtra("duration", -1) ?: -1 - Log.d("MPV", "Position: $position, Duration: $duration") - updateDurationAndPosition(position.toLong(), duration.toLong()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt deleted file mode 100644 index 348be440a..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.lagradost.cloudstream3.actions.temp - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.net.toUri -import com.lagradost.cloudstream3.actions.OpenInAppAction -import com.lagradost.cloudstream3.ui.result.LinkLoadingResult -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.utils.txt - -/** https://github.com/Kindness-Kismet/only_player/tree/main - * https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */ -class OnlyPlayer : OpenInAppAction( - txt("Only Player"), - "one.only.player", - intentClass = "one.only.player.feature.player.PlayerActivity" -) { - override val oneSource = true - override suspend fun putExtra( - context: Context, - intent: Intent, - video: ResultEpisode, - result: LinkLoadingResult, - index: Int? - ) { - /** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */ - intent.apply { - val link = result.links[index!!] - setData(link.url.toUri()) - - putExtra("headers", Bundle().apply { - for ((key, value) in link.headers) { - putExtra(key, value) - } - }) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - /* onResult does not get called */ - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index e1496db06..efa028d14 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.actions.VideoClickAction import com.lagradost.cloudstream3.actions.VideoClickActionHolder import kotlin.Throws + abstract class Plugin : BasePlugin() { /** * Called when your Plugin is loaded @@ -25,7 +26,9 @@ abstract class Plugin : BasePlugin() { fun registerVideoClickAction(element: VideoClickAction) { Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction") element.sourcePlugin = this.filename - VideoClickActionHolder.allVideoClickActions.add(element) + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.add(element) + } } /** @@ -37,4 +40,4 @@ abstract class Plugin : BasePlugin() { * This will add a button in the settings allowing you to add custom settings */ var openSettings: ((context: Context) -> Unit)? = null -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index debd3f0eb..eae14a6c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -610,7 +610,7 @@ object PluginManager { return false } InputStreamReader(stream).use { reader -> - manifest = parseJson(reader.readText()) + manifest = parseJson(reader, BasePlugin.Manifest::class.java) } } @@ -651,15 +651,9 @@ object PluginManager { context.resources.configuration ) } - synchronized(plugins) { - plugins[filePath] = pluginInstance - } - synchronized(classLoaders) { - classLoaders[loader] = pluginInstance - } - synchronized(urlPlugins) { - urlPlugins[data.url ?: filePath] = pluginInstance - } + plugins[filePath] = pluginInstance + classLoaders[loader] = pluginInstance + urlPlugins[data.url ?: filePath] = pluginInstance if (pluginInstance is Plugin) { pluginInstance.load(context) } else { @@ -695,20 +689,21 @@ object PluginManager { } // remove all registered apis - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { - removePluginMapping(it) + synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { + removePluginMapping(it) + } + } + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - APIHolder.allProviders.withLock { - APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename } + synchronized(extractorApis) { + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } } - extractorApis.withLock { - extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename } - } - - VideoClickActionHolder.allVideoClickActions.withLock { - VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename } + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename } } synchronized(classLoaders) { 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 184a9fbcc..83a7a0984 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -36,9 +36,11 @@ 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.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.txt +import me.xdrop.fuzzywuzzy.FuzzySearch import java.net.URL import java.security.SecureRandom import java.util.Date diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt index 0b8c3e5ae..7a93f96f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt @@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf /** Stateless safe abstraction of SubtitleAPI */ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @@ -24,30 +24,26 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { ) // maybe make this a generic struct? right now there is a lot of boilerplate - private val searchCache = atomicListOf() + private val searchCache = threadSafeListOf() private var searchCacheIndex: Int = 0 - private val resourceCache = atomicListOf() + private val resourceCache = threadSafeListOf() private var resourceCacheIndex: Int = 0 const val CACHE_SIZE = 20 } @WorkerThread suspend fun resource(data: SubtitleEntity): Result = runCatching { - val cached = resourceCache.withLock { - var found: SubtitleResource? = null + synchronized(resourceCache) { for (item in resourceCache) { // 20 min save if (item.query == data && (unixTime - item.unixTime) < 60 * 20) { - found = item.response - break + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached val returnValue = api.resource(freshAuth(), data) - resourceCache.withLock { + synchronized(resourceCache) { val add = SavedResourceResponse(unixTime, returnValue, data) if (resourceCache.size > CACHE_SIZE) { resourceCache[resourceCacheIndex] = add // rolling cache @@ -62,25 +58,22 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @WorkerThread suspend fun search(query: SubtitleSearch): Result> { return runCatching { - val cached = searchCache.withLock { - var found: List? = null + synchronized(searchCache) { for (item in searchCache) { // 120 min save if (item.query == query && (unixTime - item.unixTime) < 60 * 120) { - found = item.response - break + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached - val returnValue = api.search(freshAuth(), query) ?: emptyList() + val returnValue = + api.search(freshAuth(), query) ?: emptyList() // only cache valid return values if (returnValue.isNotEmpty()) { val add = SavedSearchResponse(unixTime, returnValue, query) - searchCache.withLock { + synchronized(searchCache) { if (searchCache.size > CACHE_SIZE) { searchCache[searchCacheIndex] = add // rolling cache searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE @@ -93,3 +86,4 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { } } } + diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index f30a64748..e5f9aca84 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -10,8 +10,8 @@ import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting -import com.lagradost.cloudstream3.utils.Levenshtein import com.lagradost.cloudstream3.utils.UiText +import me.xdrop.fuzzywuzzy.FuzzySearch import java.util.Date /** @@ -138,7 +138,7 @@ abstract class SyncAPI : AuthAPI() { ListSorting.Query -> if (query != null) { items.sortedBy { - -Levenshtein.partialRatio( + -FuzzySearch.partialRatio( query.lowercase(), it.name.lowercase() ) } @@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() { override var score: Score? = null, val tags: List? = null ) : SearchResponse -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 177018e19..7a46b4113 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -50,8 +50,7 @@ class AniListApi : SyncAPI() { override suspend fun login(redirectUrl: String, payload: String?): AuthToken? { val sanitizer = splitRedirectUrl(redirectUrl) val token = AuthToken( - accessToken = sanitizer["access_token"] - ?: throw ErrorLoadingException("No access token"), + accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"), //refreshToken = sanitizer["refresh_token"], accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(), ) @@ -84,8 +83,8 @@ class AniListApi : SyncAPI() { return "$mainUrl/anime/$id" } - override suspend fun search(auth: AuthData?, query: String): List? { - val data = searchShows(query) ?: return null + override suspend fun search(auth : AuthData?, query: String): List? { + val data = searchShows(name) ?: return null return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, @@ -97,7 +96,7 @@ class AniListApi : SyncAPI() { } } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") val season = getSeason(internalId).data.media @@ -159,7 +158,7 @@ class AniListApi : SyncAPI() { ) } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val internalId = id.toIntOrNull() ?: return null val data = getDataAboutId(auth ?: return null, internalId) ?: return null @@ -460,7 +459,7 @@ class AniListApi : SyncAPI() { } } - private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? { + private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? { val q = """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) @@ -507,7 +506,7 @@ class AniListApi : SyncAPI() { } - private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? { + private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? { return app.post( "https://graphql.anilist.co/", headers = mapOf( @@ -639,7 +638,7 @@ class AniListApi : SyncAPI() { } } - override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? { + override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? { val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy { convertAniListStringToStatus(it.status ?: "").stringRes }?.mapValues { group -> @@ -667,7 +666,7 @@ class AniListApi : SyncAPI() { ) } - private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? { + private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? { val userID = auth.user.id val mediaType = "ANIME" @@ -715,7 +714,7 @@ class AniListApi : SyncAPI() { return text?.toKotlinObject() } - suspend fun toggleLike(auth: AuthData, id: Int): Boolean { + suspend fun toggleLike(auth : AuthData, id: Int): Boolean { val q = """mutation (${'$'}animeId: Int = $id) { ToggleFavourite (animeId: ${'$'}animeId) { anime { @@ -738,7 +737,7 @@ class AniListApi : SyncAPI() { data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( - auth: AuthData, + auth : AuthData, id: Int, type: AniListStatusType, score: Score?, @@ -787,7 +786,7 @@ class AniListApi : SyncAPI() { return data != "" } - private suspend fun getUser(token: AuthToken): AniListUser? { + private suspend fun getUser(token : AuthToken): AniListUser? { val q = """ { Viewer { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt index e15a77c64..29c3c0c17 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt @@ -27,8 +27,9 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.text.SimpleDateFormat +import java.time.Instant import java.time.LocalDate -import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale @@ -201,7 +202,7 @@ class KitsuApi: SyncAPI() { id = id, totalEpisodes = anime.episodeCount, title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(), - publicScore = Score.from(anime.ratingTwenty, 20), + publicScore = Score.from(anime.ratingTwenty.toString(), 20), duration = anime.episodeLength, synopsis = anime.synopsis, airStatus = when(anime.status) { @@ -249,7 +250,7 @@ class KitsuApi: SyncAPI() { } return SyncStatus( - score = Score.from(anime.ratingTwenty, 20), + score = Score.from(anime.ratingTwenty.toString(), 20), status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)), isFavorite = null, watchedEpisodes = anime.progress, @@ -453,8 +454,8 @@ class KitsuApi: SyncAPI() { private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array { - val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount") - val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","updatedAt", "status") + val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount") + val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status") val limit = 500 var url = "$apiUrl/library-entries?filter[userId]=$userId&filter[kind]=anime&include=anime&page[limit]=$limit&page[offset]=0&fields[anime]=${animeSelectedFields.joinToString(",")}&fields[libraryEntries]=${libraryEntriesSelectedFields.joinToString(",")}" @@ -525,7 +526,7 @@ class KitsuApi: SyncAPI() { this.id, this.attributes.progress, numEpisodes, - Score.from(this.attributes.ratingTwenty, 20), + Score.from(this.attributes.ratingTwenty.toString(), 20), parseDateLong(this.attributes.updatedAt), "Kitsu", TvType.Anime, @@ -534,9 +535,12 @@ class KitsuApi: SyncAPI() { null, plot = synopsis, releaseDate = if (startDate == null) null else try { - Date.from(LocalDate.parse(startDate).atStartOfDay() - .atZone(ZoneId.systemDefault()) - .toInstant()) + Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(startDate) + ) + ) } catch (_: RuntimeException) { null } @@ -579,7 +583,7 @@ class KitsuApi: SyncAPI() { @JsonProperty("avatar") val avatar: KitsuUserAvatar?, /* User list anime attributes */ @JsonProperty("progress") val progress: Int?, - @JsonProperty("ratingTwenty") val ratingTwenty: Int?, + @JsonProperty("ratingTwenty") val ratingTwenty: Float?, @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("status") val status: String?, ) @@ -628,7 +632,7 @@ class KitsuApi: SyncAPI() { const val KITSU_CACHED_LIST: String = "kitsu_cached_list" private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index c0a80b3c9..ba0195be6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -98,9 +98,9 @@ class MALApi : SyncAPI() { ) } - override suspend fun search(auth: AuthData?, query: String): List? { + override suspend fun search(auth : AuthData?, query: String): List? { val auth = auth?.token?.accessToken ?: return null - val url = "$apiUrl/v2/anime?q=$query&limit=$MAL_MAX_SEARCH_LIMIT" + val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" val res = app.get( url, headers = mapOf( "Authorization" to "Bearer $auth", @@ -122,7 +122,7 @@ class MALApi : SyncAPI() { Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() override suspend fun updateStatus( - auth: AuthData?, + auth : AuthData?, id: String, newStatus: SyncAPI.AbstractSyncStatus ): Boolean { @@ -225,7 +225,7 @@ class MALApi : SyncAPI() { ) } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val auth = auth?.token?.accessToken ?: return null val internalId = id.toIntOrNull() ?: return null val url = @@ -271,7 +271,7 @@ class MALApi : SyncAPI() { } } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val auth = auth?.token?.accessToken ?: return null // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get @@ -477,7 +477,7 @@ class MALApi : SyncAPI() { @JsonProperty("start_time") val startTime: String? ) - override suspend fun library(auth: AuthData?): LibraryMetadata? { + override suspend fun library(auth : AuthData?): LibraryMetadata? { val list = getMalAnimeListSmart(auth ?: return null)?.groupBy { convertToStatus(it.listStatus?.status ?: "").stringRes }?.mapValues { group -> @@ -505,7 +505,7 @@ class MALApi : SyncAPI() { ) } - private suspend fun getMalAnimeListSmart(auth: AuthData): Array? { + private suspend fun getMalAnimeListSmart(auth : AuthData): Array? { return if (requireLibraryRefresh) { val list = getMalAnimeList(auth.token) setKey(MAL_CACHED_LIST, auth.user.id.toString(), list) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 3110b23ac..c4095e2d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.Score import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING @@ -29,7 +30,6 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import com.lagradost.cloudstream3.utils.txt import java.math.BigInteger @@ -117,8 +117,13 @@ class SimklApi : SyncAPI() { * Gets cached object, if object is not fresh returns null and removes it from cache */ inline fun getKey(path: String): T? { + // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject" + val type = mapper.typeFactory.constructParametricType( + SimklCacheWrapper::class.java, + T::class.java + ) val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson>(it) + mapper.readValue>(it, type) } return if (cache?.isFresh() == true) { @@ -911,7 +916,7 @@ class SimklApi : SyncAPI() { override suspend fun search(auth: AuthData?, query: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 8ec082520..93a79689e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.newSearchResponseList -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) { val hash: Pair ) - private val cache = atomicListOf() + private val cache = threadSafeListOf() private var cacheIndex: Int = 0 const val CACHE_SIZE = 20 @@ -66,7 +66,9 @@ class APIRepository(val api: MainAPI) { private fun afterPluginsLoaded(forceReload: Boolean) { if (forceReload) { - cache.clear() + synchronized(cache) { + cache.clear() + } } } @@ -89,25 +91,21 @@ class APIRepository(val api: MainAPI) { val fixedUrl = api.fixUrl(url) val lookingForHash = Pair(api.name, fixedUrl) - val cached = cache.withLock { - var found: LoadResponse? = null + synchronized(cache) { for (item in cache) { // 10 min save if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { - found = item.response - break + return@withTimeout item.response } } - found } - if (cached != null) return@withTimeout cached api.load(fixedUrl)?.also { response -> // Remove all blank tags as early as possible response.tags = response.tags?.filter { it.isNotBlank() } val add = SavedLoadResponse(unixTime, response, lookingForHash) - cache.withLock { + synchronized(cache) { if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache cacheIndex = (cacheIndex + 1) % CACHE_SIZE @@ -217,4 +215,4 @@ class APIRepository(val api: MainAPI) { return false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 2aadfb13c..f91d40f28 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -12,6 +12,9 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListView import androidx.appcompat.app.AlertDialog +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.google.android.gms.cast.MediaLoadOptions import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaSeekOptions @@ -102,6 +105,9 @@ data class MetadataHolder( class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { + private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() + init { view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setOnClickListener { @@ -443,4 +449,4 @@ class ControllerActivity : ExpandedControllerActivity() { SkipNextEpisodeController(skipOpButton) ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 8d48f5a68..e0609c0e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(apis.withLock { apis.first { it.hasMainPage } }) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index c5f8fa3d9..6e28c128d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -210,13 +210,14 @@ class LibraryFragment : BaseFragment( syncId: SyncIdName, apiName: String? = null, ) { - val availableProviders = allProviders.filter { - it.supportedSyncNames.contains(syncId) - }.map { it.name } + - // Add the api if it exists - (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } - ?: emptyList()) - + val availableProviders = synchronized(allProviders) { + allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } + ?: emptyList()) + } val baseOptions = listOf( LibraryOpenerType.Default, LibraryOpenerType.None, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index d7e10c814..aa44b9235 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -96,7 +96,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus -import com.lagradost.cloudstream3.utils.CLEARKEY_DRM_UUID +import com.lagradost.cloudstream3.utils.CLEARKEY_UUID import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount @@ -104,9 +104,9 @@ import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.PLAYREADY_DRM_UUID +import com.lagradost.cloudstream3.utils.PLAYREADY_UUID import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName -import com.lagradost.cloudstream3.utils.WIDEVINE_DRM_UUID +import com.lagradost.cloudstream3.utils.WIDEVINE_UUID import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp import kotlinx.coroutines.delay import okhttp3.Interceptor @@ -118,7 +118,6 @@ import java.util.concurrent.Executors import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession -import kotlin.uuid.toJavaUuid const val TAG = "CS3ExoPlayer" const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language" @@ -1279,7 +1278,7 @@ class CS3IPlayer : IPlayer { item.drm?.let { drm -> when (drm.uuid) { - CLEARKEY_DRM_UUID.toJavaUuid() -> { + CLEARKEY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1300,8 +1299,8 @@ class CS3IPlayer : IPlayer { .createMediaSource(item.mediaItem) } - WIDEVINE_DRM_UUID.toJavaUuid(), - PLAYREADY_DRM_UUID.toJavaUuid() -> { + WIDEVINE_UUID, + PLAYREADY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1915,7 +1914,7 @@ class CS3IPlayer : IPlayer { drm = DrmMetadata( kid = link.kid, key = link.key, - uuid = link.uuid.toJavaUuid(), + uuid = link.uuid, kty = link.kty, licenseUrl = link.licenseUrl, keyRequestParameters = link.keyRequestParameters, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index a086cc16f..7a42cea93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -58,23 +58,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { enableEdgeToEdgeCompat() setContentView(R.layout.empty_layout) Log.i(TAG, "onCreate") - handleIntent(intent) - /** - * Use moveTaskToBack instead of finish() so there is always exactly one task - * entry in recents, always reflecting the current file. - * - * finish() destroys the Activity but may leave the task in recents. Each new file - * open can create a new task entry, so recents accumulates stale entries for old - * files. The user then taps a stale entry and gets the wrong file. - * - * moveTaskToBack keeps the Activity alive in the background. There is only ever - * one task entry in recents. New files opened from the file manager arrive via - * onNewIntent on the live instance, updating the player immediately. The single - * recents entry always reflects the current state, ensuring we load the - * correct file. - */ - attachBackPressedCallback("DownloadedPlayerActivity") { moveTaskToBack(true) } + handleIntent(intent) + attachBackPressedCallback("DownloadedPlayerActivity") { finish() } } private fun handleIntent(intent: Intent) { @@ -97,11 +83,11 @@ class DownloadedPlayerActivity : AppCompatActivity() { url != null -> playLink(this, url) data != null -> playUri(this, data) extraText != null -> playLink(this, extraText) - else -> finishAndRemoveTask() + else -> { finish(); return } } } else if (data?.scheme == "content") { playUri(this, data) - } else finishAndRemoveTask() + } else finish() } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 4ba933e13..26706699b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -945,18 +945,12 @@ open class FullScreenPlayer : AbstractPlayerFragment( player.handleEvent(CSPlayerEvent.SkipCurrentChapter) } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER -> { // space is not captured due to navigation + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation player.handleEvent(CSPlayerEvent.PlayPauseToggle) } - // KEYCODE_DPAD_CENTER and KEYCODE_ENTER both act as a "select/confirm" button. - // Some remotes (e.g. LG Magic Remote) send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER. - // When the player UI or a dialog is visible, we let the event pass through (return null) - // so the focused button/item can handle the click normally, rather than always toggling - // play/pause. Only when the UI is hidden do we treat it as a play/pause toggle. - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER -> { - if (isShowing || isDialogOpen()) { + KeyEvent.KEYCODE_DPAD_CENTER -> { + if (isShowing) { return null } // If UI is not shown make click instantly skip to next chapter even if locked diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 17bef3ec0..2dfd5ef4d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -1732,11 +1732,11 @@ class GeneratorPlayer : FullScreenPlayer() { ): SubtitleData? { val langCode = preferredAutoSelectSubtitles ?: return null if (downloads) { - sortSubs(subtitles).firstOrNull { + return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode( langCode ) - }?.let { return it } + } } if (!settings) return null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index dcf976612..ac25347b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.net.Uri import androidx.core.content.ContextCompat.getString -import androidx.navigation.NavOptions import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.actions.temp.CloudStreamPackage import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -13,15 +12,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile object OfflinePlaybackHelper { - /** - * Pop any existing player off the nav back stack before pushing the new one, - * keeping the stack flat (at most one player at a time). This prevents an - * OOM when many files are opened in sequence via DownloadedPlayerActivity. - */ - private val replacePlayerNavOptions = NavOptions.Builder() - .setPopUpTo(R.id.navigation_player, inclusive = true, saveState = false) - .build() - fun playLink(activity: Activity, url: String) { activity.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( @@ -30,8 +20,7 @@ object OfflinePlaybackHelper { BasicLink(url) ), id = url.hashCode() ), 0 - ), - replacePlayerNavOptions + ) ) } @@ -63,8 +52,7 @@ object OfflinePlaybackHelper { subs, if (id != -1) id else null, ), 0 - ), - replacePlayerNavOptions + ) ) return true } @@ -88,8 +76,7 @@ object OfflinePlaybackHelper { ) ) ), 0 - ), - replacePlayerNavOptions + ) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index c519e0de2..7dfe3cf59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -83,7 +83,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -1325,7 +1324,7 @@ class ResultViewModel2 : ViewModel() { episodeIds: Array, watchState: VideoWatchState ) { - val watchStateString = watchState.toJson() + val watchStateString = DataStore.mapper.writeValueAsString(watchState) episodeIds.forEach { if (getVideoWatchState(it.toInt()) != watchState) { editor.setKeyRaw( @@ -1686,13 +1685,14 @@ class ResultViewModel2 : ViewModel() { } val realRecommendations = ArrayList() - val apiNames = apis.filter { - it.name.contains("gogoanime", true) || - it.name.contains("9anime", true) - }.map { - it.name + val apiNames = synchronized(apis) { + apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name + } } - meta.recommendations?.forEach { rec -> apiNames.forEach { name -> realRecommendations.add(rec.copy(apiName = name)) @@ -2706,4 +2706,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index f60588e35..27db8d1ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() { private var suggestionJob: Job? = null - private var repos = apis.withLock { apis.map { APIRepository(it) } } + private var repos = synchronized(apis) { apis.map { APIRepository(it) } } fun clearSearch() { _searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false))) @@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun reloadRepos() { - repos = apis.withLock { apis.map { APIRepository(it) } } + repos = synchronized(apis) { apis.map { APIRepository(it) } } } fun searchAndCancel( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 57f5aa870..dbf2ff1dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -219,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { } fun showAdd() { - val providers = allProviders.distinctBy { it::class }.sortedBy { it.name } + val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( providers.map { "${it.name} (${it.mainUrl})" }, -1, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index c8478a840..076f17a0a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() { getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { currentLangTags -> - val languagesTagName = APIHolder.apis.withLock { - listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) + - APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } - .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } + val languagesTagName = synchronized(APIHolder.apis) { + listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) + + APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } + .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji } val currentIndexList = currentLangTags.map { langTag -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 0cbef9cf2..dfc61eba5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -23,7 +23,7 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread -import com.lagradost.cloudstream3.utils.Levenshtein +import me.xdrop.fuzzywuzzy.FuzzySearch import java.io.File // String => repository url @@ -246,7 +246,7 @@ class PluginsViewModel : ViewModel() { this.sortedBy { it.plugin.second.name } } else { this.sortedBy { - -Levenshtein.partialRatio( + -FuzzySearch.partialRatio( it.plugin.second.name.lowercase(), query.lowercase() ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 22500d931..818f1fd79 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -40,7 +40,7 @@ class TestViewModel : ViewModel() { get() = scope != null private var filter = ProviderFilter.All - private val providers = atomicListOf>() + private val providers = threadSafeListOf>() private var passed = 0 private var failed = 0 private var total = 0 @@ -51,9 +51,9 @@ class TestViewModel : ViewModel() { } private fun postProviders() { - providers.withLock { + synchronized(providers) { val filtered = when (filter) { - ProviderFilter.All -> providers.toList() + ProviderFilter.All -> providers ProviderFilter.Passed -> providers.filter { it.second.success } ProviderFilter.Failed -> providers.filter { !it.second.success } } @@ -68,7 +68,7 @@ class TestViewModel : ViewModel() { } private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) { - providers.withLock { + synchronized(providers) { val index = providers.indexOfFirst { it.first == api } if (index == -1) { providers.add(api to results) @@ -81,14 +81,14 @@ class TestViewModel : ViewModel() { } fun init() { - total = APIHolder.allProviders.withLock { APIHolder.allProviders.size } + total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size } updateProgress() } fun startTest() { scope = CoroutineScope(Dispatchers.Default) - val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() } + val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() } total = apis.size failed = 0 passed = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 8c2e8e344..501ee0eef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment( if (isSetup) if ( // If any available languages - apis.distinctBy { it.lang }.size > 1 + synchronized(apis) { apis.distinctBy { it.lang }.size > 1 } ) { findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index c18be8a2f..3c4a09ade 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment 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 7278fcdd7..1377ccd08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -369,10 +369,28 @@ object AppContextUtils { } fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name }) + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } + .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list return hashSet } @@ -463,7 +481,9 @@ object AppContextUtils { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } return if (currentPrefMedia.isEmpty()) { allApis } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 62426197e..88cb7481c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -10,6 +10,7 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R @@ -20,12 +21,11 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi.Companion.KITSU_CACHED_LIST -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs +import com.lagradost.cloudstream3.utils.DataStore.mapper import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream @@ -133,7 +133,9 @@ object BackupUtils { ) @Suppress("UNCHECKED_CAST") - private fun getBackup(context: Context): BackupFile { + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } @@ -212,7 +214,7 @@ object BackupUtils { fileStream = stream.openNew() printStream = PrintWriter(fileStream) - printStream.print(backupFile.toJson()) + printStream.print(mapper.writeValueAsString(backupFile)) showToast( R.string.backup_success, @@ -257,8 +259,8 @@ object BackupUtils { val input = activity.contentResolver.openInputStream(uri) ?: return@ioSafe - val text = input.bufferedReader().readText() - val restoredValue = parseJson(text) + val restoredValue = + mapper.readValue(input) restore( activity, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 02ee69791..0a1db85fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -2,16 +2,17 @@ package com.lagradost.cloudstream3.utils import android.content.Context import android.content.SharedPreferences -import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral import kotlin.reflect.KClass import kotlin.reflect.KProperty +import androidx.core.content.edit /** Used to display metadata about downloads and resume watching */ const val DOWNLOAD_HEADER_CACHE = "download_header_cache" @@ -87,18 +88,8 @@ data class Editor( } object DataStore { - // Extensions shouldn't have really been using this version of it, but it seems - // some have. Since there has always been a very easy alternative, we won't - // need to deprecate it that long, and should be able to fully remove it - // once extensions at least use the other version. - @Deprecated( - "Please do not use the mapper version from DataStore. Preferably use methods from AppUtils " + - "to parse JSON. However, you can use the stable-API version of the mapper at " + - "com.lagradost.cloudstream3.mapper to access the mapper directly if necessary.", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("com.lagradost.cloudstream3.mapper"), - ) - val mapper = com.lagradost.cloudstream3.mapper + val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() private fun getPreferences(context: Context): SharedPreferences { return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) @@ -108,6 +99,7 @@ object DataStore { return getPreferences(this) } + fun getFolderName(folder: String, path: String): String { return "${folder}/${path}" } @@ -173,17 +165,17 @@ object DataStore { fun Context.setKey(path: String, value: T) { try { getSharedPrefs().edit { - putString(path, value?.toJsonLiteral()) + putString(path, mapper.writeValueAsString(value)) } } catch (e: Exception) { logError(e) } } - fun Context.getKey(path: String, valueType: Class): T? { + fun Context.getKey(path: String, valueType: Class): T? { try { val json: String = getSharedPrefs().getString(path, null) ?: return null - return parseJson(json, valueType.kotlin) + return json.toKotlinObject(valueType) } catch (e: Exception) { return null } @@ -194,11 +186,11 @@ object DataStore { } inline fun String.toKotlinObject(): T { - return parseJson(this) + return mapper.readValue(this, T::class.java) } - fun String.toKotlinObject(valueType: Class): T { - return parseJson(this, valueType.kotlin) + fun String.toKotlinObject(valueType: Class): T { + return mapper.readValue(this, valueType) } // GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR @@ -222,4 +214,4 @@ object DataStore { inline fun Context.getKey(folder: String, path: String, defVal: T?): T? { return getKey(getFolderName(folder, path), defVal) ?: defVal } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt index 96193fe45..9d5c75289 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri -import android.os.Build import android.os.Build.VERSION.SDK_INT import android.util.Log import android.widget.ImageView @@ -12,7 +11,6 @@ import coil3.EventListener import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader -import coil3.decode.BitmapFactoryDecoder import coil3.disk.DiskCache import coil3.dispose import coil3.load @@ -24,86 +22,82 @@ import coil3.request.CachePolicy import coil3.request.ErrorResult import coil3.request.ImageRequest import coil3.request.allowHardware -import coil3.request.bitmapConfig import coil3.request.crossfade import coil3.util.DebugLogger import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.network.buildDefaultClient +import okhttp3.HttpUrl import okio.Path.Companion.toOkioPath import java.io.File import java.nio.ByteBuffer object ImageLoader { + private const val TAG = "CoilImgLoader" - internal fun buildImageLoader(context: PlatformContext): ImageLoader { - val isBrokenHardware = hasPotentialBrokenHardware() - return ImageLoader.Builder(context) + + internal fun buildImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context) .crossfade(200) - .allowHardware(SDK_INT >= 28 && !isBrokenHardware) + .allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder .diskCachePolicy(CachePolicy.ENABLED) .networkCachePolicy(CachePolicy.ENABLED) .memoryCache { - MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache - .strongReferencesEnabled(false) + MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching .build() } .diskCache { DiskCache.Builder() .directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath()) .maxSizeBytes(512L * 1024 * 1024) // 512 MB - .maxSizePercent(0.04) // max 4% of storage for disk caching + .maxSizePercent(0.04) // Use 4 % of the device's storage space for disk caching .build() } /** Pass interceptors with care, unnecessary passing tokens to servers or image hosting services causes unauthorized exceptions **/ - .components { - add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) - if (isBrokenHardware) { - add(BitmapFactoryDecoder.Factory()) - } // sw decoder - } - .apply { - if (isBrokenHardware) { // coil will auto choose optimal config on modern device - bitmapConfig(Bitmap.Config.ARGB_8888) - } - setupCoilLogger() + .components { add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) } + .also { + it.setupCoilLogger() + Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.") } .build() - } - /** DebugLogger on debug builds which won't slow down release builds & use EventListener for + /** Use DebugLogger on debug builds which won't slow down release builds & use EventListener for Errors on release builds. **/ internal fun ImageLoader.Builder.setupCoilLogger() { if (BuildConfig.DEBUG) { logger(DebugLogger()) + Log.d(TAG, "setupCoilLogger: Activated DEBUG_LOGGER FOR COIL") } else { eventListener(object : EventListener() { override fun onError(request: ImageRequest, result: ErrorResult) { super.onError(request, result) - Log.e(TAG, "Image load error: ${result.throwable.message ?: result.throwable}") - Log.e(TAG, " URL: ${request.data}") - Log.e(TAG, " allowHardware: ${request.allowHardware}") - Log.e(TAG, " hardware: ${Build.HARDWARE}, board: ${Build.BOARD}") + Log.e(TAG, "Error loading image: ${result.throwable}") } }) + Log.d(TAG, "setupCoilLogger: Activated EVENT_LISTENER FOR COIL") } } - /** coil's built in loader attached w/ global synchronized instance **/ + /** we use coil's built in loader with our global synchronized instance, this way we achieve + latest and complete functionality as well as stability **/ private fun ImageView.loadImageInternal( imageData: Any?, headers: Map? = null, builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations ) { - // clear image to avoid loading & flickering issue at fast scrolling (~recycler view/lazy column) + // clear image to avoid loading & flickering issue at fast scrolling (e.g, an image recycler) this.dispose() - if (imageData == null) return + + if(imageData == null) return // Just in case + // setImageResource is better than coil3 on resources due to attr - if (imageData is Int) { - this.setImageResource(imageData); return + if(imageData is Int) { + this.setImageResource(imageData) + return } - // headers can be overridden by extensions. + + // Use Coil's built-in load method but with our custom module & a decent USER-AGENT always + // which can be overridden by extensions. this.load(imageData, SingletonImageLoader.get(context)) { this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder -> headerBuilder["User-Agent"] = USER_AGENT @@ -111,22 +105,11 @@ object ImageLoader { headerBuilder[key] = value } }.build()) + builder() // if passed } } - private fun hasPotentialBrokenHardware(): Boolean { - val hardware = Build.HARDWARE?.lowercase() ?: "" - val board = Build.BOARD?.lowercase() ?: "" - val model = Build.MODEL?.lowercase() ?: "" - val manufacturer = Build.MANUFACTURER?.lowercase() ?: "" - val allwinnerPatterns = listOf("sun50iw9", "h713", "allwinner", "sunxi") - val problematicModels = - listOf("hy320", "hy300", "a10plus", "magcubic", "sinoy", "android tv box") - return allwinnerPatterns.any { it in hardware || it in board || it in manufacturer } || - problematicModels.any { it in model } - } - /** TYPE_SAFE_LOADERS **/ fun ImageView.loadImage( imageData: UiImage?, @@ -155,6 +138,12 @@ object ImageLoader { builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( + imageData: HttpUrl?, + headers: Map? = null, + builder: ImageRequest.Builder.() -> Unit = {} + ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( imageData: File?, builder: ImageRequest.Builder.() -> Unit = {} @@ -184,4 +173,4 @@ object ImageLoader { imageData: ByteBuffer?, builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, builder = builder) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index b01f6e07e..8bcd1b88e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -93,9 +93,9 @@ object InAppUpdater { private suspend fun Activity.getReleaseUpdate(): Update { val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(url, headers = headers).text - ).toList() + ) val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""") val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""") @@ -103,7 +103,9 @@ object InAppUpdater { !rel.prerelease }.sortedWith(compareBy { release -> release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> - versionRegex.find(it1)?.groupValues?.let { + versionRegex.find( + it1 + )?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } } @@ -148,9 +150,9 @@ object InAppUpdater { "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/git/ref/tags/pre-release" val releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(releaseUrl, headers = headers).text - ).toList() + ) val found = response.lastOrNull { rel -> rel.prerelease || rel.tagName == "pre-release" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 6e74fa00a..351e77c8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.APIHolder.apis //import com.lagradost.cloudstream3.animeproviders.AniflixProvider import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import java.util.concurrent.TimeUnit object SyncUtil { @@ -71,7 +71,7 @@ object SyncUtil { val url = "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text - val mapped = tryParseJson(response) + val mapped = parseJson(response) val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id @@ -96,8 +96,10 @@ object SyncUtil { .mapNotNull { it.url }.toMutableList() if (type == "anilist") { // TODO MAKE BETTER - apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { - current.add("${it.mainUrl}/anime/$id") + synchronized(apis) { + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } } } return current @@ -167,4 +169,4 @@ object SyncUtil { @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("deletedAt") val deletedAt: String? ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 8c50afee7..91c8a2fc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* +import org.junit.Assert import kotlin.random.Random object TestingUtils { - open class TestResult(val success: Boolean) { companion object { val Pass = TestResult(true) @@ -48,10 +48,6 @@ object TestingUtils { messageLog.add(Message(LogLevel.Error, message)) } } - - private fun fail(message: String): Nothing = throw AssertionError(message) - private fun assertTrue(message: String, condition: Boolean) { if (!condition) fail(message) } - private fun assertNotNull(message: String, value: Any?) { if (value == null) fail(message) } class TestResultList(val results: List) : TestResult(true) class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) @@ -91,7 +87,7 @@ object TestingUtils { } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider marked as hasMainPage, while in reality is has not been implemented") + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") } is CancellationException -> { @@ -119,7 +115,7 @@ object TestingUtils { api.search(query, 1)?.items?.takeIf { it.isNotEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented search()") + Assert.fail("Provider has not implemented search()") } else if (e is CancellationException) { throw e } @@ -129,7 +125,7 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - fail("Api ${api.name} did not return any search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { TestResultList(searchResults) @@ -220,7 +216,7 @@ object TestingUtils { // return TestResult(validResults) } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented load()") + Assert.fail("Provider has not implemented load()") } throw e } @@ -232,14 +228,14 @@ object TestingUtils { url: String?, logger: Logger ): TestResult { - assertNotNull("Api ${api.name} has invalid url on episode", url) + Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> logger.log("Video loaded: ${link.name}") - assertTrue( + Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 ) @@ -249,12 +245,12 @@ object TestingUtils { logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { - fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") + Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") } } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider has not implemented loadLinks()") + Assert.fail("Provider has not implemented loadLinks()") } else -> { @@ -280,7 +276,7 @@ object TestingUtils { // Test Homepage val homepage = testHomepage(api, logger) - assertTrue("Homepage failed to load", homepage.success) + Assert.assertTrue("Homepage failed to load", homepage.success) val homePageList = (homepage as? TestResultList)?.results ?: emptyList() // Test Search Results @@ -291,7 +287,7 @@ object TestingUtils { listOf("over", "iron", "guy")).take(3) val searchResults = testSearch(api, searchQueries, logger) - assertTrue("Failed to get search results", searchResults.success) + Assert.assertTrue("Failed to get search results", searchResults.success) searchResults as TestResultList // Test Load and LoadLinks @@ -325,4 +321,4 @@ object TestingUtils { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt deleted file mode 100644 index 7c73a6889..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import android.net.Uri -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -/** - * Custom KSerializer for Android's [Uri] type. - * - * Uri is an Android platform type and cannot be annotated with @Serializable directly. - * Registering it in a SerializersModule globally would require a custom module passed to - * every Json instance, which adds hidden coupling. This serializer is also used sparingly - * across the codebase, so the overhead of a global registration isn't justified. - * Instead, we keep it explicit so that each usage site opts in intentionally and the - * serialization behavior remains visible. - * - * Usage: - * - * @Serializable - * data class MyData( - * @Serializable(with = UriSerializer::class) - * val uri: Uri, - * ) - */ -object UriSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Uri) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Uri { - return Uri.parse(decoder.decodeString()) - } -} diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 04a2a1f1f..407de4a3f 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -11,7 +11,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="640dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 077929d87..3a3076943 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -12,7 +12,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="680dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 88a318874..76231a2d3 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -11,7 +11,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="640dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/values-b+ar/strings.xml b/app/src/main/res/values-b+ar/strings.xml index f543136dc..57b2bb628 100644 --- a/app/src/main/res/values-b+ar/strings.xml +++ b/app/src/main/res/values-b+ar/strings.xml @@ -753,7 +753,4 @@ %d تنزيل قيد الانتظار عرض واجهة منبثقة للبيانات الوصفية للمشغِّل - مقطع - استعراض - البث قائم diff --git a/app/src/main/res/values-b+de/strings.xml b/app/src/main/res/values-b+de/strings.xml index e674fafd6..6ccb9bc69 100644 --- a/app/src/main/res/values-b+de/strings.xml +++ b/app/src/main/res/values-b+de/strings.xml @@ -252,7 +252,7 @@ Update Bevorzugte Videoqualität (WLAN) Videoplayertitel max. Zeichen - Zeige Playerinformationen + Playerinformationen anzeigen Videopuffergröße Videopufferlänge Video-Cache in Speicher @@ -587,7 +587,7 @@ Sicherheit Konten Repository öffnen - Besuche %s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein + Besuche%s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung Zur Zeit sind keine Downloads verfügbar. Lokales Video öffnen @@ -712,8 +712,8 @@ Zusätzliche Helligkeit Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist Erhöhte Helligkeit aktiviert - Zeige Cast-Panel - Mediainfo + Cast-Panel zeigen + Medieninfo Quellname Alle herunterladen Möchtest du Episode %s herunter laden? @@ -731,8 +731,4 @@ Es befinden sich keine Downloads in der Warteschlange. Quellpriorität Entscheide, wie Videoquellen im Player sortiert werden sollen - Zeige Player-Metadaten - Video - Vorschau - Live diff --git a/app/src/main/res/values-b+mk/strings.xml b/app/src/main/res/values-b+mk/strings.xml index 4e37afdea..ea467833e 100644 --- a/app/src/main/res/values-b+mk/strings.xml +++ b/app/src/main/res/values-b+mk/strings.xml @@ -244,7 +244,7 @@ TC Претплатен на %s Преводи - Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! + Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! Недостасуваат дозволи за складирање. Обиди се повторно. Зачувај Вчитај од датотека @@ -445,7 +445,7 @@ Грешка при правење резервна копија на %s Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат. - Прикажи информации за плеерот + Резолуција на видео плеер Големина на видео баферот Распоред Стандардно @@ -705,37 +705,4 @@ Горе во центар Горе на десно Пушти ја целата серија - Редица за преземање - Моментално нема преземања во редицата. - Дополнителна осветленост - Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот - овозможена_дополнителна_осветленост - Предлози за пребарување - Прикажувај предлози за пребарување додека пишуваш - Исчисти предлози - Прикажи преклоп со метаподатоци на плеерот - Прикажи панел за емитување - Инсталирај предиздавачка верзија - Предиздавачката верзија е веќе инсталирана. - Неуспешна инсталација на предиздавачката верзија. - Видео - Текст на епизода - Информации за медиумот - Преглед - Приоритет на извор - Одреди како ќе се подредуваат видео изворите во плеерот - Име на изворот - Преземи сѐ - Откажи сѐ - Дали сакате да ја преземете епизодата %s? - Дали сакате да ги откажете сите преземања во редицата? - - %d активно преземање - %d активни преземања - - - %d преземање во редицата - %d преземања во редицата - - Во живо diff --git a/app/src/main/res/values-b+ms/strings.xml b/app/src/main/res/values-b+ms/strings.xml index a15759939..ab0d0a54f 100644 --- a/app/src/main/res/values-b+ms/strings.xml +++ b/app/src/main/res/values-b+ms/strings.xml @@ -25,8 +25,8 @@ Episod %d akan disiarkan dalam Pelakon:%s Mod Selamat Hidup - %1$dh %2$dj %3$dm - %1$dj %2$dm + %1$dd %2$dh %3$dm + %1$dh %2$dm %dm Poster Episod Poster Utama @@ -485,7 +485,7 @@ Kemaskini dan sandaran Ketik dua kali untuk mencari Gunakan kecerahan sistem dalam pemain apl dan bukannya tindanan gelap - %1$dj %2$dm %3$ds + %1$dh %2$dm %3$ds %1$dm %2$ds %1$ds Pengecaman pertuturan tidak tersedia diff --git a/app/src/main/res/values-b+nl/strings.xml b/app/src/main/res/values-b+nl/strings.xml index 3164e6b4e..ace3aa0d4 100644 --- a/app/src/main/res/values-b+nl/strings.xml +++ b/app/src/main/res/values-b+nl/strings.xml @@ -661,20 +661,4 @@ Fout bij toegang tot het Klembord, Probeer het opnieuw. Fout bij het kopiëren. Kopieer alsjeblieft de logcat en neem contact op met de app-ondersteuning. Afwijzen - Video - Voorbeeld - Bron Prioriteit - Bepaal hoe de videobronnen worden gesorteerd in de speler - Ontgrendel CloudStream - Versleutel met Biometrie - Reset - verschijningsdatum (Nieuw naar Oud) - verschijningsdatum (Oud naar Nieuw) - Verberg de namen van de besturingselementen van de speler - Ondertiteling nog niet geladen - Back-up folder locatie - Aangepast - Bevestig voor afsluiten - Toon dialoogvenster voordat de app wordt afgesloten - Randgrote diff --git a/app/src/main/res/values-b+nn/strings.xml b/app/src/main/res/values-b+nn/strings.xml index b29e142b0..014e2b0a9 100644 --- a/app/src/main/res/values-b+nn/strings.xml +++ b/app/src/main/res/values-b+nn/strings.xml @@ -188,14 +188,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - Sesong %1$d Episode %2$d blir sleppt om - Spel av frå start - Nedlastingskø - Semmegjenkjenning er ikkje tilgjengeleg - Snakk no… - Nettlesar - Fjerna - Strøm Torrent - Spel heile serien - Denne filmen er ein Torrent, som betyr at bruken din kan bli spora\nSett deg inn i bruk av Torrent-resursar før du fortsetter. diff --git a/app/src/main/res/values-b+pt+BR/strings.xml b/app/src/main/res/values-b+pt+BR/strings.xml index 455478b5a..00e3a6229 100644 --- a/app/src/main/res/values-b+pt+BR/strings.xml +++ b/app/src/main/res/values-b+pt+BR/strings.xml @@ -616,7 +616,7 @@ Reproduzir do começo Reprovou alguns testes Excluir plugin - Atualmente não há downloads disponíveis. + Você não baixou nada :/ Ocultar os nomes dos controles do player Abrir arquivo de vídeo Data de lançamento (do novo ao antigo) @@ -736,7 +736,7 @@ Nome da fonte Baixar tudo Cancelar tudo - Você deseja baixar o episódio%s? + Você deseja baixar o episódio%s Você gostaria de cancelar todos os downloads da fila? %ddownload ativo @@ -748,7 +748,7 @@ %d downloads na sequência %d downloads na sequência - Mostrar sobreposição de metadados do reprodutor + Exibir sobreposição de metadados do player Vídeo Visualização Ao vivo diff --git a/app/src/main/res/values-b+pt/strings.xml b/app/src/main/res/values-b+pt/strings.xml index 960a91d0d..6dad27011 100644 --- a/app/src/main/res/values-b+pt/strings.xml +++ b/app/src/main/res/values-b+pt/strings.xml @@ -61,7 +61,7 @@ Transmitir Erro a Carregar Links Armazenamento Interno - Dub + Dob Leg Eliminar Ficheiro Reproduzir Ficheiro @@ -100,7 +100,7 @@ Importar fontes colocando em %s Continuar a Assistir Remover - Mais informações + Mais info Uma VPN pode ser necessária para que este fornecedor funcione corretamente Este fornecedor é um torrent, uma VPN é recomendada Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site. @@ -142,7 +142,7 @@ Procurar Contas e segurança Atualizações e cópias de segurança - Informações + Info Procura Avançada Mostra resultados separados por fornecedor Mostrar episódios de enchimento para anime @@ -318,9 +318,9 @@ Carregar de arquivo Carregar da Internet Arquivo baixado - Principal - Suporte - Plano de fundo + Protagonista + Coadjuvante + Figurante Aleatório Em breve… Imagem de Poster @@ -523,7 +523,7 @@ Repositório não encontrado, verifique o URL e tente a VPN Você já votou Cancelar Inscrição - Inscrever-se + Subscrever Favoritos A recarregar links Frequência de Backup @@ -686,7 +686,7 @@ Imagem Atualizada com Sucesso Marcar como assistido o episódio Removar marcação de assistido até esse episódio - Recarregar + Recarregado Provedor de Recarregamento Reproduzir do servidor alternativo" Nome @@ -733,8 +733,4 @@ %d downloads na fila %d downloads na fila - Mostrar sobreposição de metadados do player - Vídeo - Pré-visualização - Ao Vivo diff --git a/app/src/main/res/values-b+ru/strings.xml b/app/src/main/res/values-b+ru/strings.xml index 38e576baa..3f6a5d89d 100644 --- a/app/src/main/res/values-b+ru/strings.xml +++ b/app/src/main/res/values-b+ru/strings.xml @@ -735,8 +735,4 @@ Отменить всё Вы хотите загрузить эпизод %s? Вы хотите отменить всё запланированные загрузки? - Показывать наложения метаданных проигрывателя - Видео - Предпросмотр - Прямой эфир diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 952bafee7..842b97080 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -166,7 +166,7 @@ Ngôn ngữ ứng dụng Nguồn phim này chưa hỗ trợ Chromecast Không tìm thấy liên kết - Đã sao chép liên kết vào bảng nhớ tạm + Đã sao chép liên kết vào bộ nhớ tạm Phát Tập phim Đặt lại giá trị mặc định Mùa @@ -254,7 +254,7 @@ Cập nhật Chất lượng xem ưu tiên (WiFi) Số ký tự tối đa tiêu đề trình phát - Hiển thị thông tin trình phát + Hiện thông tin trình phát Kích thước bộ nhớ đệm video Thời lượng bộ nhớ đệm Bộ nhớ đệm video trên thiết bị @@ -417,7 +417,7 @@ Âm thanh & video Âm thanh Video - Khởi động lại ứng dụng để thấy các thay đổi. + Khởi động lại ứng dụng để thấy câc thay đổi. Chế độ an toàn được bật Tất cả tiện ích mở rộng đã được tắt do ứng dụng bị ngừng bất thường để giúp bạn tìm ra vấn đề gây lỗi. Xem thông tin sự cố @@ -469,8 +469,8 @@ Danh đề Giới thiệu Xoá lịch sử - Hiển thị cửa sổ bật lên của bỏ qua giới thiệu cho mở đầu/kết thúc - Văn bản quá dài. Không thể lưu vào bảng nhớ tạm. + Hiện các popup bỏ qua cho mở đầu/kết thúc + Văn bản quá dài. Không thể lưu vào bộ nhớ tạm. Xoá khỏi đã xem Bạn có chắc muốn thoát? @@ -575,7 +575,7 @@ Bật tự động xoay màn hình theo hướng của video Tự động xoay đã sao chép! - Lỗi truy cập Bảng nhớ tạm, Vui lòng thử lại. + Lỗi truy cập Bộ nhớ tạm, Vui lòng thử lại. Lỗi sao chép, Vui lòng sao chép logcat và liên hệ hỗ trợ ứng dụng. Yêu thích OK @@ -635,7 +635,7 @@ Xem trước trên thanh tua Chưa tải phụ đề nào Xác nhận trước khi thoát - Hiển thị hộp thoại xác nhận trước khi thoát ứng dụng + Hiện hộp thoại xác nhận trước khi thoát ứng dụng Không hiển thị Hiển thị Vị trí thư mục sao lưu @@ -644,7 +644,7 @@ Video này là Torrent, điều này có nghĩa là hoạt động video của bạn có thể được theo dõi.\nHãy đảm bảo rằng bạn hiểu về Torrent trước khi tiếp tục. Lỗi mã hóa Giải mã phần mềm cho phép phát các tệp video không được thiết bị của bạn hỗ trợ, nhưng có thể gây ra phản hồi chậm hoặc phát lại không ổn định ở độ phân giải cao. - Giải mã phần mềm + Bộ giải mã ứng dụng Khởi động lại ứng dụng và chấp nhận cửa sổ bật lên của Stream Torrent để tiếp tục. Kích hoạt torrent trong Cài đặt/Nguồn phim/Thể loại ưu tiên Tải phụ đề đầu tiên có sẵn @@ -731,7 +731,7 @@ Tên nguồn Hàng đợi tải xuống Không có tải xuống đang chờ nào. - Quyết định cách sắp xếp các nguồn video trong trình phát + Quyết định cách sắp xếp các nguồn video trong trình phát. Ưu tiên nguồn Tải xuống tất cả Hủy tất cả diff --git a/build.gradle.kts b/build.gradle.kts index 609a94b3a..e35c1f611 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,6 @@ plugins { alias(libs.plugins.dokka) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.multiplatform) apply false - alias(libs.plugins.kotlin.serialization) apply false } allprojects { diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar-SA/short_description.txt index e56a2e8be..7ccd9743b 100644 --- a/fastlane/metadata/android/ar-SA/short_description.txt +++ b/fastlane/metadata/android/ar-SA/short_description.txt @@ -1 +1 @@ -بث وتحميل الأفلام, المسلسلات التلفزيونية والأنمي. +بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية. diff --git a/fastlane/metadata/android/pt/short_description.txt b/fastlane/metadata/android/pt/short_description.txt index 08ad5a778..d0392f34b 100644 --- a/fastlane/metadata/android/pt/short_description.txt +++ b/fastlane/metadata/android/pt/short_description.txt @@ -1 +1 @@ -Transmita e descarga filmes, séries de TV e anime. +Transmita e transfira filmes, séries de TV e anime. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80e342c2c..a97145c3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,8 @@ androidGradlePlugin = "9.1.1" animeDb = "1.0.2" annotation = "1.10.0" appcompat = "1.7.1" -biometric = "1.4.0-alpha07" -buildkonfigGradlePlugin = "0.21.2" +biometric = "1.4.0-alpha06" +buildkonfigGradlePlugin = "0.18.0" coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later colorpicker = "6b46b49" conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything @@ -17,26 +17,22 @@ desugar_jdk_libs_nio = "2.1.5" dokkaGradlePlugin = "2.2.0" espressoCore = "3.7.0" fragmentKtx = "1.8.9" -instancioCore = "5.6.0" +fuzzywuzzy = "1.4.0" jacksonModuleKotlin = { strictly = "2.13.1" } # Later versions don't support minSdk <26 (Crashes on Android TV's and FireSticks) -json = "20260522" +json = "20251224" jsoup = "1.22.1" junit = "4.13.2" junitKtx = "1.3.0" junitVersion = "1.3.0" juniversalchardet = "2.5.0" kotlinGradlePlugin = "2.3.20" -kotlinxAtomicfu = "0.33.0" kotlinxCollectionsImmutable = "0.4.0" -kotlinxCoroutinesCore = "1.11.0" -kotlinxDatetime = "0.8.0" -kotlinxSerializationJson = "1.11.0" -ktor = "3.5.0" +kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" -material = "1.14.0" +material = "1.14.0-beta01" media3 = "1.9.3" -navigationKtx = "2.9.8" -newpipeextractor = "v0.26.3" +navigationKtx = "2.9.7" +newpipeextractor = "v0.26.0" nextlibMedia3 = "1.9.3-0.12.0" nicehttp = "0.4.18" overlappingpanels = "0.1.5" @@ -60,9 +56,6 @@ minSdk = "23" compileSdk = "36" targetSdk = "36" -versionCode = "68" -versionName = "4.7.0" - [libraries] activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } anime-db = { module = "com.github.recloudstream:anime-db", version.ref = "animeDb" } @@ -81,20 +74,15 @@ desugar_jdk_libs_nio = { module = "com.android.tools:desugar_jdk_libs_nio", vers espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } -instancio-core = { group = "org.instancio", name = "instancio-core", version.ref = "instancioCore" } +fuzzywuzzy = { module = "me.xdrop:fuzzywuzzy", version.ref = "fuzzywuzzy" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } json = { module = "org.json:json", version.ref = "json" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit" } junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitKtx" } juniversalchardet = { module = "com.github.albfernandez:juniversalchardet", version.ref = "juniversalchardet" } -kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlinGradlePlugin" } -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" } -ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" } lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" } material = { module = "com.google.android.material:material", version.ref = "material" } @@ -137,7 +125,6 @@ buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfigG dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaGradlePlugin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm" , version.ref = "kotlinGradlePlugin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlinGradlePlugin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinGradlePlugin" } [bundles] coil = ["coil", "coil-network-okhttp"] diff --git a/library/build.gradle.kts b/library/build.gradle.kts index a1f30fede..073e49e64 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -12,7 +12,6 @@ plugins { alias(libs.plugins.android.multiplatform.library) alias(libs.plugins.buildkonfig) alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.serialization) } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) @@ -57,31 +56,12 @@ kotlin { implementation(libs.annotation) // Annotations implementation(libs.nicehttp) // HTTP Lib 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.ktor.http) + implementation(libs.fuzzywuzzy) // Match Extractors implementation(libs.jsoup) // HTML Parser implementation(libs.rhino) // Run JavaScript + implementation(libs.newpipeextractor) implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit - - // 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) - } - - // We will eventually add a new jvmCommonMain source set - // for things shared between Android and JVM. - androidMain.dependencies { - implementation(libs.newpipeextractor) - } - - jvmMain.dependencies { - implementation(libs.newpipeextractor) } } } @@ -104,11 +84,6 @@ buildkonfig { "MDL_API_KEY", (System.getenv("MDL_API_KEY") ?: localProperties["mdl.key"]).toString() ) - - buildConfigField( - FieldSpec.Type.STRING, - "TRAKT_CLIENT_ID", (System.getenv("TRAKT_CLIENT_ID") ?: localProperties["trakt.id"]).toString() - ) } } diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.android.kt deleted file mode 100644 index 345aa7185..000000000 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.android.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.newAudioFile -import com.lagradost.cloudstream3.newSubtitleFile -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.newExtractorLink -import org.schabi.newpipe.extractor.stream.StreamInfo -import org.schabi.newpipe.extractor.stream.StreamType - -actual open class YoutubeExtractor actual constructor() : ExtractorApi() { - - actual override val mainUrl = "https://www.youtube.com" - actual override val name = "YouTube" - actual override val requiresReferer = false - - actual override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ) { - val videoId = extractYouTubeId(url) - val watchUrl = "$mainUrl/watch?v=$videoId" - - val info = StreamInfo.getInfo(watchUrl) - val isLive = info.streamType == StreamType.LIVE_STREAM - || info.streamType == StreamType.AUDIO_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_AUDIO_STREAM - - if (isLive && info.hlsUrl != null) { - callback( - newExtractorLink( - source = name, - name = "YouTube Live", - url = info.hlsUrl - ) { - type = ExtractorLinkType.M3U8 - } - ) - } else { - processVideo(info, subtitleCallback, callback) - } - } - - private suspend fun processVideo( - info: StreamInfo, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ): Boolean { - val videoStreams = info.videoOnlyStreams.orEmpty() - if (videoStreams.isEmpty()) return false - - val audioStreams = info.audioStreams.orEmpty() - videoStreams.forEach { video -> - callback( - newExtractorLink( - source = name, - name = "YouTube ${normalizeCodec(video.codec)}", - url = video.content - ) { - quality = video.height - audioTracks = audioStreams.map { newAudioFile(it.content) } - } - ) - } - - info.subtitles.forEach { subtitle -> - subtitleCallback( - newSubtitleFile( - lang = subtitle.displayLanguageName - ?: subtitle.languageTag - ?: "Unknown", - url = subtitle.content - ) - ) - } - - return true - } - - private fun extractYouTubeId(url: String): String { - val regex = Regex( - "(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})" - ) - - return regex.find(url)?.groupValues?.get(1) - ?: throw IllegalArgumentException("Invalid YouTube URL: $url") - } - - private fun normalizeCodec(codec: String?): String { - if (codec.isNullOrBlank()) return "" - val c = codec.lowercase() - return when { - c.startsWith("av01") -> "AV1" - c.startsWith("vp9") -> "VP9" - c.startsWith("avc1") || c.startsWith("h264") -> "H264" - c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265" - else -> codec.substringBefore('.').uppercase() - } - } -} diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index 2f9c9b628..975572d05 100644 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -10,18 +10,17 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.nicehttp.requestCreator -import io.ktor.http.Url -import io.ktor.http.decodeURLPart import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response +import java.net.URI /** * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) @@ -120,7 +119,7 @@ actual class WebViewResolver actual constructor( } var fixedRequest: Request? = null - val extraRequestList = atomicListOf() + val extraRequestList = threadSafeListOf() main { try { @@ -212,7 +211,7 @@ actual class WebViewResolver actual constructor( * */ return@runBlocking try { when { - blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith( + blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith( "/favicon.ico" ) -> WebResourceResponse( "image/png", diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index ffc0a938d..c590165a1 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -17,34 +17,21 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.mainWork +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.nicehttp.RequestBodyTypes -import io.ktor.http.Url -import io.ktor.http.URLBuilder -import io.ktor.http.encodedPath -import io.ktor.http.takeFrom 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 kotlinx.serialization.json.Json +import java.net.URI +import java.text.SimpleDateFormat +import java.util.* 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. @@ -87,27 +74,20 @@ const val USER_AGENT = class ErrorLoadingException(message: String? = null) : Exception(message) //val baseHeader = mapOf("User-Agent" to USER_AGENT) - -@Prerelease -val json = Json { - encodeDefaults = true - explicitNulls = false - ignoreUnknownKeys = true -} - val mapper = JsonMapper.builder().addModule(kotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! object APIHolder { - val unixTimeMS: Long - get() = Clock.System.now().toEpochMilliseconds() val unixTime: Long - get() = unixTimeMS / 1000L + get() = System.currentTimeMillis() / 1000L + val unixTimeMS: Long + get() = System.currentTimeMillis() - val allProviders = atomicListOf() + // ConcurrentModificationException is possible!!! + val allProviders = threadSafeListOf() fun initAll() { - allProviders.withLock { + synchronized(allProviders) { for (api in allProviders) { api.init() } @@ -117,28 +97,28 @@ object APIHolder { /** String extension function to Capitalize first char of string.*/ fun String.capitalize(): String { - return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } - var apis: AtomicList = atomicListOf() + var apis: List = threadSafeListOf() var apiMap: Map? = null fun addPluginMapping(plugin: MainAPI) { - apis.withLock { + synchronized(apis) { apis = apis + plugin } initMap(true) } fun removePluginMapping(plugin: MainAPI) { - apis.withLock { + synchronized(apis) { apis = apis.filter { it != plugin } } initMap(true) } private fun initMap(forcedUpdate: Boolean = false) { - apis.withLock { + synchronized(apis) { if (apiMap == null || forcedUpdate) apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() } @@ -146,21 +126,24 @@ object APIHolder { fun getApiFromNameNull(apiName: String?): MainAPI? { if (apiName == null) return null - return allProviders.withLock { + synchronized(allProviders) { initMap() - apis.withLock { - apiMap?.get(apiName)?.let { apis.getOrNull(it) } + synchronized(apis) { + return apiMap?.get(apiName)?.let { apis.getOrNull(it) } // Leave the ?. null check, it can crash regardless - ?: allProviders.firstOrNull { it.name == apiName } + ?: allProviders.firstOrNull { it.name == apiName } } } } fun getApiFromUrlNull(url: String?): MainAPI? { if (url == null) return null - return allProviders.withLock { - allProviders.firstOrNull { url.startsWith(it.mainUrl) } + synchronized(allProviders) { + allProviders.forEach { api -> + if (url.startsWith(api.mainUrl)) return api + } } + return null } /** @@ -178,9 +161,9 @@ object APIHolder { // To get the key suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { try { - val _url = Url(url) + val uri = URI.create(url) val domain = base64Encode( - (_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(), + (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), ).replace("\n", "").replace("=", ".") val vToken = @@ -489,7 +472,7 @@ abstract class MainAPI { } fun init() { - overrideData?.get(this::class.simpleName)?.let { data -> + overrideData?.get(this.javaClass.simpleName)?.let { data -> overrideWithNewData(data) } } @@ -703,22 +686,17 @@ abstract class MainAPI { } } +/** Might need a different implementation for desktop*/ fun base64Decode(string: String): String { - // ISO-8859-1 decoding: each byte maps directly to its Unicode code point (0-255), - // so we mask each byte to unsigned and convert to the corresponding Char manually. - // decodeToString() can't be used here as it assumes UTF-8. - val bytes = base64DecodeArray(string) - return buildString(bytes.size) { - for (b in bytes) { - append((b.toInt() and 0xFF).toChar()) - } - } + return String(base64DecodeArray(string), Charsets.ISO_8859_1) } +@OptIn(ExperimentalEncodingApi::class) fun base64DecodeArray(string: String): ByteArray { return Base64.decode(string) } +@OptIn(ExperimentalEncodingApi::class) fun base64Encode(array: ByteArray): String { return Base64.encode(array) } @@ -1330,23 +1308,23 @@ fun getQualityFromString(string: String?): SearchQuality? { * ``` */ fun MainAPI.updateUrl(url: String): String { - return try { - val original = Url(url) - val updated = Url(mainUrl) + try { + val original = URI(url) + val updated = URI(mainUrl) - URLBuilder().apply { - takeFrom(updated) - user = original.user - password = original.password - encodedPath = original.encodedPath - fragment = original.fragment - - parameters.clear() - parameters.appendAll(original.parameters) - }.buildString() + // URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment) + return URI( + updated.scheme, + original.userInfo, + updated.host, + updated.port, + original.path, + original.query, + original.fragment + ).toString() } catch (t: Throwable) { logError(t) - url + return url } } @@ -1510,7 +1488,7 @@ constructor( override var posterUrl: String? = null, var year: Int? = null, - var dubStatus: MutableSet? = null, + var dubStatus: EnumSet? = null, var otherName: String? = null, var episodes: MutableMap = mutableMapOf(), @@ -1519,10 +1497,46 @@ constructor( override var quality: SearchQuality? = null, override var posterHeaders: Map? = null, override var score: Score? = null, -) : SearchResponse +) : SearchResponse { + @Suppress("DEPRECATION_ERROR") + @Deprecated( + "Use newAnimeSearchResponse", + level = DeprecationLevel.ERROR + ) + constructor( + name: String, + url: String, + apiName: String, + type: TvType? = null, + + posterUrl: String? = null, + year: Int? = null, + dubStatus: EnumSet? = null, + + otherName: String? = null, + episodes: MutableMap = mutableMapOf(), + + id: Int? = null, + quality: SearchQuality? = null, + posterHeaders: Map? = null, + ) : this( + name, + url, + apiName, + type, + posterUrl, + year, + dubStatus, + otherName, + episodes, + id, + quality, + posterHeaders, null + ) +} fun AnimeSearchResponse.addDubStatus(status: DubStatus, episodes: Int? = null) { - this.dubStatus = dubStatus?.also { it.add(status) } ?: mutableSetOf(status) + this.dubStatus = dubStatus?.also { it.add(status) } ?: EnumSet.of(status) if (this.type?.isMovieType() != true) if (episodes != null && episodes > 0) this.episodes[status] = episodes @@ -2521,45 +2535,15 @@ constructor( get() = score?.toInt(100) } -@OptIn(FormatStringsInDatetimeFormats::class) fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") { - 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() + try { + this.date = SimpleDateFormat(format, Locale.getDefault()).parse(date ?: return)?.time + } catch (e: Exception) { + logError(e) + } } -@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?) { +fun Episode.addDate(date: Date?) { this.date = date?.time } @@ -2696,27 +2680,6 @@ fun fetchUrls(text: String?): List { 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/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt index 127b075da..4b163867d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -1,33 +1,39 @@ package com.lagradost.cloudstream3 -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import kotlin.reflect.KClass // Short name for requests client to make it nicer to use -private val jsonResponseParser = object : ResponseParser { +private val jacksonResponseParser = object : ResponseParser { + val mapper: ObjectMapper = jacksonObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false + ) + override fun parse(text: String, kClass: KClass): T { - return parseJson(text, kClass) + return mapper.readValue(text, kClass.java) } override fun parseSafe(text: String, kClass: KClass): T? { return try { - parse(text, kClass) - } catch (_: Exception) { + mapper.readValue(text, kClass.java) + } catch (e: Exception) { null } } override fun writeValueAsString(obj: Any): String { - return obj.toJson() + return mapper.writeValueAsString(obj) } } /** The default networking helper. This helper performs SSL checks. * If you need to make requests to websites with invalid SSL certificates use insecureApp instead. */ -var app = Requests(responseParser = jsonResponseParser).apply { +var app = Requests(responseParser = jacksonResponseParser).apply { defaultHeaders = mapOf("user-agent" to USER_AGENT) } @@ -35,6 +41,6 @@ var app = Requests(responseParser = jsonResponseParser).apply { * This should NEVER be used for sensitive networking operations such as logins. Only use this when required. */ @Prerelease @UnsafeSSL -var insecureApp = Requests(responseParser = jsonResponseParser).apply { +var insecureApp = Requests(responseParser = jacksonResponseParser).apply { defaultHeaders = mapOf("user-agent" to USER_AGENT) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt index b29d29f5d..38d35da2e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt @@ -8,8 +8,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper -import io.ktor.http.Url -import io.ktor.http.decodeURLPart +import java.net.URI +import java.nio.charset.StandardCharsets import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec @@ -46,11 +46,11 @@ open class ByseSX : ExtractorApi() { } private fun getBaseUrl(url: String): String { - return Url(url).let { "${it.protocol.name}://${it.host}" } + return URI(url).let { "${it.scheme}://${it.host}" } } private fun getCodeFromUrl(url: String): String { - val path = Url(url).encodedPath.decodeURLPart() + val path = URI(url).path ?: "" return path.trimEnd('/').substringAfterLast('/') } @@ -94,7 +94,7 @@ open class ByseSX : ExtractorApi() { cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) val plainBytes = cipher.doFinal(cipherBytes) - var jsonStr = plainBytes.decodeToString() + var jsonStr = String(plainBytes, StandardCharsets.UTF_8) if (jsonStr.startsWith("\uFEFF")) jsonStr = jsonStr.substring(1) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt index 5c9f58efc..fc155bdd9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt @@ -6,14 +6,15 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl import com.lagradost.cloudstream3.utils.newExtractorLink +import java.net.URLDecoder open class Cda : ExtractorApi() { override var mainUrl = "https://ebd.cda.pl" override var name = "Cda" override val requiresReferer = false + override suspend fun getUrl(url: String, referer: String?): List? { val mediaId = url .split("/").last() @@ -64,10 +65,10 @@ open class Cda : ExtractorApi() { .replace("_QWE", "") .replace("_Q5", "") .replace("_IKSDE", "") - a = a.decodeUrl() + a = URLDecoder.decode(a, "UTF-8") a = a.map { char -> if (char.code in 33..126) { - return@map (33 + (char.code + 14) % 94).toChar().toString() + return@map String.format("%c", 33 + (char.code + 14) % 94) } else { return@map char } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt index a85aff8d4..62c450073 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt @@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.loadExtractor -import io.ktor.http.Url +import okhttp3.HttpUrl.Companion.toHttpUrl // deobfuscated from https://hglink.to/main.js?v=1.1.3 using https://deobfuscate.io/ private val mirrors = arrayOf( @@ -90,7 +90,7 @@ abstract class CineMMRedirect : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val videoId = Url(url).encodedPath + val videoId = url.toHttpUrl().encodedPath val mirror = mirrors.random() // re-use existing extractors by calling the ExtractorApi @@ -98,4 +98,4 @@ abstract class CineMMRedirect : ExtractorApi() { val mirrorUrlWithVideoId = "https://$mirror$videoId" loadExtractor(mirrorUrlWithVideoId, referer, subtitleCallback, callback) } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt index 7f6d98049..3c79baf3a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt @@ -27,7 +27,7 @@ open class CloudMailRu : ExtractorApi() { "Origin" to mainUrl, "User-Agent" to USER_AGENT, ) - val vidId = url.substringAfter("public/").encodeToByteArray() + val vidId = url.substringAfter("public/").toByteArray() val vidIdEnc = base64Encode(vidId) val videoReq = app.get(url, headers=headers).text val regex = Regex(pattern = "videowl_view\":\\{\"count\":\"1\",\"url\":\"([^\"]*)\"\\}", options = setOf(RegexOption.IGNORE_CASE)) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 4732cafcf..db6db39d5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -7,8 +7,9 @@ import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 -import io.ktor.http.Url -import io.ktor.http.decodeURLPart +import java.net.URI + + class Geodailymotion : Dailymotion() { override val name = "GeoDailymotion" @@ -56,6 +57,7 @@ open class Dailymotion : ExtractorApi() { } } + private fun getEmbedUrl(url: String): String? { if (url.contains("/embed/") || url.contains("/video/")) return url if (url.contains("geo.dailymotion.com")) { @@ -65,8 +67,9 @@ open class Dailymotion : ExtractorApi() { return null } + private fun getVideoId(url: String): String? { - val path = Url(url).encodedPath.decodeURLPart() + val path = URI(url).path val id = path.substringAfter("/video/") return if (id.matches(videoIdRegex)) id else null } @@ -79,6 +82,7 @@ open class Dailymotion : ExtractorApi() { return generateM3u8(name, streamLink, "").forEach(callback) } + data class MetaData( val qualities: Map>?, val subtitles: SubtitlesWrapper? @@ -98,4 +102,5 @@ open class Dailymotion : ExtractorApi() { val label: String, val urls: List ) + } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index bce017276..12bc5a0c5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url +import java.net.URI class Doodspro : DoodLaExtractor() { override var mainUrl = "https://doods.pro" @@ -138,6 +138,8 @@ open class DoodLaExtractor : ExtractorApi() { } private fun getBaseUrl(url: String): String { - return Url(url).let { "${it.protocol.name}://${it.host}" } + return URI(url).let { + "${it.scheme}://${it.host}" + } } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Flyfile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Flyfile.kt deleted file mode 100644 index eb6d474a5..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Flyfile.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.Prerelease -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.newExtractorLink -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Prerelease -open class Flyfile : ExtractorApi() { - override val name: String = "FlyFile" - override val mainUrl: String = "https://flyfile.app" - open val apiUrl: String = "https://api.flyfile.app" - override val requiresReferer: Boolean = false - - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val videoId = url.substringAfterLast("/") - val videoInfo = app.get("$apiUrl/api/streaming/assign/$videoId") - .parsed() - - val streamUrl = "${videoInfo.url}/hls/${videoInfo.token}/master.m3u8" - callback.invoke( - newExtractorLink( - source = name, - name = name, - url = streamUrl, - type = ExtractorLinkType.M3U8 - ) - ) - } - - @Serializable - private data class StreamInfo( - @SerialName("url") - val url: String, - @SerialName("token") - val token: String - ) -} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt index ba297067e..e0fefe8aa 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt @@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.loadExtractor -import io.ktor.http.Url +import java.net.URI class Techinmind: GDMirrorbot() { override var name = "Techinmind Cloud AIO" @@ -103,7 +103,7 @@ open class GDMirrorbot : ExtractorApi() { } private fun getBaseUrl(url: String): String { - return Url(url).let { "${it.protocol.name}://${it.host}" } + return URI(url).let { "${it.scheme}://${it.host}" } } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt index 5fc55aac6..61c22e929 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt @@ -82,7 +82,7 @@ open class Gdriveplayer : ExtractorApi() { val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) ?.split(Regex("\\D+")) ?.joinToString("") { - it.toInt().toChar().toString() + Char(it.toInt()).toString() }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } ?: throw ErrorLoadingException("can't find password") val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "") @@ -125,4 +125,4 @@ open class Gdriveplayer : ExtractorApi() { @JsonProperty("label") val label: String ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt index 2b42e42c7..ced827eec 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.newExtractorLink -import kotlin.math.round open class Gofile : ExtractorApi() { override val name = "Gofile" @@ -68,19 +67,10 @@ open class Gofile : ExtractorApi() { ?: Qualities.Unknown.value } - private fun roundTo2Decimals(value: Double): String { - val rounded = round(value * 100) / 100.0 - val intPart = rounded.toLong() - val decPart = round((rounded - intPart) * 100).toLong() - return "$intPart.${decPart.toString().padStart(2, '0')}" - } - private fun formatBytes(bytes: Long): String { - val mb = 1024L * 1024 - val gb = mb * 1024 return when { - bytes < gb -> "${roundTo2Decimals(bytes.toDouble() / mb)} MB" - else -> "${roundTo2Decimals(bytes.toDouble() / gb)} GB" + bytes < 1024L * 1024 * 1024 -> "%.2f MB".format(bytes.toDouble() / (1024 * 1024)) + else -> "%.2f GB".format(bytes.toDouble() / (1024 * 1024 * 1024)) } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 1ccd3e4d5..ea6fba73b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.api.Log import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.AesHelper import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.extractors.helper.AesHelper +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue open class HDMomPlayer : ExtractorApi() { override val name = "HDMomPlayer" @@ -23,7 +24,7 @@ open class HDMomPlayer : ExtractorApi() { if (bePlayer != null) { val bePlayerPass = bePlayer.get(1) val bePlayerData = bePlayer.get(2) - val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.encodeToByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") + val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) } else { @@ -31,7 +32,7 @@ open class HDMomPlayer : ExtractorApi() { val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) if (trackStr != null) { - val tracks:List = parseJson>("[${trackStr}]") + val tracks:List = jacksonObjectMapper().readValue("[${trackStr}]") for (track in tracks) { if (track.file == null || track.label == null) continue @@ -67,4 +68,4 @@ open class HDMomPlayer : ExtractorApi() { @JsonProperty("language") val language: String?, @JsonProperty("default") val default: String? ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt index a974df15c..4f83bad25 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt @@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url +import java.net.URI class HubCloud : ExtractorApi() { override val name = "Hub-Cloud" @@ -24,7 +24,7 @@ class HubCloud : ExtractorApi() { ) { val tag = "HubCloud" val realUrl = url.takeIf { - try { Url(it); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false } + try { URI(it).toURL(); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false } } ?: return val baseUrl=getBaseUrl(realUrl) @@ -161,7 +161,7 @@ class HubCloud : ExtractorApi() { private fun getBaseUrl(url: String): String { return try { - Url(url).let { "${it.protocol.name}://${it.host}" } + URI(url).let { "${it.scheme}://${it.host}" } } catch (_: Exception) { "" } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt index 1ac6c789c..40d817e99 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl +import com.lagradost.cloudstream3.utils.StringUtils.decodeUri import com.lagradost.cloudstream3.utils.newExtractorLink import org.jsoup.nodes.Document @@ -96,7 +96,7 @@ open class InternetArchive : ExtractorApi() { if (mediaUrl.isNotEmpty()) { val name = if (mediaUrl.count() > 1) { val fileExtension = mediaUrl.substringAfterLast(".") - val fileNameCleaned = fileName.decodeUrl().substringBeforeLast('.') + val fileNameCleaned = fileName.decodeUri().substringBeforeLast('.') "$fileNameCleaned ($fileExtension)" } else this.name callback( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt index 84b818723..76f14d33b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt @@ -14,10 +14,11 @@ open class Mvidoo : ExtractorApi() { private fun String.decodeHex(): String { require(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() - .decodeToString() + return String( + chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + ) } override suspend fun getUrl( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 69c3b7759..8af77c1df 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -32,7 +32,7 @@ open class Odnoklassniki : ExtractorApi() { val embedUrl = url.replace("/video/","/videoembed/") val videoReq = app.get(embedUrl, headers=headers).text.replace("\\"", "\"").replace("\\\\", "\\") .replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> - matchResult.groupValues[1].toInt(16).toChar().toString() + Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString() } val videosStr = Regex(""""videos":(\[[^]]*])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") val videos = AppUtils.tryParseJson>(videosStr) ?: throw ErrorLoadingException("Video not found") diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index 3f7408eb3..98598dd28 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import java.nio.charset.StandardCharsets import java.security.MessageDigest import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec @@ -170,7 +171,7 @@ open class Rabbitstream : ExtractorApi() { IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) ) val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found") - return decryptedData.decodeToString() + return String(decryptedData, StandardCharsets.UTF_8) } data class Tracks( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index 822a5eed7..9654e5f38 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -35,14 +35,14 @@ open class RapidVid : ExtractorApi() { if (extractedValue != null) { val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - decoded = bytes.decodeToString() + decoded = String(bytes, Charsets.UTF_8) } else { val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\") extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "") val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() - decoded = bytes?.decodeToString() ?: throw ErrorLoadingException("File not found") + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") } callback.invoke( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt index 58aa25c8c..c721db6b9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log -import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app @@ -13,6 +12,7 @@ import com.lagradost.cloudstream3.utils.getAndUnpack import com.lagradost.cloudstream3.utils.getPacked import com.lagradost.cloudstream3.network.WebViewResolver + class Mwish : StreamWishExtractor() { override val name = "Mwish" override val mainUrl = "https://mwish.pro" @@ -28,12 +28,6 @@ class Ewish : StreamWishExtractor() { override val mainUrl = "https://embedwish.com" } -@Prerelease -class Hgcloudto : StreamWishExtractor() { - override val name = "Hgcloud" - override val mainUrl = "https://Hgcloud.to" -} - class WishembedPro : StreamWishExtractor() { override val name = "Wishembed" override val mainUrl = "https://wishembed.pro" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt index 2cb9a5e5d..611711e39 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt @@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.JsUnpacker import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.newExtractorLink +import java.net.URI open class Streamhub : ExtractorApi() { override var mainUrl = "https://streamhub.to" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt index 9886300aa..98481970b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import io.ktor.http.Url +import java.net.URI open class Streamplay : ExtractorApi() { override val name = "Streamplay" @@ -22,7 +22,9 @@ open class Streamplay : ExtractorApi() { ) { val request = app.get(url, referer = referer) val redirectUrl = request.url - val mainServer = Url(redirectUrl).let { "${it.protocol.name}://${it.host}" } + val mainServer = URI(redirectUrl).let { + "${it.scheme}://${it.host}" + } val key = redirectUrl.substringAfter("embed-").substringBefore(".html") val token = request.document.select("script").find { it.data().contains("sitekey:") }?.data() @@ -77,4 +79,4 @@ open class Streamplay : ExtractorApi() { @JsonProperty("label") val label: String? = null, ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt index 08dcb634e..582be8afb 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt @@ -6,6 +6,8 @@ import com.lagradost.cloudstream3.utils.* import org.mozilla.javascript.Context import org.mozilla.javascript.EvaluatorException import org.mozilla.javascript.Scriptable +import java.util.* + open class Userload : ExtractorApi() { override var name = "Userload" @@ -14,7 +16,7 @@ open class Userload : ExtractorApi() { private fun splitInput(input: String): List { var counter = 0 - val array = mutableListOf() + val array = ArrayList() var buffer = "" for (c in input) { when (c) { @@ -69,7 +71,7 @@ open class Userload : ExtractorApi() { } var txtresult = "" subchar.forEach{ - txtresult = txtresult.plus(it.toInt(8).toChar()) + txtresult = txtresult.plus(Char(it.toInt(8))) } val val1 = Regex(""""morocco="((.|\\n)*?)"&mycountry="""").find(txtresult)?.groups?.get(1)?.value.toString().drop(1).dropLast(1) val val2 = txtresult.substringAfter("""&mycountry="+""").substringBefore(")") 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 6b82ee454..974549fcb 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt @@ -1,7 +1,6 @@ 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 @@ -22,7 +21,7 @@ open class Vicloud : ExtractorApi() { ) { val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1) app.get( - "$mainUrl/api/?$id=&_=$unixTimeMS", + "$mainUrl/api/?$id=&_=${System.currentTimeMillis()}", headers = mapOf( "X-Requested-With" to "XMLHttpRequest" ), diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index a23f6b683..36acf7f7a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -35,14 +35,14 @@ open class VidMoxy : ExtractorApi() { if (extractedValue != null) { val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - decoded = bytes.decodeToString() + decoded = String(bytes, Charsets.UTF_8) } else { val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\") extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "") val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() - decoded = bytes?.decodeToString() ?: throw ErrorLoadingException("File not found") + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") } callback.invoke( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt index 63ceb1f3d..846fd851d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt @@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.fixUrl import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url +import java.net.URI import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @@ -84,7 +84,7 @@ open class VidStack : ExtractorApi() { private fun getBaseUrl(url: String): String { return try { - Url(url).let { "${it.protocol.name}://${it.host}" } + URI(url).let { "${it.scheme}://${it.host}" } } catch (e: Exception) { Log.e("Vidstack", "getBaseUrl fallback: ${e.message}") mainUrl diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt index 59d7a7f2e..47840a08a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt @@ -47,7 +47,7 @@ class Videa : ExtractorApi() { rawBytes[4] == 0x6C.toByte() // 'l' val videaXml = if (isXml) { - rawBytes.decodeToString() + String(rawBytes, Charsets.UTF_8) } else { // Handle encrypted XML response val xsHeader = response.headers["X-Videa-Xs"] ?: return @@ -179,7 +179,7 @@ class Videa : ExtractorApi() { } val actualEncryptedBytes = if (isBase64) { - val base64String = encryptedBytes.decodeToString() + val base64String = String(encryptedBytes, Charsets.UTF_8) .replace("\r", "") .replace("\n", "") .replace(" ", "") @@ -189,7 +189,7 @@ class Videa : ExtractorApi() { encryptedBytes } - val keyBytes = key.encodeToByteArray() + val keyBytes = key.toByteArray(Charsets.UTF_8) // RC4 key-scheduling algorithm (KSA) val s = IntArray(256) { it } @@ -211,6 +211,6 @@ class Videa : ExtractorApi() { result[k] = ((actualEncryptedBytes[k].toInt() and 0xFF) xor keyStreamByte).toByte() } - return result.decodeToString() + return String(result, Charsets.UTF_8) } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index bc94ae0cc..583d92322 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -2,11 +2,12 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue open class VideoSeyred : ExtractorApi() { override val name = "VideoSeyred" @@ -19,7 +20,7 @@ open class VideoSeyred : ExtractorApi() { val videoUrl = "${mainUrl}/playlist/${videoId}.json" val responseRaw = app.get(videoUrl) - val responseList: List = tryParseJson>(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") + val responseList:List = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") val response = responseList[0] for (track in response.tracks) { @@ -67,4 +68,4 @@ open class VideoSeyred : ExtractorApi() { @JsonProperty("label") val label: String? = null, @JsonProperty("default") val default: String? = null ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt index 49560d456..5c871b54b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt @@ -38,13 +38,13 @@ class Vidsonic() : ExtractorApi() { .substringBefore(";") .replace("'", "") - // (improved) Kotlin implementation of the JavaScript code from above + // (improved) Java implementation of the JavaScript code from above val streamUrl = encodedStreamUrl .replace("|", "") // always two base16 digits together build one ASCII char .chunked(2) .map { - it.toInt(16).toChar() + Integer.parseInt(it, 16).toChar() } .joinToString("") .reversed() diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt index fb310401a..dd8511eae 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt @@ -1,20 +1,14 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.newAudioFile +import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink - -expect open class YoutubeExtractor() : ExtractorApi { - override val mainUrl: String - override val name: String - override val requiresReferer: Boolean - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ) -} +import com.lagradost.cloudstream3.utils.newExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamType class YoutubeShortLinkExtractor : YoutubeExtractor() { override val mainUrl = "https://youtu.be" @@ -27,3 +21,107 @@ class YoutubeMobileExtractor : YoutubeExtractor() { class YoutubeNoCookieExtractor : YoutubeExtractor() { override val mainUrl = "https://www.youtube-nocookie.com" } + +open class YoutubeExtractor : ExtractorApi() { + + override val mainUrl = "https://www.youtube.com" + override val name = "YouTube" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val videoId = extractYouTubeId(url) + val watchUrl = "$mainUrl/watch?v=$videoId" + + val info = StreamInfo.getInfo(watchUrl) + + val isLive = + info.streamType == StreamType.LIVE_STREAM + || info.streamType == StreamType.AUDIO_LIVE_STREAM + || info.streamType == StreamType.POST_LIVE_STREAM + || info.streamType == StreamType.POST_LIVE_AUDIO_STREAM + + if (isLive && info.hlsUrl != null) { + callback( + newExtractorLink( + source = name, + name = "YouTube Live", + url = info.hlsUrl + ) { + type = ExtractorLinkType.M3U8 + } + ) + } else { + processVideo(info, subtitleCallback, callback) + } + } + + private suspend fun processVideo( + info: StreamInfo, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val videoStreams = info.videoOnlyStreams.orEmpty() + + if (videoStreams.isEmpty()) return false + + val audioStreams = info.audioStreams.orEmpty() + + videoStreams.forEach { video -> + + callback( + newExtractorLink( + source = name, + name = "YouTube ${normalizeCodec(video.codec)}", + url = video.content + ) { + quality = video.height + audioTracks = audioStreams.map { newAudioFile(it.content) } + } + ) + } + + + info.subtitles.forEach { subtitle -> + subtitleCallback( + newSubtitleFile( + lang = subtitle.displayLanguageName + ?: subtitle.languageTag + ?: "Unknown", + url = subtitle.content + ) + ) + } + + return true + } + + // ---------------- HELPERS ---------------- + + private fun extractYouTubeId(url: String): String { + val regex = Regex( + "(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})" + ) + return regex.find(url)?.groupValues?.get(1) + ?: throw IllegalArgumentException("Invalid YouTube URL: $url") + } + + private fun normalizeCodec(codec: String?): String { + if (codec.isNullOrBlank()) return "" + + val c = codec.lowercase() + + return when { + c.startsWith("av01") -> "AV1" + c.startsWith("vp9") -> "VP9" + c.startsWith("avc1") || c.startsWith("h264") -> "H264" + c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265" + else -> codec.substringBefore('.').uppercase() + } + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt index 6ad0524e8..af59b6f7d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt @@ -2,11 +2,13 @@ package com.lagradost.cloudstream3.extractors.helper import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.base64Encode +import java.util.Arrays import java.security.MessageDigest import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.IvParameterSpec +import java.nio.charset.StandardCharsets import kotlin.math.min /** @@ -46,9 +48,9 @@ object CryptoJS { // Create CryptoJS-like encrypted! val sBytes = APPEND.toByteArray() val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size) - sBytes.copyInto(destination = b, destinationOffset = 0) - saltBytes.copyInto(destination = b, destinationOffset = sBytes.size) - cipherText.copyInto(destination = b, destinationOffset = sBytes.size + saltBytes.size) + System.arraycopy(sBytes, 0, b, 0, sBytes.size) + System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size) + System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size) return base64Encode(b) } @@ -61,8 +63,8 @@ object CryptoJS { */ fun decrypt(password: String, cipherText: String): String { val ctBytes = base64DecodeArray(cipherText) - val saltBytes = ctBytes.copyOfRange(8, 16) - val cipherTextBytes = ctBytes.copyOfRange(16, ctBytes.size) + val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16) + val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size) val key = ByteArray(KEY_SIZE / 8) val iv = ByteArray(IV_SIZE / 8) @@ -105,18 +107,16 @@ object CryptoJS { hash.reset() } - block!!.copyInto( - destination = derivedBytes, - destinationOffset = numberOfDerivedWords * 4, - startIndex = 0, - endIndex = min(block.size, (targetKeySize - numberOfDerivedWords) * 4) + System.arraycopy( + block!!, 0, derivedBytes, numberOfDerivedWords * 4, + min(block.size, (targetKeySize - numberOfDerivedWords) * 4) ) numberOfDerivedWords += block.size / 4 } - derivedBytes.copyInto(destination = resultKey, destinationOffset = 0, startIndex = 0, endIndex = keySize * 4) - derivedBytes.copyInto(destination = resultIv, destinationOffset = 0, startIndex = keySize * 4, endIndex = (keySize * 4) + (ivSize * 4)) + System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4) + System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4) return derivedBytes // key + iv } @@ -126,4 +126,4 @@ object CryptoJS { SecureRandom().nextBytes(this) } } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt index 31618a32b..a16d41943 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt @@ -12,8 +12,8 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url import org.jsoup.nodes.Document +import java.net.URI import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @@ -88,8 +88,8 @@ object GogoHelper { val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall val foundDecryptKey = secretDecryptKey ?: foundKey - val url = Url(iframeUrl) - val mainUrl = "https://${url.host}" + val uri = URI(iframeUrl) + val mainUrl = "https://" + uri.host val encryptedId = cryptoHandler(id, foundIv, foundKey) val encryptRequestData = if (isUsingAdaptiveData) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt index c3b50c7a5..2563d40e1 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt @@ -1,7 +1,7 @@ package com.lagradost.cloudstream3.extractors.helper -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl -import com.lagradost.cloudstream3.utils.StringUtils.encodeUrl +import com.lagradost.cloudstream3.utils.StringUtils.decodeUri +import com.lagradost.cloudstream3.utils.StringUtils.encodeUri // Taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/NineAnime.kt // GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md @@ -108,6 +108,8 @@ object NineAnimeHelper { } } - fun encode(input: String): String = input.encodeUrl() - private fun decode(input: String): String = input.decodeUrl() + fun encode(input: String): String = + input.encodeUri().replace("+", "%20") + + private fun decode(input: String): String = input.decodeUri() } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt index 7076e407f..6fde6efe3 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt @@ -30,9 +30,11 @@ class CrossTmdbProvider : TmdbProvider() { } private val validApis - get() = apis.filter { it.lang == this.lang && it::class != this::class } + get() = + synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } } //.distinctBy { it.uniqueId } + data class CrossMetaData( @JsonProperty("isSuccess") val isSuccess: Boolean, @JsonProperty("movies") val movies: List>? = null, @@ -119,4 +121,4 @@ class CrossTmdbProvider : TmdbProvider() { return base } -} +} \ No newline at end of file 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 e7e1175f0..cf3e28a8d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt @@ -18,7 +18,6 @@ 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 @@ -31,6 +30,8 @@ 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() { @@ -191,6 +192,17 @@ 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 @@ -439,4 +451,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 59dcd2711..63f6d564c 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.metaproviders import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.api.BuildConfig import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.Actor import com.lagradost.cloudstream3.ActorData @@ -23,8 +22,8 @@ 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 import com.lagradost.cloudstream3.newHomePageResponse import com.lagradost.cloudstream3.newMovieLoadResponse @@ -34,6 +33,8 @@ 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" @@ -45,10 +46,10 @@ open class TraktProvider : MainAPI() { TvType.Anime, ) + private val traktClientId = + "d9f434f48b55683a279ffe88ddc68351cc04c9dc9372bd95af5de780b794e770" private val traktApiUrl = "https://api.trakt.tv" - val traktClientId: String = BuildConfig.TRAKT_CLIENT_ID - override val mainPage = mainPageOf( "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time @@ -113,7 +114,6 @@ open class TraktProvider : MainAPI() { val posterUrl = fixPath(mediaDetails?.images?.poster?.firstOrNull()) val backDropUrl = fixPath(mediaDetails?.images?.fanart?.firstOrNull()) - val logoUrl = fixPath(mediaDetails?.images?.logo?.firstOrNull()) val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=full,images") @@ -183,7 +183,6 @@ open class TraktProvider : MainAPI() { this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders this.backgroundPosterUrl = backDropUrl - this.logoUrl = logoUrl this.contentRating = mediaDetails.certification addTrailer(mediaDetails.trailer) addImdbId(mediaDetails.ids?.imdb) @@ -274,7 +273,6 @@ open class TraktProvider : MainAPI() { //posterHeaders this.nextAiring = nextAir this.backgroundPosterUrl = backDropUrl - this.logoUrl = logoUrl this.contentRating = mediaDetails.certification addTrailer(mediaDetails.trailer) addImdbId(mediaDetails.ids?.imdb) @@ -291,7 +289,18 @@ open class TraktProvider : MainAPI() { "trakt-api-version" to "2", "trakt-api-key" to traktClientId, ) - ).text + ).toString() + } + + 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 { @@ -446,4 +455,4 @@ open class TraktProvider : MainAPI() { @JsonProperty("is_bollywood") val isBollywood: Boolean = false, @JsonProperty("is_cartoon") val isCartoon: Boolean = false, ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt index f4fce2ef3..61f87b8ba 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt @@ -17,7 +17,10 @@ abstract class BasePlugin { fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") element.sourcePlugin = this.filename - APIHolder.allProviders.add(element) + // Race condition causing which would case duplicates if not for distinctBy + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.add(element) + } APIHolder.addPluginMapping(element) } @@ -28,7 +31,9 @@ abstract class BasePlugin { fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") element.sourcePlugin = this.filename - extractorApis.add(element) + synchronized(extractorApis) { + extractorApis.add(element) + } } /** diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt index 180aee11a..e07f32c0a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.InternalAPI -import kotlin.concurrent.Volatile @InternalAPI object AppDebug { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt index 1c635013f..6832ab8d2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -1,91 +1,21 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.module.kotlin.readValue -import com.lagradost.cloudstream3.InternalAPI -import com.lagradost.cloudstream3.json import com.lagradost.cloudstream3.mapper -import com.lagradost.cloudstream3.mvvm.logError -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.serializer -import kotlinx.serialization.serializerOrNull -import kotlin.reflect.KClass +import java.io.Reader -@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) object AppUtils { - /** Any object as a JSON string */ + /** Any object as json string */ fun Any.toJson(): String { if (this is String) return this - return toJsonLiteral() + return mapper.writeValueAsString(this) } - /** Sometimes we want to encode as JSON even if it is already a String. */ - @InternalAPI - fun Any.toJsonLiteral(): String { - // @Serializable generates a serializer at compile time; contextual serializers are - // registered manually in serializersModule, we need both to support all cases - val serializer = - this::class.serializerOrNull() ?: json.serializersModule.getContextual(this::class) - return if (serializer != null) { - try { - @Suppress("UNCHECKED_CAST") - json.encodeToString(serializer as KSerializer, this) - } catch (e: SerializationException) { - logError(e) - mapper.writeValueAsString(this) - } - } else { - mapper.writeValueAsString(this) - } - } - - @InternalAPI - fun parseJson(value: String, kClass: KClass): T { - val serializer = kClass.serializerOrNull() ?: json.serializersModule.getContextual(kClass) - if (serializer != null) { - try { - return json.decodeFromString(serializer, value) - } catch (e: SerializationException) { - logError(e) - } - } - - return mapper.readValue(value, kClass.java) - } - - // This is inlined code and can easily cause breakage in extensions! - // Watch out when editing this to make sure stable also supports all inlined code! - inline fun parseJson(value: String): T { - // @Serializable generates a serializer at compile time; contextual serializers are - // registered manually in serializersModule, we need both to support all cases - val serializer = runCatching { serializer() } - .recoverCatching { json.serializersModule.getContextual(T::class) } - .getOrNull() - - // Prefer Kotlin Serialization over Jackson - if (serializer != null) { - try { - return json.decodeFromString(serializer, value) - } catch (e: SerializationException) { - logError(e) - } catch (_: Throwable) { - // Pass, the above code will trigger a NoSuchMethodError on stable due to our previously undefined json variable - } - } - + inline fun parseJson(value: String): T { return mapper.readValue(value) } - @Deprecated( - "This overload was only ever used for BasePlugin.Manifest which has since been migrated. " + - "No other code should be using this. Use reader.readText() and call parseJson(String) instead.", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("parseJson(reader.readText())") - ) - inline fun parseJson(reader: java.io.Reader, valueType: Class): T { - // Reader-based parsing has no kotlinx equivalent, fall back to Jackson + inline fun parseJson(reader: Reader, valueType: Class): T { return mapper.readValue(reader, valueType) } @@ -96,4 +26,4 @@ object AppUtils { null } } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt deleted file mode 100644 index 0be02ac6f..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import kotlinx.atomicfu.locks.SynchronizedObject -import kotlinx.atomicfu.locks.synchronized - -/** - * A thread-safe list backed by [SynchronizedObject]. - * - * For iteration, wrap block in [withLock] to hold the lock for the duration: - * list.withLock { list.forEach { ... } } - */ -open class AtomicList( - private val delegate: List = emptyList(), -) : List, SynchronizedObject() { - - fun withLock(block: () -> R): R = synchronized(this) { block() } - - fun filter(predicate: (T) -> Boolean): AtomicList = synchronized(this) { AtomicList(delegate.filter(predicate)) } - fun distinctBy(selector: (T) -> Any?): AtomicList = synchronized(this) { AtomicList(delegate.distinctBy(selector)) } - - override val size: Int get() = synchronized(this) { delegate.size } - override fun isEmpty(): Boolean = synchronized(this) { delegate.isEmpty() } - override fun contains(element: T): Boolean = synchronized(this) { delegate.contains(element) } - override fun containsAll(elements: Collection): Boolean = synchronized(this) { delegate.containsAll(elements) } - override fun get(index: Int): T = synchronized(this) { delegate[index] } - override fun indexOf(element: T): Int = synchronized(this) { delegate.indexOf(element) } - override fun lastIndexOf(element: T): Int = synchronized(this) { delegate.lastIndexOf(element) } - - // Iterators intentionally NOT synchronized, callers must use withLock { } for safe iteration. - override fun iterator(): Iterator = delegate.iterator() - override fun listIterator(): ListIterator = delegate.listIterator() - override fun listIterator(index: Int): ListIterator = delegate.listIterator(index) - override fun subList(fromIndex: Int, toIndex: Int): List = delegate.subList(fromIndex, toIndex) - - operator fun plus(element: T): AtomicList = synchronized(this) { AtomicList(delegate + element) } - operator fun plus(elements: Collection): AtomicList = synchronized(this) { AtomicList(delegate + elements) } -} - -class AtomicMutableList( - private val mutableDelegate: MutableList = mutableListOf(), -) : AtomicList(mutableDelegate), MutableList { - - override fun add(element: T): Boolean = synchronized(this) { mutableDelegate.add(element) } - override fun add(index: Int, element: T) = synchronized(this) { mutableDelegate.add(index, element) } - override fun addAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.addAll(elements) } - override fun addAll(index: Int, elements: Collection): Boolean = synchronized(this) { mutableDelegate.addAll(index, elements) } - override fun remove(element: T): Boolean = synchronized(this) { mutableDelegate.remove(element) } - override fun removeAt(index: Int): T = synchronized(this) { mutableDelegate.removeAt(index) } - override fun removeAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.removeAll(elements) } - override fun retainAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.retainAll(elements) } - override fun set(index: Int, element: T): T = synchronized(this) { mutableDelegate.set(index, element) } - override fun clear() = synchronized(this) { mutableDelegate.clear() } - - // Iterators intentionally NOT synchronized, callers must use withLock { } for safe iteration. - override fun iterator(): MutableIterator = mutableDelegate.iterator() - override fun listIterator(): MutableListIterator = mutableDelegate.listIterator() - override fun listIterator(index: Int): MutableListIterator = mutableDelegate.listIterator(index) - override fun subList(fromIndex: Int, toIndex: Int): MutableList = mutableDelegate.subList(fromIndex, toIndex) -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index d15ea129c..c525a1f36 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.annotation.WorkerThread -import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* +import java.util.Collections.synchronizedList @AnyThread expect fun runOnMainThreadNative(@MainThread work: (() -> Unit)) @@ -64,18 +64,9 @@ object Coroutines { /** * Safe to add and remove how you want * If you want to iterate over the list then you need to do: - * list.withLock { code here } + * synchronized(allProviders) { code here } */ - @Prerelease - fun atomicListOf(vararg items: T): AtomicMutableList { - return AtomicMutableList(items.toMutableList()) + fun threadSafeListOf(vararg items: T): MutableList { + return synchronizedList(items.toMutableList()) } - - // Deprecate after next stable - /*@Deprecated( - message = "Use atomicListOf() instead.", - replaceWith = ReplaceWith("atomicListOf(*items)"), - level = DeprecationLevel.WARNING, - )*/ - fun threadSafeListOf(vararg items: T): MutableList = atomicListOf(*items) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index f9167d08c..ab80cf2ca 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,11 +1,8 @@ -@file:OptIn(ExperimentalUuidApi::class) - package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonIgnore import com.lagradost.cloudstream3.AudioFile import com.lagradost.cloudstream3.IDownloadableMinimum -import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app @@ -81,7 +78,6 @@ import com.lagradost.cloudstream3.extractors.FilemoonV2 import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Multimoviesshg import com.lagradost.cloudstream3.extractors.FlaswishCom -import com.lagradost.cloudstream3.extractors.Flyfile import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.FourPlayRu @@ -110,7 +106,6 @@ import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.HDStreamAble import com.lagradost.cloudstream3.extractors.Habetar import com.lagradost.cloudstream3.extractors.Haxloppd -import com.lagradost.cloudstream3.extractors.Hgcloudto import com.lagradost.cloudstream3.extractors.HglinkTo import com.lagradost.cloudstream3.extractors.HgplayCDN import com.lagradost.cloudstream3.extractors.Hotlinger @@ -312,18 +307,14 @@ import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf -import io.ktor.http.Url -import io.ktor.http.decodeURLPart import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive +import me.xdrop.fuzzywuzzy.FuzzySearch import org.jsoup.Jsoup +import java.net.URI +import java.util.UUID import kotlin.coroutines.cancellation.CancellationException -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid -import kotlin.uuid.toJavaUuid -import kotlin.uuid.toKotlinUuid /** * For use in the ConcatenatingMediaSource. @@ -422,7 +413,7 @@ enum class ExtractorLinkType { private fun inferTypeFromUrl(url: String): ExtractorLinkType { val path = try { - Url(url).encodedPath.decodeURLPart() + URI(url).path } catch (_: Throwable) { // don't log magnet links as errors null @@ -439,43 +430,29 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { val INFER_TYPE: ExtractorLinkType? = null /** - * [Uuid] for the ClearKey DRM scheme. + * UUID for the ClearKey DRM scheme. * * * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. */ -@Prerelease -val CLEARKEY_DRM_UUID = Uuid.fromLongs(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) +val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) /** - * [Uuid] for the Widevine DRM scheme. + * UUID for the Widevine DRM scheme. * * * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. */ -@Prerelease -val WIDEVINE_DRM_UUID = Uuid.fromLongs(-0x121074568629b532L, -0x5c37d8232ae2de13L) +val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) /** - * [Uuid] for the PlayReady DRM scheme. + * UUID for the PlayReady DRM scheme. * * * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not * provide PlayReady support. */ -@Prerelease -val PLAYREADY_DRM_UUID = Uuid.fromLongs(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) - -// Deprecate after next stable - -// @Deprecated("Use CLEARKEY_DRM_UUID", ReplaceWith("CLEARKEY_DRM_UUID"), level = DeprecationLevel.WARNING) -val CLEARKEY_UUID = CLEARKEY_DRM_UUID.toJavaUuid() - -// @Deprecated("Use WIDEVINE_DRM_UUID", ReplaceWith("WIDEVINE_DRM_UUID"), level = DeprecationLevel.WARNING) -val WIDEVINE_UUID = WIDEVINE_DRM_UUID.toJavaUuid() - -// @Deprecated("Use PLAYREADY_DRM_UUID", ReplaceWith("PLAYREADY_DRM_UUID"), level = DeprecationLevel.WARNING) -val PLAYREADY_UUID = PLAYREADY_DRM_UUID.toJavaUuid() +val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) suspend fun newExtractorLink( source: String, @@ -498,42 +475,15 @@ suspend fun newExtractorLink( return builder } -// Deprecate after next stable -/* @Deprecated( - message = "Use Kotlin Uuid (kotlin.uuid.Uuid) instead of Java UUID.", - level = DeprecationLevel.WARNING, -) */ suspend fun newDrmExtractorLink( source: String, name: String, url: String, type: ExtractorLinkType? = null, - uuid: java.util.UUID, + uuid: UUID, initializer: suspend DrmExtractorLink.() -> Unit = { } ): DrmExtractorLink { - @Suppress("DEPRECATION_ERROR") - val builder = - DrmExtractorLink( - source = source, - name = name, - url = url, - uuid = uuid.toKotlinUuid(), - type = type ?: INFER_TYPE - ) - builder.initializer() - return builder -} - -@Prerelease -suspend fun newDrmExtractorLink( - source: String, - name: String, - url: String, - type: ExtractorLinkType? = null, - uuid: Uuid, - initializer: suspend DrmExtractorLink.() -> Unit = {}, -): DrmExtractorLink { @Suppress("DEPRECATION_ERROR") val builder = DrmExtractorLink( @@ -559,7 +509,7 @@ suspend fun newDrmExtractorLink( * @property type the type of the media, use [INFER_TYPE] if you want to auto infer the type from the url * @property kid Base64 value of The KID element (Key Id) contains the identifier of the key associated with a license. * @property key Base64 value of Key to be used to decrypt the media file. - * @property uuid Drm [Uuid] [WIDEVINE_DRM_UUID], [PLAYREADY_DRM_UUID], [CLEARKEY_DRM_UUID] (by default) .. etc + * @property uuid Drm UUID [WIDEVINE_UUID], [PLAYREADY_UUID], [CLEARKEY_UUID] (by default) .. etc * @property kty Key type "oct" (octet sequence) by default * @property keyRequestParameters Parameters that will used to request the key. * @see newDrmExtractorLink @@ -577,7 +527,7 @@ open class DrmExtractorLink private constructor( override var type: ExtractorLinkType, open var kid: String? = null, open var key: String? = null, - open var uuid: Uuid, + open var uuid: UUID, open var kty: String? = null, open var keyRequestParameters: HashMap, open var licenseUrl: String? = null, @@ -599,7 +549,7 @@ open class DrmExtractorLink private constructor( extractorData: String? = null, kid: String? = null, key: String? = null, - uuid: Uuid = CLEARKEY_DRM_UUID, + uuid: UUID = CLEARKEY_UUID, kty: String? = "oct", keyRequestParameters: HashMap = hashMapOf(), licenseUrl: String? = null, @@ -634,7 +584,7 @@ open class DrmExtractorLink private constructor( extractorData: String? = null, kid: String? = null, key: String? = null, - uuid: Uuid = CLEARKEY_DRM_UUID, + uuid: UUID = CLEARKEY_UUID, kty: String? = "oct", keyRequestParameters: HashMap = hashMapOf(), licenseUrl: String? = null, @@ -654,14 +604,6 @@ open class DrmExtractorLink private constructor( kty = kty, licenseUrl = licenseUrl, ) - - @Deprecated(message = "Use Kotlin Uuid", level = DeprecationLevel.HIDDEN) - fun setUuid(uuid: java.util.UUID) { - this.uuid = uuid.toKotlinUuid() - } - - @Deprecated(message = "Use Kotlin Uuid", level = DeprecationLevel.HIDDEN) - fun getUuid(): java.util.UUID = this.uuid.toJavaUuid() } /** Class holds extracted media info to be passed to the player. @@ -821,7 +763,7 @@ constructor( /** * Removes https:// and www. - * To match urls regardless of schema, perhaps Url() can be used? + * To match urls regardless of schema, perhaps Uri() can be used? */ val schemaStripRegex = Regex("""^(https:|)//(www\.|)""") @@ -940,7 +882,7 @@ suspend fun loadExtractor( // this is to match mirror domains - like example.com, example.net for (index in extractorApis.lastIndex downTo 0) { val extractor = extractorApis[index] - if (Levenshtein.partialRatio( + if (FuzzySearch.partialRatio( extractor.mainUrl, currentUrl ) > 80 @@ -961,7 +903,7 @@ suspend fun loadExtractor( return false } -val extractorApis: AtomicMutableList = atomicListOf( +val extractorApis: MutableList = arrayListOf( //AllProvider(), Mp4Upload(), StreamTape(), @@ -1252,7 +1194,6 @@ val extractorApis: AtomicMutableList = atomicListOf( MetaGnathTuggers(), Geodailymotion(), Mwish(), - Hgcloudto(), Dwish(), Ewish(), Kswplayer(), @@ -1299,7 +1240,6 @@ val extractorApis: AtomicMutableList = atomicListOf( GUpload(), HlsWish(), ByseQekaho(), - Flyfile() ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt index cd5d752b3..01e5bb862 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt @@ -19,11 +19,12 @@ */ package com.lagradost.cloudstream3.utils -import com.lagradost.cloudstream3.base64DecodeArray -import io.ktor.http.Url import java.io.IOException +import java.net.URI import java.nio.ByteBuffer import java.util.UUID +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi @Suppress("unused") object HlsPlaylistParser { @@ -233,7 +234,7 @@ object HlsPlaylistParser { if (codecArray.isEmpty()) { return null } - val builder = StringBuilder() + val builder = java.lang.StringBuilder() for (codec in codecArray) { if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { if (builder.isNotEmpty()) { @@ -262,7 +263,7 @@ object HlsPlaylistParser { if (codecArray.isEmpty()) { return null } - val builder = StringBuilder() + val builder = java.lang.StringBuilder() for (codec in codecArray) { if (trackType != MimeTypes.getTrackTypeOfCodec(codec)) { if (builder.isNotEmpty()) { @@ -275,29 +276,29 @@ object HlsPlaylistParser { } } - object UrlUtil { - fun resolveToUrl(baseUrl: String?, referenceUrl: String?): Url { - return Url(resolve(baseUrl, referenceUrl)) + object UriUtil { + fun resolveToUri(baseUri: String?, referenceUri: String?): URI { + return URI.create(resolve(baseUri, referenceUri)) } - /** The length of arrays returned by [.getUrlIndices]. */ + /** The length of arrays returned by [.getUriIndices]. */ private const val INDEX_COUNT: Int = 4 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the ':' after the scheme. Equals -1 - * if the URL is a relative reference (no scheme). The hier-part starts at (schemeColon + 1), - * including when the URL has no scheme. + * if the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1), + * including when the URI has no scheme. */ private const val SCHEME_COLON: Int = 0 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the path part. Equals (schemeColon + @@ -309,7 +310,7 @@ object HlsPlaylistParser { const val PATH: Int = 1 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the query part, including the '?' @@ -320,87 +321,87 @@ object HlsPlaylistParser { const val QUERY: Int = 2 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the fragment part, including the '#' - * before the fragment. Equal to the length of the URL if no fragment part, and (length - 1) if + * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if * the fragment part is a single '#' with no data. */ private const val FRAGMENT: Int = 3 /** - * Performs relative resolution of a `referenceUrl` with respect to a `baseUrl`. + * Performs relative resolution of a `referenceUri` with respect to a `baseUri`. * * * The resolution is performed as specified by RFC-3986. * - * @param baseUrl The base URL. - * @param referenceUrl The reference URL to resolve. + * @param baseUri The base URI. + * @param referenceUri The reference URI to resolve. */ - private fun resolve(baseUrl: String?, referenceUrl: String?): String { - var baseUrl = baseUrl - var referenceUrl = referenceUrl - val url = StringBuilder() + private fun resolve(baseUri: String?, referenceUri: String?): String { + var baseUri = baseUri + var referenceUri = referenceUri + val uri = StringBuilder() // Map null onto empty string, to make the following logic simpler. - baseUrl = baseUrl ?: "" - referenceUrl = referenceUrl ?: "" + baseUri = baseUri ?: "" + referenceUri = referenceUri ?: "" - val refIndices: IntArray = getUrlIndices(referenceUrl) + val refIndices: IntArray = getUriIndices(referenceUri) if (refIndices[SCHEME_COLON] != -1) { - // The reference is absolute. The target Url is the reference. - url.append(referenceUrl) - removeDotSegments(url, refIndices[PATH], refIndices[QUERY]) - return url.toString() + // The reference is absolute. The target Uri is the reference. + uri.append(referenceUri) + removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]) + return uri.toString() } - val baseIndices: IntArray = getUrlIndices(baseUrl) + val baseIndices: IntArray = getUriIndices(baseUri) if (refIndices[FRAGMENT] == 0) { - // The reference is empty or contains just the fragment part, then the target Url is the - // concatenation of the base Url without its fragment, and the reference. - return url.append(baseUrl, 0, baseIndices[FRAGMENT]).append(referenceUrl).toString() + // The reference is empty or contains just the fragment part, then the target Uri is the + // concatenation of the base Uri without its fragment, and the reference. + return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString() } if (refIndices[QUERY] == 0) { // The reference starts with the query part. The target is the base up to (but excluding) the // query, plus the reference. - return url.append(baseUrl, 0, baseIndices[QUERY]).append(referenceUrl).toString() + return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString() } if (refIndices[PATH] != 0) { // The reference has authority. The target is the base scheme plus the reference. val baseLimit = baseIndices[SCHEME_COLON] + 1 - url.append(baseUrl, 0, baseLimit).append(referenceUrl) + uri.append(baseUri, 0, baseLimit).append(referenceUri) return removeDotSegments( - url, + uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY] ) } - if (referenceUrl[refIndices[PATH]] == '/') { + if (referenceUri[refIndices[PATH]] == '/') { // The reference path is rooted. The target is the base scheme and authority (if any), plus // the reference. - url.append(baseUrl, 0, baseIndices[PATH]).append(referenceUrl) + uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri) return removeDotSegments( - url, + uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] ) } - // The target Url is the concatenation of the base Url up to (but excluding) the last segment, + // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment, // and the reference. This can be split into 2 cases: if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH] && baseIndices[PATH] == baseIndices[QUERY] ) { // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is // needed after the authority, before appending the reference. - url.append(baseUrl, 0, baseIndices[PATH]).append('/').append(referenceUrl) + uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri) return removeDotSegments( - url, + uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1 ) @@ -409,22 +410,22 @@ object HlsPlaylistParser { // it. If base hier-part has no '/', it could only mean that it is completely empty or // contains only one segment, in which case the whole hier-part is excluded and the reference // is appended right after the base scheme colon without an added '/'. - val lastSlashIndex = baseUrl.lastIndexOf('/', baseIndices[QUERY] - 1) + val lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1) val baseLimit = if (lastSlashIndex == -1) baseIndices[PATH] else lastSlashIndex + 1 - url.append(baseUrl, 0, baseLimit).append(referenceUrl) - return removeDotSegments(url, baseIndices[PATH], baseLimit + refIndices[QUERY]) + uri.append(baseUri, 0, baseLimit).append(referenceUri) + return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]) } } /** - * Removes dot segments from the path of a URL. + * Removes dot segments from the path of a URI. * - * @param url A [StringBuilder] containing the URL. - * @param offset The index of the start of the path in `url`. - * @param limit The limit (exclusive) of the path in `url`. + * @param uri A [StringBuilder] containing the URI. + * @param offset The index of the start of the path in `uri`. + * @param limit The limit (exclusive) of the path in `uri`. */ private fun removeDotSegments( - url: StringBuilder, + uri: java.lang.StringBuilder, offset: Int, limit: Int ): String { @@ -432,9 +433,9 @@ object HlsPlaylistParser { var limit = limit if (offset >= limit) { // Nothing to do. - return url.toString() + return uri.toString() } - if (url[offset] == '/') { + if (uri[offset] == '/') { // If the path starts with a /, always retain it. offset++ } @@ -444,7 +445,7 @@ object HlsPlaylistParser { while (i <= limit) { val nextSegmentStart = if (i == limit) { i - } else if (url[i] == '/') { + } else if (uri[i] == '/') { i + 1 } else { i++ @@ -452,16 +453,16 @@ object HlsPlaylistParser { } // We've encountered the end of a segment or the end of the path. If the final segment was // "." or "..", remove the appropriate segments of the path. - if (i == segmentStart + 1 && url[segmentStart] == '.') { + if (i == segmentStart + 1 && uri[segmentStart] == '.') { // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". - url.delete(segmentStart, nextSegmentStart) + uri.delete(segmentStart, nextSegmentStart) limit -= nextSegmentStart - segmentStart i = segmentStart - } else if (i == segmentStart + 2 && url[segmentStart] == '.' && url[segmentStart + 1] == '.') { + } else if (i == segmentStart + 2 && uri[segmentStart] == '.' && uri[segmentStart + 1] == '.') { // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi". - val prevSegmentStart = url.lastIndexOf("/", segmentStart - 2) + 1 + val prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1 val removeFrom = if (prevSegmentStart > offset) prevSegmentStart else offset - url.delete(removeFrom, nextSegmentStart) + uri.delete(removeFrom, nextSegmentStart) limit -= nextSegmentStart - removeFrom segmentStart = prevSegmentStart i = prevSegmentStart @@ -470,41 +471,41 @@ object HlsPlaylistParser { segmentStart = i } } - return url.toString() + return uri.toString() } /** - * Calculates indices of the constituent components of a URL. + * Calculates indices of the constituent components of a URI. * - * @param urlString The URL as a string. + * @param uriString The URI as a string. * @return The corresponding indices. */ - private fun getUrlIndices(urlString: String?): IntArray { + private fun getUriIndices(uriString: String?): IntArray { val indices = IntArray(INDEX_COUNT) - if (urlString.isNullOrEmpty()) { + if (uriString.isNullOrEmpty()) { indices[SCHEME_COLON] = -1 return indices } // Determine outer structure from right to left. - // Url = scheme ":" hier-part [ "?" query ] [ "#" fragment ] - val length = urlString.length - var fragmentIndex = urlString.indexOf('#') + // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + val length = uriString.length + var fragmentIndex = uriString.indexOf('#') if (fragmentIndex == -1) { fragmentIndex = length } - var queryIndex = urlString.indexOf('?') + var queryIndex = uriString.indexOf('?') if (queryIndex == -1 || queryIndex > fragmentIndex) { // '#' before '?': '?' is within the fragment. queryIndex = fragmentIndex } // Slashes are allowed only in hier-part so any colon after the first slash is part of the // hier-part, not the scheme colon separator. - var schemeIndexLimit = urlString.indexOf('/') + var schemeIndexLimit = uriString.indexOf('/') if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) { schemeIndexLimit = queryIndex } - var schemeIndex = urlString.indexOf(':') + var schemeIndex = uriString.indexOf(':') if (schemeIndex > schemeIndexLimit) { // '/' before ':' schemeIndex = -1 @@ -513,10 +514,10 @@ object HlsPlaylistParser { // Determine hier-part structure: hier-part = "//" authority path / path // This block can also cope with schemeIndex == -1. val hasAuthority = - schemeIndex + 2 < queryIndex && urlString[schemeIndex + 1] == '/' && urlString[schemeIndex + 2] == '/' + schemeIndex + 2 < queryIndex && uriString[schemeIndex + 1] == '/' && uriString[schemeIndex + 2] == '/' var pathIndex: Int if (hasAuthority) { - pathIndex = urlString.indexOf('/', schemeIndex + 3) // find first '/' after "://" + pathIndex = uriString.indexOf('/', schemeIndex + 3) // find first '/' after "://" if (pathIndex == -1 || pathIndex > queryIndex) { pathIndex = queryIndex } @@ -805,7 +806,7 @@ object HlsPlaylistParser { const val APPLICATION_MEDIA3_CUES: String = "$BASE_TYPE_APPLICATION/x-media3-cues" - /** MIME type for an image URL loaded from an external image management framework. */ + /** MIME type for an image URI loaded from an external image management framework. */ const val APPLICATION_EXTERNALLY_LOADED_IMAGE: String = "$BASE_TYPE_APPLICATION/x-image-uri" @@ -1168,6 +1169,7 @@ object HlsPlaylistParser { return parseOptionalStringAttr(line, pattern, null, variableDefinitions) } + @OptIn(ExperimentalEncodingApi::class) @Throws(ParserException::class) private fun parseDrmSchemeData( line: String, keyFormat: String, variableDefinitions: Map @@ -1175,22 +1177,22 @@ object HlsPlaylistParser { val keyFormatVersions = parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions) if (KEYFORMAT_WIDEVINE_PSSH_BINARY == keyFormat) { - val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions) + val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions) return SchemeData( - uuid = C.WIDEVINE_UUID, + uuid = WIDEVINE_UUID, mimeType = MimeTypes.VIDEO_MP4, - data = base64DecodeArray(urlString.substring(urlString.indexOf(','))) + data = Base64.Default.decode(uriString.substring(uriString.indexOf(','))) ) } else if (KEYFORMAT_WIDEVINE_PSSH_JSON == keyFormat) { return SchemeData( uuid = C.WIDEVINE_UUID, mimeType = "hls", - data = line.encodeToByteArray() + data = line.toByteArray(charset = Charsets.UTF_8) ) } else if (KEYFORMAT_PLAYREADY == keyFormat && "1" == keyFormatVersions) { - val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions) + val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions) val data: ByteArray = - base64DecodeArray(urlString.substring(urlString.indexOf(','))) + Base64.Default.decode(uriString.substring(uriString.indexOf(','))) val psshData: ByteArray = PsshAtomUtil.buildPsshAtom( systemId = C.PLAYREADY_UUID, @@ -1268,7 +1270,7 @@ object HlsPlaylistParser { } data class Variant( - val url: Url, + val url: URI, val format: Format, val videoGroupId: String?, val audioGroupId: String?, @@ -1321,7 +1323,7 @@ object HlsPlaylistParser { data class Rendition( /** The rendition's url, or null if the tag does not have a URI attribute. */ - val url: Url?, + val url: URI?, /** Format information associated with this rendition. */ val format: Format, @@ -1334,14 +1336,14 @@ object HlsPlaylistParser { ) data class HlsMultivariantPlaylist( - /** The base url. Used to resolve relative paths. */ + /** The base uri. Used to resolve relative paths. */ val baseUri: String, /** The list of tags in the playlist. */ val tags: List, /** All of the media playlist URLs referenced by the playlist. */ - //val mediaPlaylistUrls: List, + //val mediaPlaylistUrls: List, /** The variants declared by the playlist. */ val variants: List, @@ -1727,8 +1729,8 @@ object HlsPlaylistParser { private fun parseMultivariantPlaylist( iterator: Iterator, baseUri: String ): HlsMultivariantPlaylist { - val urlToVariantInfos: HashMap?> = - HashMap?>() + val urlToVariantInfos: HashMap?> = + HashMap?>() val variableDefinitions = HashMap() val variants: ArrayList = ArrayList() val videos: ArrayList = ArrayList() @@ -1851,10 +1853,10 @@ object HlsPlaylistParser { parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions) val closedCaptionsGroupId: String? = parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions) - val url: Url + val uri: URI if (isIFrameOnlyVariant) { - url = - UrlUtil.resolveToUrl( + uri = + UriUtil.resolveToUri( baseUri, parseStringAttr(line, REGEX_URI, variableDefinitions) ) @@ -1863,14 +1865,14 @@ object HlsPlaylistParser { "#EXT-X-STREAM-INF must be followed by another line", /* cause= */null ) } else { - // The following line contains #EXT-X-STREAM-INF's URL. + // The following line contains #EXT-X-STREAM-INF's URI. line = replaceVariableReferences(iterator.next(), variableDefinitions) - url = UrlUtil.resolveToUrl(baseUri, line) + uri = UriUtil.resolveToUri(baseUri, line) } val variant = Variant( - url = url, + url = uri, format = Format( id = variants.size.toString(), containerMimeType = MimeTypes.APPLICATION_M3U8, @@ -1888,10 +1890,10 @@ object HlsPlaylistParser { captionGroupId = closedCaptionsGroupId ) variants.add(variant) - var variantInfosForUrl: ArrayList? = urlToVariantInfos[url] + var variantInfosForUrl: ArrayList? = urlToVariantInfos[uri] if (variantInfosForUrl == null) { variantInfosForUrl = ArrayList() - urlToVariantInfos[url] = variantInfosForUrl + urlToVariantInfos[uri] = variantInfosForUrl } variantInfosForUrl.add( VariantInfo( @@ -1909,7 +1911,7 @@ object HlsPlaylistParser { // TODO: Don't deduplicate variants by URL. val deduplicatedVariants = variants.distinctBy { it.url } /*val deduplicatedVariants: ArrayList = ArrayList() - val urlsInDeduplicatedVariants = HashSet() + val urlsInDeduplicatedVariants = HashSet() for (i in variants.indices) { val variant: Variant = variants[i] if (urlsInDeduplicatedVariants.add(variant.url)) { @@ -1943,10 +1945,10 @@ object HlsPlaylistParser { containerMimeType = MimeTypes.APPLICATION_M3U8, ) - val referenceUrl: String? = + val referenceUri: String? = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions) - val url: Url? = - if (referenceUrl == null) null else UrlUtil.resolveToUrl(baseUri, referenceUrl) + val uri: URI? = + if (referenceUri == null) null else UriUtil.resolveToUri(baseUri, referenceUri) //val metadata = // Metadata(HlsTrackMetadataEntry(groupId, name, emptyList())) when (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) { @@ -1961,11 +1963,11 @@ object HlsPlaylistParser { codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO) ) } - if (url == null) { - // TODO: Remove this case and add a Rendition with a null url to videos. + if (uri == null) { + // TODO: Remove this case and add a Rendition with a null uri to videos. } else { //formatBuilder.setMetadata(metadata) - videos.add(Rendition(url = url, format = formatBuilder, groupId, name)) + videos.add(Rendition(url = uri, format = formatBuilder, groupId, name)) } } @@ -1993,11 +1995,11 @@ object HlsPlaylistParser { } } val format = formatBuilder.copy(sampleMimeType = sampleMimeType) - if (url != null) { + if (uri != null) { //formatBuilder.setMetadata(metadata) - audios.add(Rendition(url, format, groupId, name)) + audios.add(Rendition(uri, format, groupId, name)) } else if (variant != null) { - // TODO: Remove muxedAudioFormat and add a Rendition with a null url to audios. + // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios. muxedAudioFormat = format } } @@ -2016,10 +2018,10 @@ object HlsPlaylistParser { if (sampleMimeType == null) { sampleMimeType = MimeTypes.TEXT_VTT } - if (url != null) { + if (uri != null) { subtitles.add( Rendition( - url, + uri, formatBuilder.copy(sampleMimeType = sampleMimeType), groupId, name @@ -2076,4 +2078,4 @@ object HlsPlaylistParser { sessionKeyDrmInitData = sessionKeyDrmInitData ) } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt index 8aabeb1f1..1ec442d7b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt @@ -1,9 +1,11 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.mvvm.logError +import java.util.regex.Matcher +import java.util.regex.Pattern import kotlin.math.pow -// author: https://github.com/daarkdemon +//author: https://github.com/daarkdemon class JsHunter(private val hunterJS: String) { /** @@ -12,8 +14,9 @@ class JsHunter(private val hunterJS: String) { * @return true if it's H.U.N.T.E.R coded. */ fun detect(): Boolean { - val regex = Regex("eval\\(function\\(h,u,n,t,e,r\\)") - return regex.containsMatchIn(hunterJS) + val p = Pattern.compile("eval\\(function\\(h,u,n,t,e,r\\)") + val searchResults = p.matcher(hunterJS) + return searchResults.find() } /** @@ -21,15 +24,20 @@ class JsHunter(private val hunterJS: String) { * * @return the javascript unhunt or null. */ + fun dehunt(): String? { try { - val regex = Regex("""\}\("([^"]+)",[^,]+,\s*"([^"]+)",\s*(\d+),\s*(\d+)""", RegexOption.DOT_MATCHES_ALL) - val match = regex.find(hunterJS) - if (match != null && match.groupValues.size == 5) { - val h = match.groupValues[1] - val n = match.groupValues[2] - val t = match.groupValues[3].toInt() - val e = match.groupValues[4].toInt() + val p: Pattern = + Pattern.compile( + """\}\("([^"]+)",[^,]+,\s*"([^"]+)",\s*(\d+),\s*(\d+)""", + Pattern.DOTALL + ) + val searchResults: Matcher = p.matcher(hunterJS) + if (searchResults.find() && searchResults.groupCount() == 4) { + val h = searchResults.group(1)!! + val n = searchResults.group(2)!! + val t = searchResults.group(3)!!.toInt() + val e = searchResults.group(4)!!.toInt() return hunter(h, n, t, e) } } catch (e: Exception) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 2dbbfddb0..7ed2e9be2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.mvvm.logError +import java.util.regex.Pattern import kotlin.math.pow // https://github.com/cylonu87/JsUnpacker @@ -14,7 +15,9 @@ class JsUnpacker(packedJS: String?) { */ fun detect(): Boolean { val js = packedJS!!.replace(" ", "") - return Regex("eval\\(function\\(p,a,c,k,e,[rd]").containsMatchIn(js) + val p = Pattern.compile("eval\\(function\\(p,a,c,k,e,[rd]") + val m = p.matcher(js) + return m.find() } /** @@ -25,42 +28,41 @@ class JsUnpacker(packedJS: String?) { fun unpack(): String? { val js = packedJS ?: return null try { - val match = Regex( - """\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", - RegexOption.DOT_MATCHES_ALL - ).find(js) - if (match != null && match.groupValues.size == 5) { - val payload = match.groupValues[1].replace("\\'", "'") - val radixStr = match.groupValues[2] - val countStr = match.groupValues[3] - val symtab = match.groupValues[4].split("|").toTypedArray() + var p = + Pattern.compile("""\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", Pattern.DOTALL) + var m = p.matcher(js) + if (m.find() && m.groupCount() == 4) { + val payload = m.group(1)?.replace("\\'", "'") ?: "" + val radixStr = m.group(2) + val countStr = m.group(3) + val symtab = (m.group(4)?.split("\\|".toRegex()) ?: emptyList()).toTypedArray() var radix = 36 var count = 0 try { - radix = radixStr.toIntOrNull() ?: radix + radix = radixStr?.toIntOrNull() ?: radix } catch (_: Exception) { } try { - count = countStr.toIntOrNull() ?: 0 + count = countStr?.toIntOrNull() ?: 0 } catch (_: Exception) { } if (symtab.size != count) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - val wordRegex = Regex("""\b[a-zA-Z0-9_]+\b""") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") + m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 - wordRegex.findAll(payload).forEach { wordMatch -> - val word = wordMatch.value - val x = unbase.unbase(word) - val value = if (x in symtab.indices) symtab[x] else null - if (!value.isNullOrEmpty()) { - decoded.replace( - wordMatch.range.first + replaceOffset, - wordMatch.range.last + 1 + replaceOffset, - value - ) + while (m.find()) { + val word = m.group(0) + val x = if (word == null) 0 else unbase.unbase(word) + var value: String? = null + if (x < symtab.size && x >= 0) { + value = symtab[x] + } + if (!value.isNullOrEmpty() && !word.isNullOrEmpty()) { + decoded.replace(m.start() + replaceOffset, m.end() + replaceOffset, value) replaceOffset += value.length - word.length } } @@ -78,7 +80,6 @@ class JsUnpacker(packedJS: String?) { " !\"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" private var alphabet: String? = null private var dictionary: HashMap? = null - fun unbase(str: String): Int { var ret = 0 if (alphabet == null) { @@ -95,10 +96,18 @@ class JsUnpacker(packedJS: String?) { init { if (radix > 36) { when { - radix < 62 -> alphabet = ALPHABET_62.substring(0, radix) - radix in 63..94 -> alphabet = ALPHABET_95.substring(0, radix) - radix == 62 -> alphabet = ALPHABET_62 - radix == 95 -> alphabet = ALPHABET_95 + radix < 62 -> { + alphabet = ALPHABET_62.substring(0, radix) + } + radix in 63..94 -> { + alphabet = ALPHABET_95.substring(0, radix) + } + radix == 62 -> { + alphabet = ALPHABET_62 + } + radix == 95 -> { + alphabet = ALPHABET_95 + } } dictionary = HashMap(95) for (i in 0 until alphabet!!.length) { @@ -115,20 +124,74 @@ class JsUnpacker(packedJS: String?) { this.packedJS = packedJS } + companion object { - val c = listOf( - 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x61, 0x6e, 0x64, - 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x67, 0x6d, 0x73, 0x2e, 0x61, 0x64, 0x73, 0x2e, 0x4d, - 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x41, 0x64, 0x73 - ) - val z = listOf( - 0x63, 0x6f, 0x6d, 0x2e, 0x66, 0x61, 0x63, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2e, 0x61, - 0x64, 0x73, 0x2e, 0x41, 0x64 - ) + val c = + listOf( + 0x63, + 0x6f, + 0x6d, + 0x2e, + 0x67, + 0x6f, + 0x6f, + 0x67, + 0x6c, + 0x65, + 0x2e, + 0x61, + 0x6e, + 0x64, + 0x72, + 0x6f, + 0x69, + 0x64, + 0x2e, + 0x67, + 0x6d, + 0x73, + 0x2e, + 0x61, + 0x64, + 0x73, + 0x2e, + 0x4d, + 0x6f, + 0x62, + 0x69, + 0x6c, + 0x65, + 0x41, + 0x64, + 0x73 + ) + val z = + listOf( + 0x63, + 0x6f, + 0x6d, + 0x2e, + 0x66, + 0x61, + 0x63, + 0x65, + 0x62, + 0x6f, + 0x6f, + 0x6b, + 0x2e, + 0x61, + 0x64, + 0x73, + 0x2e, + 0x41, + 0x64 + ) fun String.load(): String? { return try { var load = this + for (q in c.indices) { if (c[q % 4] > 270) { load += c[q % 3] @@ -136,6 +199,7 @@ class JsUnpacker(packedJS: String?) { load += c[q].toChar() } } + Class.forName(load.substring(load.length - c.size, load.length)).name } catch (_: Exception) { try { @@ -143,7 +207,7 @@ class JsUnpacker(packedJS: String?) { for (w in z.indices) { f += z[w].toChar() } - Class.forName(f.substring(0b001, f.length)).name + return Class.forName(f.substring(0b001, f.length)).name } catch (_: Exception) { null } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Levenshtein.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Levenshtein.kt deleted file mode 100644 index 2f3957630..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Levenshtein.kt +++ /dev/null @@ -1,515 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2026 Konstantin Tskhovrebov - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.lagradost.cloudstream3.utils - -import com.lagradost.cloudstream3.Prerelease -import kotlin.math.round - -// Taken from https://github.com/terrakok/FuzzyKot/blob/f794d43/fuzzykot/src/commonMain/kotlin/com/github/terrakok/fuzzykot/Levenshtein.kt -@Prerelease -object Levenshtein { - fun ratio(s1: String, s2: String, processor: (String) -> String = { it }): Int { - val p1 = processor(s1) - val p2 = processor(s2) - return round(100 * basicRatio(p1, p2)).toInt() - } - - fun partialRatio(s1: String, s2: String, processor: (String) -> String = { it }): Int { - val p1 = processor(s1) - val p2 = processor(s2) - - val shorter: String - val longer: String - - if (p1.length < p2.length) { - shorter = p1 - longer = p2 - } else { - shorter = p2 - longer = p1 - } - - if (shorter.isEmpty()) return if (longer.isEmpty()) 100 else 0 - - val matchingBlocks = getMatchingBlocks(shorter.length, longer.length, getEditOps(shorter, longer)) - val scores = mutableListOf() - - for (mb in matchingBlocks) { - val dist = mb.dpos - mb.spos - val longStart = if (dist > 0) dist else 0 - var longEnd = longStart + shorter.length - if (longEnd > longer.length) longEnd = longer.length - - val longSubstr = longer.substring(longStart, longEnd) - val ratio = basicRatio(shorter, longSubstr) - - if (ratio > .995) return 100 - scores.add(ratio) - } - - return round(100 * (scores.maxOrNull() ?: 0.0)).toInt() - } - - private fun basicRatio(s1: String, s2: String): Double { - val lensum = s1.length + s2.length - if (lensum == 0) return 1.0 - val editDistance = levEditDistance(s1, s2, 1) - return (lensum - editDistance) / lensum.toDouble() - } -} - -private enum class EditType { - DELETE, - EQUAL, - INSERT, - REPLACE, - KEEP -} - -private data class EditOp( - var type: EditType? = null, - var spos: Int = 0, - var dpos: Int = 0 -) { - override fun toString(): String = "${type?.name ?: "null"}($spos,$dpos)" -} - -private data class MatchingBlock( - val spos: Int = 0, - val dpos: Int = 0, - val length: Int = 0 -) { - override fun toString(): String = "($spos,$dpos,$length)" -} - -private fun getEditOps(s1: String, s2: String): Array { - var len1Copy = s1.length - var len2Copy = s2.length - - var len1o = 0 - var i = 0 - - val matrix: IntArray - - val c1 = s1 - val c2 = s2 - - var p1 = 0 - var p2 = 0 - - while (len1Copy > 0 && len2Copy > 0 && c1[p1] == c2[p2]) { - len1Copy-- - len2Copy-- - p1++ - p2++ - len1o++ - } - - val len2o = len1o - - while (len1Copy > 0 && len2Copy > 0 && c1[p1 + len1Copy - 1] == c2[p2 + len2Copy - 1]) { - len1Copy-- - len2Copy-- - } - - len1Copy++ - len2Copy++ - - matrix = IntArray(len2Copy * len1Copy) - - while (i < len2Copy) { - matrix[i] = i - i++ - } - i = 1 - while (i < len1Copy) { - matrix[len2Copy * i] = i - i++ - } - - i = 1 - while (i < len1Copy) { - var ptrPrev = (i - 1) * len2Copy - var ptrC = i * len2Copy - val ptrEnd = ptrC + len2Copy - 1 - - val char1 = c1[p1 + i - 1] - var ptrChar2 = p2 - - var x = i - ptrC++ - - while (ptrC <= ptrEnd) { - var c3 = matrix[ptrPrev++] + if (char1 != c2[ptrChar2++]) 1 else 0 - x++ - if (x > c3) x = c3 - c3 = matrix[ptrPrev] + 1 - if (x > c3) x = c3 - matrix[ptrC++] = x - } - i++ - } - - return editOpsFromCostMatrix(len1Copy, c1, p1, len1o, len2Copy, c2, p2, len2o, matrix) -} - -private fun editOpsFromCostMatrix( - len1: Int, c1: String, p1: Int, o1: Int, - len2: Int, c2: String, p2: Int, o2: Int, - matrix: IntArray -): Array { - var i: Int = len1 - 1 - var j: Int = len2 - 1 - var pos: Int = matrix[len1 * len2 - 1] - var ptr: Int = len1 * len2 - 1 - val ops: Array = arrayOfNulls(pos) - var dir = 0 - - while (i > 0 || j > 0) { - if (dir < 0 && j != 0 && matrix[ptr] == matrix[ptr - 1] + 1) { - val eop = EditOp() - pos-- - ops[pos] = eop - eop.type = EditType.INSERT - eop.spos = i + o1 - eop.dpos = --j + o2 - ptr-- - continue - } - - if (dir > 0 && i != 0 && matrix[ptr] == matrix[ptr - len2] + 1) { - val eop = EditOp() - pos-- - ops[pos] = eop - eop.type = EditType.DELETE - eop.spos = --i + o1 - eop.dpos = j + o2 - ptr -= len2 - continue - } - - if (i != 0 && j != 0 && matrix[ptr] == matrix[ptr - len2 - 1] && c1[p1 + i - 1] == c2[p2 + j - 1]) { - i-- - j-- - ptr -= len2 + 1 - dir = 0 - continue - } - - if (i != 0 && j != 0 && matrix[ptr] == matrix[ptr - len2 - 1] + 1) { - pos-- - val eop = EditOp() - ops[pos] = eop - eop.type = EditType.REPLACE - eop.spos = --i + o1 - eop.dpos = --j + o2 - ptr -= len2 + 1 - dir = 0 - continue - } - - if (dir == 0 && j != 0 && matrix[ptr] == matrix[ptr - 1] + 1) { - pos-- - val eop = EditOp() - ops[pos] = eop - eop.type = EditType.INSERT - eop.spos = i + o1 - eop.dpos = --j + o2 - ptr-- - dir = -1 - continue - } - - if (dir == 0 && i != 0 && matrix[ptr] == matrix[ptr - len2] + 1) { - pos-- - val eop = EditOp() - ops[pos] = eop - eop.type = EditType.DELETE - eop.spos = --i + o1 - eop.dpos = j + o2 - ptr -= len2 - dir = 1 - continue - } - } - - return ops.requireNoNulls() -} - -private fun getMatchingBlocks(len1: Int, len2: Int, ops: Array): Array { - val n = ops.size - var numberOfMatchingBlocks = 0 - var i: Int - var spos: Int - var dpos: Int - var o = 0 - - dpos = 0 - spos = dpos - - i = n - while (i != 0) { - while (ops[o].type === EditType.KEEP && --i != 0) { - o++ - } - if (i == 0) break - if (spos < ops[o].spos || dpos < ops[o].dpos) { - numberOfMatchingBlocks++ - spos = ops[o].spos - dpos = ops[o].dpos - } - val type = ops[o].type!! - when (type) { - EditType.REPLACE -> do { - spos++ - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.DELETE -> do { - spos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.INSERT -> do { - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - else -> {} - } - } - - if (spos < len1 || dpos < len2) numberOfMatchingBlocks++ - - val matchingBlocks = arrayOfNulls(numberOfMatchingBlocks + 1) - o = 0 - dpos = 0 - spos = dpos - var mbIndex = 0 - - i = n - while (i != 0) { - while (ops[o].type === EditType.KEEP && --i != 0) o++ - if (i == 0) break - if (spos < ops[o].spos || dpos < ops[o].dpos) { - val mb = MatchingBlock( - spos = spos, - dpos = dpos, - length = ops[o].spos - spos - ) - spos = ops[o].spos - dpos = ops[o].dpos - matchingBlocks[mbIndex++] = mb - } - val type = ops[o].type!! - when (type) { - EditType.REPLACE -> do { - spos++ - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.DELETE -> do { - spos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.INSERT -> do { - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - else -> {} - } - } - - if (spos < len1 || dpos < len2) { - val mb = MatchingBlock( - spos = spos, - dpos = dpos, - length = len1 - spos - ) - matchingBlocks[mbIndex++] = mb - } - - val finalBlock = MatchingBlock( - spos = len1, - dpos = len2, - length = 0 - ) - matchingBlocks[mbIndex] = finalBlock - - return matchingBlocks.filterNotNull().toTypedArray() -} - -private fun levEditDistance(s1: String, s2: String, xcost: Int): Int { - var i: Int - val half: Int - - var c1 = s1 - var c2 = s2 - - var str1 = 0 - var str2 = 0 - - var len1 = s1.length - var len2 = s2.length - - while (len1 > 0 && len2 > 0 && c1[str1] == c2[str2]) { - len1-- - len2-- - str1++ - str2++ - } - - while (len1 > 0 && len2 > 0 && c1[str1 + len1 - 1] == c2[str2 + len2 - 1]) { - len1-- - len2-- - } - - if (len1 == 0) return len2 - if (len2 == 0) return len1 - - if (len1 > len2) { - val nx = len1 - val temp = str1 - len1 = len2 - len2 = nx - str1 = str2 - str2 = temp - val t = c2 - c2 = c1 - c1 = t - } - - if (len1 == 1) { - return if (xcost != 0) { - len2 + 1 - 2 * memchr(c2, str2, c1[str1], len2) - } else { - len2 - memchr(c2, str2, c1[str1], len2) - } - } - - len1++ - len2++ - half = len1 shr 1 - - val row = IntArray(len2) - var end = len2 - 1 - - i = 0 - while (i < len2 - if (xcost != 0) 0 else half) { - row[i] = i - i++ - } - - if (xcost != 0) { - i = 1 - while (i < len1) { - var p = 1 - val ch1 = c1[str1 + i - 1] - var c2p = str2 - var D = i - var x = i - while (p <= end) { - if (ch1 == c2[c2p++]) { - x = --D - } else { - x++ - } - D = row[p] - D++ - if (x > D) x = D - row[p++] = x - } - i++ - } - } else { - row[0] = len1 - half - 1 - i = 1 - while (i < len1) { - var p: Int - val ch1 = c1[str1 + i - 1] - var c2p: Int - var D: Int - var x: Int - - if (i >= len1 - half) { - val offset = i - (len1 - half) - c2p = str2 + offset - p = offset - val c3 = row[p++] + if (ch1 != c2[c2p++]) 1 else 0 - x = row[p] - x++ - D = x - if (x > c3) x = c3 - row[p++] = x - } else { - p = 1 - c2p = str2 - x = i - D = x - } - if (i <= half + 1) end = len2 + i - half - 2 - while (p <= end) { - val c3 = --D + if (ch1 != c2[c2p++]) 1 else 0 - x++ - if (x > c3) x = c3 - D = row[p] - D++ - if (x > D) x = D - row[p++] = x - } - if (i <= half) { - val c3 = --D + if (ch1 != c2[c2p]) 1 else 0 - x++ - if (x > c3) x = c3 - row[p] = x - } - i++ - } - } - - return row[end] -} - -private fun memchr(haystack: String, offset: Int, needle: Char, num: Int): Int { - var numCopy = num - if (numCopy != 0) { - var p = 0 - do { - if (haystack[offset + p] == needle) return 1 - p++ - } while (--numCopy != 0) - } - return 0 -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 00d448d94..23226418b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -112,8 +112,8 @@ object M3u8Helper2 { return c.doFinal(data) } - private fun getParentLink(url: String): String { - val split = url.split("/").toMutableList() + private fun getParentLink(uri: String): String { + val split = uri.split("/").toMutableList() split.removeAt(split.lastIndex) return split.joinToString("/") } @@ -322,15 +322,15 @@ object M3u8Helper2 { if (!match.isNullOrEmpty()) { encryptionState = true - var encryptionUrl = match[2] + var encryptionUri = match[2] - if (isNotCompleteUrl(encryptionUrl)) { - encryptionUrl = "${getParentLink(playlistStream.streamUrl)}/$encryptionUrl" + if (isNotCompleteUrl(encryptionUri)) { + encryptionUri = "${getParentLink(playlistStream.streamUrl)}/$encryptionUri" } - encryptionIv = match[3].encodeToByteArray() + encryptionIv = match[3].toByteArray() val encryptionKeyResponse = - app.get(encryptionUrl, headers = playlistStream.headers, verify = false) + app.get(encryptionUri, headers = playlistStream.headers, verify = false) val body = encryptionKeyResponse.body encryptionData = body.bytes() body.close() diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt index b9233490a..1e3a2ffb7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt @@ -1,32 +1,14 @@ package com.lagradost.cloudstream3.utils -import com.lagradost.cloudstream3.Prerelease -import io.ktor.http.decodeURLQueryComponent -import io.ktor.http.encodeURLParameter +import java.net.URLDecoder +import java.net.URLEncoder object StringUtils { - @Prerelease - fun String.decodeUrl(): String { - return this.decodeURLQueryComponent() + fun String.encodeUri(): String { + return URLEncoder.encode(this, "UTF-8") } - @Prerelease - fun String.encodeUrl(): String { - return this.encodeURLParameter() + fun String.decodeUri(): String { + return URLDecoder.decode(this, "UTF-8") } - - // Deprecate after next stable - - /* @Deprecated( - message = "Use Ktor 'Url' naming convention instead.", - replaceWith = ReplaceWith("this.encodeUrl()") - ) */ - fun String.encodeUri(): String = encodeUrl() - - /* @Deprecated( - message = "Use Ktor 'Url' naming convention instead.", - replaceWith = ReplaceWith("this.decodeUrl()") - ) */ - fun String.decodeUri(): String = decodeUrl() -} - +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt index a6362efd0..8d5479cc0 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.utils +import me.xdrop.fuzzywuzzy.FuzzySearch import java.util.Locale // If you find a way to use SettingsGeneral getCurrentLocale() @@ -111,8 +112,8 @@ object SubtitleHelper { for (lang in languages) { val score = maxOf( - Levenshtein.ratio(lowLangName, lang.languageName.lowercase()), - Levenshtein.ratio( + FuzzySearch.ratio(lowLangName, lang.languageName.lowercase()), + FuzzySearch.ratio( lowLangName, lang.nativeName.lowercase() ) ) @@ -314,23 +315,10 @@ object SubtitleHelper { val flagOffset = 0x1F1E6 // regional indicator "[A]" val offset = flagOffset - asciiOffset - /** - * Unicode surrogate pairs encode code points above U+FFFF (outside the Basic Multilingual Plane). - * The code point is offset by 0x10000, then split into two 10-bit halves: - * high surrogate: upper 10 bits, biased into the range 0xD800-0xDBFF - * low surrogate: lower 10 bits (masked with 0x3FF), biased into the range 0xDC00-0xDFFF - */ - fun toSurrogatePair(codePoint: Int): String { - val high = ((codePoint - 0x10000) shr 10) + 0xD800 - val low = ((codePoint - 0x10000) and 0x3FF) + 0xDC00 - return "${high.toChar()}${low.toChar()}" - } + val firstChar: Int = Character.codePointAt(countryLetters, 0) + offset + val secondChar: Int = Character.codePointAt(countryLetters, 1) + offset - val upperLetters = countryLetters.uppercase() - val first = upperLetters[0].code + offset - val second = upperLetters[1].code + offset - - return toSurrogatePair(first) + toSurrogatePair(second) + return String(Character.toChars(firstChar)) + String(Character.toChars(secondChar)) } // when (langTag = country) or (langTag contains country) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt index 8bdbf3788..206b0f29f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt @@ -2,9 +2,9 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Decode -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl import com.lagradost.nicehttp.NiceResponse -import io.ktor.http.Url +import java.net.URI +import java.net.URLDecoder // Code heavily based on unshortenit.py form kodiondemand /addon @@ -48,8 +48,8 @@ object ShortLink { } } - suspend fun unshorten(url: String, type: String? = null): String { - var currentUrl = url + suspend fun unshorten(uri: String, type: String? = null): String { + var currentUrl = uri val visitedUrls = mutableSetOf() var count = 10 @@ -57,7 +57,9 @@ object ShortLink { visitedUrls += currentUrl count -= 1 - val domain = Url(currentUrl.trim()).host + val domain = + URI(currentUrl.trim()).host + ?: throw IllegalArgumentException("No domain found in URI!") currentUrl = shortList.firstOrNull { it.regex.find(domain) != null || type == it.type }?.function?.let { it(currentUrl) } ?: break @@ -65,8 +67,8 @@ object ShortLink { return currentUrl.trim() } - suspend fun unshortenAdfly(url: String): String { - val html = app.get(url).text + suspend fun unshortenAdfly(uri: String): String { + val html = app.get(uri).text val ysmm = Regex("""var ysmm =.*;?""").find(html)!!.value if (ysmm.isNotEmpty()) { @@ -79,46 +81,46 @@ object ShortLink { left += c[0] right = c[1] + right } - val encodedUrl = (left + right).toMutableList() + val encodedUri = (left + right).toMutableList() val numbers = - encodedUrl.mapIndexed { i, n -> Pair(i, n) }.filter { it.second.isDigit() } + encodedUri.mapIndexed { i, n -> Pair(i, n) }.filter { it.second.isDigit() } for (el in numbers.chunked(2).dropLastWhile { it.size == 1 }) { val xor = (el[0].second).code.xor(el[1].second.code) if (xor < 10) { - encodedUrl[el[0].first] = xor.digitToChar() + encodedUri[el[0].first] = xor.digitToChar() } } - val encodedbytearray = encodedUrl.map { it.code.toByte() }.toByteArray() - var decodedUrl = + val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray() + var decodedUri = base64Decode(encodedbytearray.toString()).dropLast(16) .drop(16) - if (Regex("""go\.php\?u=""").find(decodedUrl) != null) { - decodedUrl = - base64Decode(decodedUrl.replace(Regex("""(.*?)u="""), "")) + if (Regex("""go\.php\?u=""").find(decodedUri) != null) { + decodedUri = + base64Decode(decodedUri.replace(Regex("""(.*?)u="""), "")) } - return decodedUrl + return decodedUri } else { - return url + return uri } } - suspend fun unshortenLinkup(url: String): String { + suspend fun unshortenLinkup(uri: String): String { var r: NiceResponse? = null - var url = url + var uri = uri when { - url.contains("/tv/") -> url = url.replace("/tv/", "/tva/") - url.contains("delta") -> url = url.replace("/delta/", "/adelta/") - (url.contains("/ga/") || url.contains("/ga2/")) -> url = - base64Decode(url.split('/').last()).trim() + uri.contains("/tv/") -> uri = uri.replace("/tv/", "/tva/") + uri.contains("delta") -> uri = uri.replace("/delta/", "/adelta/") + (uri.contains("/ga/") || uri.contains("/ga2/")) -> uri = + base64Decode(uri.split('/').last()).trim() - url.contains("/speedx/") -> url = - url.replace("http://linkup.pro/speedx", "http://speedvideo.net") + uri.contains("/speedx/") -> uri = + uri.replace("http://linkup.pro/speedx", "http://speedvideo.net") else -> { - r = app.get(url, allowRedirects = true) - url = r.url + r = app.get(uri, allowRedirects = true) + uri = r.url val link = Regex("]*src=\\'([^'>]*)\\'[^<>]*>").find(r.text)?.value ?: Regex("""action="(?:[^/]+.*?/[^/]+/([a-zA-Z0-9_]+))">""").find(r.text)?.value @@ -126,40 +128,40 @@ object ShortLink { .elementAtOrNull(1)?.groupValues?.get(1) if (link != null) { - url = link + uri = link } } } - val short = Regex("""^https?://.*?(https?://.*)""").find(url)?.value + val short = Regex("""^https?://.*?(https?://.*)""").find(uri)?.value if (short != null) { - url = short + uri = short } if (r == null) { r = app.get( - url, + uri, allowRedirects = false ) if (r.headers["location"] != null) { - url = r.headers["location"].toString() + uri = r.headers["location"].toString() } } - if (url.contains("snip.")) { - if (url.contains("out_generator")) { - url = Regex("url=(.*)\$").find(url)!!.value - } else if (url.contains("/decode/")) { - url = app.get(url, allowRedirects = true).url + if (uri.contains("snip.")) { + if (uri.contains("out_generator")) { + uri = Regex("url=(.*)\$").find(uri)!!.value + } else if (uri.contains("/decode/")) { + uri = app.get(uri, allowRedirects = true).url } } - return url + return uri } - fun unshortenLinksafe(url: String): String { - return base64Decode(url.split("?url=").last()) + fun unshortenLinksafe(uri: String): String { + return base64Decode(uri.split("?url=").last()) } - suspend fun unshortenNuovoIndirizzo(url: String): String { - val soup = app.get(url, allowRedirects = true) + suspend fun unshortenNuovoIndirizzo(uri: String): String { + val soup = app.get(uri, allowRedirects = true) val header = soup.headers["refresh"] val link: String = if (header != null) { soup.headers["refresh"]!!.substringAfter("=") @@ -169,29 +171,29 @@ object ShortLink { return link } - suspend fun unshortenNuovoLink(url: String): String { - return app.get(url, allowRedirects = true).document.selectFirst("a")!!.attr("href") + suspend fun unshortenNuovoLink(uri: String): String { + return app.get(uri, allowRedirects = true).document.selectFirst("a")!!.attr("href") } - suspend fun unshortenUprot(url: String): String { - val page = app.get(url).text + suspend fun unshortenUprot(uri: String): String { + val page = app.get(uri).text Regex("""]+href="([^"]+)".*Continue""").findAll(page) .map { it.value.replace(""" - if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != url) { + if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != uri) { return link } } - return url + return uri } - fun unshortenDavisonbarker(url: String): String { - return url.substringAfter("dest=").decodeUrl() + fun unshortenDavisonbarker(uri: String): String { + return URLDecoder.decode(uri.substringAfter("dest="), "UTF-8") } - suspend fun unshortenIsecure(url: String): String { - val doc = app.get(url).document - return doc.selectFirst("iframe")?.attr("src")?.trim() ?: url + suspend fun unshortenIsecure(uri: String): String { + val doc = app.get(uri).document + return doc.selectFirst("iframe")?.attr("src")?.trim() ?: uri } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/NonEmptySerializer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/NonEmptySerializer.kt deleted file mode 100644 index 82de9f7f7..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/NonEmptySerializer.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import com.lagradost.cloudstream3.Prerelease -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.JsonTransformingSerializer - -/** - * Replicates Jackson's @JsonInclude(JsonInclude.Include.NON_EMPTY) behaviour. - * Strips null, empty strings, empty arrays, and empty objects from the serialized - * output. Requires the enclosing Json instance to have encodeDefaults = true, - * which is already in our default global Json instance. - * - * Usage: - * - * @OptIn(ExperimentalSerializationApi::class) - * @KeepGeneratedSerializer - * @Serializable(with = MyData.Serializer::class) - * data class MyData( - * val tags: List = emptyList(), - * val title: String = "", - * val meta: Map = emptyMap(), - * ) { - * object Serializer : NonEmptySerializer(MyData.generatedSerializer()) - * } - */ -@Prerelease -abstract class NonEmptySerializer(tSerializer: KSerializer) : - JsonTransformingSerializer(tSerializer) { - - override fun transformSerialize(element: JsonElement): JsonElement { - if (element !is JsonObject) return element - - return JsonObject(element.filterValues { value -> - when (value) { - is JsonPrimitive -> value.content.isNotEmpty() - is JsonArray -> value.isNotEmpty() - is JsonObject -> value.isNotEmpty() - JsonNull -> false - } - }) - } -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/WriteOnlySerializer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/WriteOnlySerializer.kt deleted file mode 100644 index c7f412eaa..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/WriteOnlySerializer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import com.lagradost.cloudstream3.Prerelease -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonTransformingSerializer - -/** - * Replicates Jackson's @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) behaviour. - * Properties in [keysToIgnore] are deserialized normally but omitted from serialized output. - * - * Usage: - * - * @OptIn(ExperimentalSerializationApi::class) - * @KeepGeneratedSerializer - * @Serializable(with = MyData.Serializer::class) - * data class MyData( - * val fieldA: String = "", - * val fieldB: String = "", - * ) { - * object Serializer : WriteOnlySerializer( - * MyData.generatedSerializer(), - * setOf("fieldB"), - * ) - * } - */ -@Prerelease -abstract class WriteOnlySerializer( - tSerializer: KSerializer, - private val keysToIgnore: Set, -) : JsonTransformingSerializer(tSerializer) { - - override fun transformSerialize(element: JsonElement): JsonElement { - if (element !is JsonObject) return element - return JsonObject(element.filterKeys { it !in keysToIgnore }) - } -} diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt deleted file mode 100644 index 4dc56978e..000000000 --- a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt +++ /dev/null @@ -1,196 +0,0 @@ -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")) - } -} diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.jvm.kt deleted file mode 100644 index 345aa7185..000000000 --- a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.jvm.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.newAudioFile -import com.lagradost.cloudstream3.newSubtitleFile -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.newExtractorLink -import org.schabi.newpipe.extractor.stream.StreamInfo -import org.schabi.newpipe.extractor.stream.StreamType - -actual open class YoutubeExtractor actual constructor() : ExtractorApi() { - - actual override val mainUrl = "https://www.youtube.com" - actual override val name = "YouTube" - actual override val requiresReferer = false - - actual override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ) { - val videoId = extractYouTubeId(url) - val watchUrl = "$mainUrl/watch?v=$videoId" - - val info = StreamInfo.getInfo(watchUrl) - val isLive = info.streamType == StreamType.LIVE_STREAM - || info.streamType == StreamType.AUDIO_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_AUDIO_STREAM - - if (isLive && info.hlsUrl != null) { - callback( - newExtractorLink( - source = name, - name = "YouTube Live", - url = info.hlsUrl - ) { - type = ExtractorLinkType.M3U8 - } - ) - } else { - processVideo(info, subtitleCallback, callback) - } - } - - private suspend fun processVideo( - info: StreamInfo, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ): Boolean { - val videoStreams = info.videoOnlyStreams.orEmpty() - if (videoStreams.isEmpty()) return false - - val audioStreams = info.audioStreams.orEmpty() - videoStreams.forEach { video -> - callback( - newExtractorLink( - source = name, - name = "YouTube ${normalizeCodec(video.codec)}", - url = video.content - ) { - quality = video.height - audioTracks = audioStreams.map { newAudioFile(it.content) } - } - ) - } - - info.subtitles.forEach { subtitle -> - subtitleCallback( - newSubtitleFile( - lang = subtitle.displayLanguageName - ?: subtitle.languageTag - ?: "Unknown", - url = subtitle.content - ) - ) - } - - return true - } - - private fun extractYouTubeId(url: String): String { - val regex = Regex( - "(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})" - ) - - return regex.find(url)?.groupValues?.get(1) - ?: throw IllegalArgumentException("Invalid YouTube URL: $url") - } - - private fun normalizeCodec(codec: String?): String { - if (codec.isNullOrBlank()) return "" - val c = codec.lowercase() - return when { - c.startsWith("av01") -> "AV1" - c.startsWith("vp9") -> "VP9" - c.startsWith("avc1") || c.startsWith("h264") -> "H264" - c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265" - else -> codec.substringBefore('.').uppercase() - } - } -}