mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-06-19 20:05:41 +00:00
Compare commits
1 commit
master
...
fix-downlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
274943c1a6 |
120 changed files with 1117 additions and 2729 deletions
1
.github/workflows/build_to_archive.yml
vendored
1
.github/workflows/build_to_archive.yml
vendored
|
|
@ -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
98
.github/workflows/issue_action.yml
vendored
Normal 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'
|
||||||
1
.github/workflows/prerelease.yml
vendored
1
.github/workflows/prerelease.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/pull_request.yml
vendored
2
.github/workflows/pull_request.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -603,4 +601,4 @@ object CommonActivity {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -408,10 +408,13 @@ 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)) {
|
||||||
return true
|
loadResult(str, api.name, "")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,7 +26,9 @@ 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
|
||||||
VideoClickActionHolder.allVideoClickActions.add(element)
|
synchronized(VideoClickActionHolder.allVideoClickActions) {
|
||||||
|
VideoClickActionHolder.allVideoClickActions.add(element)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,4 +40,4 @@ abstract class Plugin : BasePlugin() {
|
||||||
* This will add a button in the settings allowing you to add custom settings
|
* This will add a button in the settings allowing you to add custom settings
|
||||||
*/
|
*/
|
||||||
var openSettings: ((context: Context) -> Unit)? = null
|
var openSettings: ((context: Context) -> Unit)? = null
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
classLoaders[loader] = pluginInstance
|
||||||
}
|
urlPlugins[data.url ?: filePath] = pluginInstance
|
||||||
synchronized(classLoaders) {
|
|
||||||
classLoaders[loader] = pluginInstance
|
|
||||||
}
|
|
||||||
synchronized(urlPlugins) {
|
|
||||||
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
|
||||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
|
synchronized(APIHolder.apis) {
|
||||||
removePluginMapping(it)
|
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
|
||||||
|
removePluginMapping(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(APIHolder.allProviders) {
|
||||||
|
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
|
||||||
}
|
}
|
||||||
|
|
||||||
APIHolder.allProviders.withLock {
|
synchronized(extractorApis) {
|
||||||
APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename }
|
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
|
||||||
}
|
}
|
||||||
|
|
||||||
extractorApis.withLock {
|
synchronized(VideoClickActionHolder.allVideoClickActions) {
|
||||||
extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename }
|
VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename }
|
||||||
}
|
|
||||||
|
|
||||||
VideoClickActionHolder.allVideoClickActions.withLock {
|
|
||||||
VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(classLoaders) {
|
synchronized(classLoaders) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() {
|
||||||
override var score: Score? = null,
|
override var score: Score? = null,
|
||||||
val tags: List<String>? = null
|
val tags: List<String>? = null
|
||||||
) : SearchResponse
|
) : SearchResponse
|
||||||
}
|
}
|
||||||
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
@ -84,8 +83,8 @@ class AniListApi : SyncAPI() {
|
||||||
return "$mainUrl/anime/$id"
|
return "$mainUrl/anime/$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -97,7 +96,7 @@ class AniListApi : SyncAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
|
override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? {
|
||||||
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
|
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
|
||||||
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
|
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
|
||||||
val season = getSeason(internalId).data.media
|
val season = getSeason(internalId).data.media
|
||||||
|
|
@ -159,7 +158,7 @@ class AniListApi : SyncAPI() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
|
override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
val data = getDataAboutId(auth ?: return null, internalId) ?: return null
|
val data = getDataAboutId(auth ?: return null, internalId) ?: return null
|
||||||
|
|
||||||
|
|
@ -460,7 +459,7 @@ class AniListApi : SyncAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? {
|
private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? {
|
||||||
val q =
|
val q =
|
||||||
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
|
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
|
||||||
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
||||||
|
|
@ -507,7 +506,7 @@ class AniListApi : SyncAPI() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? {
|
private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? {
|
||||||
return app.post(
|
return app.post(
|
||||||
"https://graphql.anilist.co/",
|
"https://graphql.anilist.co/",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
|
|
@ -639,7 +638,7 @@ class AniListApi : SyncAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? {
|
override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? {
|
||||||
val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
|
val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
|
||||||
convertAniListStringToStatus(it.status ?: "").stringRes
|
convertAniListStringToStatus(it.status ?: "").stringRes
|
||||||
}?.mapValues { group ->
|
}?.mapValues { group ->
|
||||||
|
|
@ -667,7 +666,7 @@ class AniListApi : SyncAPI() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? {
|
private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? {
|
||||||
val userID = auth.user.id
|
val userID = auth.user.id
|
||||||
val mediaType = "ANIME"
|
val mediaType = "ANIME"
|
||||||
|
|
||||||
|
|
@ -715,7 +714,7 @@ class AniListApi : SyncAPI() {
|
||||||
return text?.toKotlinObject()
|
return text?.toKotlinObject()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toggleLike(auth: AuthData, id: Int): Boolean {
|
suspend fun toggleLike(auth : AuthData, id: Int): Boolean {
|
||||||
val q = """mutation (${'$'}animeId: Int = $id) {
|
val q = """mutation (${'$'}animeId: Int = $id) {
|
||||||
ToggleFavourite (animeId: ${'$'}animeId) {
|
ToggleFavourite (animeId: ${'$'}animeId) {
|
||||||
anime {
|
anime {
|
||||||
|
|
@ -738,7 +737,7 @@ class AniListApi : SyncAPI() {
|
||||||
data class MediaListId(@JsonProperty("id") val id: Long? = null)
|
data class MediaListId(@JsonProperty("id") val id: Long? = null)
|
||||||
|
|
||||||
private suspend fun postDataAboutId(
|
private suspend fun postDataAboutId(
|
||||||
auth: AuthData,
|
auth : AuthData,
|
||||||
id: Int,
|
id: Int,
|
||||||
type: AniListStatusType,
|
type: AniListStatusType,
|
||||||
score: Score?,
|
score: Score?,
|
||||||
|
|
@ -787,7 +786,7 @@ class AniListApi : SyncAPI() {
|
||||||
return data != ""
|
return data != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getUser(token: AuthToken): AniListUser? {
|
private suspend fun getUser(token : AuthToken): AniListUser? {
|
||||||
val q = """
|
val q = """
|
||||||
{
|
{
|
||||||
Viewer {
|
Viewer {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -98,9 +98,9 @@ class MALApi : SyncAPI() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||||
val auth = auth?.token?.accessToken ?: return null
|
val 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",
|
||||||
|
|
@ -122,7 +122,7 @@ class MALApi : SyncAPI() {
|
||||||
Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
|
Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
|
||||||
|
|
||||||
override suspend fun updateStatus(
|
override suspend fun updateStatus(
|
||||||
auth: AuthData?,
|
auth : AuthData?,
|
||||||
id: String,
|
id: String,
|
||||||
newStatus: SyncAPI.AbstractSyncStatus
|
newStatus: SyncAPI.AbstractSyncStatus
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
@ -225,7 +225,7 @@ class MALApi : SyncAPI() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
|
override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? {
|
||||||
val auth = auth?.token?.accessToken ?: return null
|
val auth = auth?.token?.accessToken ?: return null
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
val url =
|
val url =
|
||||||
|
|
@ -271,7 +271,7 @@ class MALApi : SyncAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
|
override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
|
||||||
val auth = auth?.token?.accessToken ?: return null
|
val auth = auth?.token?.accessToken ?: return null
|
||||||
|
|
||||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
||||||
|
|
@ -477,7 +477,7 @@ class MALApi : SyncAPI() {
|
||||||
@JsonProperty("start_time") val startTime: String?
|
@JsonProperty("start_time") val startTime: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun library(auth: AuthData?): LibraryMetadata? {
|
override suspend fun library(auth : AuthData?): LibraryMetadata? {
|
||||||
val list = getMalAnimeListSmart(auth ?: return null)?.groupBy {
|
val list = getMalAnimeListSmart(auth ?: return null)?.groupBy {
|
||||||
convertToStatus(it.listStatus?.status ?: "").stringRes
|
convertToStatus(it.listStatus?.status ?: "").stringRes
|
||||||
}?.mapValues { group ->
|
}?.mapValues { group ->
|
||||||
|
|
@ -505,7 +505,7 @@ class MALApi : SyncAPI() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getMalAnimeListSmart(auth: AuthData): Array<Data>? {
|
private suspend fun getMalAnimeListSmart(auth : AuthData): Array<Data>? {
|
||||||
return if (requireLibraryRefresh) {
|
return if (requireLibraryRefresh) {
|
||||||
val list = getMalAnimeList(auth.token)
|
val list = getMalAnimeList(auth.token)
|
||||||
setKey(MAL_CACHED_LIST, auth.user.id.toString(), list)
|
setKey(MAL_CACHED_LIST, auth.user.id.toString(), list)
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +66,9 @@ class APIRepository(val api: MainAPI) {
|
||||||
|
|
||||||
private fun afterPluginsLoaded(forceReload: Boolean) {
|
private fun afterPluginsLoaded(forceReload: Boolean) {
|
||||||
if (forceReload) {
|
if (forceReload) {
|
||||||
cache.clear()
|
synchronized(cache) {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -217,4 +215,4 @@ class APIRepository(val api: MainAPI) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -443,4 +449,4 @@ class ControllerActivity : ExpandedControllerActivity() {
|
||||||
SkipNextEpisodeController(skipOpButton)
|
SkipNextEpisodeController(skipOpButton)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
it.supportedSyncNames.contains(syncId)
|
allProviders.filter {
|
||||||
}.map { it.name } +
|
it.supportedSyncNames.contains(syncId)
|
||||||
// Add the api if it exists
|
}.map { it.name } +
|
||||||
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
|
// Add the api if it exists
|
||||||
?: emptyList())
|
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
|
||||||
|
?: emptyList())
|
||||||
|
}
|
||||||
val baseOptions = listOf(
|
val baseOptions = listOf(
|
||||||
LibraryOpenerType.Default,
|
LibraryOpenerType.Default,
|
||||||
LibraryOpenerType.None,
|
LibraryOpenerType.None,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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) {
|
||||||
it.name.contains("gogoanime", true) ||
|
apis.filter {
|
||||||
it.name.contains("9anime", true)
|
it.name.contains("gogoanime", true) ||
|
||||||
}.map {
|
it.name.contains("9anime", true)
|
||||||
it.name
|
}.map {
|
||||||
|
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))
|
||||||
|
|
@ -2706,4 +2706,4 @@ class ResultViewModel2 : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -222,4 +214,4 @@ object DataStore {
|
||||||
inline fun <reified T : Any> Context.getKey(folder: String, path: String, defVal: T?): T? {
|
inline fun <reified T : Any> Context.getKey(folder: String, path: String, defVal: T?): T? {
|
||||||
return getKey(getFolderName(folder, path), defVal) ?: defVal
|
return getKey(getFolderName(folder, path), defVal) ?: defVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 = {}
|
||||||
|
|
@ -184,4 +173,4 @@ object ImageLoader {
|
||||||
imageData: ByteBuffer?,
|
imageData: ByteBuffer?,
|
||||||
builder: ImageRequest.Builder.() -> Unit = {}
|
builder: ImageRequest.Builder.() -> Unit = {}
|
||||||
) = loadImageInternal(imageData = imageData, builder = builder)
|
) = loadImageInternal(imageData = imageData, builder = builder)
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,8 +96,10 @@ object SyncUtil {
|
||||||
.mapNotNull { it.url }.toMutableList()
|
.mapNotNull { it.url }.toMutableList()
|
||||||
|
|
||||||
if (type == "anilist") { // TODO MAKE BETTER
|
if (type == "anilist") { // TODO MAKE BETTER
|
||||||
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
synchronized(apis) {
|
||||||
current.add("${it.mainUrl}/anime/$id")
|
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
||||||
|
current.add("${it.mainUrl}/anime/$id")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return current
|
return current
|
||||||
|
|
@ -167,4 +169,4 @@ object SyncUtil {
|
||||||
@JsonProperty("updatedAt") val updatedAt: String?,
|
@JsonProperty("updatedAt") val updatedAt: String?,
|
||||||
@JsonProperty("deletedAt") val deletedAt: String?
|
@JsonProperty("deletedAt") val deletedAt: String?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -48,10 +48,6 @@ object TestingUtils {
|
||||||
messageLog.add(Message(LogLevel.Error, message))
|
messageLog.add(Message(LogLevel.Error, message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fail(message: String): Nothing = throw AssertionError(message)
|
|
||||||
private fun assertTrue(message: String, condition: Boolean) { if (!condition) fail(message) }
|
|
||||||
private fun assertNotNull(message: String, value: Any?) { if (value == null) fail(message) }
|
|
||||||
|
|
||||||
class TestResultList(val results: List<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
|
||||||
|
|
@ -325,4 +321,4 @@ object TestingUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -587,7 +587,7 @@
|
||||||
<string name="pref_category_security">Sicherheit</string>
|
<string name="pref_category_security">Sicherheit</string>
|
||||||
<string name="pref_category_accounts">Konten</string>
|
<string name="pref_category_accounts">Konten</string>
|
||||||
<string name="open_downloaded_repo">Repository öffnen</string>
|
<string name="open_downloaded_repo">Repository öffnen</string>
|
||||||
<string name="device_pin_url_message">Besuche <b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
|
<string name="device_pin_url_message">Besuche<b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
|
||||||
<string name="device_pin_error_message">PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung</string>
|
<string name="device_pin_error_message">PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung</string>
|
||||||
<string name="downloads_empty">Zur Zeit sind keine Downloads verfügbar.</string>
|
<string name="downloads_empty">Zur Zeit sind keine Downloads verfügbar.</string>
|
||||||
<string name="open_local_video">Lokales Video öffnen</string>
|
<string name="open_local_video">Lokales Video öffnen</string>
|
||||||
|
|
@ -712,8 +712,8 @@
|
||||||
<string name="extra_brightness_settings">Zusätzliche Helligkeit</string>
|
<string name="extra_brightness_settings">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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 & video</string>
|
<string name="tracks">Âm thanh & 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">Có</string>
|
<string name="yes">Có</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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
بث وتحميل الأفلام, المسلسلات التلفزيونية والأنمي.
|
بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية.
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Transmita e descarga filmes, séries de TV e anime.
|
Transmita e transfira filmes, séries de TV e anime.
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,21 +126,24 @@ 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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -98,4 +98,4 @@ abstract class CineMMRedirect : ExtractorApi() {
|
||||||
val mirrorUrlWithVideoId = "https://$mirror$videoId"
|
val mirrorUrlWithVideoId = "https://$mirror$videoId"
|
||||||
loadExtractor(mirrorUrlWithVideoId, referer, subtitleCallback, callback)
|
loadExtractor(mirrorUrlWithVideoId, referer, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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("\\", "")
|
||||||
|
|
@ -125,4 +125,4 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
@JsonProperty("label") val label: String
|
@JsonProperty("label") val label: String
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -67,4 +68,4 @@ open class HDMomPlayer : ExtractorApi() {
|
||||||
@JsonProperty("language") val language: String?,
|
@JsonProperty("language") val language: String?,
|
||||||
@JsonProperty("default") val default: String?
|
@JsonProperty("default") val default: String?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
.map { it.toInt(16).toByte() }
|
chunked(2)
|
||||||
.toByteArray()
|
.map { it.toInt(16).toByte() }
|
||||||
.decodeToString()
|
.toByteArray()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
|
|
|
||||||
|
|
@ -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("\\"", "\"").replace("\\\\", "\\")
|
val videoReq = app.get(embedUrl, headers=headers).text.replace("\\"", "\"").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")
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -77,4 +79,4 @@ open class Streamplay : ExtractorApi() {
|
||||||
@JsonProperty("label") val label: String? = null,
|
@JsonProperty("label") val label: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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(")")
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -67,4 +68,4 @@ open class VideoSeyred : ExtractorApi() {
|
||||||
@JsonProperty("label") val label: String? = null,
|
@JsonProperty("label") val label: String? = null,
|
||||||
@JsonProperty("default") val default: String? = null
|
@JsonProperty("default") val default: String? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -126,4 +126,4 @@ object CryptoJS {
|
||||||
SecureRandom().nextBytes(this)
|
SecureRandom().nextBytes(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -119,4 +121,4 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
|
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue