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 }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
- uses: actions/checkout@v6 - 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 }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
- name: Create pre-release - name: Create pre-release

View file

@ -27,7 +27,7 @@ jobs:
cache-read-only: false cache-read-only: false
- name: Run Gradle - name: Run Gradle
run: ./gradlew assemblePrereleaseDebug lint check run: ./gradlew assemblePrereleaseDebug lint
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7

View file

@ -8,7 +8,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.dokka) alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.serialization)
} }
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
@ -104,8 +103,8 @@ android {
applicationId = "com.lagradost.cloudstream3" applicationId = "com.lagradost.cloudstream3"
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = libs.versions.versionCode.get().toInt() versionCode = 68
versionName = libs.versions.versionName.get() versionName = "4.7.0"
manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get() manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
@ -207,11 +206,9 @@ dependencies {
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.json) testImplementation(libs.json)
androidTestImplementation(libs.core) androidTestImplementation(libs.core)
androidTestImplementation(libs.espresso.core) implementation(libs.junit.ktx)
androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.instancio.core) androidTestImplementation(libs.espresso.core)
androidTestImplementation(libs.junit.ktx)
androidTestImplementation(libs.kotlin.test)
// Android Core & Lifecycle // Android Core & Lifecycle
implementation(libs.core.ktx) implementation(libs.core.ktx)
@ -222,7 +219,6 @@ dependencies {
implementation(libs.bundles.lifecycle) implementation(libs.bundles.lifecycle)
implementation(libs.bundles.navigation) implementation(libs.bundles.navigation)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.serialization.json) // JSON Parser
// Design & UI // Design & UI
implementation(libs.preference.ktx) implementation(libs.preference.ktx)
@ -259,15 +255,13 @@ dependencies {
// Extensions & Other Libs // Extensions & Other Libs
implementation(libs.jsoup) // HTML Parser implementation(libs.jsoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript implementation(libs.rhino) // Run JavaScript
implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance
implementation(libs.safefile) // To Prevent the URI File Fu*kery implementation(libs.safefile) // To Prevent the URI File Fu*kery
coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor 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.conscrypt.android) // To Fix SSL Fu*kery on Android 9
implementation(libs.jackson.module.kotlin) // JSON Parser implementation(libs.jackson.module.kotlin) // JSON Parser
implementation(libs.zipline) 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 // Torrent Support
implementation(libs.torrentserver) implementation(libs.torrentserver)
@ -316,7 +310,6 @@ tasks.withType<KotlinJvmCompile> {
optIn.addAll( optIn.addAll(
"com.lagradost.cloudstream3.InternalAPI", "com.lagradost.cloudstream3.InternalAPI",
"com.lagradost.cloudstream3.Prerelease", "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 // TODO: Figure out why removing the check for SearchAutoComplete seems
// to break focus on TV as it shouldn't need to be used. // 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") @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) (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) { ) {
showInputMethod(act.currentFocus?.findFocus()) showInputMethod(act.currentFocus?.findFocus())

View file

@ -408,14 +408,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
return true return true
} }
val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull() synchronized(apis) {
if (matchedApi != null) { for (api in apis) {
loadResult(str, matchedApi.name, "") if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name, "")
return true return true
} }
} }
} }
} }
}
}
return false return false
} }
@ -806,11 +809,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} }
} }
private val pluginsLock = Mutex() private val pluginsLock = Mutex()
private fun onAllPluginsLoaded(success: Boolean = false) { private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe { ioSafe {
pluginsLock.withLock { pluginsLock.withLock {
allProviders.withLock { synchronized(allProviders) {
// Load cloned sites after plugins have been loaded since clones depend on plugins. // Load cloned sites after plugins have been loaded since clones depend on plugins.
try { try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list -> getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
@ -1653,7 +1657,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
ioSafe { ioSafe {
initAll() initAll()
// No duplicates (which can happen by registerMainAPI) // 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) // val navView: BottomNavigationView = findViewById(R.id.nav_view)
@ -1961,7 +1967,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n" var providersAndroidManifestString = "Current androidmanifest should be:\n"
allProviders.withLock { synchronized(allProviders) {
for (api in allProviders) { for (api in allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${ providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix( 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.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage 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.MpvYTDLPackage
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage 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.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action 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.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode 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.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.UiText
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -45,7 +43,7 @@ import java.util.concurrent.FutureTask
import kotlin.reflect.jvm.jvmName import kotlin.reflect.jvm.jvmName
object VideoClickActionHolder { object VideoClickActionHolder {
val allVideoClickActions = atomicListOf( val allVideoClickActions = threadSafeListOf(
// Default // Default
PlayInBrowserAction(), PlayInBrowserAction(),
CopyClipboardAction(), CopyClipboardAction(),
@ -66,8 +64,6 @@ object VideoClickActionHolder {
MpvYTDLPackage(), MpvYTDLPackage(),
MpvKtPackage(), MpvKtPackage(),
MpvKtPreviewPackage(), MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option // Always Ask option
AlwaysAskAction(), AlwaysAskAction(),
// added by plugins // 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 com.lagradost.cloudstream3.actions.VideoClickActionHolder
import kotlin.Throws import kotlin.Throws
abstract class Plugin : BasePlugin() { abstract class Plugin : BasePlugin() {
/** /**
* Called when your Plugin is loaded * Called when your Plugin is loaded
@ -25,8 +26,10 @@ abstract class Plugin : BasePlugin() {
fun registerVideoClickAction(element: VideoClickAction) { fun registerVideoClickAction(element: VideoClickAction) {
Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction") Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction")
element.sourcePlugin = this.filename element.sourcePlugin = this.filename
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.add(element) VideoClickActionHolder.allVideoClickActions.add(element)
} }
}
/** /**
* This will contain your resources if you specified requiresResources in gradle * This will contain your resources if you specified requiresResources in gradle

View file

@ -610,7 +610,7 @@ object PluginManager {
return false return false
} }
InputStreamReader(stream).use { reader -> 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 context.resources.configuration
) )
} }
synchronized(plugins) {
plugins[filePath] = pluginInstance plugins[filePath] = pluginInstance
}
synchronized(classLoaders) {
classLoaders[loader] = pluginInstance classLoaders[loader] = pluginInstance
}
synchronized(urlPlugins) {
urlPlugins[data.url ?: filePath] = pluginInstance urlPlugins[data.url ?: filePath] = pluginInstance
}
if (pluginInstance is Plugin) { if (pluginInstance is Plugin) {
pluginInstance.load(context) pluginInstance.load(context)
} else { } else {
@ -695,20 +689,21 @@ object PluginManager {
} }
// remove all registered apis // remove all registered apis
synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it) removePluginMapping(it)
} }
}
APIHolder.allProviders.withLock { synchronized(APIHolder.allProviders) {
APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename } APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
} }
extractorApis.withLock { synchronized(extractorApis) {
extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename } extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
} }
VideoClickActionHolder.allVideoClickActions.withLock { synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename } VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename }
} }
synchronized(classLoaders) { 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.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery 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.DataStoreHelper
import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.UiText
import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.txt
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.net.URL import java.net.URL
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Date 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.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
import com.lagradost.cloudstream3.subtitles.SubtitleResource 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 */ /** Stateless safe abstraction of SubtitleAPI */
class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { 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 // 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 var searchCacheIndex: Int = 0
private val resourceCache = atomicListOf<SavedResourceResponse>() private val resourceCache = threadSafeListOf<SavedResourceResponse>()
private var resourceCacheIndex: Int = 0 private var resourceCacheIndex: Int = 0
const val CACHE_SIZE = 20 const val CACHE_SIZE = 20
} }
@WorkerThread @WorkerThread
suspend fun resource(data: SubtitleEntity): Result<SubtitleResource> = runCatching { suspend fun resource(data: SubtitleEntity): Result<SubtitleResource> = runCatching {
val cached = resourceCache.withLock { synchronized(resourceCache) {
var found: SubtitleResource? = null
for (item in resourceCache) { for (item in resourceCache) {
// 20 min save // 20 min save
if (item.query == data && (unixTime - item.unixTime) < 60 * 20) { if (item.query == data && (unixTime - item.unixTime) < 60 * 20) {
found = item.response return@runCatching item.response
break
} }
} }
found
} }
if (cached != null) return@runCatching cached
val returnValue = api.resource(freshAuth(), data) val returnValue = api.resource(freshAuth(), data)
resourceCache.withLock { synchronized(resourceCache) {
val add = SavedResourceResponse(unixTime, returnValue, data) val add = SavedResourceResponse(unixTime, returnValue, data)
if (resourceCache.size > CACHE_SIZE) { if (resourceCache.size > CACHE_SIZE) {
resourceCache[resourceCacheIndex] = add // rolling cache resourceCache[resourceCacheIndex] = add // rolling cache
@ -62,25 +58,22 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
@WorkerThread @WorkerThread
suspend fun search(query: SubtitleSearch): Result<List<SubtitleEntity>> { suspend fun search(query: SubtitleSearch): Result<List<SubtitleEntity>> {
return runCatching { return runCatching {
val cached = searchCache.withLock { synchronized(searchCache) {
var found: List<SubtitleEntity>? = null
for (item in searchCache) { for (item in searchCache) {
// 120 min save // 120 min save
if (item.query == query && (unixTime - item.unixTime) < 60 * 120) { if (item.query == query && (unixTime - item.unixTime) < 60 * 120) {
found = item.response return@runCatching item.response
break
} }
} }
found
} }
if (cached != null) return@runCatching cached val returnValue =
val returnValue = api.search(freshAuth(), query) ?: emptyList() api.search(freshAuth(), query) ?: emptyList()
// only cache valid return values // only cache valid return values
if (returnValue.isNotEmpty()) { if (returnValue.isNotEmpty()) {
val add = SavedSearchResponse(unixTime, returnValue, query) val add = SavedSearchResponse(unixTime, returnValue, query)
searchCache.withLock { synchronized(searchCache) {
if (searchCache.size > CACHE_SIZE) { if (searchCache.size > CACHE_SIZE) {
searchCache[searchCacheIndex] = add // rolling cache searchCache[searchCacheIndex] = add // rolling cache
searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE 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.TvType
import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.Levenshtein
import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.util.Date import java.util.Date
/** /**
@ -138,7 +138,7 @@ abstract class SyncAPI : AuthAPI() {
ListSorting.Query -> ListSorting.Query ->
if (query != null) { if (query != null) {
items.sortedBy { items.sortedBy {
-Levenshtein.partialRatio( -FuzzySearch.partialRatio(
query.lowercase(), it.name.lowercase() query.lowercase(), it.name.lowercase()
) )
} }

View file

@ -50,8 +50,7 @@ class AniListApi : SyncAPI() {
override suspend fun login(redirectUrl: String, payload: String?): AuthToken? { override suspend fun login(redirectUrl: String, payload: String?): AuthToken? {
val sanitizer = splitRedirectUrl(redirectUrl) val sanitizer = splitRedirectUrl(redirectUrl)
val token = AuthToken( val token = AuthToken(
accessToken = sanitizer["access_token"] accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"),
?: throw ErrorLoadingException("No access token"),
//refreshToken = sanitizer["refresh_token"], //refreshToken = sanitizer["refresh_token"],
accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(), accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(),
) )
@ -85,7 +84,7 @@ class AniListApi : SyncAPI() {
} }
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(query) ?: return null val data = searchShows(name) ?: return null
return data.data?.page?.media?.map { return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult( SyncAPI.SyncSearchResult(
it.title.romaji ?: return null, it.title.romaji ?: return null,

View file

@ -27,8 +27,9 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.format.DateTimeFormatter
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -201,7 +202,7 @@ class KitsuApi: SyncAPI() {
id = id, id = id,
totalEpisodes = anime.episodeCount, totalEpisodes = anime.episodeCount,
title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(), 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, duration = anime.episodeLength,
synopsis = anime.synopsis, synopsis = anime.synopsis,
airStatus = when(anime.status) { airStatus = when(anime.status) {
@ -249,7 +250,7 @@ class KitsuApi: SyncAPI() {
} }
return SyncStatus( return SyncStatus(
score = Score.from(anime.ratingTwenty, 20), score = Score.from(anime.ratingTwenty.toString(), 20),
status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)), status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)),
isFavorite = null, isFavorite = null,
watchedEpisodes = anime.progress, watchedEpisodes = anime.progress,
@ -453,8 +454,8 @@ class KitsuApi: SyncAPI() {
private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array<KitsuNode> { private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array<KitsuNode> {
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount") val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount")
val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","updatedAt", "status") val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status")
val limit = 500 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(",")}" 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.id,
this.attributes.progress, this.attributes.progress,
numEpisodes, numEpisodes,
Score.from(this.attributes.ratingTwenty, 20), Score.from(this.attributes.ratingTwenty.toString(), 20),
parseDateLong(this.attributes.updatedAt), parseDateLong(this.attributes.updatedAt),
"Kitsu", "Kitsu",
TvType.Anime, TvType.Anime,
@ -534,9 +535,12 @@ class KitsuApi: SyncAPI() {
null, null,
plot = synopsis, plot = synopsis,
releaseDate = if (startDate == null) null else try { releaseDate = if (startDate == null) null else try {
Date.from(LocalDate.parse(startDate).atStartOfDay() Date.from(
.atZone(ZoneId.systemDefault()) Instant.from(
.toInstant()) DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd")
.parse(startDate)
)
)
} catch (_: RuntimeException) { } catch (_: RuntimeException) {
null null
} }
@ -579,7 +583,7 @@ class KitsuApi: SyncAPI() {
@JsonProperty("avatar") val avatar: KitsuUserAvatar?, @JsonProperty("avatar") val avatar: KitsuUserAvatar?,
/* User list anime attributes */ /* User list anime attributes */
@JsonProperty("progress") val progress: Int?, @JsonProperty("progress") val progress: Int?,
@JsonProperty("ratingTwenty") val ratingTwenty: Int?, @JsonProperty("ratingTwenty") val ratingTwenty: Float?,
@JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("status") val status: String?, @JsonProperty("status") val status: String?,
) )
@ -628,7 +632,7 @@ class KitsuApi: SyncAPI() {
const val KITSU_CACHED_LIST: String = "kitsu_cached_list" const val KITSU_CACHED_LIST: String = "kitsu_cached_list"
private fun parseDateLong(string: String?): Long? { private fun parseDateLong(string: String?): Long? {
return try { 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 string ?: return null
)?.time?.div(1000) )?.time?.div(1000)
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -100,7 +100,7 @@ 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 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( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
"Authorization" to "Bearer $auth", "Authorization" to "Bearer $auth",

View file

@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.SimklSyncServices
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING 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.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppUtils.toJson 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.DataStoreHelper.toYear
import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.txt
import java.math.BigInteger 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 * Gets cached object, if object is not fresh returns null and removes it from cache
*/ */
inline fun <reified T : Any> getKey(path: String): T? { 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 { val cache = getKey<String>(SIMKL_CACHE_KEY, path)?.let {
tryParseJson<SimklCacheWrapper<T>>(it) mapper.readValue<SimklCacheWrapper<T>>(it, type)
} }
return if (cache?.isFresh() == true) { return if (cache?.isFresh() == true) {
@ -911,7 +916,7 @@ class SimklApi : SyncAPI() {
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
return app.get( 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() } ).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.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.newSearchResponseList 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 com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) {
val hash: Pair<String, String> val hash: Pair<String, String>
) )
private val cache = atomicListOf<SavedLoadResponse>() private val cache = threadSafeListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0 private var cacheIndex: Int = 0
const val CACHE_SIZE = 20 const val CACHE_SIZE = 20
@ -66,9 +66,11 @@ class APIRepository(val api: MainAPI) {
private fun afterPluginsLoaded(forceReload: Boolean) { private fun afterPluginsLoaded(forceReload: Boolean) {
if (forceReload) { if (forceReload) {
synchronized(cache) {
cache.clear() cache.clear()
} }
} }
}
init { init {
afterPluginsLoadedEvent += ::afterPluginsLoaded afterPluginsLoadedEvent += ::afterPluginsLoaded
@ -89,25 +91,21 @@ class APIRepository(val api: MainAPI) {
val fixedUrl = api.fixUrl(url) val fixedUrl = api.fixUrl(url)
val lookingForHash = Pair(api.name, fixedUrl) val lookingForHash = Pair(api.name, fixedUrl)
val cached = cache.withLock { synchronized(cache) {
var found: LoadResponse? = null
for (item in cache) { for (item in cache) {
// 10 min save // 10 min save
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
found = item.response return@withTimeout item.response
break
} }
} }
found
} }
if (cached != null) return@withTimeout cached
api.load(fixedUrl)?.also { response -> api.load(fixedUrl)?.also { response ->
// Remove all blank tags as early as possible // Remove all blank tags as early as possible
response.tags = response.tags?.filter { it.isNotBlank() } response.tags = response.tags?.filter { it.isNotBlank() }
val add = SavedLoadResponse(unixTime, response, lookingForHash) val add = SavedLoadResponse(unixTime, response, lookingForHash)
cache.withLock { synchronized(cache) {
if (cache.size > CACHE_SIZE) { if (cache.size > CACHE_SIZE) {
cache[cacheIndex] = add // rolling cache cache[cacheIndex] = add // rolling cache
cacheIndex = (cacheIndex + 1) % CACHE_SIZE cacheIndex = (cacheIndex + 1) % CACHE_SIZE

View file

@ -12,6 +12,9 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ListView import android.widget.ListView
import androidx.appcompat.app.AlertDialog 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.MediaLoadOptions
import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.MediaSeekOptions import com.google.android.gms.cast.MediaSeekOptions
@ -102,6 +105,9 @@ data class MetadataHolder(
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : class SelectSourceController(val view: ImageView, val activity: ControllerActivity) :
UIController() { UIController() {
private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
init { init {
view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setImageResource(R.drawable.ic_baseline_playlist_play_24)
view.setOnClickListener { view.setOnClickListener {

View file

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

View file

@ -210,13 +210,14 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(
syncId: SyncIdName, syncId: SyncIdName,
apiName: String? = null, apiName: String? = null,
) { ) {
val availableProviders = allProviders.filter { val availableProviders = synchronized(allProviders) {
allProviders.filter {
it.supportedSyncNames.contains(syncId) it.supportedSyncNames.contains(syncId)
}.map { it.name } + }.map { it.name } +
// Add the api if it exists // Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList()) ?: emptyList())
}
val baseOptions = listOf( val baseOptions = listOf(
LibraryOpenerType.Default, LibraryOpenerType.Default,
LibraryOpenerType.None, 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.ui.subtitles.SubtitlesFragment.Companion.applyStyle
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus 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.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount 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.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorLinkType 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.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 com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import okhttp3.Interceptor import okhttp3.Interceptor
@ -118,7 +118,6 @@ import java.util.concurrent.Executors
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
import kotlin.uuid.toJavaUuid
const val TAG = "CS3ExoPlayer" const val TAG = "CS3ExoPlayer"
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language" const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language"
@ -1279,7 +1278,7 @@ class CS3IPlayer : IPlayer {
item.drm?.let { drm -> item.drm?.let { drm ->
when (drm.uuid) { when (drm.uuid) {
CLEARKEY_DRM_UUID.toJavaUuid() -> { CLEARKEY_UUID -> {
// Use headers from DrmMetadata for media requests // Use headers from DrmMetadata for media requests
val client = dataSourceFactory val client = dataSourceFactory
?: throw IllegalArgumentException("Must supply onlineSource") ?: throw IllegalArgumentException("Must supply onlineSource")
@ -1300,8 +1299,8 @@ class CS3IPlayer : IPlayer {
.createMediaSource(item.mediaItem) .createMediaSource(item.mediaItem)
} }
WIDEVINE_DRM_UUID.toJavaUuid(), WIDEVINE_UUID,
PLAYREADY_DRM_UUID.toJavaUuid() -> { PLAYREADY_UUID -> {
// Use headers from DrmMetadata for media requests // Use headers from DrmMetadata for media requests
val client = dataSourceFactory val client = dataSourceFactory
?: throw IllegalArgumentException("Must supply onlineSource") ?: throw IllegalArgumentException("Must supply onlineSource")
@ -1915,7 +1914,7 @@ class CS3IPlayer : IPlayer {
drm = DrmMetadata( drm = DrmMetadata(
kid = link.kid, kid = link.kid,
key = link.key, key = link.key,
uuid = link.uuid.toJavaUuid(), uuid = link.uuid,
kty = link.kty, kty = link.kty,
licenseUrl = link.licenseUrl, licenseUrl = link.licenseUrl,
keyRequestParameters = link.keyRequestParameters, keyRequestParameters = link.keyRequestParameters,

View file

@ -58,23 +58,9 @@ class DownloadedPlayerActivity : AppCompatActivity() {
enableEdgeToEdgeCompat() enableEdgeToEdgeCompat()
setContentView(R.layout.empty_layout) setContentView(R.layout.empty_layout)
Log.i(TAG, "onCreate") Log.i(TAG, "onCreate")
handleIntent(intent)
/** handleIntent(intent)
* Use moveTaskToBack instead of finish() so there is always exactly one task attachBackPressedCallback("DownloadedPlayerActivity") { finish() }
* 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) }
} }
private fun handleIntent(intent: Intent) { private fun handleIntent(intent: Intent) {
@ -97,11 +83,11 @@ class DownloadedPlayerActivity : AppCompatActivity() {
url != null -> playLink(this, url) url != null -> playLink(this, url)
data != null -> playUri(this, data) data != null -> playUri(this, data)
extraText != null -> playLink(this, extraText) extraText != null -> playLink(this, extraText)
else -> finishAndRemoveTask() else -> { finish(); return }
} }
} else if (data?.scheme == "content") { } else if (data?.scheme == "content") {
playUri(this, data) playUri(this, data)
} else finishAndRemoveTask() } else finish()
} }
override fun onResume() { override fun onResume() {

View file

@ -945,18 +945,12 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
player.handleEvent(CSPlayerEvent.SkipCurrentChapter) 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) player.handleEvent(CSPlayerEvent.PlayPauseToggle)
} }
// KEYCODE_DPAD_CENTER and KEYCODE_ENTER both act as a "select/confirm" button. KeyEvent.KEYCODE_DPAD_CENTER -> {
// Some remotes (e.g. LG Magic Remote) send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER. if (isShowing) {
// 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()) {
return null return null
} }
// If UI is not shown make click instantly skip to next chapter even if locked // 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? { ): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null val langCode = preferredAutoSelectSubtitles ?: return null
if (downloads) { if (downloads) {
sortSubs(subtitles).firstOrNull { return sortSubs(subtitles).firstOrNull {
it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode( it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode(
langCode langCode
) )
}?.let { return it } }
} }
if (!settings) return null if (!settings) return null

View file

@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.content.ContextCompat.getString import androidx.core.content.ContextCompat.getString
import androidx.navigation.NavOptions
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.temp.CloudStreamPackage import com.lagradost.cloudstream3.actions.temp.CloudStreamPackage
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
@ -13,15 +12,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.safefile.SafeFile import com.lagradost.safefile.SafeFile
object OfflinePlaybackHelper { 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) { fun playLink(activity: Activity, url: String) {
activity.navigate( activity.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance( R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
@ -30,8 +20,7 @@ object OfflinePlaybackHelper {
BasicLink(url) BasicLink(url)
), id = url.hashCode() ), id = url.hashCode()
), 0 ), 0
), )
replacePlayerNavOptions
) )
} }
@ -63,8 +52,7 @@ object OfflinePlaybackHelper {
subs, subs,
if (id != -1) id else null, if (id != -1) id else null,
), 0 ), 0
), )
replacePlayerNavOptions
) )
return true return true
} }
@ -88,8 +76,7 @@ object OfflinePlaybackHelper {
) )
) )
), 0 ), 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.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs 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.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWork
@ -1325,7 +1324,7 @@ class ResultViewModel2 : ViewModel() {
episodeIds: Array<String>, episodeIds: Array<String>,
watchState: VideoWatchState watchState: VideoWatchState
) { ) {
val watchStateString = watchState.toJson() val watchStateString = DataStore.mapper.writeValueAsString(watchState)
episodeIds.forEach { episodeIds.forEach {
if (getVideoWatchState(it.toInt()) != watchState) { if (getVideoWatchState(it.toInt()) != watchState) {
editor.setKeyRaw( editor.setKeyRaw(
@ -1686,13 +1685,14 @@ class ResultViewModel2 : ViewModel() {
} }
val realRecommendations = ArrayList<SearchResponse>() val realRecommendations = ArrayList<SearchResponse>()
val apiNames = apis.filter { val apiNames = synchronized(apis) {
apis.filter {
it.name.contains("gogoanime", true) || it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true) it.name.contains("9anime", true)
}.map { }.map {
it.name it.name
} }
}
meta.recommendations?.forEach { rec -> meta.recommendations?.forEach { rec ->
apiNames.forEach { name -> apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name)) realRecommendations.add(rec.copy(apiName = name))

View file

@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() {
private var suggestionJob: Job? = null 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() { fun clearSearch() {
_searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false))) _searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false)))
@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() {
private var onGoingSearch: Job? = null private var onGoingSearch: Job? = null
fun reloadRepos() { fun reloadRepos() {
repos = apis.withLock { apis.map { APIRepository(it) } } repos = synchronized(apis) { apis.map { APIRepository(it) } }
} }
fun searchAndCancel( fun searchAndCancel(

View file

@ -219,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() {
} }
fun showAdd() { fun showAdd() {
val providers = allProviders.distinctBy { it::class }.sortedBy { it.name } val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog( activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" }, providers.map { "${it.name} (${it.mainUrl})" },
-1, -1,

View file

@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() {
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
activity?.getApiProviderLangSettings()?.let { currentLangTags -> activity?.getApiProviderLangSettings()?.let { currentLangTags ->
val languagesTagName = APIHolder.apis.withLock { val languagesTagName = synchronized(APIHolder.apis) {
listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) + listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) +
APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } 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 -> 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.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.Levenshtein import me.xdrop.fuzzywuzzy.FuzzySearch
import java.io.File import java.io.File
// String => repository url // String => repository url
@ -246,7 +246,7 @@ class PluginsViewModel : ViewModel() {
this.sortedBy { it.plugin.second.name } this.sortedBy { it.plugin.second.name }
} else { } else {
this.sortedBy { this.sortedBy {
-Levenshtein.partialRatio( -FuzzySearch.partialRatio(
it.plugin.second.name.lowercase(), it.plugin.second.name.lowercase(),
query.lowercase() query.lowercase()
) )

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.MainAPI 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 com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -40,7 +40,7 @@ class TestViewModel : ViewModel() {
get() = scope != null get() = scope != null
private var filter = ProviderFilter.All 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 passed = 0
private var failed = 0 private var failed = 0
private var total = 0 private var total = 0
@ -51,9 +51,9 @@ class TestViewModel : ViewModel() {
} }
private fun postProviders() { private fun postProviders() {
providers.withLock { synchronized(providers) {
val filtered = when (filter) { val filtered = when (filter) {
ProviderFilter.All -> providers.toList() ProviderFilter.All -> providers
ProviderFilter.Passed -> providers.filter { it.second.success } ProviderFilter.Passed -> providers.filter { it.second.success }
ProviderFilter.Failed -> 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) { private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) {
providers.withLock { synchronized(providers) {
val index = providers.indexOfFirst { it.first == api } val index = providers.indexOfFirst { it.first == api }
if (index == -1) { if (index == -1) {
providers.add(api to results) providers.add(api to results)
@ -81,14 +81,14 @@ class TestViewModel : ViewModel() {
} }
fun init() { fun init() {
total = APIHolder.allProviders.withLock { APIHolder.allProviders.size } total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
updateProgress() updateProgress()
} }
fun startTest() { fun startTest() {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() } val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
total = apis.size total = apis.size
failed = 0 failed = 0
passed = 0 passed = 0

View file

@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
if (isSetup) if (isSetup)
if ( if (
// If any available languages // 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) findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else { } else {

View file

@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment<FragmentSetupProviderLanguage
val currentLangTags = ctx.getApiProviderLangSettings() val currentLangTags = ctx.getApiProviderLangSettings()
val languagesTagName = APIHolder.apis.withLock { val languagesTagName = synchronized(APIHolder.apis) {
listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) + listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) +
APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } 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 -> val currentIndexList = currentLangTags.map { langTag ->

View file

@ -369,10 +369,28 @@ object AppContextUtils {
} }
fun Context.getApiSettings(): HashSet<String> { fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>() val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings() val activeLangs = getApiProviderLangSettings()
val hasUniversal = activeLangs.contains(AllLanguagesName) 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 return hashSet
} }
@ -463,7 +481,9 @@ object AppContextUtils {
} ?: default } ?: default
val langs = this.getApiProviderLangSettings() val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName) 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()) { return if (currentPrefMedia.isEmpty()) {
allApis allApis
} else { } else {

View file

@ -10,6 +10,7 @@ import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R 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.AniListApi.Companion.ANILIST_CACHED_LIST
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_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.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.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs 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.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream
@ -133,7 +133,9 @@ object BackupUtils {
) )
@Suppress("UNCHECKED_CAST") @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 allData = context.getSharedPrefs().all.filter { it.key.isTransferable() }
val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
@ -212,7 +214,7 @@ object BackupUtils {
fileStream = stream.openNew() fileStream = stream.openNew()
printStream = PrintWriter(fileStream) printStream = PrintWriter(fileStream)
printStream.print(backupFile.toJson()) printStream.print(mapper.writeValueAsString(backupFile))
showToast( showToast(
R.string.backup_success, R.string.backup_success,
@ -257,8 +259,8 @@ object BackupUtils {
val input = activity.contentResolver.openInputStream(uri) val input = activity.contentResolver.openInputStream(uri)
?: return@ioSafe ?: return@ioSafe
val text = input.bufferedReader().readText() val restoredValue =
val restoredValue = parseJson<BackupFile>(text) mapper.readValue<BackupFile>(input)
restore( restore(
activity, activity,

View file

@ -2,16 +2,17 @@ package com.lagradost.cloudstream3.utils
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceManager 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.getKeyClass
import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass
import com.lagradost.cloudstream3.mvvm.logError 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.KClass
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import androidx.core.content.edit
/** Used to display metadata about downloads and resume watching */ /** Used to display metadata about downloads and resume watching */
const val DOWNLOAD_HEADER_CACHE = "download_header_cache" const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
@ -87,18 +88,8 @@ data class Editor(
} }
object DataStore { object DataStore {
// Extensions shouldn't have really been using this version of it, but it seems val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
// some have. Since there has always been a very easy alternative, we won't .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
// 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
private fun getPreferences(context: Context): SharedPreferences { private fun getPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
@ -108,6 +99,7 @@ object DataStore {
return getPreferences(this) return getPreferences(this)
} }
fun getFolderName(folder: String, path: String): String { fun getFolderName(folder: String, path: String): String {
return "${folder}/${path}" return "${folder}/${path}"
} }
@ -173,17 +165,17 @@ object DataStore {
fun <T> Context.setKey(path: String, value: T) { fun <T> Context.setKey(path: String, value: T) {
try { try {
getSharedPrefs().edit { getSharedPrefs().edit {
putString(path, value?.toJsonLiteral()) putString(path, mapper.writeValueAsString(value))
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
} }
fun <T : Any> Context.getKey(path: String, valueType: Class<T>): T? { fun <T> Context.getKey(path: String, valueType: Class<T>): T? {
try { try {
val json: String = getSharedPrefs().getString(path, null) ?: return null val json: String = getSharedPrefs().getString(path, null) ?: return null
return parseJson(json, valueType.kotlin) return json.toKotlinObject(valueType)
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }
@ -194,11 +186,11 @@ object DataStore {
} }
inline fun <reified T : Any> String.toKotlinObject(): T { 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 { fun <T> String.toKotlinObject(valueType: Class<T>): T {
return parseJson(this, valueType.kotlin) return mapper.readValue(this, valueType)
} }
// GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR // 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.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.util.Log import android.util.Log
import android.widget.ImageView import android.widget.ImageView
@ -12,7 +11,6 @@ import coil3.EventListener
import coil3.ImageLoader import coil3.ImageLoader
import coil3.PlatformContext import coil3.PlatformContext
import coil3.SingletonImageLoader import coil3.SingletonImageLoader
import coil3.decode.BitmapFactoryDecoder
import coil3.disk.DiskCache import coil3.disk.DiskCache
import coil3.dispose import coil3.dispose
import coil3.load import coil3.load
@ -24,86 +22,82 @@ import coil3.request.CachePolicy
import coil3.request.ErrorResult import coil3.request.ErrorResult
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.allowHardware import coil3.request.allowHardware
import coil3.request.bitmapConfig
import coil3.request.crossfade import coil3.request.crossfade
import coil3.util.DebugLogger import coil3.util.DebugLogger
import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.buildDefaultClient import com.lagradost.cloudstream3.network.buildDefaultClient
import okhttp3.HttpUrl
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import java.io.File import java.io.File
import java.nio.ByteBuffer import java.nio.ByteBuffer
object ImageLoader { object ImageLoader {
private const val TAG = "CoilImgLoader" private const val TAG = "CoilImgLoader"
internal fun buildImageLoader(context: PlatformContext): ImageLoader {
val isBrokenHardware = hasPotentialBrokenHardware() internal fun buildImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context)
return ImageLoader.Builder(context)
.crossfade(200) .crossfade(200)
.allowHardware(SDK_INT >= 28 && !isBrokenHardware) .allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder
.diskCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.ENABLED)
.networkCachePolicy(CachePolicy.ENABLED) .networkCachePolicy(CachePolicy.ENABLED)
.memoryCache { .memoryCache {
MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching
.strongReferencesEnabled(false)
.build() .build()
} }
.diskCache { .diskCache {
DiskCache.Builder() DiskCache.Builder()
.directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath()) .directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath())
.maxSizeBytes(512L * 1024 * 1024) // 512 MB .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() .build()
} }
/** Pass interceptors with care, unnecessary passing tokens to servers /** Pass interceptors with care, unnecessary passing tokens to servers
or image hosting services causes unauthorized exceptions **/ or image hosting services causes unauthorized exceptions **/
.components { .components { add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) }
add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) .also {
if (isBrokenHardware) { it.setupCoilLogger()
add(BitmapFactoryDecoder.Factory()) Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.")
} // sw decoder
}
.apply {
if (isBrokenHardware) { // coil will auto choose optimal config on modern device
bitmapConfig(Bitmap.Config.ARGB_8888)
}
setupCoilLogger()
} }
.build() .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. **/ Errors on release builds. **/
internal fun ImageLoader.Builder.setupCoilLogger() { internal fun ImageLoader.Builder.setupCoilLogger() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
logger(DebugLogger()) logger(DebugLogger())
Log.d(TAG, "setupCoilLogger: Activated DEBUG_LOGGER FOR COIL")
} else { } else {
eventListener(object : EventListener() { eventListener(object : EventListener() {
override fun onError(request: ImageRequest, result: ErrorResult) { override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result) super.onError(request, result)
Log.e(TAG, "Image load error: ${result.throwable.message ?: result.throwable}") Log.e(TAG, "Error loading image: ${result.throwable}")
Log.e(TAG, " URL: ${request.data}")
Log.e(TAG, " allowHardware: ${request.allowHardware}")
Log.e(TAG, " hardware: ${Build.HARDWARE}, board: ${Build.BOARD}")
} }
}) })
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( private fun ImageView.loadImageInternal(
imageData: Any?, imageData: Any?,
headers: Map<String, String>? = null, headers: Map<String, String>? = null,
builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations 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() this.dispose()
if (imageData == null) return
if(imageData == null) return // Just in case
// setImageResource is better than coil3 on resources due to attr // setImageResource is better than coil3 on resources due to attr
if(imageData is Int) { if(imageData is Int) {
this.setImageResource(imageData); return 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.load(imageData, SingletonImageLoader.get(context)) {
this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder -> this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder ->
headerBuilder["User-Agent"] = USER_AGENT headerBuilder["User-Agent"] = USER_AGENT
@ -111,22 +105,11 @@ object ImageLoader {
headerBuilder[key] = value headerBuilder[key] = value
} }
}.build()) }.build())
builder() // if passed 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 **/ /** TYPE_SAFE_LOADERS **/
fun ImageView.loadImage( fun ImageView.loadImage(
imageData: UiImage?, imageData: UiImage?,
@ -155,6 +138,12 @@ object ImageLoader {
builder: ImageRequest.Builder.() -> Unit = {} builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) ) = 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( fun ImageView.loadImage(
imageData: File?, imageData: File?,
builder: ImageRequest.Builder.() -> Unit = {} builder: ImageRequest.Builder.() -> Unit = {}

View file

@ -93,9 +93,9 @@ object InAppUpdater {
private suspend fun Activity.getReleaseUpdate(): Update { private suspend fun Activity.getReleaseUpdate(): Update {
val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases"
val headers = mapOf("Accept" to "application/vnd.github.v3+json") 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 app.get(url, headers = headers).text
).toList() )
val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""") val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""")
val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""") val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""")
@ -103,7 +103,9 @@ object InAppUpdater {
!rel.prerelease !rel.prerelease
}.sortedWith(compareBy { release -> }.sortedWith(compareBy { release ->
release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> 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() 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" "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 releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases"
val headers = mapOf("Accept" to "application/vnd.github.v3+json") 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 app.get(releaseUrl, headers = headers).text
).toList() )
val found = response.lastOrNull { rel -> val found = response.lastOrNull { rel ->
rel.prerelease || rel.tagName == "pre-release" 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.animeproviders.AniflixProvider
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError 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 import java.util.concurrent.TimeUnit
object SyncUtil { object SyncUtil {
@ -71,7 +71,7 @@ object SyncUtil {
val url = val url =
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" "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 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 overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId
val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id
@ -96,10 +96,12 @@ object SyncUtil {
.mapNotNull { it.url }.toMutableList() .mapNotNull { it.url }.toMutableList()
if (type == "anilist") { // TODO MAKE BETTER if (type == "anilist") { // TODO MAKE BETTER
synchronized(apis) {
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id") current.add("${it.mainUrl}/anime/$id")
} }
} }
}
return current return current
} }

View file

@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.junit.Assert
import kotlin.random.Random import kotlin.random.Random
object TestingUtils { object TestingUtils {
open class TestResult(val success: Boolean) { open class TestResult(val success: Boolean) {
companion object { companion object {
val Pass = TestResult(true) 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 TestResultList(val results: List<SearchResponse>) : TestResult(true)
class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true)
@ -91,7 +87,7 @@ object TestingUtils {
} catch (e: Throwable) { } catch (e: Throwable) {
when (e) { when (e) {
is NotImplementedError -> { 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 -> { is CancellationException -> {
@ -119,7 +115,7 @@ object TestingUtils {
api.search(query, 1)?.items?.takeIf { it.isNotEmpty() } api.search(query, 1)?.items?.takeIf { it.isNotEmpty() }
} catch (e: Throwable) { } catch (e: Throwable) {
if (e is NotImplementedError) { if (e is NotImplementedError) {
fail("Provider has not implemented search()") Assert.fail("Provider has not implemented search()")
} else if (e is CancellationException) { } else if (e is CancellationException) {
throw e throw e
} }
@ -129,7 +125,7 @@ object TestingUtils {
} }
return if (searchResults.isNullOrEmpty()) { 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 TestResult.Fail // Should not be reached
} else { } else {
TestResultList(searchResults) TestResultList(searchResults)
@ -220,7 +216,7 @@ object TestingUtils {
// return TestResult(validResults) // return TestResult(validResults)
} catch (e: Throwable) { } catch (e: Throwable) {
if (e is NotImplementedError) { if (e is NotImplementedError) {
fail("Provider has not implemented load()") Assert.fail("Provider has not implemented load()")
} }
throw e throw e
} }
@ -232,14 +228,14 @@ object TestingUtils {
url: String?, url: String?,
logger: Logger logger: Logger
): TestResult { ): 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 if (url == null) return TestResult.Fail // Should never trigger
var linksLoaded = 0 var linksLoaded = 0
try { try {
val success = api.loadLinks(url, false, {}) { link -> val success = api.loadLinks(url, false, {}) { link ->
logger.log("Video loaded: ${link.name}") logger.log("Video loaded: ${link.name}")
assertTrue( Assert.assertTrue(
"Api ${api.name} returns link with invalid url ${link.url}", "Api ${api.name} returns link with invalid url ${link.url}",
link.url.length > 4 link.url.length > 4
) )
@ -249,12 +245,12 @@ object TestingUtils {
logger.log("Links loaded: $linksLoaded") logger.log("Links loaded: $linksLoaded")
return TestResult(linksLoaded > 0) return TestResult(linksLoaded > 0)
} else { } 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) { } catch (e: Throwable) {
when (e) { when (e) {
is NotImplementedError -> { is NotImplementedError -> {
fail("Provider has not implemented loadLinks()") Assert.fail("Provider has not implemented loadLinks()")
} }
else -> { else -> {
@ -280,7 +276,7 @@ object TestingUtils {
// Test Homepage // Test Homepage
val homepage = testHomepage(api, logger) 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() val homePageList = (homepage as? TestResultList)?.results ?: emptyList()
// Test Search Results // Test Search Results
@ -291,7 +287,7 @@ object TestingUtils {
listOf("over", "iron", "guy")).take(3) listOf("over", "iron", "guy")).take(3)
val searchResults = testSearch(api, searchQueries, logger) 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 searchResults as TestResultList
// Test Load and LoadLinks // 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:id="@+id/player_metadata_scrim"
android:layout_width="640dp" android:layout_width="640dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix" android:background="@drawable/bg_player_metadata_scrim_netflix"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View file

@ -12,7 +12,6 @@
android:id="@+id/player_metadata_scrim" android:id="@+id/player_metadata_scrim"
android:layout_width="680dp" android:layout_width="680dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix" android:background="@drawable/bg_player_metadata_scrim_netflix"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -11,7 +11,6 @@
android:id="@+id/player_metadata_scrim" android:id="@+id/player_metadata_scrim"
android:layout_width="640dp" android:layout_width="640dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix" android:background="@drawable/bg_player_metadata_scrim_netflix"
android:visibility="gone" android:visibility="gone"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

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

View file

@ -252,7 +252,7 @@
<string name="update">Update</string> <string name="update">Update</string>
<string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string> <string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string>
<string name="limit_title">Videoplayertitel max. Zeichen</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_size_settings">Videopuffergröße</string>
<string name="video_buffer_length_settings">Videopufferlänge</string> <string name="video_buffer_length_settings">Videopufferlänge</string>
<string name="video_buffer_disk_settings">Video-Cache in Speicher</string> <string name="video_buffer_disk_settings">Video-Cache in Speicher</string>
@ -712,8 +712,8 @@
<string name="extra_brightness_settings">Zusätzliche Helligkeit</string> <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_settings_des">Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist</string>
<string name="extra_brightness_key">Erhöhte Helligkeit aktiviert</string> <string name="extra_brightness_key">Erhöhte Helligkeit aktiviert</string>
<string name="show_cast_in_details">Zeige Cast-Panel</string> <string name="show_cast_in_details">Cast-Panel zeigen</string>
<string name="video_info">Mediainfo</string> <string name="video_info">Medieninfo</string>
<string name="source_name">Quellname</string> <string name="source_name">Quellname</string>
<string name="download_all">Alle herunterladen</string> <string name="download_all">Alle herunterladen</string>
<string name="download_episode_range">Möchtest du Episode %s herunter laden?</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="queue_empty_message">Es befinden sich keine Downloads in der Warteschlange.</string>
<string name="source_priority">Quellpriorität</string> <string name="source_priority">Quellpriorität</string>
<string name="source_priority_help">Entscheide, wie Videoquellen im Player sortiert werden sollen</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> </resources>

View file

@ -244,7 +244,7 @@
<string name="quality_tc">TC</string> <string name="quality_tc">TC</string>
<string name="subscription_new">Претплатен на %s</string> <string name="subscription_new">Претплатен на %s</string>
<string name="pref_category_subtitles">Преводи</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="backup_failed">Недостасуваат дозволи за складирање. Обиди се повторно.</string>
<string name="sort_save">Зачувај</string> <string name="sort_save">Зачувај</string>
<string name="player_load_subtitles">Вчитај од датотека</string> <string name="player_load_subtitles">Вчитај од датотека</string>
@ -445,7 +445,7 @@
<string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string> <string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string>
<string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string> <string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string>
<string name="apk_installer_settings_des">Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат.</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="video_buffer_size_settings">Големина на видео баферот</string>
<string name="pref_category_player_layout">Распоред</string> <string name="pref_category_player_layout">Распоред</string>
<string name="pref_category_defaults">Стандардно</string> <string name="pref_category_defaults">Стандардно</string>
@ -705,37 +705,4 @@
<string name="top_center">Горе во центар</string> <string name="top_center">Горе во центар</string>
<string name="top_right">Горе на десно</string> <string name="top_right">Горе на десно</string>
<string name="play_full_series_button">Пушти ја целата серија</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> </resources>

View file

@ -25,8 +25,8 @@
<string name="next_episode_format" formatted="true">Episod %d akan disiarkan dalam</string> <string name="next_episode_format" formatted="true">Episod %d akan disiarkan dalam</string>
<string name="cast_format" formatted="true">Pelakon:%s</string> <string name="cast_format" formatted="true">Pelakon:%s</string>
<string name="safe_mode_title">Mod Selamat Hidup</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_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dj %2$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="next_episode_time_min_format" formatted="true">%dm</string>
<string name="episode_poster_img_des">Poster Episod</string> <string name="episode_poster_img_des">Poster Episod</string>
<string name="home_main_poster_img_des">Poster Utama</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="category_updates">Kemaskini dan sandaran</string>
<string name="double_tap_to_seek_settings">Ketik dua kali untuk mencari</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="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_min_sec_format" formatted="true">%1$dm %2$ds</string>
<string name="download_time_left_sec_format" formatted="true">%1$ds</string> <string name="download_time_left_sec_format" formatted="true">%1$ds</string>
<string name="speech_recognition_unavailable">Pengecaman pertuturan tidak tersedia</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_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="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="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> </resources>

View file

@ -188,14 +188,4 @@
<string name="picture_in_picture">Bilde i bilde</string> <string name="picture_in_picture">Bilde i bilde</string>
<string name="continue_watching">Fortsett å sjå</string> <string name="continue_watching">Fortsett å sjå</string>
<string name="reload_error">Prøv tilkopling på nytt…</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> </resources>

View file

@ -616,7 +616,7 @@
<string name="play_from_beginning_img_des">Reproduzir do começo</string> <string name="play_from_beginning_img_des">Reproduzir do começo</string>
<string name="test_warning">Reprovou alguns testes</string> <string name="test_warning">Reprovou alguns testes</string>
<string name="delete_plugin">Excluir plugin</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="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="open_local_video">Abrir arquivo de vídeo</string>
<string name="sort_release_date_new">Data de lançamento (do novo ao antigo)</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="source_name">Nome da fonte</string>
<string name="download_all">Baixar tudo</string> <string name="download_all">Baixar tudo</string>
<string name="cancel_all">Cancelar 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> <string name="cancel_queue_message">Você gostaria de cancelar todos os downloads da fila?</string>
<plurals name="downloads_active"> <plurals name="downloads_active">
<item quantity="one">%ddownload ativo</item> <item quantity="one">%ddownload ativo</item>
@ -748,7 +748,7 @@
<item quantity="many">%d downloads na sequência</item> <item quantity="many">%d downloads na sequência</item>
<item quantity="other">%d downloads na sequência</item> <item quantity="other">%d downloads na sequência</item>
</plurals> </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="video_singular">Vídeo</string>
<string name="skip_type_preview">Visualização</string> <string name="skip_type_preview">Visualização</string>
<string name="player_is_live">Ao vivo</string> <string name="player_is_live">Ao vivo</string>

View file

@ -61,7 +61,7 @@
<string name="stream">Transmitir</string> <string name="stream">Transmitir</string>
<string name="error_loading_links_toast">Erro a Carregar Links</string> <string name="error_loading_links_toast">Erro a Carregar Links</string>
<string name="download_storage_text">Armazenamento Interno</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="app_subbed_text">Leg</string>
<string name="popup_delete_file">Eliminar Ficheiro</string> <string name="popup_delete_file">Eliminar Ficheiro</string>
<string name="popup_play_file">Reproduzir 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="subs_import_text" formatted="true">Importar fontes colocando em %s</string>
<string name="continue_watching">Continuar a Assistir</string> <string name="continue_watching">Continuar a Assistir</string>
<string name="action_remove_watching">Remover</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_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="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> <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="search">Procurar</string>
<string name="category_account">Contas e segurança</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="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">Procura Avançada</string>
<string name="advanced_search_des">Mostra resultados separados por fornecedor</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> <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">Carregar de arquivo</string>
<string name="player_load_subtitles_online">Carregar da Internet</string> <string name="player_load_subtitles_online">Carregar da Internet</string>
<string name="downloaded_file">Arquivo baixado</string> <string name="downloaded_file">Arquivo baixado</string>
<string name="actor_main">Principal</string> <string name="actor_main">Protagonista</string>
<string name="actor_supporting">Suporte</string> <string name="actor_supporting">Coadjuvante</string>
<string name="actor_background">Plano de fundo</string> <string name="actor_background">Figurante</string>
<string name="home_random">Aleatório</string> <string name="home_random">Aleatório</string>
<string name="coming_soon">Em breve…</string> <string name="coming_soon">Em breve…</string>
<string name="poster_image">Imagem de Poster</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="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="already_voted">Você já votou</string>
<string name="action_unsubscribe">Cancelar Inscrição</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="favorites_list_name">Favoritos</string>
<string name="links_reloaded_toast">A recarregar links</string> <string name="links_reloaded_toast">A recarregar links</string>
<string name="backup_frequency">Frequência de Backup</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="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_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_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="reload_provider">Provedor de Recarregamento</string>
<string name="episode_action_play_mirror">Reproduzir do servidor alternativo</string>" <string name="episode_action_play_mirror">Reproduzir do servidor alternativo</string>"
<string name="name">Nome</string> <string name="name">Nome</string>
@ -733,8 +733,4 @@
<item quantity="many">%d downloads na fila</item> <item quantity="many">%d downloads na fila</item>
<item quantity="other">%d downloads na fila</item> <item quantity="other">%d downloads na fila</item>
</plurals> </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> </resources>

View file

@ -735,8 +735,4 @@
<string name="cancel_all">Отменить всё</string> <string name="cancel_all">Отменить всё</string>
<string name="download_episode_range">Вы хотите загрузить эпизод %s?</string> <string name="download_episode_range">Вы хотите загрузить эпизод %s?</string>
<string name="cancel_queue_message">Вы хотите отменить всё запланированные загрузки?</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> </resources>

View file

@ -166,7 +166,7 @@
<string name="app_language">Ngôn ngữ ứng dụng</string> <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_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="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="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="subs_default_reset_toast">Đặt lại giá trị mặc định</string>
<string name="season">Mùa</string> <string name="season">Mùa</string>
@ -254,7 +254,7 @@
<string name="update">Cập nhật</string> <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="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">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_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_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> <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="tracks">Âm thanh &amp; video</string>
<string name="audio_tracks">Âm thanh</string> <string name="audio_tracks">Âm thanh</string>
<string name="video_tracks">Video</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_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_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> <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_credits">Danh đề</string>
<string name="skip_type_intro">Giới thiệu</string> <string name="skip_type_intro">Giới thiệu</string>
<string name="clear_history">Xoá lịch sử</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="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ảng nhớ tạm.</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="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="confirm_exit_dialog">Bạn có chắc muốn thoát?</string>
<string name="yes"></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_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="auto_rotate_video">Tự động xoay</string>
<string name="toast_copied">đã sao chép!</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="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="favorite">Yêu thích</string>
<string name="ok">OK</string> <string name="ok">OK</string>
@ -635,7 +635,7 @@
<string name="preview_seekbar">Xem trước trên thanh tua</string> <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="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_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="dont_show">Không hiển thị</string>
<string name="show">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> <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="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="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_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_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="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> <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="source_name">Tên nguồn</string>
<string name="download_queue">Hàng đợi tải xuống</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="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="source_priority">Ưu tiên nguồn</string>
<string name="download_all">Tải xuống tất cả</string> <string name="download_all">Tải xuống tất cả</string>
<string name="cancel_all">Hủy 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.dokka) apply false
alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.kotlin.serialization) apply false
} }
allprojects { 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" animeDb = "1.0.2"
annotation = "1.10.0" annotation = "1.10.0"
appcompat = "1.7.1" appcompat = "1.7.1"
biometric = "1.4.0-alpha07" biometric = "1.4.0-alpha06"
buildkonfigGradlePlugin = "0.21.2" buildkonfigGradlePlugin = "0.18.0"
coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later
colorpicker = "6b46b49" colorpicker = "6b46b49"
conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything 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" dokkaGradlePlugin = "2.2.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
fragmentKtx = "1.8.9" 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) 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" jsoup = "1.22.1"
junit = "4.13.2" junit = "4.13.2"
junitKtx = "1.3.0" junitKtx = "1.3.0"
junitVersion = "1.3.0" junitVersion = "1.3.0"
juniversalchardet = "2.5.0" juniversalchardet = "2.5.0"
kotlinGradlePlugin = "2.3.20" kotlinGradlePlugin = "2.3.20"
kotlinxAtomicfu = "0.33.0"
kotlinxCollectionsImmutable = "0.4.0" kotlinxCollectionsImmutable = "0.4.0"
kotlinxCoroutinesCore = "1.11.0" kotlinxCoroutinesCore = "1.10.2"
kotlinxDatetime = "0.8.0"
kotlinxSerializationJson = "1.11.0"
ktor = "3.5.0"
lifecycleKtx = "2.10.0" lifecycleKtx = "2.10.0"
material = "1.14.0" material = "1.14.0-beta01"
media3 = "1.9.3" media3 = "1.9.3"
navigationKtx = "2.9.8" navigationKtx = "2.9.7"
newpipeextractor = "v0.26.3" newpipeextractor = "v0.26.0"
nextlibMedia3 = "1.9.3-0.12.0" nextlibMedia3 = "1.9.3-0.12.0"
nicehttp = "0.4.18" nicehttp = "0.4.18"
overlappingpanels = "0.1.5" overlappingpanels = "0.1.5"
@ -60,9 +56,6 @@ minSdk = "23"
compileSdk = "36" compileSdk = "36"
targetSdk = "36" targetSdk = "36"
versionCode = "68"
versionName = "4.7.0"
[libraries] [libraries]
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
anime-db = { module = "com.github.recloudstream:anime-db", version.ref = "animeDb" } 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" } espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } 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" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
json = { module = "org.json:json", version.ref = "json" } json = { module = "org.json:json", version.ref = "json" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { module = "junit:junit", version.ref = "junit" } junit = { module = "junit:junit", version.ref = "junit" }
junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitKtx" } junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitKtx" }
juniversalchardet = { module = "com.github.albfernandez:juniversalchardet", version.ref = "juniversalchardet" } 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-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-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-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-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" } 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" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaGradlePlugin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm" , version.ref = "kotlinGradlePlugin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm" , version.ref = "kotlinGradlePlugin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", 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] [bundles]
coil = ["coil", "coil-network-okhttp"] coil = ["coil", "coil-network-okhttp"]

View file

@ -12,7 +12,6 @@ plugins {
alias(libs.plugins.android.multiplatform.library) alias(libs.plugins.android.multiplatform.library)
alias(libs.plugins.buildkonfig) alias(libs.plugins.buildkonfig)
alias(libs.plugins.dokka) alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.serialization)
} }
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
@ -57,31 +56,12 @@ kotlin {
implementation(libs.annotation) // Annotations implementation(libs.annotation) // Annotations
implementation(libs.nicehttp) // HTTP Lib implementation(libs.nicehttp) // HTTP Lib
implementation(libs.jackson.module.kotlin) // JSON Parser implementation(libs.jackson.module.kotlin) // JSON Parser
implementation(libs.kotlinx.atomicfu)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime) implementation(libs.fuzzywuzzy) // Match Extractors
implementation(libs.kotlinx.serialization.json) // JSON Parser
implementation(libs.ktor.http)
implementation(libs.jsoup) // HTML Parser implementation(libs.jsoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript implementation(libs.rhino) // Run JavaScript
implementation(libs.newpipeextractor)
implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit 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", "MDL_API_KEY",
(System.getenv("MDL_API_KEY") ?: localProperties["mdl.key"]).toString() (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.debugException
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe 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.main
import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.nicehttp.requestCreator import com.lagradost.nicehttp.requestCreator
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import java.net.URI
/** /**
* When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...)
@ -120,7 +119,7 @@ actual class WebViewResolver actual constructor(
} }
var fixedRequest: Request? = null var fixedRequest: Request? = null
val extraRequestList = atomicListOf<Request>() val extraRequestList = threadSafeListOf<Request>()
main { main {
try { try {
@ -212,7 +211,7 @@ actual class WebViewResolver actual constructor(
* */ * */
return@runBlocking try { return@runBlocking try {
when { when {
blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith( blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
"/favicon.ico" "/favicon.ico"
) -> WebResourceResponse( ) -> WebResourceResponse(
"image/png", "image/png",

View file

@ -17,34 +17,21 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson 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.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF
import com.lagradost.nicehttp.RequestBodyTypes 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.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import kotlinx.datetime.LocalDate import java.net.URI
import kotlinx.datetime.LocalTime import java.text.SimpleDateFormat
import kotlinx.datetime.TimeZone import java.util.*
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 kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Clock
import kotlin.time.Instant
/** /**
* API available only on prerelease builds. * API available only on prerelease builds.
@ -87,27 +74,20 @@ const val USER_AGENT =
class ErrorLoadingException(message: String? = null) : Exception(message) class ErrorLoadingException(message: String? = null) : Exception(message)
//val baseHeader = mapOf("User-Agent" to USER_AGENT) //val baseHeader = mapOf("User-Agent" to USER_AGENT)
@Prerelease
val json = Json {
encodeDefaults = true
explicitNulls = false
ignoreUnknownKeys = true
}
val mapper = JsonMapper.builder().addModule(kotlinModule()) val mapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
object APIHolder { object APIHolder {
val unixTimeMS: Long
get() = Clock.System.now().toEpochMilliseconds()
val unixTime: Long 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() { fun initAll() {
allProviders.withLock { synchronized(allProviders) {
for (api in allProviders) { for (api in allProviders) {
api.init() api.init()
} }
@ -117,28 +97,28 @@ object APIHolder {
/** String extension function to Capitalize first char of string.*/ /** String extension function to Capitalize first char of string.*/
fun String.capitalize(): 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 var apiMap: Map<String, Int>? = null
fun addPluginMapping(plugin: MainAPI) { fun addPluginMapping(plugin: MainAPI) {
apis.withLock { synchronized(apis) {
apis = apis + plugin apis = apis + plugin
} }
initMap(true) initMap(true)
} }
fun removePluginMapping(plugin: MainAPI) { fun removePluginMapping(plugin: MainAPI) {
apis.withLock { synchronized(apis) {
apis = apis.filter { it != plugin } apis = apis.filter { it != plugin }
} }
initMap(true) initMap(true)
} }
private fun initMap(forcedUpdate: Boolean = false) { private fun initMap(forcedUpdate: Boolean = false) {
apis.withLock { synchronized(apis) {
if (apiMap == null || forcedUpdate) if (apiMap == null || forcedUpdate)
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
} }
@ -146,10 +126,10 @@ object APIHolder {
fun getApiFromNameNull(apiName: String?): MainAPI? { fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null if (apiName == null) return null
return allProviders.withLock { synchronized(allProviders) {
initMap() initMap()
apis.withLock { synchronized(apis) {
apiMap?.get(apiName)?.let { apis.getOrNull(it) } return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
// Leave the ?. null check, it can crash regardless // Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it.name == apiName } ?: allProviders.firstOrNull { it.name == apiName }
} }
@ -158,10 +138,13 @@ object APIHolder {
fun getApiFromUrlNull(url: String?): MainAPI? { fun getApiFromUrlNull(url: String?): MainAPI? {
if (url == null) return null if (url == null) return null
return allProviders.withLock { synchronized(allProviders) {
allProviders.firstOrNull { url.startsWith(it.mainUrl) } allProviders.forEach { api ->
if (url.startsWith(api.mainUrl)) return api
} }
} }
return null
}
/** /**
* Gets the website captcha token * Gets the website captcha token
@ -178,9 +161,9 @@ object APIHolder {
// To get the key // To get the key
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
try { try {
val _url = Url(url) val uri = URI.create(url)
val domain = base64Encode( val domain = base64Encode(
(_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(), (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
).replace("\n", "").replace("=", ".") ).replace("\n", "").replace("=", ".")
val vToken = val vToken =
@ -489,7 +472,7 @@ abstract class MainAPI {
} }
fun init() { fun init() {
overrideData?.get(this::class.simpleName)?.let { data -> overrideData?.get(this.javaClass.simpleName)?.let { data ->
overrideWithNewData(data) overrideWithNewData(data)
} }
} }
@ -703,22 +686,17 @@ abstract class MainAPI {
} }
} }
/** Might need a different implementation for desktop*/
fun base64Decode(string: String): String { fun base64Decode(string: String): String {
// ISO-8859-1 decoding: each byte maps directly to its Unicode code point (0-255), return String(base64DecodeArray(string), Charsets.ISO_8859_1)
// 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())
}
}
} }
@OptIn(ExperimentalEncodingApi::class)
fun base64DecodeArray(string: String): ByteArray { fun base64DecodeArray(string: String): ByteArray {
return Base64.decode(string) return Base64.decode(string)
} }
@OptIn(ExperimentalEncodingApi::class)
fun base64Encode(array: ByteArray): String { fun base64Encode(array: ByteArray): String {
return Base64.encode(array) return Base64.encode(array)
} }
@ -1330,23 +1308,23 @@ fun getQualityFromString(string: String?): SearchQuality? {
* ``` * ```
*/ */
fun MainAPI.updateUrl(url: String): String { fun MainAPI.updateUrl(url: String): String {
return try { try {
val original = Url(url) val original = URI(url)
val updated = Url(mainUrl) val updated = URI(mainUrl)
URLBuilder().apply { // URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment)
takeFrom(updated) return URI(
user = original.user updated.scheme,
password = original.password original.userInfo,
encodedPath = original.encodedPath updated.host,
fragment = original.fragment updated.port,
original.path,
parameters.clear() original.query,
parameters.appendAll(original.parameters) original.fragment
}.buildString() ).toString()
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
url return url
} }
} }
@ -1510,7 +1488,7 @@ constructor(
override var posterUrl: String? = null, override var posterUrl: String? = null,
var year: Int? = null, var year: Int? = null,
var dubStatus: MutableSet<DubStatus>? = null, var dubStatus: EnumSet<DubStatus>? = null,
var otherName: String? = null, var otherName: String? = null,
var episodes: MutableMap<DubStatus, Int> = mutableMapOf(), var episodes: MutableMap<DubStatus, Int> = mutableMapOf(),
@ -1519,10 +1497,46 @@ constructor(
override var quality: SearchQuality? = null, override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
override var score: Score? = 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) { 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 (this.type?.isMovieType() != true)
if (episodes != null && episodes > 0) if (episodes != null && episodes > 0)
this.episodes[status] = episodes this.episodes[status] = episodes
@ -2521,45 +2535,15 @@ constructor(
get() = score?.toInt(100) get() = score?.toInt(100)
} }
@OptIn(FormatStringsInDatetimeFormats::class)
fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") { fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") {
if (date == null) return try {
this.date = runCatching { this.date = SimpleDateFormat(format, Locale.getDefault()).parse(date ?: return)?.time
// First try standard ISO 8601 (e.g. "2026-01-01T12:30:00.000Z", "2026-05-17T14:35+02:00") } catch (e: Exception) {
runCatching { Instant.parse(date).toEpochMilliseconds() } logError(e)
.getOrElse {
val fmt = DateTimeComponents.Format { byUnicodePattern(format) }
val components = DateTimeComponents.parse(date, fmt)
/**
* Try multiple conversions in order of precision for non-ISO-8601 formats,
* since the date string may or may not include time and/or timezone offset:
* 1. If the custom format produced a UTC offset (e.g. "2026-05-17 14:35+02:00"), use it directly
* 2. If it has time but no offset (e.g. "2026-05-17 14:35"), fall back to device timezone
* 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
*/
runCatching { components.toInstantUsingOffset().toEpochMilliseconds() }
.recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
.getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
} }
}.onFailure { logError(it) }.getOrNull()
} }
@Prerelease fun Episode.addDate(date: Date?) {
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?) {
this.date = date?.time this.date = date?.time
} }
@ -2696,27 +2680,6 @@ fun fetchUrls(text: String?): List<String> {
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList() 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( @Deprecated(
"toRatingInt() is deprecated. Use new score API instead.", "toRatingInt() is deprecated. Use new score API instead.",
level = DeprecationLevel.ERROR level = DeprecationLevel.ERROR

View file

@ -1,33 +1,39 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.fasterxml.jackson.databind.DeserializationFeature
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import kotlin.reflect.KClass import kotlin.reflect.KClass
// Short name for requests client to make it nicer to use // 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 { 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? { override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): T? {
return try { return try {
parse(text, kClass) mapper.readValue(text, kClass.java)
} catch (_: Exception) { } catch (e: Exception) {
null null
} }
} }
override fun writeValueAsString(obj: Any): String { override fun writeValueAsString(obj: Any): String {
return obj.toJson() return mapper.writeValueAsString(obj)
} }
} }
/** The default networking helper. This helper performs SSL checks. /** The default networking helper. This helper performs SSL checks.
* If you need to make requests to websites with invalid SSL certificates use insecureApp instead. */ * 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) 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. */ * This should NEVER be used for sensitive networking operations such as logins. Only use this when required. */
@Prerelease @Prerelease
@UnsafeSSL @UnsafeSSL
var insecureApp = Requests(responseParser = jsonResponseParser).apply { var insecureApp = Requests(responseParser = jacksonResponseParser).apply {
defaultHeaders = mapOf("user-agent" to USER_AGENT) 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
import io.ktor.http.Url import java.net.URI
import io.ktor.http.decodeURLPart import java.nio.charset.StandardCharsets
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -46,11 +46,11 @@ open class ByseSX : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { 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 { private fun getCodeFromUrl(url: String): String {
val path = Url(url).encodedPath.decodeURLPart() val path = URI(url).path ?: ""
return path.trimEnd('/').substringAfterLast('/') return path.trimEnd('/').substringAfterLast('/')
} }
@ -94,7 +94,7 @@ open class ByseSX : ExtractorApi() {
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
val plainBytes = cipher.doFinal(cipherBytes) val plainBytes = cipher.doFinal(cipherBytes)
var jsonStr = plainBytes.decodeToString() var jsonStr = String(plainBytes, StandardCharsets.UTF_8)
if (jsonStr.startsWith("\uFEFF")) jsonStr = jsonStr.substring(1) 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import java.net.URLDecoder
open class Cda : ExtractorApi() { open class Cda : ExtractorApi() {
override var mainUrl = "https://ebd.cda.pl" override var mainUrl = "https://ebd.cda.pl"
override var name = "Cda" override var name = "Cda"
override val requiresReferer = false override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val mediaId = url val mediaId = url
.split("/").last() .split("/").last()
@ -64,10 +65,10 @@ open class Cda : ExtractorApi() {
.replace("_QWE", "") .replace("_QWE", "")
.replace("_Q5", "") .replace("_Q5", "")
.replace("_IKSDE", "") .replace("_IKSDE", "")
a = a.decodeUrl() a = URLDecoder.decode(a, "UTF-8")
a = a.map { char -> a = a.map { char ->
if (char.code in 33..126) { 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 { } else {
return@map char 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor 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/ // deobfuscated from https://hglink.to/main.js?v=1.1.3 using https://deobfuscate.io/
private val mirrors = arrayOf( private val mirrors = arrayOf(
@ -90,7 +90,7 @@ abstract class CineMMRedirect : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
) { ) {
val videoId = Url(url).encodedPath val videoId = url.toHttpUrl().encodedPath
val mirror = mirrors.random() val mirror = mirrors.random()
// re-use existing extractors by calling the ExtractorApi // re-use existing extractors by calling the ExtractorApi

View file

@ -27,7 +27,7 @@ open class CloudMailRu : ExtractorApi() {
"Origin" to mainUrl, "Origin" to mainUrl,
"User-Agent" to USER_AGENT, "User-Agent" to USER_AGENT,
) )
val vidId = url.substringAfter("public/").encodeToByteArray() val vidId = url.substringAfter("public/").toByteArray()
val vidIdEnc = base64Encode(vidId) val vidIdEnc = base64Encode(vidId)
val videoReq = app.get(url, headers=headers).text val videoReq = app.get(url, headers=headers).text
val regex = Regex(pattern = "videowl_view\":\\{\"count\":\"1\",\"url\":\"([^\"]*)\"\\}", options = setOf(RegexOption.IGNORE_CASE)) 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import io.ktor.http.Url import java.net.URI
import io.ktor.http.decodeURLPart
class Geodailymotion : Dailymotion() { class Geodailymotion : Dailymotion() {
override val name = "GeoDailymotion" override val name = "GeoDailymotion"
@ -56,6 +57,7 @@ open class Dailymotion : ExtractorApi() {
} }
} }
private fun getEmbedUrl(url: String): String? { private fun getEmbedUrl(url: String): String? {
if (url.contains("/embed/") || url.contains("/video/")) return url if (url.contains("/embed/") || url.contains("/video/")) return url
if (url.contains("geo.dailymotion.com")) { if (url.contains("geo.dailymotion.com")) {
@ -65,8 +67,9 @@ open class Dailymotion : ExtractorApi() {
return null return null
} }
private fun getVideoId(url: String): String? { private fun getVideoId(url: String): String? {
val path = Url(url).encodedPath.decodeURLPart() val path = URI(url).path
val id = path.substringAfter("/video/") val id = path.substringAfter("/video/")
return if (id.matches(videoIdRegex)) id else null return if (id.matches(videoIdRegex)) id else null
} }
@ -79,6 +82,7 @@ open class Dailymotion : ExtractorApi() {
return generateM3u8(name, streamLink, "").forEach(callback) return generateM3u8(name, streamLink, "").forEach(callback)
} }
data class MetaData( data class MetaData(
val qualities: Map<String, List<Quality>>?, val qualities: Map<String, List<Quality>>?,
val subtitles: SubtitlesWrapper? val subtitles: SubtitlesWrapper?
@ -98,4 +102,5 @@ open class Dailymotion : ExtractorApi() {
val label: String, val label: String,
val urls: List<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.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url import java.net.URI
class Doodspro : DoodLaExtractor() { class Doodspro : DoodLaExtractor() {
override var mainUrl = "https://doods.pro" override var mainUrl = "https://doods.pro"
@ -138,6 +138,8 @@ open class DoodLaExtractor : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import io.ktor.http.Url import java.net.URI
class Techinmind: GDMirrorbot() { class Techinmind: GDMirrorbot() {
override var name = "Techinmind Cloud AIO" override var name = "Techinmind Cloud AIO"
@ -103,7 +103,7 @@ open class GDMirrorbot : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { 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) val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+")) ?.split(Regex("\\D+"))
?.joinToString("") { ?.joinToString("") {
it.toInt().toChar().toString() Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password") ?: throw ErrorLoadingException("can't find password")
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "") 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.ExtractorLinkType
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import kotlin.math.round
open class Gofile : ExtractorApi() { open class Gofile : ExtractorApi() {
override val name = "Gofile" override val name = "Gofile"
@ -68,19 +67,10 @@ open class Gofile : ExtractorApi() {
?: Qualities.Unknown.value ?: 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 { private fun formatBytes(bytes: Long): String {
val mb = 1024L * 1024
val gb = mb * 1024
return when { return when {
bytes < gb -> "${roundTo2Decimals(bytes.toDouble() / mb)} MB" bytes < 1024L * 1024 * 1024 -> "%.2f MB".format(bytes.toDouble() / (1024 * 1024))
else -> "${roundTo2Decimals(bytes.toDouble() / gb)} GB" else -> "%.2f GB".format(bytes.toDouble() / (1024 * 1024 * 1024))
} }
} }

View file

@ -2,12 +2,13 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.api.Log import com.lagradost.api.Log
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.extractors.helper.AesHelper
import com.lagradost.cloudstream3.utils.* 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() { open class HDMomPlayer : ExtractorApi() {
override val name = "HDMomPlayer" override val name = "HDMomPlayer"
@ -23,7 +24,7 @@ open class HDMomPlayer : ExtractorApi() {
if (bePlayer != null) { if (bePlayer != null) {
val bePlayerPass = bePlayer.get(1) val bePlayerPass = bePlayer.get(1)
val bePlayerData = bePlayer.get(2) 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) m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1)
} else { } else {
@ -31,7 +32,7 @@ open class HDMomPlayer : ExtractorApi() {
val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1)
if (trackStr != null) { if (trackStr != null) {
val tracks:List<Track> = parseJson<List<Track>>("[${trackStr}]") val tracks:List<Track> = jacksonObjectMapper().readValue("[${trackStr}]")
for (track in tracks) { for (track in tracks) {
if (track.file == null || track.label == null) continue 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.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url import java.net.URI
class HubCloud : ExtractorApi() { class HubCloud : ExtractorApi() {
override val name = "Hub-Cloud" override val name = "Hub-Cloud"
@ -24,7 +24,7 @@ class HubCloud : ExtractorApi() {
) { ) {
val tag = "HubCloud" val tag = "HubCloud"
val realUrl = url.takeIf { 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 } ?: return
val baseUrl=getBaseUrl(realUrl) val baseUrl=getBaseUrl(realUrl)
@ -161,7 +161,7 @@ class HubCloud : ExtractorApi() {
private fun getBaseUrl(url: String): String { private fun getBaseUrl(url: String): String {
return try { return try {
Url(url).let { "${it.protocol.name}://${it.host}" } URI(url).let { "${it.scheme}://${it.host}" }
} catch (_: Exception) { } catch (_: Exception) {
"" ""
} }

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities 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 com.lagradost.cloudstream3.utils.newExtractorLink
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -96,7 +96,7 @@ open class InternetArchive : ExtractorApi() {
if (mediaUrl.isNotEmpty()) { if (mediaUrl.isNotEmpty()) {
val name = if (mediaUrl.count() > 1) { val name = if (mediaUrl.count() > 1) {
val fileExtension = mediaUrl.substringAfterLast(".") val fileExtension = mediaUrl.substringAfterLast(".")
val fileNameCleaned = fileName.decodeUrl().substringBeforeLast('.') val fileNameCleaned = fileName.decodeUri().substringBeforeLast('.')
"$fileNameCleaned ($fileExtension)" "$fileNameCleaned ($fileExtension)"
} else this.name } else this.name
callback( callback(

View file

@ -14,10 +14,11 @@ open class Mvidoo : ExtractorApi() {
private fun String.decodeHex(): String { private fun String.decodeHex(): String {
require(length % 2 == 0) { "Must have an even length" } require(length % 2 == 0) { "Must have an even length" }
return chunked(2) return String(
chunked(2)
.map { it.toInt(16).toByte() } .map { it.toInt(16).toByte() }
.toByteArray() .toByteArray()
.decodeToString() )
} }
override suspend fun getUrl( override suspend fun getUrl(

View file

@ -32,7 +32,7 @@ open class Odnoklassniki : ExtractorApi() {
val embedUrl = url.replace("/video/","/videoembed/") val embedUrl = url.replace("/video/","/videoembed/")
val videoReq = app.get(embedUrl, headers=headers).text.replace("\\&quot;", "\"").replace("\\\\", "\\") val videoReq = app.get(embedUrl, headers=headers).text.replace("\\&quot;", "\"").replace("\\\\", "\\")
.replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> .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 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") 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
import java.nio.charset.StandardCharsets
import java.security.MessageDigest import java.security.MessageDigest
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
@ -170,7 +171,7 @@ open class Rabbitstream : ExtractorApi() {
IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size))
) )
val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found") val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found")
return decryptedData.decodeToString() return String(decryptedData, StandardCharsets.UTF_8)
} }
data class Tracks( data class Tracks(

View file

@ -35,14 +35,14 @@ open class RapidVid : ExtractorApi() {
if (extractedValue != null) { if (extractedValue != null) {
val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
decoded = bytes.decodeToString() decoded = String(bytes, Charsets.UTF_8)
} else { } else {
val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") 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("\\\\", "\\") val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\")
extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "") extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "")
val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() 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( callback.invoke(

View file

@ -1,7 +1,6 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.api.Log import com.lagradost.api.Log
import com.lagradost.cloudstream3.Prerelease
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app 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.utils.getPacked
import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.network.WebViewResolver
class Mwish : StreamWishExtractor() { class Mwish : StreamWishExtractor() {
override val name = "Mwish" override val name = "Mwish"
override val mainUrl = "https://mwish.pro" override val mainUrl = "https://mwish.pro"
@ -28,12 +28,6 @@ class Ewish : StreamWishExtractor() {
override val mainUrl = "https://embedwish.com" override val mainUrl = "https://embedwish.com"
} }
@Prerelease
class Hgcloudto : StreamWishExtractor() {
override val name = "Hgcloud"
override val mainUrl = "https://Hgcloud.to"
}
class WishembedPro : StreamWishExtractor() { class WishembedPro : StreamWishExtractor() {
override val name = "Wishembed" override val name = "Wishembed"
override val mainUrl = "https://wishembed.pro" 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.JsUnpacker
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import java.net.URI
open class Streamhub : ExtractorApi() { open class Streamhub : ExtractorApi() {
override var mainUrl = "https://streamhub.to" 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.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import io.ktor.http.Url import java.net.URI
open class Streamplay : ExtractorApi() { open class Streamplay : ExtractorApi() {
override val name = "Streamplay" override val name = "Streamplay"
@ -22,7 +22,9 @@ open class Streamplay : ExtractorApi() {
) { ) {
val request = app.get(url, referer = referer) val request = app.get(url, referer = referer)
val redirectUrl = request.url 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 key = redirectUrl.substringAfter("embed-").substringBefore(".html")
val token = val token =
request.document.select("script").find { it.data().contains("sitekey:") }?.data() 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.Context
import org.mozilla.javascript.EvaluatorException import org.mozilla.javascript.EvaluatorException
import org.mozilla.javascript.Scriptable import org.mozilla.javascript.Scriptable
import java.util.*
open class Userload : ExtractorApi() { open class Userload : ExtractorApi() {
override var name = "Userload" override var name = "Userload"
@ -14,7 +16,7 @@ open class Userload : ExtractorApi() {
private fun splitInput(input: String): List<String> { private fun splitInput(input: String): List<String> {
var counter = 0 var counter = 0
val array = mutableListOf<String>() val array = ArrayList<String>()
var buffer = "" var buffer = ""
for (c in input) { for (c in input) {
when (c) { when (c) {
@ -69,7 +71,7 @@ open class Userload : ExtractorApi() {
} }
var txtresult = "" var txtresult = ""
subchar.forEach{ 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 val1 = Regex(""""morocco="((.|\\n)*?)"&mycountry="""").find(txtresult)?.groups?.get(1)?.value.toString().drop(1).dropLast(1)
val val2 = txtresult.substringAfter("""&mycountry="+""").substringBefore(")") val val2 = txtresult.substringAfter("""&mycountry="+""").substringBefore(")")

View file

@ -1,7 +1,6 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi 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) val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
app.get( app.get(
"$mainUrl/api/?$id=&_=$unixTimeMS", "$mainUrl/api/?$id=&_=${System.currentTimeMillis()}",
headers = mapOf( headers = mapOf(
"X-Requested-With" to "XMLHttpRequest" "X-Requested-With" to "XMLHttpRequest"
), ),

View file

@ -35,14 +35,14 @@ open class VidMoxy : ExtractorApi() {
if (extractedValue != null) { if (extractedValue != null) {
val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
decoded = bytes.decodeToString() decoded = String(bytes, Charsets.UTF_8)
} else { } else {
val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") 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("\\\\", "\\") val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\")
extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "") extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "")
val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() 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( 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.Qualities
import com.lagradost.cloudstream3.utils.fixUrl import com.lagradost.cloudstream3.utils.fixUrl
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url import java.net.URI
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -84,7 +84,7 @@ open class VidStack : ExtractorApi() {
private fun getBaseUrl(url: String): String { private fun getBaseUrl(url: String): String {
return try { return try {
Url(url).let { "${it.protocol.name}://${it.host}" } URI(url).let { "${it.scheme}://${it.host}" }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Vidstack", "getBaseUrl fallback: ${e.message}") Log.e("Vidstack", "getBaseUrl fallback: ${e.message}")
mainUrl mainUrl

View file

@ -47,7 +47,7 @@ class Videa : ExtractorApi() {
rawBytes[4] == 0x6C.toByte() // 'l' rawBytes[4] == 0x6C.toByte() // 'l'
val videaXml = if (isXml) { val videaXml = if (isXml) {
rawBytes.decodeToString() String(rawBytes, Charsets.UTF_8)
} else { } else {
// Handle encrypted XML response // Handle encrypted XML response
val xsHeader = response.headers["X-Videa-Xs"] ?: return val xsHeader = response.headers["X-Videa-Xs"] ?: return
@ -179,7 +179,7 @@ class Videa : ExtractorApi() {
} }
val actualEncryptedBytes = if (isBase64) { val actualEncryptedBytes = if (isBase64) {
val base64String = encryptedBytes.decodeToString() val base64String = String(encryptedBytes, Charsets.UTF_8)
.replace("\r", "") .replace("\r", "")
.replace("\n", "") .replace("\n", "")
.replace(" ", "") .replace(" ", "")
@ -189,7 +189,7 @@ class Videa : ExtractorApi() {
encryptedBytes encryptedBytes
} }
val keyBytes = key.encodeToByteArray() val keyBytes = key.toByteArray(Charsets.UTF_8)
// RC4 key-scheduling algorithm (KSA) // RC4 key-scheduling algorithm (KSA)
val s = IntArray(256) { it } val s = IntArray(256) { it }
@ -211,6 +211,6 @@ class Videa : ExtractorApi() {
result[k] = ((actualEncryptedBytes[k].toInt() and 0xFF) xor keyStreamByte).toByte() 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 package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.api.Log import com.lagradost.api.Log
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.* 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() { open class VideoSeyred : ExtractorApi() {
override val name = "VideoSeyred" override val name = "VideoSeyred"
@ -19,7 +20,7 @@ open class VideoSeyred : ExtractorApi() {
val videoUrl = "${mainUrl}/playlist/${videoId}.json" val videoUrl = "${mainUrl}/playlist/${videoId}.json"
val responseRaw = app.get(videoUrl) 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] val response = responseList[0]
for (track in response.tracks) { for (track in response.tracks) {

View file

@ -38,13 +38,13 @@ class Vidsonic() : ExtractorApi() {
.substringBefore(";") .substringBefore(";")
.replace("'", "") .replace("'", "")
// (improved) Kotlin implementation of the JavaScript code from above // (improved) Java implementation of the JavaScript code from above
val streamUrl = encodedStreamUrl val streamUrl = encodedStreamUrl
.replace("|", "") .replace("|", "")
// always two base16 digits together build one ASCII char // always two base16 digits together build one ASCII char
.chunked(2) .chunked(2)
.map { .map {
it.toInt(16).toChar() Integer.parseInt(it, 16).toChar()
} }
.joinToString("") .joinToString("")
.reversed() .reversed()

View file

@ -1,20 +1,14 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.newExtractorLink
expect open class YoutubeExtractor() : ExtractorApi { import com.lagradost.cloudstream3.utils.ExtractorLinkType
override val mainUrl: String import org.schabi.newpipe.extractor.stream.StreamInfo
override val name: String import org.schabi.newpipe.extractor.stream.StreamType
override val requiresReferer: Boolean
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
)
}
class YoutubeShortLinkExtractor : YoutubeExtractor() { class YoutubeShortLinkExtractor : YoutubeExtractor() {
override val mainUrl = "https://youtu.be" override val mainUrl = "https://youtu.be"
@ -27,3 +21,107 @@ class YoutubeMobileExtractor : YoutubeExtractor() {
class YoutubeNoCookieExtractor : YoutubeExtractor() { class YoutubeNoCookieExtractor : YoutubeExtractor() {
override val mainUrl = "https://www.youtube-nocookie.com" 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.base64DecodeArray
import com.lagradost.cloudstream3.base64Encode import com.lagradost.cloudstream3.base64Encode
import java.util.Arrays
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import java.nio.charset.StandardCharsets
import kotlin.math.min import kotlin.math.min
/** /**
@ -46,9 +48,9 @@ object CryptoJS {
// Create CryptoJS-like encrypted! // Create CryptoJS-like encrypted!
val sBytes = APPEND.toByteArray() val sBytes = APPEND.toByteArray()
val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size) val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size)
sBytes.copyInto(destination = b, destinationOffset = 0) System.arraycopy(sBytes, 0, b, 0, sBytes.size)
saltBytes.copyInto(destination = b, destinationOffset = sBytes.size) System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size)
cipherText.copyInto(destination = b, destinationOffset = sBytes.size + saltBytes.size) System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size)
return base64Encode(b) return base64Encode(b)
} }
@ -61,8 +63,8 @@ object CryptoJS {
*/ */
fun decrypt(password: String, cipherText: String): String { fun decrypt(password: String, cipherText: String): String {
val ctBytes = base64DecodeArray(cipherText) val ctBytes = base64DecodeArray(cipherText)
val saltBytes = ctBytes.copyOfRange(8, 16) val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
val cipherTextBytes = ctBytes.copyOfRange(16, ctBytes.size) val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
val key = ByteArray(KEY_SIZE / 8) val key = ByteArray(KEY_SIZE / 8)
val iv = ByteArray(IV_SIZE / 8) val iv = ByteArray(IV_SIZE / 8)
@ -105,18 +107,16 @@ object CryptoJS {
hash.reset() hash.reset()
} }
block!!.copyInto( System.arraycopy(
destination = derivedBytes, block!!, 0, derivedBytes, numberOfDerivedWords * 4,
destinationOffset = numberOfDerivedWords * 4, min(block.size, (targetKeySize - numberOfDerivedWords) * 4)
startIndex = 0,
endIndex = min(block.size, (targetKeySize - numberOfDerivedWords) * 4)
) )
numberOfDerivedWords += block.size / 4 numberOfDerivedWords += block.size / 4
} }
derivedBytes.copyInto(destination = resultKey, destinationOffset = 0, startIndex = 0, endIndex = keySize * 4) System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4)
derivedBytes.copyInto(destination = resultIv, destinationOffset = 0, startIndex = keySize * 4, endIndex = (keySize * 4) + (ivSize * 4)) System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4)
return derivedBytes // key + iv 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.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import java.net.URI
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -88,8 +88,8 @@ object GogoHelper {
val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall
val foundDecryptKey = secretDecryptKey ?: foundKey val foundDecryptKey = secretDecryptKey ?: foundKey
val url = Url(iframeUrl) val uri = URI(iframeUrl)
val mainUrl = "https://${url.host}" val mainUrl = "https://" + uri.host
val encryptedId = cryptoHandler(id, foundIv, foundKey) val encryptedId = cryptoHandler(id, foundIv, foundKey)
val encryptRequestData = if (isUsingAdaptiveData) { val encryptRequestData = if (isUsingAdaptiveData) {

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors.helper package com.lagradost.cloudstream3.extractors.helper
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
import com.lagradost.cloudstream3.utils.StringUtils.encodeUrl 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 // 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 // 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() fun encode(input: String): String =
private fun decode(input: String): String = input.decodeUrl() input.encodeUri().replace("+", "%20")
private fun decode(input: String): String = input.decodeUri()
} }

View file

@ -30,9 +30,11 @@ class CrossTmdbProvider : TmdbProvider() {
} }
private val validApis 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 } //.distinctBy { it.uniqueId }
data class CrossMetaData( data class CrossMetaData(
@JsonProperty("isSuccess") val isSuccess: Boolean, @JsonProperty("isSuccess") val isSuccess: Boolean,
@JsonProperty("movies") val movies: List<Pair<String, String>>? = null, @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