Compare commits

..

1 commit

Author SHA1 Message Date
fire-light43
274943c1a6
shared buffer to decrease alloc 2026-05-12 16:25:14 +00:00
120 changed files with 1117 additions and 2729 deletions

View file

@ -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

98
.github/workflows/issue_action.yml vendored Normal file
View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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<KotlinJvmCompile> {
optIn.addAll(
"com.lagradost.cloudstream3.InternalAPI",
"com.lagradost.cloudstream3.Prerelease",
"kotlin.uuid.ExperimentalUuidApi",
)
}
}

View file

@ -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<KClass<*>> {
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<Any>
return kotlinxMapper.encodeToString(serializer, value)
}
}

View file

@ -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<String> = emptyList(),
val meta: Map<String, String> = emptyMap(),
val name: String = "hello",
) {
object Serializer : NonEmptySerializer<NonEmptyData>(NonEmptyData.generatedSerializer())
}
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = WriteOnlyData.Serializer::class)
data class WriteOnlyData(
val fieldA: String = "",
val fieldB: String = "",
) {
object Serializer : WriteOnlySerializer<WriteOnlyData>(
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>(
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<NonEmptyData>(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<WriteOnlyData>(input)
assertEquals("hello", result.fieldA)
assertEquals("secret", result.fieldB)
}
@Test
fun writeOnlySerializerDeserializesMissingAsDefault() {
val input = """{"fieldA":"hello"}"""
val result = parseJson<WriteOnlyData>(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<UriData>(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<UriData>(encoded)
assertEquals(data.uri, decoded.uri)
}
}

View file

@ -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())

View file

@ -408,14 +408,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
return true
}
val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull()
if (matchedApi != null) {
loadResult(str, matchedApi.name, "")
synchronized(apis) {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name, "")
return true
}
}
}
}
}
}
return false
}
@ -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<Array<SettingsGeneral.CustomSite>>(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 += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix(

View file

@ -20,10 +20,8 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage
import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage
import com.lagradost.cloudstream3.actions.temp.OnlyPlayer
import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
@ -34,8 +32,8 @@ import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.UiText
import kotlinx.coroutines.Dispatchers
@ -45,7 +43,7 @@ import java.util.concurrent.FutureTask
import kotlin.reflect.jvm.jvmName
object VideoClickActionHolder {
val allVideoClickActions = atomicListOf(
val allVideoClickActions = threadSafeListOf(
// Default
PlayInBrowserAction(),
CopyClipboardAction(),
@ -66,8 +64,6 @@ object VideoClickActionHolder {
MpvYTDLPackage(),
MpvKtPackage(),
MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option
AlwaysAskAction(),
// added by plugins

View file

@ -1,75 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.lagradost.api.Log
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/Riteshp2001/mpvRx
*
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L132
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L56
* */
class MpvRxPackage : OpenInAppAction(
appName = txt("mpvRx"),
packageName = "app.gyrolet.mpvrx",
intentClass = "app.gyrolet.mpvrx.ui.player.PlayerActivity"
) {
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.apply {
putExtra("title", video.name)
val link = result.links[index!!]
val headers = link.headers
setData(link.url.toUri())
if (headers.isNotEmpty()) {
// PlayerActivity expects a flat array: [key1, value1, key2, value2, ...]
val flat = headers.entries.flatMap { listOf(it.key, it.value) }.toTypedArray()
intent.putExtra("headers", flat)
}
/*val subs = result.subs // disabled due to https://github.com/Riteshp2001/mpvRx/issues/146
intent.putExtra("subs", subs.map { it.url.toUri() }.toTypedArray())
intent.putExtra(
"subs.titles",
subs.map { it.name }.toTypedArray(),
)
intent.putExtra(
"subs.langs",
subs.map { it.languageCode }.toTypedArray(),
)
val selected = subs.firstOrNull { it.matchesLanguageCode("en") }?.url?.toUri()
intent.putExtra("subs.enable", selected?.let { arrayOf(it) } ?: arrayOf<Uri>() )*/
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())
}
}

View file

@ -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 */
}
}

View file

@ -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,8 +26,10 @@ abstract class Plugin : BasePlugin() {
fun registerVideoClickAction(element: VideoClickAction) {
Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction")
element.sourcePlugin = this.filename
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.add(element)
}
}
/**
* This will contain your resources if you specified requiresResources in gradle

View file

@ -610,7 +610,7 @@ object PluginManager {
return false
}
InputStreamReader(stream).use { reader ->
manifest = parseJson<BasePlugin.Manifest>(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
}
if (pluginInstance is Plugin) {
pluginInstance.load(context)
} else {
@ -695,20 +689,21 @@ object PluginManager {
}
// remove all registered apis
synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it)
}
APIHolder.allProviders.withLock {
APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename }
}
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
}
extractorApis.withLock {
extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename }
synchronized(extractorApis) {
extractorApis.removeIf { provider: ExtractorApi -> 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) {

View file

@ -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

View file

@ -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<SavedSearchResponse>()
private val searchCache = threadSafeListOf<SavedSearchResponse>()
private var searchCacheIndex: Int = 0
private val resourceCache = atomicListOf<SavedResourceResponse>()
private val resourceCache = threadSafeListOf<SavedResourceResponse>()
private var resourceCacheIndex: Int = 0
const val CACHE_SIZE = 20
}
@WorkerThread
suspend fun resource(data: SubtitleEntity): Result<SubtitleResource> = 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<List<SubtitleEntity>> {
return runCatching {
val cached = searchCache.withLock {
var found: List<SubtitleEntity>? = 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) {
}
}
}

View file

@ -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()
)
}

View file

@ -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<SyncAPI.SyncSearchResult>? {
val data = searchShows(query) ?: return null
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
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 {

View file

@ -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<KitsuNode> {
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) {

View file

@ -98,9 +98,9 @@ class MALApi : SyncAPI() {
)
}
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
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<Data>? {
private suspend fun getMalAnimeListSmart(auth : AuthData): Array<Data>? {
return if (requireLibraryRefresh) {
val list = getMalAnimeList(auth.token)
setKey(MAL_CACHED_LIST, auth.user.id.toString(), list)

View file

@ -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 <reified T : Any> 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<String>(SIMKL_CACHE_KEY, path)?.let {
tryParseJson<SimklCacheWrapper<T>>(it)
mapper.readValue<SimklCacheWrapper<T>>(it, type)
}
return if (cache?.isFresh() == true) {
@ -911,7 +916,7 @@ class SimklApi : SyncAPI() {
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
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<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
}

View file

@ -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<String, String>
)
private val cache = atomicListOf<SavedLoadResponse>()
private val cache = threadSafeListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0
const val CACHE_SIZE = 20
@ -66,9 +66,11 @@ class APIRepository(val api: MainAPI) {
private fun afterPluginsLoaded(forceReload: Boolean) {
if (forceReload) {
synchronized(cache) {
cache.clear()
}
}
}
init {
afterPluginsLoadedEvent += ::afterPluginsLoaded
@ -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

View file

@ -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 {

View file

@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() {
private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository {
return APIRepository(apis.withLock { apis.first { it.hasMainPage } })
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } })
}
private val _availableWatchStatusTypes =

View file

@ -210,13 +210,14 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(
syncId: SyncIdName,
apiName: String? = null,
) {
val availableProviders = allProviders.filter {
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,

View file

@ -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,

View file

@ -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() {

View file

@ -945,18 +945,12 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
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

View file

@ -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

View file

@ -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
)
)
}
}

View file

@ -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<String>,
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<SearchResponse>()
val apiNames = apis.filter {
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))

View file

@ -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(

View file

@ -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,

View file

@ -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))) +
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() }
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
}
val currentIndexList = currentLangTags.map { langTag ->

View file

@ -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()
)

View file

@ -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<Pair<MainAPI, TestingUtils.TestResultProvider>>()
private val providers = threadSafeListOf<Pair<MainAPI, TestingUtils.TestResultProvider>>()
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

View file

@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
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 {

View file

@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment<FragmentSetupProviderLanguage
val currentLangTags = ctx.getApiProviderLangSettings()
val languagesTagName = APIHolder.apis.withLock {
listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) +
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
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
}
val currentIndexList = currentLangTags.map { langTag ->

View file

@ -369,10 +369,28 @@ object AppContextUtils {
}
fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>()
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<String>()
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 {

View file

@ -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<BackupFile>(text)
val restoredValue =
mapper.readValue<BackupFile>(input)
restore(
activity,

View file

@ -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 <T> Context.setKey(path: String, value: T) {
try {
getSharedPrefs().edit {
putString(path, value?.toJsonLiteral())
putString(path, mapper.writeValueAsString(value))
}
} catch (e: Exception) {
logError(e)
}
}
fun <T : Any> Context.getKey(path: String, valueType: Class<T>): T? {
fun <T> Context.getKey(path: String, valueType: Class<T>): 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 <reified T : Any> String.toKotlinObject(): T {
return parseJson(this)
return mapper.readValue(this, T::class.java)
}
fun <T : Any> String.toKotlinObject(valueType: Class<T>): T {
return parseJson(this, valueType.kotlin)
fun <T> String.toKotlinObject(valueType: Class<T>): T {
return mapper.readValue(this, valueType)
}
// GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR

View file

@ -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<String, String>? = 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<String, String>? = null,
builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder)
fun ImageView.loadImage(
imageData: File?,
builder: ImageRequest.Builder.() -> Unit = {}

View file

@ -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<Array<GithubRelease>>(
val response = parseJson<List<GithubRelease>>(
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<Array<GithubRelease>>(
val response = parseJson<List<GithubRelease>>(
app.get(releaseUrl, headers = headers).text
).toList()
)
val found = response.lastOrNull { rel ->
rel.prerelease || rel.tagName == "pre-release"

View file

@ -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<MalSyncPage?>(response)
val mapped = parseJson<MalSyncPage?>(response)
val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId
val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id
@ -96,10 +96,12 @@ object SyncUtil {
.mapNotNull { it.url }.toMutableList()
if (type == "anilist") { // TODO MAKE BETTER
synchronized(apis) {
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id")
}
}
}
return current
}

View file

@ -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)
@ -49,10 +49,6 @@ object TestingUtils {
}
}
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<SearchResponse>) : 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

View file

@ -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<Uri> {
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())
}
}

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -753,7 +753,4 @@
<item quantity="other">%d تنزيل قيد الانتظار</item>
</plurals>
<string name="show_player_metadata_overlay">عرض واجهة منبثقة للبيانات الوصفية للمشغِّل</string>
<string name="video_singular">مقطع</string>
<string name="skip_type_preview">استعراض</string>
<string name="player_is_live">البث قائم</string>
</resources>

View file

@ -252,7 +252,7 @@
<string name="update">Update</string>
<string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string>
<string name="limit_title">Videoplayertitel max. Zeichen</string>
<string name="limit_title_rez">Zeige Playerinformationen</string>
<string name="limit_title_rez">Playerinformationen anzeigen</string>
<string name="video_buffer_size_settings">Videopuffergröße</string>
<string name="video_buffer_length_settings">Videopufferlänge</string>
<string name="video_buffer_disk_settings">Video-Cache in Speicher</string>
@ -587,7 +587,7 @@
<string name="pref_category_security">Sicherheit</string>
<string name="pref_category_accounts">Konten</string>
<string name="open_downloaded_repo">Repository öffnen</string>
<string name="device_pin_url_message">Besuche <b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
<string name="device_pin_url_message">Besuche<b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
<string name="device_pin_error_message">PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung</string>
<string name="downloads_empty">Zur Zeit sind keine Downloads verfügbar.</string>
<string name="open_local_video">Lokales Video öffnen</string>
@ -712,8 +712,8 @@
<string name="extra_brightness_settings">Zusätzliche Helligkeit</string>
<string name="extra_brightness_settings_des">Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist</string>
<string name="extra_brightness_key">Erhöhte Helligkeit aktiviert</string>
<string name="show_cast_in_details">Zeige Cast-Panel</string>
<string name="video_info">Mediainfo</string>
<string name="show_cast_in_details">Cast-Panel zeigen</string>
<string name="video_info">Medieninfo</string>
<string name="source_name">Quellname</string>
<string name="download_all">Alle herunterladen</string>
<string name="download_episode_range">Möchtest du Episode %s herunter laden?</string>
@ -731,8 +731,4 @@
<string name="queue_empty_message">Es befinden sich keine Downloads in der Warteschlange.</string>
<string name="source_priority">Quellpriorität</string>
<string name="source_priority_help">Entscheide, wie Videoquellen im Player sortiert werden sollen</string>
<string name="show_player_metadata_overlay">Zeige Player-Metadaten</string>
<string name="video_singular">Video</string>
<string name="skip_type_preview">Vorschau</string>
<string name="player_is_live">Live</string>
</resources>

View file

@ -244,7 +244,7 @@
<string name="quality_tc">TC</string>
<string name="subscription_new">Претплатен на %s</string>
<string name="pref_category_subtitles">Преводи</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="backup_failed">Недостасуваат дозволи за складирање. Обиди се повторно.</string>
<string name="sort_save">Зачувај</string>
<string name="player_load_subtitles">Вчитај од датотека</string>
@ -445,7 +445,7 @@
<string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string>
<string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string>
<string name="apk_installer_settings_des">Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат.</string>
<string name="limit_title_rez">Прикажи информации за плеерот</string>
<string name="limit_title_rez">Резолуција на видео плеер</string>
<string name="video_buffer_size_settings">Големина на видео баферот</string>
<string name="pref_category_player_layout">Распоред</string>
<string name="pref_category_defaults">Стандардно</string>
@ -705,37 +705,4 @@
<string name="top_center">Горе во центар</string>
<string name="top_right">Горе на десно</string>
<string name="play_full_series_button">Пушти ја целата серија</string>
<string name="download_queue">Редица за преземање</string>
<string name="queue_empty_message">Моментално нема преземања во редицата.</string>
<string name="extra_brightness_settings">Дополнителна осветленост</string>
<string name="extra_brightness_settings_des">Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот</string>
<string name="extra_brightness_key">овозможенаополнителна_осветленост</string>
<string name="search_suggestions">Предлози за пребарување</string>
<string name="search_suggestions_des">Прикажувај предлози за пребарување додека пишуваш</string>
<string name="clear_suggestions">Исчисти предлози</string>
<string name="show_player_metadata_overlay">Прикажи преклоп со метаподатоци на плеерот</string>
<string name="show_cast_in_details">Прикажи панел за емитување</string>
<string name="install_prerelease">Инсталирај предиздавачка верзија</string>
<string name="prerelease_already_installed">Предиздавачката верзија е веќе инсталирана.</string>
<string name="prerelease_install_failed">Неуспешна инсталација на предиздавачката верзија.</string>
<string name="video_singular">Видео</string>
<string name="show_episode_text">Текст на епизода</string>
<string name="video_info">Информации за медиумот</string>
<string name="skip_type_preview">Преглед</string>
<string name="source_priority">Приоритет на извор</string>
<string name="source_priority_help">Одреди како ќе се подредуваат видео изворите во плеерот</string>
<string name="source_name">Име на изворот</string>
<string name="download_all">Преземи сѐ</string>
<string name="cancel_all">Откажи сѐ</string>
<string name="download_episode_range">Дали сакате да ја преземете епизодата %s?</string>
<string name="cancel_queue_message">Дали сакате да ги откажете сите преземања во редицата?</string>
<plurals name="downloads_active">
<item quantity="one">%d активно преземање</item>
<item quantity="other">%d активни преземања</item>
</plurals>
<plurals name="downloads_queued">
<item quantity="one">%d преземање во редицата</item>
<item quantity="other">%d преземања во редицата</item>
</plurals>
<string name="player_is_live">Во живо</string>
</resources>

View file

@ -25,8 +25,8 @@
<string name="next_episode_format" formatted="true">Episod %d akan disiarkan dalam</string>
<string name="cast_format" formatted="true">Pelakon:%s</string>
<string name="safe_mode_title">Mod Selamat Hidup</string>
<string name="next_episode_time_day_format" formatted="true">%1$dh %2$dj %3$dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dj %2$dm</string>
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="episode_poster_img_des">Poster Episod</string>
<string name="home_main_poster_img_des">Poster Utama</string>
@ -485,7 +485,7 @@
<string name="category_updates">Kemaskini dan sandaran</string>
<string name="double_tap_to_seek_settings">Ketik dua kali untuk mencari</string>
<string name="use_system_brightness_settings_des">Gunakan kecerahan sistem dalam pemain apl dan bukannya tindanan gelap</string>
<string name="download_time_left_hour_min_sec_format" formatted="true">%1$dj %2$dm %3$ds</string>
<string name="download_time_left_hour_min_sec_format" formatted="true">%1$dh %2$dm %3$ds</string>
<string name="download_time_left_min_sec_format" formatted="true">%1$dm %2$ds</string>
<string name="download_time_left_sec_format" formatted="true">%1$ds</string>
<string name="speech_recognition_unavailable">Pengecaman pertuturan tidak tersedia</string>

View file

@ -661,20 +661,4 @@
<string name="clipboard_permission_error">Fout bij toegang tot het Klembord, Probeer het opnieuw.</string>
<string name="clipboard_unknown_error">Fout bij het kopiëren. Kopieer alsjeblieft de logcat en neem contact op met de app-ondersteuning.</string>
<string name="dismiss">Afwijzen</string>
<string name="video_singular">Video</string>
<string name="skip_type_preview">Voorbeeld</string>
<string name="source_priority">Bron Prioriteit</string>
<string name="source_priority_help">Bepaal hoe de videobronnen worden gesorteerd in de speler</string>
<string name="biometric_authentication_title">Ontgrendel CloudStream</string>
<string name="biometric_setting">Versleutel met Biometrie</string>
<string name="reset_btn">Reset</string>
<string name="sort_release_date_new">verschijningsdatum (Nieuw naar Oud)</string>
<string name="sort_release_date_old">verschijningsdatum (Oud naar Nieuw)</string>
<string name="hide_player_control_names">Verberg de namen van de besturingselementen van de speler</string>
<string name="no_subtitles_loaded">Ondertiteling nog niet geladen</string>
<string name="backup_path_title">Back-up folder locatie</string>
<string name="custom">Aangepast</string>
<string name="confirm_before_exiting_title">Bevestig voor afsluiten</string>
<string name="confirm_before_exiting_desc">Toon dialoogvenster voordat de app wordt afgesloten</string>
<string name="subs_edge_size">Randgrote</string>
</resources>

View file

@ -188,14 +188,4 @@
<string name="picture_in_picture">Bilde i bilde</string>
<string name="continue_watching">Fortsett å sjå</string>
<string name="reload_error">Prøv tilkopling på nytt…</string>
<string name="next_season_episode_format" formatted="true">Sesong %1$d Episode %2$d blir sleppt om</string>
<string name="play_from_beginning_img_des">Spel av frå start</string>
<string name="download_queue">Nedlastingskø</string>
<string name="speech_recognition_unavailable">Semmegjenkjenning er ikkje tilgjengeleg</string>
<string name="begin_speaking">Snakk no…</string>
<string name="browser">Nettlesar</string>
<string name="type_dropped">Fjerna</string>
<string name="play_torrent_button">Strøm Torrent</string>
<string name="play_full_series_button">Spel heile serien</string>
<string name="torrent_info">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.</string>
</resources>

View file

@ -616,7 +616,7 @@
<string name="play_from_beginning_img_des">Reproduzir do começo</string>
<string name="test_warning">Reprovou alguns testes</string>
<string name="delete_plugin">Excluir plugin</string>
<string name="downloads_empty">Atualmente não há downloads disponíveis.</string>
<string name="downloads_empty">Você não baixou nada :/</string>
<string name="hide_player_control_names">Ocultar os nomes dos controles do player</string>
<string name="open_local_video">Abrir arquivo de vídeo</string>
<string name="sort_release_date_new">Data de lançamento (do novo ao antigo)</string>
@ -736,7 +736,7 @@
<string name="source_name">Nome da fonte</string>
<string name="download_all">Baixar tudo</string>
<string name="cancel_all">Cancelar tudo</string>
<string name="download_episode_range">Você deseja baixar o episódio%s?</string>
<string name="download_episode_range">Você deseja baixar o episódio%s</string>
<string name="cancel_queue_message">Você gostaria de cancelar todos os downloads da fila?</string>
<plurals name="downloads_active">
<item quantity="one">%ddownload ativo</item>
@ -748,7 +748,7 @@
<item quantity="many">%d downloads na sequência</item>
<item quantity="other">%d downloads na sequência</item>
</plurals>
<string name="show_player_metadata_overlay">Mostrar sobreposição de metadados do reprodutor</string>
<string name="show_player_metadata_overlay">Exibir sobreposição de metadados do player</string>
<string name="video_singular">Vídeo</string>
<string name="skip_type_preview">Visualização</string>
<string name="player_is_live">Ao vivo</string>

View file

@ -61,7 +61,7 @@
<string name="stream">Transmitir</string>
<string name="error_loading_links_toast">Erro a Carregar Links</string>
<string name="download_storage_text">Armazenamento Interno</string>
<string name="app_dubbed_text">Dub</string>
<string name="app_dubbed_text">Dob</string>
<string name="app_subbed_text">Leg</string>
<string name="popup_delete_file">Eliminar Ficheiro</string>
<string name="popup_play_file">Reproduzir Ficheiro</string>
@ -100,7 +100,7 @@
<string name="subs_import_text" formatted="true">Importar fontes colocando em %s</string>
<string name="continue_watching">Continuar a Assistir</string>
<string name="action_remove_watching">Remover</string>
<string name="action_open_watching">Mais informações</string>
<string name="action_open_watching">Mais info</string>
<string name="vpn_might_be_needed">Uma VPN pode ser necessária para que este fornecedor funcione corretamente</string>
<string name="vpn_torrent">Este fornecedor é um torrent, uma VPN é recomendada</string>
<string name="provider_info_meta">Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site.</string>
@ -142,7 +142,7 @@
<string name="search">Procurar</string>
<string name="category_account">Contas e segurança</string>
<string name="category_updates">Atualizações e cópias de segurança</string>
<string name="settings_info">Informações</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Procura Avançada</string>
<string name="advanced_search_des">Mostra resultados separados por fornecedor</string>
<string name="show_fillers_settings">Mostrar episódios de enchimento para anime</string>
@ -318,9 +318,9 @@
<string name="player_load_subtitles">Carregar de arquivo</string>
<string name="player_load_subtitles_online">Carregar da Internet</string>
<string name="downloaded_file">Arquivo baixado</string>
<string name="actor_main">Principal</string>
<string name="actor_supporting">Suporte</string>
<string name="actor_background">Plano de fundo</string>
<string name="actor_main">Protagonista</string>
<string name="actor_supporting">Coadjuvante</string>
<string name="actor_background">Figurante</string>
<string name="home_random">Aleatório</string>
<string name="coming_soon">Em breve…</string>
<string name="poster_image">Imagem de Poster</string>
@ -523,7 +523,7 @@
<string name="no_repository_found_error">Repositório não encontrado, verifique o URL e tente a VPN</string>
<string name="already_voted">Você já votou</string>
<string name="action_unsubscribe">Cancelar Inscrição</string>
<string name="action_subscribe">Inscrever-se</string>
<string name="action_subscribe">Subscrever</string>
<string name="favorites_list_name">Favoritos</string>
<string name="links_reloaded_toast">A recarregar links</string>
<string name="backup_frequency">Frequência de Backup</string>
@ -686,7 +686,7 @@
<string name="edit_profile_image_success">Imagem Atualizada com Sucesso</string>
<string name="action_mark_watched_up_to_this_episode">Marcar como assistido o episódio</string>
<string name="action_remove_mark_watched_up_to_this_episode">Removar marcação de assistido até esse episódio</string>
<string name="action_reload">Recarregar</string>
<string name="action_reload">Recarregado</string>
<string name="reload_provider">Provedor de Recarregamento</string>
<string name="episode_action_play_mirror">Reproduzir do servidor alternativo</string>"
<string name="name">Nome</string>
@ -733,8 +733,4 @@
<item quantity="many">%d downloads na fila</item>
<item quantity="other">%d downloads na fila</item>
</plurals>
<string name="show_player_metadata_overlay">Mostrar sobreposição de metadados do player</string>
<string name="video_singular">Vídeo</string>
<string name="skip_type_preview">Pré-visualização</string>
<string name="player_is_live">Ao Vivo</string>
</resources>

View file

@ -735,8 +735,4 @@
<string name="cancel_all">Отменить всё</string>
<string name="download_episode_range">Вы хотите загрузить эпизод %s?</string>
<string name="cancel_queue_message">Вы хотите отменить всё запланированные загрузки?</string>
<string name="show_player_metadata_overlay">Показывать наложения метаданных проигрывателя</string>
<string name="video_singular">Видео</string>
<string name="skip_type_preview">Предпросмотр</string>
<string name="player_is_live">Прямой эфир</string>
</resources>

View file

@ -166,7 +166,7 @@
<string name="app_language">Ngôn ngữ ứng dụng</string>
<string name="no_chromecast_support_toast">Nguồn phim này chưa hỗ trợ Chromecast</string>
<string name="no_links_found_toast">Không tìm thấy liên kết</string>
<string name="copy_link_toast">Đã sao chép liên kết vào bảng nhớ tạm</string>
<string name="copy_link_toast">Đã sao chép liên kết vào b nhớ tạm</string>
<string name="play_episode_toast">Phát Tập phim</string>
<string name="subs_default_reset_toast">Đặt lại giá trị mặc định</string>
<string name="season">Mùa</string>
@ -254,7 +254,7 @@
<string name="update">Cập nhật</string>
<string name="watch_quality_pref">Chất lượng xem ưu tiên (WiFi)</string>
<string name="limit_title">Số ký tự tối đa tiêu đề trình phát</string>
<string name="limit_title_rez">Hiển thị thông tin trình phát</string>
<string name="limit_title_rez">Hiện thông tin trình phát</string>
<string name="video_buffer_size_settings">Kích thước bộ nhớ đệm video</string>
<string name="video_buffer_length_settings">Thời lượng bộ nhớ đệm</string>
<string name="video_buffer_disk_settings">Bộ nhớ đệm video trên thiết bị</string>
@ -417,7 +417,7 @@
<string name="tracks">Âm thanh &amp; video</string>
<string name="audio_tracks">Âm thanh</string>
<string name="video_tracks">Video</string>
<string name="apply_on_restart">Khởi động lại ứng dụng để thấy các thay đổi.</string>
<string name="apply_on_restart">Khởi động lại ứng dụng để thấy câc thay đổi.</string>
<string name="safe_mode_title">Chế độ an toàn được bật</string>
<string name="safe_mode_description">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.</string>
<string name="safe_mode_crash_info">Xem thông tin sự cố</string>
@ -469,8 +469,8 @@
<string name="skip_type_credits">Danh đề</string>
<string name="skip_type_intro">Giới thiệu</string>
<string name="clear_history">Xoá lịch sử</string>
<string name="enable_skip_op_from_database_des">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</string>
<string name="clipboard_too_large">Văn bản quá dài. Không thể lưu vào bảng nhớ tạm.</string>
<string name="enable_skip_op_from_database_des">Hiện các popup bỏ qua cho mở đầu/kết thúc</string>
<string name="clipboard_too_large">Văn bản quá dài. Không thể lưu vào b nhớ tạm.</string>
<string name="action_remove_from_watched">Xoá khỏi đã xem</string>
<string name="confirm_exit_dialog">Bạn có chắc muốn thoát?</string>
<string name="yes"></string>
@ -575,7 +575,7 @@
<string name="auto_rotate_video_desc">Bật tự động xoay màn hình theo hướng của video</string>
<string name="auto_rotate_video">Tự động xoay</string>
<string name="toast_copied">đã sao chép!</string>
<string name="clipboard_permission_error">Lỗi truy cập Bảng nhớ tạm, Vui lòng thử lại.</string>
<string name="clipboard_permission_error">Lỗi truy cập B nhớ tạm, Vui lòng thử lại.</string>
<string name="clipboard_unknown_error">Lỗi sao chép, Vui lòng sao chép logcat và liên hệ hỗ trợ ứng dụng.</string>
<string name="favorite">Yêu thích</string>
<string name="ok">OK</string>
@ -635,7 +635,7 @@
<string name="preview_seekbar">Xem trước trên thanh tua</string>
<string name="no_subtitles_loaded">Chưa tải phụ đề nào</string>
<string name="confirm_before_exiting_title">Xác nhận trước khi thoát</string>
<string name="confirm_before_exiting_desc">Hiển thị hộp thoại xác nhận trước khi thoát ứng dụng</string>
<string name="confirm_before_exiting_desc">Hiện hộp thoại xác nhận trước khi thoát ứng dụng</string>
<string name="dont_show">Không hiển thị</string>
<string name="show">Hiển thị</string>
<string name="backup_path_title">Vị trí thư mục sao lưu</string>
@ -644,7 +644,7 @@
<string name="torrent_info">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.</string>
<string name="encoding_error">Lỗi mã hóa</string>
<string name="software_decoding_desc">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.</string>
<string name="software_decoding">Giải mã phần mềm</string>
<string name="software_decoding">Bộ giải mã ứng dụng</string>
<string name="torrent_not_accepted">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.</string>
<string name="torrent_preferred_media">Kích hoạt torrent trong Cài đặt/Nguồn phim/Thể loại ưu tiên</string>
<string name="player_load_one_subtitle_online">Tải phụ đề đầu tiên có sẵn</string>
@ -731,7 +731,7 @@
<string name="source_name">Tên nguồn</string>
<string name="download_queue">Hàng đợi tải xuống</string>
<string name="queue_empty_message">Không có tải xuống đang chờ nào.</string>
<string name="source_priority_help">Quyết định cách sắp xếp các nguồn video trong trình phát</string>
<string name="source_priority_help">Quyết định cách sắp xếp các nguồn video trong trình phát.</string>
<string name="source_priority">Ưu tiên nguồn</string>
<string name="download_all">Tải xuống tất cả</string>
<string name="cancel_all">Hủy tất cả</string>

View file

@ -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 {

View file

@ -1 +1 @@
بث وتحميل الأفلام, المسلسلات التلفزيونية والأنمي.
بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية.

View file

@ -1 +1 @@
Transmita e descarga filmes, séries de TV e anime.
Transmita e transfira filmes, séries de TV e anime.

View file

@ -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"]

View file

@ -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()
)
}
}

View file

@ -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()
}
}
}

View file

@ -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<Request>()
val extraRequestList = threadSafeListOf<Request>()
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",

View file

@ -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<MainAPI>()
// ConcurrentModificationException is possible!!!
val allProviders = threadSafeListOf<MainAPI>()
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<MainAPI> = atomicListOf()
var apis: List<MainAPI> = threadSafeListOf()
var apiMap: Map<String, Int>? = 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,10 +126,10 @@ 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 }
}
@ -158,10 +138,13 @@ object APIHolder {
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
}
/**
* Gets the website captcha token
@ -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<DubStatus>? = null,
var dubStatus: EnumSet<DubStatus>? = null,
var otherName: String? = null,
var episodes: MutableMap<DubStatus, Int> = mutableMapOf(),
@ -1519,10 +1497,46 @@ constructor(
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = 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<DubStatus>? = null,
otherName: String? = null,
episodes: MutableMap<DubStatus, Int> = mutableMapOf(),
id: Int? = null,
quality: SearchQuality? = null,
posterHeaders: Map<String, String>? = 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() }
try {
this.date = SimpleDateFormat(format, Locale.getDefault()).parse(date ?: return)?.time
} catch (e: Exception) {
logError(e)
}
}.onFailure { logError(it) }.getOrNull()
}
@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<String> {
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
}
@Prerelease
fun isUpcoming(dateString: String?): Boolean {
return runCatching {
val fmt = DateTimeComponents.Format {
year(); char('-'); monthNumber(); char('-'); day()
}
val components = DateTimeComponents.parse(dateString ?: return false, fmt)
/**
* Try multiple conversions in order of precision, since the date string format
* may or may not include time and/or timezone offset information:
* 1. If the string has a UTC offset (e.g. "2026-05-17T14:35+02:00"), use it directly
* 2. If it has time but no offset (e.g. "2026-05-17T14:35"), fall back to device timezone
* 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
*/
val instant = runCatching { components.toInstantUsingOffset() }
.recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()) }
.getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()) }
Clock.System.now() < instant
}.onFailure { logError(it) }.getOrElse { false }
}
@Deprecated(
"toRatingInt() is deprecated. Use new score API instead.",
level = DeprecationLevel.ERROR

View file

@ -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 <T : Any> parse(text: String, kClass: KClass<T>): T {
return parseJson(text, kClass)
return mapper.readValue(text, kClass.java)
}
override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): 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)
}

View file

@ -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)

View file

@ -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<ExtractorLink>? {
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
}

View file

@ -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

View file

@ -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))

View file

@ -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<String, List<Quality>>?,
val subtitles: SubtitlesWrapper?
@ -98,4 +102,5 @@ open class Dailymotion : ExtractorApi() {
val label: String,
val urls: List<String>
)
}

View file

@ -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}"
}
}
}

View file

@ -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<StreamInfo>()
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
)
}

View file

@ -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}" }
}
}

View file

@ -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("\\", "")

View file

@ -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))
}
}

View file

@ -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<Track> = parseJson<List<Track>>("[${trackStr}]")
val tracks:List<Track> = jacksonObjectMapper().readValue("[${trackStr}]")
for (track in tracks) {
if (track.file == null || track.label == null) continue

View file

@ -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) {
""
}

View file

@ -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(

View file

@ -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)
return String(
chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
.decodeToString()
)
}
override suspend fun getUrl(

View file

@ -32,7 +32,7 @@ open class Odnoklassniki : ExtractorApi() {
val embedUrl = url.replace("/video/","/videoembed/")
val videoReq = app.get(embedUrl, headers=headers).text.replace("\\&quot;", "\"").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<List<OkRuVideo>>(videosStr) ?: throw ErrorLoadingException("Video not found")

View file

@ -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(

View file

@ -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(

View file

@ -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"

View file

@ -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"

View file

@ -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()

View file

@ -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<String> {
var counter = 0
val array = mutableListOf<String>()
val array = ArrayList<String>()
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(")")

View file

@ -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"
),

View file

@ -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(

View file

@ -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

View file

@ -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)
}
}

View file

@ -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<VideoSeyredSource> = tryParseJson<List<VideoSeyredSource>>(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred")
val responseList:List<VideoSeyredSource> = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred")
val response = responseList[0]
for (track in response.tracks) {

View file

@ -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()

View file

@ -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()
}
}
}

View file

@ -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
}

View file

@ -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) {

View file

@ -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()
}

View file

@ -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<Pair<String, String>>? = null,

Some files were not shown because too many files have changed in this diff Show more