diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 056022d22..b5960d5d9 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -71,10 +71,7 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - MAL_KEY: ${{ secrets.MAL_KEY }} - ANILIST_KEY: ${{ secrets.ANILIST_KEY }} - uses: actions/checkout@v6 with: diff --git a/.github/workflows/instrumented-tests.yml b/.github/workflows/instrumented-tests.yml deleted file mode 100644 index 34c6eaf0b..000000000 --- a/.github/workflows/instrumented-tests.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Instrumented Tests - -on: - issue_comment: - types: [created] - -permissions: - contents: read - pull-requests: write - -jobs: - instrumented-tests: - runs-on: ubuntu-latest - if: | - github.event.issue.pull_request && - github.event.comment.body == '/run-tests' - steps: - - name: Check permission - uses: actions/github-script@v9 - with: - script: | - const login = context.payload.comment.user.login; - const association = context.payload.comment.author_association; - const allowed = ['OWNER', 'MEMBER']; - - const isAllowed = - login === 'Luna712' || - allowed.includes(association); - - if (!isAllowed) { - core.setFailed(`User ${login} is not permitted to trigger this workflow.`); - } - - - uses: actions/checkout@v6 - with: - ref: refs/pull/${{ github.event.issue.number }}/head - - - name: Post started comment - uses: actions/github-script@v9 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Instrumented tests are running. [View live progress](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' - }) - - - name: Set up JDK 17 - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 - with: - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - cache-read-only: false - - - name: Get target SDK - id: sdk - run: | - TARGET_SDK=$(grep 'targetSdk' gradle/libs.versions.toml | grep -o '[0-9]\+' | head -1) - echo "version=$TARGET_SDK" >> $GITHUB_OUTPUT - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Run Instrumented Tests - id: tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ steps.sdk.outputs.version }} - arch: x86_64 - profile: Nexus 6 - script: ./gradlew connectedPrereleaseDebugAndroidTest - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v7 - with: - name: instrumented-test-results - path: '**/build/reports/androidTests/' - - - name: Post finished comment - if: always() - uses: actions/github-script@v9 - with: - script: | - const success = '${{ steps.tests.outcome }}' === 'success'; - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: success - ? 'Instrumented tests passed. [View results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' - : 'Instrumented tests failed. [View results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' - }) diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml new file mode 100644 index 000000000..e354d657d --- /dev/null +++ b/.github/workflows/issue_action.yml @@ -0,0 +1,98 @@ +name: Issue automatic actions + +on: + issues: + types: [opened] + +permissions: + contents: read + issues: write + +jobs: + issue-moderator: + runs-on: ubuntu-latest + steps: + - name: Generate access token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.GH_APP_ID }} + private_key: ${{ secrets.GH_APP_KEY }} + + - name: Similarity analysis + id: similarity + uses: actions-cool/issues-similarity-analysis@v1 + with: + token: ${{ steps.generate_token.outputs.token }} + filter-threshold: 0.60 + title-excludes: '' + comment-title: | + ### Your issue looks similar to these issues: + Please close if duplicate. + comment-body: '${index}. ${similarity} #${number}' + + - name: Label if possible duplicate + if: steps.similarity.outputs.similar-issues-found =='true' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible duplicate"] + }) + + - uses: actions/checkout@v6 + + - name: Automatically close issues that dont follow the issue template + uses: lucasbento/auto-close-issues@v1.0.2 + with: + github-token: ${{ steps.generate_token.outputs.token }} + issue-close-message: | + @${issue.user.login}: hello! :wave: + This issue is being automatically closed because it does not follow the issue template." + closed-issues-label: "invalid" + + - name: Check if issue mentions a provider + id: provider_check + env: + GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}" + run: | + wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" + pip3 install httpx + RES="$(python3 ./check_issue.py)" + echo "name=${RES}" >> $GITHUB_OUTPUT + + - name: Comment if issue mentions a provider + if: steps.provider_check.outputs.name != 'none' + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ steps.generate_token.outputs.token }} + body: | + Hello ${{ github.event.issue.user.login }}. + Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). + + Found provider name: `${{ steps.provider_check.outputs.name }}` + + - name: Label if mentions provider + if: steps.provider_check.outputs.name != 'none' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible provider issue"] + }) + + - name: Add eyes reaction to all issues + uses: actions-cool/emoji-helper@v1.0.0 + with: + type: 'issue' + token: ${{ steps.generate_token.outputs.token }} + emoji: 'eyes' diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index f089afa8f..d9a20a04b 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -62,10 +62,7 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - MAL_KEY: ${{ secrets.MAL_KEY }} - ANILIST_KEY: ${{ secrets.ANILIST_KEY }} - name: Create pre-release uses: marvinpinto/action-automatic-releases@latest diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 433816e0f..675ce3b2f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,13 +26,8 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache-read-only: false - - name: Ensure binary compatibility - # This is to ensure that your code is backwards compatible. - # If this fails you need to add a @Prerelease annotation to new code. - run: ./gradlew library:checkKotlinAbi - - name: Run Gradle - run: ./gradlew assemblePrereleaseDebug lint check + run: ./gradlew assemblePrereleaseDebug lint - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/COMPOSE.md b/COMPOSE.md deleted file mode 100644 index 8d83a50ae..000000000 --- a/COMPOSE.md +++ /dev/null @@ -1,21 +0,0 @@ -# Migration guide to Compose - -### 1. MVI instead of MVVM - -The current design of CloudStream loosely uses the MVVM architecture. - -This means that the UI invokes the viewmodel with function calls, and it responds with LiveData fields that are observed. While this has worked, it generates a lot of boilerplate and has created some friction. - -To make it easier to work with Compose, the new architecture will be based on MVI. In short this means that the viewmodel exposes a singular immutable class that is observed, and receives all UI events with a singular event that is a sealed class. All the UI should be able to be recreated based on this singular state class, and all interactions should be able to be replayed using only the event callback. - -For a more detailed overview, see: https://www.youtube.com/watch?v=b2z1jvD4VMQ - -This is part of the effort to make CloudStream cross platform, as it allows us to decouple UI and logic. - -### 2. KMP-compatible libraries - -We plan to leverage Kotlin's KMP project to compile our code to different architectures. However, this requires us to only use KMP-compatible libraries, no Java. Therefore any pull requests must ensure that they use KMP-compatible libraries only. - -### 3. UI Changes - -While migrating to the new compose UI, you also have the opportunity to change the UI. However, this should only be to freshen up the UI, not completely redesign it. It is also important to stress that this process should not lose any features of the old UI, and be very conservative with adding new features. \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ad8451a3..ae5301929 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { alias(libs.plugins.android.application) alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.serialization) } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) @@ -104,8 +103,8 @@ android { applicationId = "com.lagradost.cloudstream3" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = libs.versions.versionCode.get().toInt() - versionName = libs.versions.versionName.get() + versionCode = 68 + versionName = "4.7.0" manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get() @@ -127,16 +126,6 @@ android { "SIMKL_CLIENT_SECRET", "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\"" ) - buildConfigField( - "String", - "MAL_KEY", - "\"" + (System.getenv("MAL_KEY") ?: localProperties["mal.key"]) + "\"" - ) - buildConfigField( - "String", - "ANILIST_KEY", - "\"" + (System.getenv("ANILIST_KEY") ?: localProperties["anilist.key"]) + "\"" - ) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -217,11 +206,9 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.json) androidTestImplementation(libs.core) - androidTestImplementation(libs.espresso.core) + implementation(libs.junit.ktx) androidTestImplementation(libs.ext.junit) - androidTestImplementation(libs.instancio.core) - androidTestImplementation(libs.junit.ktx) - androidTestImplementation(libs.kotlin.test) + androidTestImplementation(libs.espresso.core) // Android Core & Lifecycle implementation(libs.core.ktx) @@ -232,7 +219,6 @@ dependencies { implementation(libs.bundles.lifecycle) implementation(libs.bundles.navigation) implementation(libs.kotlinx.collections.immutable) - implementation(libs.kotlinx.serialization.json) // JSON Parser // Design & UI implementation(libs.preference.ktx) @@ -268,19 +254,14 @@ dependencies { // Extensions & Other Libs implementation(libs.jsoup) // HTML Parser - implementation(libs.ksoup) // HTML Parser implementation(libs.rhino) // Run JavaScript + implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance implementation(libs.safefile) // To Prevent the URI File Fu*kery coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9 implementation(libs.jackson.module.kotlin) // JSON Parser implementation(libs.zipline) - // Temp/deprecated; will be removed once extensions have time to migrate from using it - implementation("com.google.code.gson:gson:2.11.0") - // Deprecated; will be removed once extensions have time to migrate from using it - implementation("me.xdrop:fuzzywuzzy:1.4.0") - // Torrent Support implementation(libs.torrentserver) @@ -325,6 +306,7 @@ tasks.withType { compilerOptions { jvmTarget.set(javaTarget) jvmDefault.set(JvmDefaultMode.ENABLE) + freeCompilerArgs.add("-Xannotation-default-target=param-property") optIn.addAll( "com.lagradost.cloudstream3.InternalAPI", "com.lagradost.cloudstream3.Prerelease", diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index e854356a0..4c5cdea5b 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -55,6 +55,12 @@ class ExampleInstrumentedTest { return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView } } + @Test + fun providersExist() { + Assert.assertTrue(getAllProviders().isNotEmpty()) + println("Done providersExist") + } + @Throws private inline fun testAllLayouts( activity: Activity, diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt deleted file mode 100644 index 84ef1fee0..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.lagradost.cloudstream3 - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.lagradost.cloudstream3.SkipSerializationTest -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}") - - val failures = mutableListOf() - - serializableClasses.forEach { kClass -> - runCatching { - val instance = Instancio.of(kClass.java).withMaxDepth(10).create() - - 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}") - }.onFailure { e -> - failures.add("FAILED ${kClass.qualifiedName}: ${e.message}") - } - } - - if (failures.isNotEmpty()) { - throw AssertionError("${failures.size} class(es) failed:\n${failures.joinToString("\n")}") - } - } - - @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) - @Test - fun isIdenticalDeserialization() { - val serializableClasses = findSerializableClasses("com.lagradost") - println("Number of serializable classes: ${serializableClasses.size}") - - val failures = mutableListOf() - - serializableClasses.forEach { kClass -> - runCatching { - val instance = Instancio.of(kClass.java).withMaxDepth(10).create() - // 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}") - }.onFailure { e -> - failures.add("FAILED ${kClass.qualifiedName}: ${e.message}") - } - } - - if (failures.isNotEmpty()) { - throw AssertionError("${failures.size} class(es) failed:\n${failures.joinToString("\n")}") - } - } - - // DEX files are the best solution to read all our classes dynamically. - // classgraph could be used instead, but it only gives results on the JVM, not Android. - @Suppress("DEPRECATION") - private fun findSerializableClasses(packageName: String): List> { - val context = InstrumentationRegistry - .getInstrumentation() - .targetContext - - val dexFile = DexFile(context.packageCodePath) - return dexFile.entries() - .toList() - .filter { it.startsWith(packageName) } - .mapNotNull { - runCatching { Class.forName(it).kotlin }.getOrNull() - }.filter { kClass -> - // Not possible to use .hasAnnotation() on newer Android versions. - kClass.java.annotations.any { it is Serializable } - && kClass.java.annotations.none { it is SkipSerializationTest } - && !kClass.isAbstract - } - } - - @OptIn(InternalSerializationApi::class) - @Suppress("UNCHECKED_CAST") - private fun serializeWithKotlinx( - kClass: KClass<*>, - value: Any - ): String { - val serializer = kClass.serializer() as KSerializer - return kotlinxMapper.encodeToString(serializer, value) - } -} diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/UriSerializerTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/UriSerializerTest.kt deleted file mode 100644 index 3ffd37124..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/UriSerializerTest.kt +++ /dev/null @@ -1,41 +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.SerialName -import kotlinx.serialization.Serializable -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test - -@Serializable -data class UriData( - @Serializable(with = UriSerializer::class) - @SerialName("uri") val uri: Uri = Uri.EMPTY, -) - -class UriSerializerTest { - - @Test - fun uriSerializerSerializesUriToString() { - val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) - val result = data.toJson() - assertTrue(result.contains("https://example.com/path?query=1")) - } - - @Test - fun uriSerializerDeserializesStringToUri() { - val input = """{"uri":"https://example.com/path?query=1"}""" - val result = parseJson(input) - assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri) - } - - @Test - fun uriSerializerRoundtripsCorrectly() { - val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) - val encoded = data.toJson() - val decoded = parseJson(encoded) - assertEquals(data.uri, decoded.uri) - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ee4c978f2..b2c7091b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,47 +22,6 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setKey(path: String, value: T) = CloudStreamApp.setKey(path, value) @@ -38,7 +38,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(folder, path, value)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) fun setKey(folder: String, path: String, value: T) = CloudStreamApp.setKey(folder, path, value) @@ -46,7 +46,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path, defVal)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(path: String, defVal: T?): T? = CloudStreamApp.getKey(path, defVal) @@ -54,7 +54,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(path: String): T? = CloudStreamApp.getKey(path) @@ -62,7 +62,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(folder: String, path: String): T? = CloudStreamApp.getKey(folder, path) @@ -70,7 +70,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path, defVal)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) inline fun getKey(folder: String, path: String, defVal: T?): T? = CloudStreamApp.getKey(folder, path, defVal) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt index 466ef5a00..a9cd9c01e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt @@ -113,7 +113,7 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { get() = _context?.get() private set(value) { _context = WeakReference(value) - setContext(value) + setContext(WeakReference(value)) } fun getKeyClass(path: String, valueType: Class): T? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 4ce09bd44..ed0aaf9b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -579,10 +579,8 @@ object CommonActivity { // TODO: Figure out why removing the check for SearchAutoComplete seems // to break focus on TV as it shouldn't need to be used. - // Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote) - // send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button. @SuppressLint("RestrictedApi") - if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) && + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) ) { showInputMethod(act.currentFocus?.findFocus()) @@ -603,4 +601,4 @@ object CommonActivity { } return null } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5d39f6554..8a98bd297 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -171,17 +171,13 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API -import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.setTextHtml import com.lagradost.cloudstream3.utils.txt import com.lagradost.safefile.SafeFile -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -191,8 +187,10 @@ import java.net.URLDecoder import java.nio.charset.Charset import kotlin.math.abs import kotlin.math.absoluteValue -import kotlin.reflect.full.createInstance import kotlin.system.exitProcess +import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback { companion object { @@ -410,10 +408,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa return true } - val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull() - if (matchedApi != null) { - loadResult(str, matchedApi.name, "") - return true + synchronized(apis) { + for (api in apis) { + if (str.startsWith(api.mainUrl)) { + loadResult(str, api.name, "") + return true + } + } } } } @@ -786,6 +787,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true) .setEnterAnim(R.anim.enter_anim) .setExitAnim(R.anim.exit_anim) @@ -807,33 +809,32 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + private val pluginsLock = Mutex() private fun onAllPluginsLoaded(success: Boolean = false) { ioSafe { pluginsLock.withLock { - allProviders.withLock { + synchronized(allProviders) { // Load cloned sites after plugins have been loaded since clones depend on plugins. try { getKey>(USER_PROVIDER_API)?.let { list -> list.forEach { custom -> - allProviders.firstOrNull { - it::class.simpleName == custom.parentClassName - }?.let { - allProviders.add( - it::class.createInstance().apply { - name = custom.name - lang = custom.lang - mainUrl = custom.url.trimEnd('/') - canBeOverridden = false - } - ) - } + allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } + ?.let { + allProviders.add( + it.javaClass.getDeclaredConstructor().newInstance() + .apply { + name = custom.name + lang = custom.lang + mainUrl = custom.url.trimEnd('/') + canBeOverridden = false + }) + } } } // it.hashCode() is not enough to make sure they are distinct - apis = allProviders.distinctBy { - it.lang + it.name + it.mainUrl + it::class.qualifiedName - } + apis = + allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } APIHolder.apiMap = null } catch (e: Exception) { logError(e) @@ -1215,7 +1216,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? safe { val appVer = BuildConfig.VERSION_NAME - val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" + val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) if (lastAppAutoBackup.isEmpty()) return@safe @@ -1429,9 +1430,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa else -> { resultviewPreviewBookmark.isEnabled = false - resultviewPreviewBookmark.showProgress() - //resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24) - //resultviewPreviewBookmark.setText(R.string.loading) + resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24) + resultviewPreviewBookmark.setText(R.string.loading) } } } @@ -1657,7 +1657,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ioSafe { initAll() // No duplicates (which can happen by registerMainAPI) - apis = allProviders.distinctBy { it } + apis = synchronized(allProviders) { + allProviders.distinctBy { it } + } } // val navView: BottomNavigationView = findViewById(R.id.nav_view) @@ -1965,7 +1967,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (BuildConfig.DEBUG) { var providersAndroidManifestString = "Current androidmanifest should be:\n" - allProviders.withLock { + synchronized(allProviders) { for (api in allProviders) { providersAndroidManifestString += "(HAS_DONE_SETUP_KEY, false) != true) { + if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) // If no plugins bring up extensions screen } else if (PluginManager.getPluginsOnline().isEmpty() diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt index f4e8768d8..4843b7617 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt @@ -20,10 +20,8 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage import com.lagradost.cloudstream3.actions.temp.MpvPackage -import com.lagradost.cloudstream3.actions.temp.MpvRxPackage import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage -import com.lagradost.cloudstream3.actions.temp.OnlyPlayer import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action @@ -34,17 +32,18 @@ import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.LinkLoadingResult import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.UiText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.concurrent.Callable import java.util.concurrent.FutureTask +import kotlin.reflect.jvm.jvmName object VideoClickActionHolder { - val allVideoClickActions = atomicListOf( + val allVideoClickActions = threadSafeListOf( // Default PlayInBrowserAction(), CopyClipboardAction(), @@ -65,8 +64,6 @@ object VideoClickActionHolder { MpvYTDLPackage(), MpvKtPackage(), MpvKtPreviewPackage(), - OnlyPlayer(), - MpvRxPackage(), // Always Ask option AlwaysAskAction(), // added by plugins @@ -160,7 +157,7 @@ abstract class VideoClickAction { } } - fun uniqueId() = "$sourcePlugin:${this::class.qualifiedName}" + fun uniqueId() = "$sourcePlugin:${this::class.jvmName}" @Throws abstract fun shouldShow(context: Context?, video: ResultEpisode?): Boolean diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt index a2bb53a16..d414b6117 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt @@ -7,7 +7,6 @@ import android.net.Uri import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.actions.OpenInAppAction import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.SkipSerializationTest import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleOrigin @@ -23,10 +22,7 @@ import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF -import com.lagradost.cloudstream3.utils.serializers.UriSerializer import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable /** * If you want to support CloudStream 3 as an external player, then this shows how to play any video link @@ -53,17 +49,19 @@ class CloudStreamPackage : OpenInAppAction( const val DURATION_EXTRA: String = "dur" // Duration time in MS (Long) } - @Serializable - @SkipSerializationTest //.Uri has issues with Jackson data class MinimalVideoLink( - @JsonProperty("uri") @SerialName("uri") - @Serializable(with = UriSerializer::class) + @JsonProperty("uri") val uri: Uri?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("mimeType") @SerialName("mimeType") val mimeType: String = "video/mp4", - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("headers") @SerialName("headers") var headers: Map = mapOf(), - @JsonProperty("quality") @SerialName("quality") val quality: Int?, + @JsonProperty("url") + val url: String?, + @JsonProperty("mimeType") + val mimeType: String = "video/mp4", + @JsonProperty("name") + val name: String?, + @JsonProperty("headers") + var headers: Map = mapOf(), + @JsonProperty("quality") + val quality: Int?, ) { companion object { fun fromExtractor(link: ExtractorLink): MinimalVideoLink = MinimalVideoLink( @@ -99,12 +97,16 @@ class CloudStreamPackage : OpenInAppAction( } } - @Serializable + data class MinimalSubtitleLink( - @JsonProperty("url") @SerialName("url") val url: String, - @JsonProperty("mimeType") @SerialName("mimeType") val mimeType: String = "text/vtt", - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("headers") @SerialName("headers") var headers: Map = mapOf(), + @JsonProperty("url") + val url: String, + @JsonProperty("mimeType") + val mimeType: String = "text/vtt", + @JsonProperty("name") + val name: String?, + @JsonProperty("headers") + var headers: Map = mapOf(), ) { companion object { fun fromSubtitle(sub: SubtitleData): MinimalSubtitleLink = MinimalSubtitleLink( @@ -157,4 +159,4 @@ class CloudStreamPackage : OpenInAppAction( override fun onResult(activity: Activity, intent: Intent?) { // No results yet } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt deleted file mode 100644 index e8bb93a99..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt +++ /dev/null @@ -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() )*/ - - if (video.tvType.isEpisodeBased()) { - video.season?.let { intent.putExtra("introdb_season", it) } - video.episode.let { intent.putExtra("introdb_episode", it) } - } - - val position = getViewPos(video.id)?.position - if (position != null) - putExtra("position", position.toInt()) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - val position = intent?.getIntExtra("position", -1) ?: -1 - val duration = intent?.getIntExtra("duration", -1) ?: -1 - Log.d("MPV", "Position: $position, Duration: $duration") - updateDurationAndPosition(position.toLong(), duration.toLong()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt deleted file mode 100644 index 348be440a..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.lagradost.cloudstream3.actions.temp - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.net.toUri -import com.lagradost.cloudstream3.actions.OpenInAppAction -import com.lagradost.cloudstream3.ui.result.LinkLoadingResult -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.utils.txt - -/** https://github.com/Kindness-Kismet/only_player/tree/main - * https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */ -class OnlyPlayer : OpenInAppAction( - txt("Only Player"), - "one.only.player", - intentClass = "one.only.player.feature.player.PlayerActivity" -) { - override val oneSource = true - override suspend fun putExtra( - context: Context, - intent: Intent, - video: ResultEpisode, - result: LinkLoadingResult, - index: Int? - ) { - /** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */ - intent.apply { - val link = result.links[index!!] - setData(link.url.toUri()) - - putExtra("headers", Bundle().apply { - for ((key, value) in link.headers) { - putExtra(key, value) - } - }) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - /* onResult does not get called */ - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt index b6478b1d9..46b46a2c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt @@ -60,7 +60,7 @@ open class VlcPackage: OpenInAppAction( intent.putExtra("secure_uri", true) intent.putExtra("title", video.name) - val subsLang = getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" + val subsLang = getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" result.subs.firstOrNull { subsLang == it.languageCode }?.let { @@ -74,4 +74,4 @@ open class VlcPackage: OpenInAppAction( Log.d("VLC", "Position: $position, Duration: $duration") updateDurationAndPosition(position, duration) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt index d54791e89..26f5cec53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/Packets.kt @@ -1,9 +1,5 @@ package com.lagradost.cloudstream3.actions.temp.fcast -import com.fasterxml.jackson.annotation.JsonProperty -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - // See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 enum class Opcode(val value: Byte) { None(0), @@ -22,18 +18,18 @@ enum class Opcode(val value: Byte) { Pong(13); } -@Serializable + data class PlayMessage( - @JsonProperty("container") @SerialName("container") val container: String, - @JsonProperty("url") @SerialName("url") val url: String? = null, - @JsonProperty("content") @SerialName("content") val content: String? = null, - @JsonProperty("time") @SerialName("time") val time: Double? = null, - @JsonProperty("speed") @SerialName("speed") val speed: Double? = null, - @JsonProperty("headers") @SerialName("headers") val headers: Map? = null, + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null ) data class SeekMessage( - val time: Double, + val time: Double ) data class PlaybackUpdateMessage( @@ -41,26 +37,26 @@ data class PlaybackUpdateMessage( val time: Double, val duration: Double, val state: Int, - val speed: Double, + val speed: Double ) data class VolumeUpdateMessage( val generationTime: Long, - val volume: Double, + val volume: Double ) data class PlaybackErrorMessage( - val message: String, + val message: String ) data class SetSpeedMessage( - val speed: Double, + val speed: Double ) data class SetVolumeMessage( - val volume: Double, + val volume: Double ) data class VersionMessage( - val version: Long, + val version: Long ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt index 203a503e9..6234297d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt @@ -22,6 +22,7 @@ fun Requests.initClient(context: Context) { } /** Only use ignoreSSL if you know what you are doing*/ +@Prerelease fun Requests.initClient(context: Context, ignoreSSL: Boolean = false) { this.baseClient = buildDefaultClient(context, ignoreSSL) } @@ -33,6 +34,7 @@ fun buildDefaultClient(context: Context): OkHttpClient { } /** Only use ignoreSSL if you know what you are doing*/ +@Prerelease fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClient { safe { Security.insertProviderAt(Conscrypt.newProvider(), 1) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index e1496db06..efa028d14 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.actions.VideoClickAction import com.lagradost.cloudstream3.actions.VideoClickActionHolder import kotlin.Throws + abstract class Plugin : BasePlugin() { /** * Called when your Plugin is loaded @@ -25,7 +26,9 @@ abstract class Plugin : BasePlugin() { fun registerVideoClickAction(element: VideoClickAction) { Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction") element.sourcePlugin = this.filename - VideoClickActionHolder.allVideoClickActions.add(element) + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.add(element) + } } /** @@ -37,4 +40,4 @@ abstract class Plugin : BasePlugin() { * This will add a button in the settings allowing you to add custom settings */ var openSettings: ((context: Context) -> Unit)? = null -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 6054bbfaf..eae14a6c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -61,8 +61,6 @@ import com.lagradost.cloudstream3.utils.txt import dalvik.system.PathClassLoader import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.File import java.io.InputStreamReader @@ -75,13 +73,12 @@ const val EXTENSIONS_CHANNEL_NAME = "Extensions" const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel" // Data class for internal storage -@Serializable data class PluginData( - @JsonProperty("internalName") @SerialName("internalName") val internalName: String, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("isOnline") @SerialName("isOnline") val isOnline: Boolean, - @JsonProperty("filePath") @SerialName("filePath") val filePath: String, - @JsonProperty("version") @SerialName("version") val version: Int, + @JsonProperty("internalName") val internalName: String, + @JsonProperty("url") val url: String?, + @JsonProperty("isOnline") val isOnline: Boolean, + @JsonProperty("filePath") val filePath: String, + @JsonProperty("version") val version: Int, ) { @WorkerThread fun toSitePlugin(): SitePlugin { @@ -176,11 +173,11 @@ object PluginManager { fun getPluginsOnline(): Array { - return getKey>(PLUGINS_KEY) ?: emptyArray() + return getKey(PLUGINS_KEY) ?: emptyArray() } fun getPluginsLocal(): Array { - return getKey>(PLUGINS_KEY_LOCAL) ?: emptyArray() + return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() } private val CLOUD_STREAM_FOLDER = @@ -224,17 +221,17 @@ object PluginManager { // Helper class for updateAllOnlinePluginsAndLoadThem data class OnlinePluginData( val savedData: PluginData, - val onlineData: PluginWrapper, + val onlineData: Pair, ) { val isOutdated = - onlineData.plugin.version > savedData.version || onlineData.plugin.version == PLUGIN_VERSION_ALWAYS_UPDATE - val isDisabled = onlineData.plugin.status == PROVIDER_STATUS_DOWN + onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE + val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN fun validOnlineData(context: Context): Boolean { return getPluginPath( context, savedData.internalName, - onlineData.repositoryData.url + onlineData.first ).absolutePath == savedData.filePath } } @@ -282,19 +279,19 @@ object PluginManager { ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - getRepoPlugins(it) ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated val outdatedPlugins = getPluginsOnline().map { savedData -> onlinePlugins - .filter { onlineData -> savedData.internalName == onlineData.plugin.internalName } + .filter { onlineData -> savedData.internalName == onlineData.second.internalName } .map { onlineData -> OnlinePluginData(savedData, onlineData) }.filter { it.validOnlineData(activity) } - }.flatten().distinctBy { it.onlineData.plugin.url } + }.flatten().distinctBy { it.onlineData.second.url } debugPrint { "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" @@ -309,14 +306,14 @@ object PluginManager { } else if (pluginData.isOutdated) { downloadPlugin( activity, - pluginData.onlineData.plugin.url, - pluginData.onlineData.plugin.fileHash, + pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, File(pluginData.savedData.filePath), true ).let { success -> if (success) - updatedPlugins.add(pluginData.onlineData.plugin.name) + updatedPlugins.add(pluginData.onlineData.second.name) } } } @@ -359,15 +356,15 @@ object PluginManager { val urls = (getKey>(REPOSITORIES_KEY) ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - getRepoPlugins(it)?.toList() ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } val providerLang = activity.getApiProviderLangSettings() //Log.i(TAG, "providerLang => ${providerLang.toJson()}") // Iterate online repos and returns not downloaded plugins val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData -> - val sitePlugin = onlineData.plugin + val sitePlugin = onlineData.second val tvtypes = sitePlugin.tvTypes ?: listOf() //Don't include empty urls @@ -379,7 +376,7 @@ object PluginManager { } //Omit already existing plugins - if (getPluginPath(activity, sitePlugin.internalName, onlineData.repositoryData.url).exists()) { + if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) { Log.i(TAG, "Skip > ${sitePlugin.internalName}") return@mapNotNull null } @@ -421,14 +418,14 @@ object PluginManager { notDownloadedPlugins.amap { pluginData -> downloadPlugin( activity, - pluginData.onlineData.plugin.url, - pluginData.onlineData.plugin.fileHash, + pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, - pluginData.onlineData.repositoryData.url, + pluginData.onlineData.first, !pluginData.isDisabled ).let { success -> if (success) - newDownloadPlugins.add(pluginData.onlineData.plugin.name) + newDownloadPlugins.add(pluginData.onlineData.second.name) } } @@ -512,9 +509,6 @@ object PluginManager { val res = dir.mkdirs() if (!res) { Log.w(TAG, "Failed to create local directories") - // We have tried to load local plugins, but exit early. - // This needs to be true to prevent the downloader waiting for plugins. - loadedLocalPlugins = true return } } @@ -616,7 +610,7 @@ object PluginManager { return false } InputStreamReader(stream).use { reader -> - manifest = parseJson(reader.readText()) + manifest = parseJson(reader, BasePlugin.Manifest::class.java) } } @@ -657,15 +651,9 @@ object PluginManager { context.resources.configuration ) } - synchronized(plugins) { - plugins[filePath] = pluginInstance - } - synchronized(classLoaders) { - classLoaders[loader] = pluginInstance - } - synchronized(urlPlugins) { - urlPlugins[data.url ?: filePath] = pluginInstance - } + plugins[filePath] = pluginInstance + classLoaders[loader] = pluginInstance + urlPlugins[data.url ?: filePath] = pluginInstance if (pluginInstance is Plugin) { pluginInstance.load(context) } else { @@ -701,20 +689,21 @@ object PluginManager { } // remove all registered apis - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { - removePluginMapping(it) + synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { + removePluginMapping(it) + } + } + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - APIHolder.allProviders.withLock { - APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename } + synchronized(extractorApis) { + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } } - extractorApis.withLock { - extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename } - } - - VideoClickActionHolder.allVideoClickActions.withLock { - VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename } + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename } } synchronized(classLoaders) { @@ -838,16 +827,16 @@ object PluginManager { val urls = (getKey>(REPOSITORIES_KEY) ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - getRepoPlugins(it) ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } val allPlugins = getPluginsOnline().flatMap { savedData -> onlinePlugins - .filter { it.plugin.internalName == savedData.internalName } + .filter { it.second.internalName == savedData.internalName } .mapNotNull { onlineData -> OnlinePluginData(savedData, onlineData).takeIf { it.validOnlineData(activity) } } - }.distinctBy { it.onlineData.plugin.url } + }.distinctBy { it.onlineData.second.url } val updatedPlugins = mutableListOf() @@ -855,7 +844,7 @@ object PluginManager { if (pluginData.isDisabled) { Log.e( "PluginManager", - "Unloading disabled plugin: ${pluginData.onlineData.plugin.name}" + "Unloading disabled plugin: ${pluginData.onlineData.second.name}" ) unloadPlugin(pluginData.savedData.filePath) } else { @@ -864,14 +853,14 @@ object PluginManager { if (downloadPlugin( activity, - pluginData.onlineData.plugin.url, - pluginData.onlineData.plugin.fileHash, + pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, existingFile, true ) ) { - updatedPlugins.add(pluginData.onlineData.plugin.name) + updatedPlugins.add(pluginData.onlineData.second.name) } } }.also { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index 3879ddca4..07d6aaa37 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -16,27 +16,26 @@ import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileNa import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.File import java.nio.file.AtomicMoveNotSupportedException import java.nio.file.Files import java.nio.file.StandardCopyOption import java.security.MessageDigest -import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger /** * Comes with the app, always available in the app, non removable. - */ -@Serializable + * */ + data class Repository( - @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("manifestVersion") @SerialName("manifestVersion") val manifestVersion: Int, - @JsonProperty("pluginLists") @SerialName("pluginLists") val pluginLists: List, + @JsonProperty("iconUrl") val iconUrl: String?, + @JsonProperty("name") val name: String, + @JsonProperty("description") val description: String?, + @JsonProperty("manifestVersion") val manifestVersion: Int, + @JsonProperty("pluginLists") val pluginLists: List ) /** @@ -45,60 +44,40 @@ data class Repository( * 1: Ok * 2: Slow * 3: Beta only - */ -@Serializable + * */ data class SitePlugin( // Url to the .cs3 file - @JsonProperty("url") @SerialName("url") val url: String, + @JsonProperty("url") val url: String, // Status to remotely disable the provider - @JsonProperty("status") @SerialName("status") val status: Int, + @JsonProperty("status") val status: Int, // Integer over 0, any change of this will trigger an auto update - @JsonProperty("version") @SerialName("version") val version: Int, + @JsonProperty("version") val version: Int, // Unused currently, used to make the api backwards compatible? // Set to 1 - @JsonProperty("apiVersion") @SerialName("apiVersion") val apiVersion: Int, + @JsonProperty("apiVersion") val apiVersion: Int, // Name to be shown in app - @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("name") val name: String, // Name to be referenced internally. Separate to make name and url changes possible - @JsonProperty("internalName") @SerialName("internalName") val internalName: String, - @JsonProperty("authors") @SerialName("authors") val authors: List, - @JsonProperty("description") @SerialName("description") val description: String?, + @JsonProperty("internalName") val internalName: String, + @JsonProperty("authors") val authors: List, + @JsonProperty("description") val description: String?, // Might be used to go directly to the plugin repo in the future - @JsonProperty("repositoryUrl") @SerialName("repositoryUrl") val repositoryUrl: String?, + @JsonProperty("repositoryUrl") val repositoryUrl: String?, // These types are yet to be mapped and used, ignore for now - @JsonProperty("tvTypes") @SerialName("tvTypes") val tvTypes: List?, + @JsonProperty("tvTypes") val tvTypes: List?, // Most often a language tag like "en" or "zh-TW" - @JsonProperty("language") @SerialName("language") val language: String?, - @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?, + @JsonProperty("language") val language: String?, + @JsonProperty("iconUrl") val iconUrl: String?, // Automatically generated by the gradle plugin - @JsonProperty("fileSize") @SerialName("fileSize") val fileSize: Long?, - @JsonProperty("fileHash") @SerialName("fileHash") val fileHash: String?, + @JsonProperty("fileSize") val fileSize: Long?, + @JsonProperty("fileHash") val fileHash: String?, ) -@Serializable -data class PluginWrapper( - @JsonProperty("repository") @SerialName("repository") val repository: Repository, - @JsonProperty("repositoryData") @SerialName("repositoryData") val repositoryData: RepositoryData, - @JsonProperty("plugin") @SerialName("plugin") val plugin: SitePlugin -) { - companion object { - private val localRepository = Repository("", "", "", 1, emptyList()) - private val localRepositoryData = RepositoryData("", "", "") - fun getLocalPluginWrapper(plugin: SitePlugin): PluginWrapper { - return PluginWrapper( - localRepository, - localRepositoryData, - plugin - ) - } - } -} - object RepositoryManager { const val ONLINE_PLUGINS_FOLDER = "Extensions" val PREBUILT_REPOSITORIES: Array by lazy { - getKey>("PREBUILT_REPOSITORIES") ?: emptyArray() + getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") @@ -141,18 +120,12 @@ object RepositoryManager { } } else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) { safeAsync { - if (fixedUrl.startsWith("!")) { - val response = app.get("https://py.md/${fixedUrl.removePrefix("!")}", allowRedirects = false) - val url = response.headers["Location"] ?: return@safeAsync null - if (url.startsWith("https://py.md/404")) return@safeAsync null - if (url.removeSuffix("/") == "https://py.md") return@safeAsync null - return@safeAsync url - } else { - val response = app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false) - val url = response.headers["Location"] ?: return@safeAsync null - if (url.startsWith("https://cutt.ly/404")) return@safeAsync null - if (url.removeSuffix("/") == "https://cutt.ly") return@safeAsync null - return@safeAsync url + app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 -> + it2.headers["Location"]?.let { url -> + if (url.startsWith("https://cutt.ly/404")) return@safeAsync null + if (url.removeSuffix("/") == "https://cutt.ly") return@safeAsync null + return@safeAsync url + } } } } else null @@ -161,16 +134,17 @@ object RepositoryManager { suspend fun parseRepository(url: String): Repository? { return safeAsync { // Take manifestVersion and such into account later - app.get(convertRawGitUrl(url), cacheTime = 5, cacheUnit = TimeUnit.MINUTES) - .parsedSafe() + app.get(convertRawGitUrl(url)).parsedSafe() } } private suspend fun parsePlugins(pluginUrls: String): List { // Take manifestVersion and such into account later return try { - app.get(convertRawGitUrl(pluginUrls), cacheTime = 5, cacheUnit = TimeUnit.MINUTES) - .parsed>().toList() + val response = app.get(convertRawGitUrl(pluginUrls)) + // Normal parsed function not working? + // return response.parsedSafe() + tryParseJson>(response.text)?.toList() ?: emptyList() } catch (t: Throwable) { logError(t) emptyList() @@ -179,17 +153,17 @@ object RepositoryManager { /** * Gets all plugins from repositories and pairs them with the repository url - */ - suspend fun getRepoPlugins(repositoryData: RepositoryData): List? { - val repo = parseRepository(repositoryData.url) ?: return null - val list = repo.pluginLists.amap { url -> + * */ + suspend fun getRepoPlugins(repositoryUrl: String): List>? { + val repo = parseRepository(repositoryUrl) ?: return null + return repo.pluginLists.amap { url -> parsePlugins(url).map { - PluginWrapper(repo, repositoryData, it) + repositoryUrl to it } }.flatten() - return list } + suspend fun downloadPluginToFile( context: Context, pluginUrl: String, @@ -240,7 +214,7 @@ object RepositoryManager { } fun getRepositories(): Array { - return getKey>(REPOSITORIES_KEY) ?: emptyArray() + return getKey(REPOSITORIES_KEY) ?: emptyArray() } // Don't want to read before we write in another thread @@ -255,7 +229,7 @@ object RepositoryManager { /** * Also deletes downloaded repository plugins - */ + * */ suspend fun removeRepository(context: Context, repository: RepositoryData) { val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index 57f6f4750..85a806f0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.plugins import android.util.Log import android.widget.Toast -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.CloudStreamApp.Companion.context import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey @@ -12,10 +11,9 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.Coroutines.main import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable object VotingApi { + private const val LOGKEY = "VotingApi" private const val API_DOMAIN = "https://api.countify.xyz" @@ -52,8 +50,8 @@ object VotingApi { votesCache[pluginUrl] = it } - fun hasVoted(pluginUrl: String): Boolean = - getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false + fun hasVoted(pluginUrl: String) = + getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean = PluginManager.urlPlugins.contains(pluginUrl) @@ -93,9 +91,8 @@ object VotingApi { } } - @Serializable private data class CountifyResult( - @JsonProperty("id") @SerialName("id") val id: String? = null, - @JsonProperty("count") @SerialName("count") val count: Int? = null, + val id: String? = null, + val count: Int? = null ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt index e07747a86..028356e76 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet @@ -187,16 +186,6 @@ class DownloadQueueService : Service() { debugAssert({ timeTaken == null }, { "Downloader startup should not time out" }) totalDownloadFlow - .debounce { (instances, queue) -> - // Filter away incorrect transient queue states. - // For example when we pop the queue and add a download instance there exists a transient state where - // there is no queue and no download instances (leading to an early exit) - if (instances.isEmpty() && queue.isEmpty()) { - 500.milliseconds - } else { - 0.milliseconds - } - } .takeWhile { (instances, queue) -> // Stop if destroyed isRunning diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 68fba9777..3bc5f2733 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -70,8 +70,7 @@ abstract class AccountManager { SubtitleRepo(openSubtitlesApi), SubtitleRepo(addic7ed), SubtitleRepo(subDlApi), - PlainAuthRepo(animeSkipApi), - SubtitleRepo(subSourceApi) + PlainAuthRepo(animeSkipApi) ) fun updateAccountIds() { @@ -121,8 +120,7 @@ abstract class AccountManager { val subtitleProviders = arrayOf( SubtitleRepo(openSubtitlesApi), SubtitleRepo(addic7ed), - SubtitleRepo(subDlApi), - SubtitleRepo(subSourceApi) + SubtitleRepo(subDlApi) ) val syncApis = arrayOf( SyncRepo(malApi), diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index c0f0e4a03..83a7a0984 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -1,18 +1,50 @@ package com.lagradost.cloudstream3.syncproviders -import com.fasterxml.jackson.annotation.JsonAlias +import android.util.Base64 +import androidx.annotation.WorkerThread import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTime -import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.base64Encode -import com.lagradost.cloudstream3.splitUrlParameters +import com.lagradost.cloudstream3.ActorData +import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey +import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser +import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.NextAiring +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.Score +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.ShowStatus +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.safe +import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID +import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed +import com.lagradost.cloudstream3.syncproviders.providers.AniListApi +import com.lagradost.cloudstream3.syncproviders.providers.LocalList +import com.lagradost.cloudstream3.syncproviders.providers.MALApi +import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi +import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi +import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi +import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi +import com.lagradost.cloudstream3.ui.SyncWatchType +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.UiText +import com.lagradost.cloudstream3.utils.txt +import me.xdrop.fuzzywuzzy.FuzzySearch +import java.net.URL import java.security.SecureRandom +import java.util.Date +import java.util.concurrent.TimeUnit data class AuthLoginPage( /** The website to open to authenticate */ @@ -20,60 +52,55 @@ data class AuthLoginPage( /** * State/control code to verify against the redirectUrl to make sure the request is valid. * This parameter will be saved, and then used in AuthAPI::login. - */ + * */ val payload: String? = null, ) -@Serializable data class AuthToken( /** * This is the general access tokens/api token representing a logged in user. - * Access tokens are the thing that applications use to make API requests on behalf of a user. - */ - @JsonProperty("accessToken") @SerialName("accessToken") + * + * `Access tokens are the thing that applications use to make API requests on behalf of a user.` + * */ + @JsonProperty("accessToken") val accessToken: String? = null, - /** For OAuth a special refresh token is issues to refresh the access token. */ - @JsonProperty("refreshToken") @SerialName("refreshToken") + /** + * For OAuth a special refresh token is issues to refresh the access token. + * */ + @JsonProperty("refreshToken") val refreshToken: String? = null, /** In UnixTime (sec) when it expires */ - @JsonProperty("accessTokenLifetime") @SerialName("accessTokenLifetime") + @JsonProperty("accessTokenLifetime") val accessTokenLifetime: Long? = null, /** In UnixTime (sec) when it expires */ - @JsonProperty("refreshTokenLifetime") @SerialName("refreshTokenLifetime") + @JsonProperty("refreshTokenLifetime") val refreshTokenLifetime: Long? = null, - /** - * Sometimes AuthToken needs to be customized to store e.g. username/password, - * this acts as a catch all to store text or JSON data. - */ - @JsonProperty("payload") @SerialName("payload") + /** Sometimes AuthToken needs to be customized to store e.g. username/password, + * this acts as a catch all to store text or JSON data. */ + @JsonProperty("payload") val payload: String? = null, ) { fun isAccessTokenExpired(marginSec: Long = 10L) = - accessTokenLifetime != null && unixTime + marginSec >= accessTokenLifetime + accessTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= accessTokenLifetime fun isRefreshTokenExpired(marginSec: Long = 10L) = - refreshTokenLifetime != null && unixTime + marginSec >= refreshTokenLifetime + refreshTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= refreshTokenLifetime } -@OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now -@Serializable data class AuthUser( /** Account display-name, can also be email if name does not exist */ - @JsonProperty("name") @SerialName("name") + @JsonProperty("name") val name: String?, - /** - * Unique account identifier. If a subsequent login is done then it - * will be refused if another account with the same id exists. - */ - @JsonProperty("id") @SerialName("id") + /** Unique account identifier, + * if a subsequent login is done then it will be refused if another account with the same id exists*/ + @JsonProperty("id") val id: Int, /** Profile picture URL */ - @JsonProperty("profilePicture") @SerialName("profilePicture") + @JsonProperty("profilePicture") val profilePicture: String? = null, /** Profile picture Headers of the URL */ - @JsonProperty("profilePictureHeaders") @JsonAlias("profilePictureHeader") - @SerialName("profilePictureHeaders") @JsonNames("profilePictureHeader") - val profilePictureHeaders: Map? = null, + @JsonProperty("profilePictureHeader") + val profilePictureHeaders: Map? = null ) /** @@ -83,11 +110,12 @@ data class AuthUser( * * Any local set/get key should use user.id.toString(), * as token.accessToken (even hashed) is unsecure, and will rotate. - */ -@Serializable + * */ data class AuthData( - @JsonProperty("user") @SerialName("user") val user: AuthUser, - @JsonProperty("token") @SerialName("token") val token: AuthToken, + @JsonProperty("user") + val user: AuthUser, + @JsonProperty("token") + val token: AuthToken, ) data class AuthPinData( @@ -110,12 +138,15 @@ data class AuthLoginRequirement( ) /** What the user responds to the AuthLoginRequirement */ -@Serializable data class AuthLoginResponse( - @JsonProperty("password") @SerialName("password") val password: String?, - @JsonProperty("username") @SerialName("username") val username: String?, - @JsonProperty("email") @SerialName("email") val email: String?, - @JsonProperty("server") @SerialName("server") val server: String?, + @JsonProperty("password") + val password: String?, + @JsonProperty("username") + val username: String?, + @JsonProperty("email") + val email: String?, + @JsonProperty("server") + val server: String?, ) /** Stateless Authentication class used for all personalized content */ @@ -148,31 +179,16 @@ abstract class AuthAPI { open val inAppLoginRequirement: AuthLoginRequirement? = null companion object { - @Deprecated( - message = "Use APIHolder.unixTime instead", - replaceWith = ReplaceWith( - expression = "APIHolder.unixTime", - imports = ["com.lagradost.cloudstream3.APIHolder"] - ), - level = DeprecationLevel.WARNING, - ) val unixTime: Long - get() = APIHolder.unixTime - - @Deprecated( - message = "Use APIHolder.unixTimeMS instead", - replaceWith = ReplaceWith( - expression = "unixTimeMS", - imports = ["com.lagradost.cloudstream3.APIHolder.unixTimeMS"] - ), - level = DeprecationLevel.WARNING, - ) + get() = System.currentTimeMillis() / 1000L val unixTimeMs: Long - get() = unixTimeMS + get() = System.currentTimeMillis() fun splitRedirectUrl(redirectUrl: String): Map { - return splitUrlParameters( - redirectUrl.replace(APP_STRING, "https").replace("/#", "?") + return splitQuery( + URL( + redirectUrl.replace(APP_STRING, "https").replace("/#", "?") + ) ) } @@ -182,8 +198,9 @@ abstract class AuthAPI { val secureRandom = SecureRandom() val codeVerifierBytes = ByteArray(96) // base64 has 6bit per char; (8/6)*96 = 128 secureRandom.nextBytes(codeVerifierBytes) - return base64Encode(codeVerifierBytes).trimEnd('=') - .replace("+", "-").replace("/", "_").replace("\n", "") + return Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=') + .replace("+", "-") + .replace("/", "_").replace("\n", "") } } @@ -226,7 +243,7 @@ abstract class AuthAPI { * * Note that this will currently only be called *once* on logout, * and as such any network issues it will fail silently, and the token will not be revoked. - */ + **/ @Throws open suspend fun invalidateToken(token: AuthToken): Nothing = throw NotImplementedError() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt index 161001611..7a93f96f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt @@ -2,10 +2,11 @@ package com.lagradost.cloudstream3.syncproviders import androidx.annotation.WorkerThread import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf /** Stateless safe abstraction of SubtitleAPI */ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @@ -13,8 +14,7 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { data class SavedSearchResponse( val unixTime: Long, val response: List, - val query: SubtitleSearch, - val idPrefix: String, + val query: SubtitleSearch ) data class SavedResourceResponse( @@ -24,30 +24,26 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { ) // maybe make this a generic struct? right now there is a lot of boilerplate - private val searchCache = atomicListOf() + private val searchCache = threadSafeListOf() private var searchCacheIndex: Int = 0 - private val resourceCache = atomicListOf() + private val resourceCache = threadSafeListOf() private var resourceCacheIndex: Int = 0 const val CACHE_SIZE = 20 } @WorkerThread suspend fun resource(data: SubtitleEntity): Result = runCatching { - val cached = resourceCache.withLock { - var found: SubtitleResource? = null + synchronized(resourceCache) { for (item in resourceCache) { // 20 min save if (item.query == data && (unixTime - item.unixTime) < 60 * 20) { - found = item.response - break + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached val returnValue = api.resource(freshAuth(), data) - resourceCache.withLock { + synchronized(resourceCache) { val add = SavedResourceResponse(unixTime, returnValue, data) if (resourceCache.size > CACHE_SIZE) { resourceCache[resourceCacheIndex] = add // rolling cache @@ -62,25 +58,22 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @WorkerThread suspend fun search(query: SubtitleSearch): Result> { return runCatching { - val cached = searchCache.withLock { - var found: List? = null + synchronized(searchCache) { for (item in searchCache) { // 120 min save - if (item.idPrefix == idPrefix && item.query == query && (unixTime - item.unixTime) < 60 * 120) { - found = item.response - break + if (item.query == query && (unixTime - item.unixTime) < 60 * 120) { + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached - val returnValue = api.search(freshAuth(), query) ?: emptyList() + val returnValue = + api.search(freshAuth(), query) ?: emptyList() // only cache valid return values if (returnValue.isNotEmpty()) { - val add = SavedSearchResponse(unixTime, returnValue, query, idPrefix) - searchCache.withLock { + val add = SavedSearchResponse(unixTime, returnValue, query) + synchronized(searchCache) { if (searchCache.size > CACHE_SIZE) { searchCache[searchCacheIndex] = add // rolling cache searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE @@ -93,3 +86,4 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { } } } + diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index f30a64748..e5f9aca84 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -10,8 +10,8 @@ import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting -import com.lagradost.cloudstream3.utils.Levenshtein import com.lagradost.cloudstream3.utils.UiText +import me.xdrop.fuzzywuzzy.FuzzySearch import java.util.Date /** @@ -138,7 +138,7 @@ abstract class SyncAPI : AuthAPI() { ListSorting.Query -> if (query != null) { items.sortedBy { - -Levenshtein.partialRatio( + -FuzzySearch.partialRatio( query.lowercase(), it.name.lowercase() ) } @@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() { override var score: Score? = null, val tags: List? = null ) : SearchResponse -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index d3e7f22c7..7a46b4113 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.Actor import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.ErrorLoadingException @@ -27,10 +25,9 @@ import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.net.URLEncoder import java.util.Locale @@ -38,7 +35,7 @@ class AniListApi : SyncAPI() { override var name = "AniList" override val idPrefix = "anilist" - private val key = BuildConfig.ANILIST_KEY + val key = "6871" override val redirectUrlIdentifier = "anilistlogin" override var requireLibraryRefresh = true override val hasOAuth2 = true @@ -53,10 +50,9 @@ class AniListApi : SyncAPI() { override suspend fun login(redirectUrl: String, payload: String?): AuthToken? { val sanitizer = splitRedirectUrl(redirectUrl) val token = AuthToken( - accessToken = sanitizer["access_token"] - ?: throw ErrorLoadingException("No access token"), - // refreshToken = sanitizer["refresh_token"], - accessTokenLifetime = APIHolder.unixTime + sanitizer["expires_in"]!!.toLong(), + accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"), + //refreshToken = sanitizer["refresh_token"], + accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(), ) return token } @@ -82,12 +78,13 @@ class AniListApi : SyncAPI() { override fun urlToId(url: String): String? = url.removePrefix("$mainUrl/anime/").removeSuffix("/") + private fun getUrlFromId(id: Int): String { return "$mainUrl/anime/$id" } - override suspend fun search(auth: AuthData?, query: String): List? { - val data = searchShows(query) ?: return null + override suspend fun search(auth : AuthData?, query: String): List? { + val data = searchShows(name) ?: return null return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, @@ -99,16 +96,17 @@ class AniListApi : SyncAPI() { } } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") val season = getSeason(internalId).data.media + return SyncAPI.SyncResult( season.id.toString(), nextAiring = season.nextAiringEpisode?.let { NextAiring( it.episode ?: return@let null, - (it.timeUntilAiring ?: return@let null) + APIHolder.unixTime + (it.timeUntilAiring ?: return@let null) + unixTime ) }, title = season.title?.userPreferred, @@ -156,13 +154,14 @@ class AniListApi : SyncAPI() { "youtube" -> listOf("https://www.youtube.com/watch?v=${season.trailer.id}") else -> null } - // TODO REST + //TODO REST ) } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val internalId = id.toIntOrNull() ?: return null val data = getDataAboutId(auth ?: return null, internalId) ?: return null + return SyncAPI.SyncStatus( score = Score.from100(data.score), watchedEpisodes = data.progress, @@ -258,24 +257,24 @@ class AniListApi : SyncAPI() { val data = mapOf( "query" to query, - "variables" to Variables( - search = name, - page = 1, - type = "ANIME", - ).toJson() + "variables" to + mapOf( + "search" to name, + "page" to 1, + "type" to "ANIME" + ).toJson() ) val res = app.post( "https://graphql.anilist.co/", - // headers = mapOf(), - data = data, // (if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) + //headers = mapOf(), + data = data,//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) timeout = 5000 // REASONABLE TIMEOUT ).text.replace("\\", "") - return parseJson(res) + return res.toKotlinObject() } catch (e: Exception) { logError(e) } - return null } @@ -298,7 +297,7 @@ class AniListApi : SyncAPI() { .replace(")", "\\)") })""" ) - // println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") + //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") val shows = searchShows(name.replace(blackListRegex, "")) shows?.data?.page?.media?.find { @@ -456,11 +455,11 @@ class AniListApi : SyncAPI() { cacheTime = 0, ).text - return tryParseJson(data) ?: throw ErrorLoadingException("Error parsing $data") + return tryParseJson(data) ?: throw ErrorLoadingException("Error parsing $data") } } - private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? { + private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? { val q = """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) @@ -504,9 +503,10 @@ class AniListApi : SyncAPI() { type = AniListStatusType.None, ) } + } - private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? { + private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? { return app.post( "https://graphql.anilist.co/", headers = mapOf( @@ -519,84 +519,71 @@ class AniListApi : SyncAPI() { q, "UTF-8" ) - ), // (if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) + ), //(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) timeout = 5 // REASONABLE TIMEOUT ).text.replace("\\/", "/") } - @Serializable - data class Variables( - @JsonProperty("search") @SerialName("search") val search: String, - @JsonProperty("page") @SerialName("page") val page: Int, - @JsonProperty("type") @SerialName("type") val type: String, - ) - @Serializable data class MediaRecommendation( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: CoverImage?, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: Title?, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("coverImage") val coverImage: CoverImage?, + @JsonProperty("averageScore") val averageScore: Int? ) - @Serializable data class FullAnilistList( - @JsonProperty("data") @SerialName("data") val data: Data?, + @JsonProperty("data") val data: Data? ) - @Serializable data class CompletedAt( - @JsonProperty("year") @SerialName("year") val year: Int, - @JsonProperty("month") @SerialName("month") val month: Int, - @JsonProperty("day") @SerialName("day") val day: Int, + @JsonProperty("year") val year: Int, + @JsonProperty("month") val month: Int, + @JsonProperty("day") val day: Int ) - @Serializable data class StartedAt( - @JsonProperty("year") @SerialName("year") val year: String?, - @JsonProperty("month") @SerialName("month") val month: String?, - @JsonProperty("day") @SerialName("day") val day: String?, + @JsonProperty("year") val year: String?, + @JsonProperty("month") val month: String?, + @JsonProperty("day") val day: String? ) - @Serializable data class Title( - @JsonProperty("english") @SerialName("english") val english: String?, - @JsonProperty("romaji") @SerialName("romaji") val romaji: String?, + @JsonProperty("english") val english: String?, + @JsonProperty("romaji") val romaji: String? ) - @Serializable data class CoverImage( - @JsonProperty("medium") @SerialName("medium") val medium: String?, - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("extraLarge") @SerialName("extraLarge") val extraLarge: String?, + @JsonProperty("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("extraLarge") val extraLarge: String? ) - @Serializable data class Media( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("season") @SerialName("season") val season: String?, - @JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int, - @JsonProperty("format") @SerialName("format") val format: String?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int, - @JsonProperty("title") @SerialName("title") val title: Title, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: CoverImage, - @JsonProperty("synonyms") @SerialName("synonyms") val synonyms: List, - @JsonProperty("nextAiringEpisode") @SerialName("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, + @JsonProperty("id") val id: Int, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("season") val season: String?, + @JsonProperty("seasonYear") val seasonYear: Int, + @JsonProperty("format") val format: String?, + //@JsonProperty("source") val source: String, + @JsonProperty("episodes") val episodes: Int, + @JsonProperty("title") val title: Title, + @JsonProperty("description") val description: String?, + @JsonProperty("coverImage") val coverImage: CoverImage, + @JsonProperty("synonyms") val synonyms: List, + @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, ) - @Serializable data class Entries( - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("completedAt") @SerialName("completedAt") val completedAt: CompletedAt, - @JsonProperty("startedAt") @SerialName("startedAt") val startedAt: StartedAt, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: Int, - @JsonProperty("progress") @SerialName("progress") val progress: Int, - @JsonProperty("score") @SerialName("score") val score: Int, - @JsonProperty("private") @SerialName("private") val private: Boolean, - @JsonProperty("media") @SerialName("media") val media: Media, + @JsonProperty("status") val status: String?, + @JsonProperty("completedAt") val completedAt: CompletedAt, + @JsonProperty("startedAt") val startedAt: StartedAt, + @JsonProperty("updatedAt") val updatedAt: Int, + @JsonProperty("progress") val progress: Int, + @JsonProperty("score") val score: Int, + @JsonProperty("private") val private: Boolean, + @JsonProperty("media") val media: Media ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( @@ -623,20 +610,17 @@ class AniListApi : SyncAPI() { } } - @Serializable data class Lists( - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("entries") @SerialName("entries") val entries: List, + @JsonProperty("status") val status: String?, + @JsonProperty("entries") val entries: List ) - @Serializable data class MediaListCollection( - @JsonProperty("lists") @SerialName("lists") val lists: List, + @JsonProperty("lists") val lists: List ) - @Serializable data class Data( - @JsonProperty("MediaListCollection") @SerialName("MediaListCollection") val mediaListCollection: MediaListCollection, + @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection ) private suspend fun getAniListAnimeListSmart(auth: AuthData): Array? { @@ -654,7 +638,7 @@ class AniListApi : SyncAPI() { } } - override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? { + override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? { val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy { convertAniListStringToStatus(it.status ?: "").stringRes }?.mapValues { group -> @@ -682,9 +666,10 @@ class AniListApi : SyncAPI() { ) } - private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? { + private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? { val userID = auth.user.id val mediaType = "ANIME" + val query = """ query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { @@ -723,53 +708,43 @@ class AniListApi : SyncAPI() { } } } - } + } """ val text = postApi(auth.token, query) - return tryParseJson(text) + return text?.toKotlinObject() } - suspend fun toggleLike(auth: AuthData, id: Int): Boolean { + suspend fun toggleLike(auth : AuthData, id: Int): Boolean { val q = """mutation (${'$'}animeId: Int = $id) { - ToggleFavourite (animeId: ${'$'}animeId) { - anime { - nodes { - id - title { - romaji - } - } - } - } - }""" + ToggleFavourite (animeId: ${'$'}animeId) { + anime { + nodes { + id + title { + romaji + } + } + } + } + }""" val data = postApi(auth.token, q) return data != "" } /** Used to query a saved MediaItem on the list to get the id for removal */ - @Serializable - data class MediaListItemRoot( - @JsonProperty("data") @SerialName("data") val data: MediaListItem? = null, - ) - - @Serializable - data class MediaListItem( - @JsonProperty("MediaList") @SerialName("MediaList") val mediaList: MediaListId? = null, - ) - - @Serializable - data class MediaListId( - @JsonProperty("id") @SerialName("id") val id: Long? = null, - ) + data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) + data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null) + data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( - auth: AuthData, + auth : AuthData, id: Int, type: AniListStatusType, score: Score?, progress: Int? ): Boolean { val userID = auth.user.id + val q = // Delete item if status type is None if (type == AniListStatusType.None) { @@ -811,24 +786,24 @@ class AniListApi : SyncAPI() { return data != "" } - private suspend fun getUser(token: AuthToken): AniListUser? { + private suspend fun getUser(token : AuthToken): AniListUser? { val q = """ - { - Viewer { - id - name - avatar { - large - } - favourites { - anime { - nodes { - id + { + Viewer { + id + name + avatar { + large + } + favourites { + anime { + nodes { + id + } } } - } - } - }""" + } + }""" val data = postApi(token, q) if (data.isNullOrBlank()) return null val userData = parseJson(data) @@ -861,356 +836,305 @@ class AniListApi : SyncAPI() { return seasons.toList() } - @Serializable data class SeasonResponse( - @JsonProperty("data") @SerialName("data") val data: SeasonData, + @JsonProperty("data") val data: SeasonData, ) - @Serializable data class SeasonData( - @JsonProperty("Media") @SerialName("Media") val media: SeasonMedia, + @JsonProperty("Media") val media: SeasonMedia, ) - @Serializable - data class RecommendedMedia( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: MediaTitle?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: MediaCoverImage?, - ) - - @Serializable - data class CharacterMedia( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: MediaTitle?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: MediaCoverImage?, - ) - - @Serializable data class SeasonMedia( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: MediaTitle?, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("format") @SerialName("format") val format: String?, - @JsonProperty("nextAiringEpisode") @SerialName("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, - @JsonProperty("relations") @SerialName("relations") val relations: SeasonEdges?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: MediaCoverImage?, - @JsonProperty("duration") @SerialName("duration") val duration: Int?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int?, - @JsonProperty("genres") @SerialName("genres") val genres: List?, - @JsonProperty("synonyms") @SerialName("synonyms") val synonyms: List?, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, - @JsonProperty("isAdult") @SerialName("isAdult") val isAdult: Boolean?, - @JsonProperty("trailer") @SerialName("trailer") val trailer: MediaTrailer?, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("characters") @SerialName("characters") val characters: CharacterConnection?, - @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: RecommendationConnection?, + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: MediaTitle?, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("format") val format: String?, + @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, + @JsonProperty("relations") val relations: SeasonEdges?, + @JsonProperty("coverImage") val coverImage: MediaCoverImage?, + @JsonProperty("duration") val duration: Int?, + @JsonProperty("episodes") val episodes: Int?, + @JsonProperty("genres") val genres: List?, + @JsonProperty("synonyms") val synonyms: List?, + @JsonProperty("averageScore") val averageScore: Int?, + @JsonProperty("isAdult") val isAdult: Boolean?, + @JsonProperty("trailer") val trailer: MediaTrailer?, + @JsonProperty("description") val description: String?, + @JsonProperty("characters") val characters: CharacterConnection?, + @JsonProperty("recommendations") val recommendations: RecommendationConnection?, ) - @Serializable data class RecommendationConnection( - @JsonProperty("edges") @SerialName("edges") val edges: List = emptyList(), - @JsonProperty("nodes") @SerialName("nodes") val nodes: List = emptyList(), + @JsonProperty("edges") val edges: List = emptyList(), + @JsonProperty("nodes") val nodes: List = emptyList(), + //@JsonProperty("pageInfo") val pageInfo: PageInfo, ) - @Serializable data class RecommendationEdge( - @JsonProperty("node") @SerialName("node") val node: Recommendation, + //@JsonProperty("rating") val rating: Int, + @JsonProperty("node") val node: Recommendation, ) - @Serializable data class Recommendation( - @JsonProperty("mediaRecommendation") @SerialName("mediaRecommendation") val mediaRecommendation: RecommendedMedia?, + val id: Long, + @JsonProperty("mediaRecommendation") val mediaRecommendation: SeasonMedia?, ) - @Serializable data class CharacterName( - @JsonProperty("name") @SerialName("name") val first: String?, - @JsonProperty("middle") @SerialName("middle") val middle: String?, - @JsonProperty("last") @SerialName("last") val last: String?, - @JsonProperty("full") @SerialName("full") val full: String?, - @JsonProperty("native") @SerialName("native") val native: String?, - @JsonProperty("alternative") @SerialName("alternative") val alternative: List?, - @JsonProperty("alternativeSpoiler") @SerialName("alternativeSpoiler") val alternativeSpoiler: List?, - @JsonProperty("userPreferred") @SerialName("userPreferred") val userPreferred: String?, + @JsonProperty("name") val first: String?, + @JsonProperty("middle") val middle: String?, + @JsonProperty("last") val last: String?, + @JsonProperty("full") val full: String?, + @JsonProperty("native") val native: String?, + @JsonProperty("alternative") val alternative: List?, + @JsonProperty("alternativeSpoiler") val alternativeSpoiler: List?, + @JsonProperty("userPreferred") val userPreferred: String?, ) - @Serializable data class CharacterImage( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) - @Serializable data class Character( - @JsonProperty("name") @SerialName("name") val name: CharacterName?, - @JsonProperty("age") @SerialName("age") val age: String?, - @JsonProperty("image") @SerialName("image") val image: CharacterImage?, + @JsonProperty("name") val name: CharacterName?, + @JsonProperty("age") val age: String?, + @JsonProperty("image") val image: CharacterImage?, ) - @Serializable data class CharacterEdge( - @JsonProperty("id") @SerialName("id") val id: Int?, + @JsonProperty("id") val id: Int?, /** - * MAIN - A primary character role in the media - * SUPPORTING - A supporting character role in the media - * BACKGROUND - A background character in the media + MAIN + A primary character role in the media + + SUPPORTING + A supporting character role in the media + + BACKGROUND + A background character in the media */ - @JsonProperty("role") @SerialName("role") val role: String?, - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("voiceActors") @SerialName("voiceActors") val voiceActors: List?, - @JsonProperty("favouriteOrder") @SerialName("favouriteOrder") val favouriteOrder: Int?, - @JsonProperty("media") @SerialName("media") val media: List?, - @JsonProperty("node") @SerialName("node") val node: Character?, + @JsonProperty("role") val role: String?, + @JsonProperty("name") val name: String?, + @JsonProperty("voiceActors") val voiceActors: List?, + @JsonProperty("favouriteOrder") val favouriteOrder: Int?, + @JsonProperty("media") val media: List?, + @JsonProperty("node") val node: Character?, ) - @Serializable data class StaffImage( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) - @Serializable data class StaffName( - @JsonProperty("name") @SerialName("name") val first: String?, - @JsonProperty("middle") @SerialName("middle") val middle: String?, - @JsonProperty("last") @SerialName("last") val last: String?, - @JsonProperty("full") @SerialName("full") val full: String?, - @JsonProperty("native") @SerialName("native") val native: String?, - @JsonProperty("alternative") @SerialName("alternative") val alternative: List?, - @JsonProperty("userPreferred") @SerialName("userPreferred") val userPreferred: String?, + @JsonProperty("name") val first: String?, + @JsonProperty("middle") val middle: String?, + @JsonProperty("last") val last: String?, + @JsonProperty("full") val full: String?, + @JsonProperty("native") val native: String?, + @JsonProperty("alternative") val alternative: List?, + @JsonProperty("userPreferred") val userPreferred: String?, ) - @Serializable data class Staff( - @JsonProperty("image") @SerialName("image") val image: StaffImage?, - @JsonProperty("name") @SerialName("name") val name: StaffName?, - @JsonProperty("age") @SerialName("age") val age: Int?, + @JsonProperty("image") val image: StaffImage?, + @JsonProperty("name") val name: StaffName?, + @JsonProperty("age") val age: Int?, ) - @Serializable data class CharacterConnection( - @JsonProperty("edges") @SerialName("edges") val edges: List?, - @JsonProperty("nodes") @SerialName("nodes") val nodes: List?, + @JsonProperty("edges") val edges: List?, + @JsonProperty("nodes") val nodes: List?, + //@JsonProperty("pageInfo") pageInfo: PageInfo ) - @Serializable data class MediaTrailer( - @JsonProperty("id") @SerialName("id") val id: String?, - @JsonProperty("site") @SerialName("site") val site: String?, - @JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: String?, + @JsonProperty("id") val id: String?, + @JsonProperty("site") val site: String?, + @JsonProperty("thumbnail") val thumbnail: String?, ) - @Serializable data class MediaCoverImage( - @JsonProperty("extraLarge") @SerialName("extraLarge") val extraLarge: String?, - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, - @JsonProperty("color") @SerialName("color") val color: String?, + @JsonProperty("extraLarge") val extraLarge: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, + @JsonProperty("color") val color: String?, ) - @Serializable data class SeasonNextAiringEpisode( - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("timeUntilAiring") @SerialName("timeUntilAiring") val timeUntilAiring: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("timeUntilAiring") val timeUntilAiring: Int?, ) - @Serializable data class SeasonEdges( - @JsonProperty("edges") @SerialName("edges") val edges: List?, + @JsonProperty("edges") val edges: List?, ) - @Serializable data class SeasonEdge( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("relationType") @SerialName("relationType") val relationType: String?, - @JsonProperty("node") @SerialName("node") val node: SeasonNode?, + @JsonProperty("id") val id: Int?, + @JsonProperty("relationType") val relationType: String?, + @JsonProperty("node") val node: SeasonNode?, ) - @Serializable data class AniListFavoritesMediaConnection( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List, + @JsonProperty("nodes") val nodes: List, ) - @Serializable data class AniListFavourites( - @JsonProperty("anime") @SerialName("anime") val anime: AniListFavoritesMediaConnection, + @JsonProperty("anime") val anime: AniListFavoritesMediaConnection, ) - @Serializable data class MediaTitle( - @JsonProperty("romaji") @SerialName("romaji") val romaji: String?, - @JsonProperty("english") @SerialName("english") val english: String?, - @JsonProperty("native") @SerialName("native") val native: String?, - @JsonProperty("userPreferred") @SerialName("userPreferred") val userPreferred: String?, + @JsonProperty("romaji") val romaji: String?, + @JsonProperty("english") val english: String?, + @JsonProperty("native") val native: String?, + @JsonProperty("userPreferred") val userPreferred: String?, ) - @Serializable data class SeasonNode( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("format") @SerialName("format") val format: String?, - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("coverImage") @SerialName("coverImage") val coverImage: CoverImage?, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, + @JsonProperty("id") val id: Int, + @JsonProperty("format") val format: String?, + @JsonProperty("title") val title: Title?, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("coverImage") val coverImage: CoverImage?, + @JsonProperty("averageScore") val averageScore: Int? +// @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, ) - @Serializable data class AniListAvatar( - @JsonProperty("large") @SerialName("large") val large: String?, + @JsonProperty("large") val large: String?, ) - @Serializable data class AniListViewer( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("avatar") @SerialName("avatar") val avatar: AniListAvatar?, - @JsonProperty("favourites") @SerialName("favourites") val favourites: AniListFavourites?, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String, + @JsonProperty("avatar") val avatar: AniListAvatar?, + @JsonProperty("favourites") val favourites: AniListFavourites?, ) - @Serializable data class AniListData( - @JsonProperty("Viewer") @SerialName("Viewer") val viewer: AniListViewer?, + @JsonProperty("Viewer") val viewer: AniListViewer?, ) - @Serializable data class AniListRoot( - @JsonProperty("data") @SerialName("data") val data: AniListData?, + @JsonProperty("data") val data: AniListData?, ) - @Serializable data class AniListUser( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("picture") @SerialName("picture") val picture: String?, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String, + @JsonProperty("picture") val picture: String?, ) - @Serializable data class LikeNode( - @JsonProperty("id") @SerialName("id") val id: Int?, + @JsonProperty("id") val id: Int?, + //@JsonProperty("idMal") public int idMal; ) - @Serializable data class LikePageInfo( - @JsonProperty("total") @SerialName("total") val total: Int?, - @JsonProperty("currentPage") @SerialName("currentPage") val currentPage: Int?, - @JsonProperty("lastPage") @SerialName("lastPage") val lastPage: Int?, - @JsonProperty("perPage") @SerialName("perPage") val perPage: Int?, - @JsonProperty("hasNextPage") @SerialName("hasNextPage") val hasNextPage: Boolean?, + @JsonProperty("total") val total: Int?, + @JsonProperty("currentPage") val currentPage: Int?, + @JsonProperty("lastPage") val lastPage: Int?, + @JsonProperty("perPage") val perPage: Int?, + @JsonProperty("hasNextPage") val hasNextPage: Boolean?, ) - @Serializable data class LikeAnime( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List?, - @JsonProperty("pageInfo") @SerialName("pageInfo") val pageInfo: LikePageInfo?, + @JsonProperty("nodes") val nodes: List?, + @JsonProperty("pageInfo") val pageInfo: LikePageInfo?, ) - @Serializable data class LikeFavourites( - @JsonProperty("anime") @SerialName("anime") val anime: LikeAnime?, + @JsonProperty("anime") val anime: LikeAnime?, ) - @Serializable data class LikeViewer( - @JsonProperty("favourites") @SerialName("favourites") val favourites: LikeFavourites?, + @JsonProperty("favourites") val favourites: LikeFavourites?, ) - @Serializable data class LikeData( - @JsonProperty("Viewer") @SerialName("Viewer") val viewer: LikeViewer?, + @JsonProperty("Viewer") val viewer: LikeViewer?, ) - @Serializable data class LikeRoot( - @JsonProperty("data") @SerialName("data") val data: LikeData?, + @JsonProperty("data") val data: LikeData?, ) - @Serializable data class AniListTitleHolder( - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("isFavourite") @SerialName("isFavourite") val isFavourite: Boolean?, - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("progress") @SerialName("progress") val progress: Int?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int?, - @JsonProperty("score") @SerialName("score") val score: Int?, - @JsonProperty("type") @SerialName("type") val type: AniListStatusType?, + @JsonProperty("title") val title: Title?, + @JsonProperty("isFavourite") val isFavourite: Boolean?, + @JsonProperty("id") val id: Int?, + @JsonProperty("progress") val progress: Int?, + @JsonProperty("episodes") val episodes: Int?, + @JsonProperty("score") val score: Int?, + @JsonProperty("type") val type: AniListStatusType?, ) - @Serializable data class GetDataMediaListEntry( - @JsonProperty("progress") @SerialName("progress") val progress: Int?, - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("score") @SerialName("score") val score: Int?, + @JsonProperty("progress") val progress: Int?, + @JsonProperty("status") val status: String?, + @JsonProperty("score") val score: Int?, ) - @Serializable data class Nodes( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("mediaRecommendation") @SerialName("mediaRecommendation") val mediaRecommendation: MediaRecommendation?, + @JsonProperty("id") val id: Int?, + @JsonProperty("mediaRecommendation") val mediaRecommendation: MediaRecommendation? ) - @Serializable data class GetDataMedia( - @JsonProperty("isFavourite") @SerialName("isFavourite") val isFavourite: Boolean?, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Int?, - @JsonProperty("title") @SerialName("title") val title: Title?, - @JsonProperty("mediaListEntry") @SerialName("mediaListEntry") val mediaListEntry: GetDataMediaListEntry?, + @JsonProperty("isFavourite") val isFavourite: Boolean?, + @JsonProperty("episodes") val episodes: Int?, + @JsonProperty("title") val title: Title?, + @JsonProperty("mediaListEntry") val mediaListEntry: GetDataMediaListEntry? ) - @Serializable data class Recommendations( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List?, + @JsonProperty("nodes") val nodes: List? ) - @Serializable data class GetDataData( - @JsonProperty("Media") @SerialName("Media") val media: GetDataMedia?, + @JsonProperty("Media") val media: GetDataMedia?, ) - @Serializable data class GetDataRoot( - @JsonProperty("data") @SerialName("data") val data: GetDataData?, + @JsonProperty("data") val data: GetDataData?, ) - @Serializable data class GetSearchTitle( - @JsonProperty("romaji") @SerialName("romaji") val romaji: String?, + @JsonProperty("romaji") val romaji: String?, ) - @Serializable data class TrailerObject( - @JsonProperty("id") @SerialName("id") val id: String?, - @JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: String?, - @JsonProperty("site") @SerialName("site") val site: String?, + @JsonProperty("id") val id: String?, + @JsonProperty("thumbnail") val thumbnail: String?, + @JsonProperty("site") val site: String?, ) - @Serializable data class GetSearchMedia( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("idMal") @SerialName("idMal") val idMal: Int?, - @JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int, - @JsonProperty("title") @SerialName("title") val title: GetSearchTitle, - @JsonProperty("startDate") @SerialName("startDate") val startDate: StartedAt, - @JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?, - @JsonProperty("meanScore") @SerialName("meanScore") val meanScore: Int?, - @JsonProperty("bannerImage") @SerialName("bannerImage") val bannerImage: String?, - @JsonProperty("trailer") @SerialName("trailer") val trailer: TrailerObject?, - @JsonProperty("nextAiringEpisode") @SerialName("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, - @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: Recommendations?, - @JsonProperty("relations") @SerialName("relations") val relations: SeasonEdges?, + @JsonProperty("id") val id: Int, + @JsonProperty("idMal") val idMal: Int?, + @JsonProperty("seasonYear") val seasonYear: Int, + @JsonProperty("title") val title: GetSearchTitle, + @JsonProperty("startDate") val startDate: StartedAt, + @JsonProperty("averageScore") val averageScore: Int?, + @JsonProperty("meanScore") val meanScore: Int?, + @JsonProperty("bannerImage") val bannerImage: String?, + @JsonProperty("trailer") val trailer: TrailerObject?, + @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, + @JsonProperty("recommendations") val recommendations: Recommendations?, + @JsonProperty("relations") val relations: SeasonEdges? ) - @Serializable data class GetSearchPage( - @JsonProperty("Page") @SerialName("Page") val page: GetSearchData?, + @JsonProperty("Page") val page: GetSearchData?, ) - @Serializable data class GetSearchData( - @JsonProperty("media") @SerialName("media") val media: List?, + @JsonProperty("media") val media: List?, ) - @Serializable data class GetSearchRoot( - @JsonProperty("data") @SerialName("data") val data: GetSearchPage?, + @JsonProperty("data") val data: GetSearchPage?, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt index c613aa6ea..29c3c0c17 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt @@ -1,8 +1,8 @@ package com.lagradost.cloudstream3.syncproviders.providers + import androidx.annotation.StringRes import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.R @@ -22,15 +22,14 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import okhttp3.Interceptor import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.text.SimpleDateFormat +import java.time.Instant import java.time.LocalDate -import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale @@ -69,9 +68,13 @@ class KitsuApi: SyncAPI() { val request: Request = chain.request() try { + val response = chain.proceed(request); + if (response.isSuccessful) return response + response.close() + } catch (_: Exception) { } @@ -80,6 +83,7 @@ class KitsuApi: SyncAPI() { .build() return chain.proceed(fallbackRequest) + } } @@ -103,7 +107,7 @@ class KitsuApi: SyncAPI() { ).parsed() return AuthToken( - accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(), + accessTokenLifetime = unixTime + token.expiresIn.toLong(), refreshToken = token.refreshToken, accessToken = token.accessToken, ) @@ -122,7 +126,7 @@ class KitsuApi: SyncAPI() { return AuthToken( accessToken = res.accessToken, refreshToken = res.refreshToken, - accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong() + accessTokenLifetime = unixTime + res.expiresIn.toLong() ) } @@ -179,9 +183,9 @@ class KitsuApi: SyncAPI() { return null } - @Serializable data class KitsuResponse( - @JsonProperty("data") @SerialName("data") val data: KitsuNode, + @field:JsonProperty(value = "data") + val data: KitsuNode, ) val url = @@ -198,10 +202,10 @@ class KitsuApi: SyncAPI() { id = id, totalEpisodes = anime.episodeCount, title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(), - publicScore = Score.from(anime.ratingTwenty, 20), + publicScore = Score.from(anime.ratingTwenty.toString(), 20), duration = anime.episodeLength, synopsis = anime.synopsis, - airStatus = when (anime.status) { + airStatus = when(anime.status) { "finished" -> ShowStatus.Completed "current" -> ShowStatus.Ongoing else -> null @@ -217,6 +221,7 @@ class KitsuApi: SyncAPI() { prevSeason = null, actors = null, ) + } override suspend fun status(auth : AuthData?, id: String): AbstractSyncStatus? { @@ -245,19 +250,21 @@ class KitsuApi: SyncAPI() { } return SyncStatus( - score = Score.from(anime.ratingTwenty, 20), + score = Score.from(anime.ratingTwenty.toString(), 20), status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)), isFavorite = null, watchedEpisodes = anime.progress, ) } - suspend fun getAnimeIdByTitle(title: String): String? { + val animeSelectedFields = arrayOf("titles","canonicalTitle") val url = "$apiUrl/anime?filter[text]=$title&page[limit]=$KITSU_MAX_SEARCH_LIMIT&fields[anime]=${animeSelectedFields.joinToString(",")}" val res = app.get(url, interceptor = apiFallbackInterceptor).parsed() + return res.data.firstOrNull()?.id + } override fun urlToId(url: String): String? = @@ -268,6 +275,7 @@ class KitsuApi: SyncAPI() { id: String, newStatus: AbstractSyncStatus ): Boolean { + return setScoreRequest( auth ?: return false, id.toIntOrNull() ?: return false, @@ -278,18 +286,21 @@ class KitsuApi: SyncAPI() { } private suspend fun setScoreRequest( - auth: AuthData, + auth : AuthData, id: Int, status: KitsuStatusType? = null, score: Int? = null, numWatchedEpisodes: Int? = null, ): Boolean { + val libraryEntryId = getAnimeLibraryEntryId(auth, id) // Exists entry for anime in library if (libraryEntryId != null) { + // Delete anime from library if (status == null || status == KitsuStatusType.None) { + val res = app.delete( "$apiUrl/library-entries/$libraryEntryId", headers = mapOf( @@ -298,7 +309,9 @@ class KitsuApi: SyncAPI() { interceptor = apiFallbackInterceptor ) + return res.isSuccessful + } return setScoreRequest( @@ -308,6 +321,7 @@ class KitsuApi: SyncAPI() { score, numWatchedEpisodes ) + } val data = mapOf( @@ -346,6 +360,7 @@ class KitsuApi: SyncAPI() { ) return res.isSuccessful + } @Suppress("UNCHECKED_CAST") @@ -378,11 +393,15 @@ class KitsuApi: SyncAPI() { interceptor = apiFallbackInterceptor ) + return res.isSuccessful + } private suspend fun getAnimeLibraryEntryId(auth: AuthData, id: Int): Int? { + val userId = auth.user.id + val res = app.get( "$apiUrl/library-entries?filter[userId]=$userId&filter[animeId]=$id", headers = mapOf( @@ -392,6 +411,7 @@ class KitsuApi: SyncAPI() { ).parsed().data.firstOrNull() ?: return null return res.id.toInt() + } override suspend fun library(auth : AuthData?): LibraryMetadata? { @@ -433,52 +453,58 @@ class KitsuApi: SyncAPI() { } private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array { - val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount") - val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","updatedAt", "status") + + val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount") + val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status") val limit = 500 var url = "$apiUrl/library-entries?filter[userId]=$userId&filter[kind]=anime&include=anime&page[limit]=$limit&page[offset]=0&fields[anime]=${animeSelectedFields.joinToString(",")}&fields[libraryEntries]=${libraryEntriesSelectedFields.joinToString(",")}" val fullList = mutableListOf() while (true) { + val data: KitsuResponse = getKitsuAnimeListSlice(token, url) + data.data.forEachIndexed { index, value -> value.anime = data.included?.get(index) } fullList.addAll(data.data) + url = data.links?.next ?: break } + return fullList.toTypedArray() } private suspend fun getKitsuAnimeListSlice(token: AuthToken, url: String): KitsuResponse { - return app.get( + val res = app.get( url, headers = mapOf( "Authorization" to "Bearer ${token.accessToken}", ), interceptor = apiFallbackInterceptor ).parsed() + return res } - @Serializable + data class ResponseToken( - @JsonProperty("token_type") @SerialName("token_type") val tokenType: String, - @JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int, - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String, - @JsonProperty("refresh_token") @SerialName("refresh_token") val refreshToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) - @Serializable data class KitsuNode( - @JsonProperty("id") @SerialName("id") val id: String, - @JsonProperty("attributes") @SerialName("attributes") val attributes: KitsuNodeAttributes, + @JsonProperty("id") val id: String, + @JsonProperty("attributes") val attributes: KitsuNodeAttributes, /* User list anime node */ - @JsonProperty("relationships") @SerialName("relationships") val relationships: KitsuRelationships?, - @JsonProperty("anime") @SerialName("anime") var anime: KitsuAnimeData?, + @JsonProperty("relationships") val relationships: KitsuRelationships?, + var anime: KitsuAnimeData? ) { fun toLibraryItem(): LibraryItem { + val animeItem = this.anime val numEpisodes = animeItem?.attributes?.episodeCount @@ -500,7 +526,7 @@ class KitsuApi: SyncAPI() { this.id, this.attributes.progress, numEpisodes, - Score.from(this.attributes.ratingTwenty, 20), + Score.from(this.attributes.ratingTwenty.toString(), 20), parseDateLong(this.attributes.updatedAt), "Kitsu", TvType.Anime, @@ -509,9 +535,12 @@ class KitsuApi: SyncAPI() { null, plot = synopsis, releaseDate = if (startDate == null) null else try { - Date.from(LocalDate.parse(startDate).atStartOfDay() - .atZone(ZoneId.systemDefault()) - .toInstant()) + Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(startDate) + ) + ) } catch (_: RuntimeException) { null } @@ -520,100 +549,93 @@ class KitsuApi: SyncAPI() { } - @Serializable data class KitsuAnimeAttributes( - @JsonProperty("titles") @SerialName("titles") val titles: KitsuTitles?, - @JsonProperty("canonicalTitle") @SerialName("canonicalTitle") val canonicalTitle: String?, - @JsonProperty("posterImage") @SerialName("posterImage") val posterImage: KitsuPosterImage?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("startDate") @SerialName("startDate") val startDate: String?, - @JsonProperty("endDate") @SerialName("endDate") val endDate: String?, - @JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Int?, - @JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Int?, + @JsonProperty("titles") val titles: KitsuTitles?, + @JsonProperty("canonicalTitle") val canonicalTitle: String?, + @JsonProperty("posterImage") val posterImage: KitsuPosterImage?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("startDate") val startDate: String?, + @JsonProperty("endDate") val endDate: String?, + @JsonProperty("episodeCount") val episodeCount: Int?, + @JsonProperty("episodeLength") val episodeLength: Int?, ) - @Serializable data class KitsuAnimeData( - @JsonProperty("id") @SerialName("id") val id: String, - @JsonProperty("attributes") @SerialName("attributes") val attributes: KitsuAnimeAttributes, + @JsonProperty("id") val id: String, + @JsonProperty("attributes") val attributes: KitsuAnimeAttributes, ) - @Serializable + data class KitsuNodeAttributes( /* General attributes */ - @JsonProperty("titles") @SerialName("titles") val titles: KitsuTitles?, - @JsonProperty("canonicalTitle") @SerialName("canonicalTitle") val canonicalTitle: String?, - @JsonProperty("posterImage") @SerialName("posterImage") val posterImage: KitsuPosterImage?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("startDate") @SerialName("startDate") val startDate: String?, - @JsonProperty("endDate") @SerialName("endDate") val endDate: String?, - @JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Int?, - @JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Int?, + @JsonProperty("titles") val titles: KitsuTitles?, + @JsonProperty("canonicalTitle") val canonicalTitle: String?, + @JsonProperty("posterImage") val posterImage: KitsuPosterImage?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("startDate") val startDate: String?, + @JsonProperty("endDate") val endDate: String?, + @JsonProperty("episodeCount") val episodeCount: Int?, + @JsonProperty("episodeLength") val episodeLength: Int?, /* User attributes */ - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("location") @SerialName("location") val location: String?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("avatar") @SerialName("avatar") val avatar: KitsuUserAvatar?, + @JsonProperty("name") val name: String?, + @JsonProperty("location") val location: String?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("avatar") val avatar: KitsuUserAvatar?, /* User list anime attributes */ - @JsonProperty("progress") @SerialName("progress") val progress: Int?, - @JsonProperty("ratingTwenty") @SerialName("ratingTwenty") val ratingTwenty: Int?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("status") @SerialName("status") val status: String?, + @JsonProperty("progress") val progress: Int?, + @JsonProperty("ratingTwenty") val ratingTwenty: Float?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("status") val status: String?, ) - @Serializable data class KitsuRelationships( - @JsonProperty("anime") @SerialName("anime") val anime: KitsuRelationshipsAnime?, + @JsonProperty("anime") val anime: KitsuRelationshipsAnime? ) - @Serializable data class KitsuRelationshipsAnime( - @JsonProperty("links") @SerialName("links") val links: KitsuLinks?, + @JsonProperty("links") val links: KitsuLinks? ) - @Serializable data class KitsuPosterImage( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) - @Serializable data class KitsuTitles( - @JsonProperty("en_jp") @SerialName("en_jp") val enJp: String?, - @JsonProperty("ja_jp") @SerialName("ja_jp") val jaJp: String?, + @JsonProperty("en_jp") val enJp: String?, + @JsonProperty("ja_jp") val jaJp: String? ) - @Serializable data class KitsuUserAvatar( - @JsonProperty("original") @SerialName("original") val original: String?, + @JsonProperty("original") val original: String? ) - @Serializable data class KitsuLinks( /* Pagination */ - @JsonProperty("first") @SerialName("first") val first: String?, - @JsonProperty("next") @SerialName("next") val next: String?, - @JsonProperty("last") @SerialName("last") val last: String?, + @JsonProperty("first") val first: String?, + @JsonProperty("next") val next: String?, + @JsonProperty("last") val last: String?, /* Relationships */ - @JsonProperty("related") @SerialName("related") val related: String?, + @JsonProperty("related") val related: String? ) - @Serializable data class KitsuResponse( - @JsonProperty("links") @SerialName("links") val links: KitsuLinks?, - @JsonProperty("data") @SerialName("data") val data: List, + @JsonProperty("links") val links: KitsuLinks?, + @JsonProperty("data") val data: List, /* When requesting related info (User library entry -> anime) */ - @JsonProperty("included") @SerialName("included") val included: List?, + @JsonProperty("included") val included: List?, ) + companion object { + const val KITSU_CACHED_LIST: String = "kitsu_cached_list" private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -622,13 +644,13 @@ class KitsuApi: SyncAPI() { arrayOf("current", "completed", "on_hold", "dropped", "planned") private fun fromIntToAnimeStatus(inp: SyncWatchType): KitsuStatusType { return when (inp) { - SyncWatchType.NONE -> KitsuStatusType.None - SyncWatchType.WATCHING -> KitsuStatusType.Watching - SyncWatchType.COMPLETED -> KitsuStatusType.Completed - SyncWatchType.ONHOLD -> KitsuStatusType.OnHold - SyncWatchType.DROPPED -> KitsuStatusType.Dropped - SyncWatchType.PLANTOWATCH -> KitsuStatusType.PlanToWatch - SyncWatchType.REWATCHING -> KitsuStatusType.Watching + SyncWatchType.NONE -> KitsuStatusType.None + SyncWatchType.WATCHING -> KitsuStatusType.Watching + SyncWatchType.COMPLETED -> KitsuStatusType.Completed + SyncWatchType.ONHOLD -> KitsuStatusType.OnHold + SyncWatchType.DROPPED -> KitsuStatusType.Dropped + SyncWatchType.PLANTOWATCH -> KitsuStatusType.PlanToWatch + SyncWatchType.REWATCHING -> KitsuStatusType.Watching } } @@ -643,12 +665,12 @@ class KitsuApi: SyncAPI() { private fun convertToStatus(string: String): KitsuStatusType { return when (string) { - "current" -> KitsuStatusType.Watching - "completed" -> KitsuStatusType.Completed - "on_hold" -> KitsuStatusType.OnHold - "dropped" -> KitsuStatusType.Dropped - "planned" -> KitsuStatusType.PlanToWatch - else -> KitsuStatusType.None + "current" -> KitsuStatusType.Watching + "completed" -> KitsuStatusType.Completed + "on_hold" -> KitsuStatusType.OnHold + "dropped" -> KitsuStatusType.Dropped + "planned" -> KitsuStatusType.PlanToWatch + else -> KitsuStatusType.None } } } @@ -670,7 +692,7 @@ object Kitsu { "https://kitsu.io/api/graphql", headers = headers, data = mapOf("query" to query) - ).parsed() + ).parsed() } private val cache: MutableMap, Map> = @@ -752,52 +774,44 @@ query { return map } - @Serializable data class KitsuResponse( - @JsonProperty("data") @SerialName("data") val data: Data? = null, + val data: Data? = null ) { - @Serializable data class Data( - @JsonProperty("lookupMapping") @SerialName("lookupMapping") val lookupMapping: LookupMapping? = null, + val lookupMapping: LookupMapping? = null ) - @Serializable data class LookupMapping( - @JsonProperty("id") @SerialName("id") val id: String? = null, - @JsonProperty("episodes") @SerialName("episodes") val episodes: Episodes? = null, + val id: String? = null, + val episodes: Episodes? = null ) - @Serializable data class Episodes( - @JsonProperty("nodes") @SerialName("nodes") val nodes: List? = null, + val nodes: List? = null ) - @Serializable data class Node( - @JsonProperty("number") @SerialName("number") val num: Int? = null, - @JsonProperty("titles") @SerialName("titles") val titles: Titles? = null, - @JsonProperty("description") @SerialName("description") val description: Description? = null, - @JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: Thumbnail? = null, + @JsonProperty("number") + val num: Int? = null, + val titles: Titles? = null, + val description: Description? = null, + val thumbnail: Thumbnail? = null ) - @Serializable data class Description( - @JsonProperty("en") @SerialName("en") val en: String? = null, + val en: String? = null ) - @Serializable data class Thumbnail( - @JsonProperty("original") @SerialName("original") val original: Original? = null, + val original: Original? = null ) - @Serializable data class Original( - @JsonProperty("url") @SerialName("url") val url: String? = null, + val url: String? = null ) - @Serializable data class Titles( - @JsonProperty("canonical") @SerialName("canonical") val canonical: String? = null, + val canonical: String? = null ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index f74102b13..ba0195be6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -2,8 +2,6 @@ package com.lagradost.cloudstream3.syncproviders.providers import androidx.annotation.StringRes import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.R @@ -21,9 +19,8 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.text.SimpleDateFormat import java.time.Instant import java.time.format.DateTimeFormatter @@ -37,7 +34,7 @@ class MALApi : SyncAPI() { override var name = "MAL" override val idPrefix = "mal" - private val key = BuildConfig.MAL_KEY + val key = "1714d6f2f4f7cc19644384f8c4629910" private val apiUrl = "https://api.myanimelist.net" override val hasOAuth2 = true override val redirectUrlIdentifier: String? = "mallogin" @@ -52,17 +49,16 @@ class MALApi : SyncAPI() { SyncWatchType.PLANTOWATCH, SyncWatchType.DROPPED, SyncWatchType.ONHOLD, - SyncWatchType.NONE, + SyncWatchType.NONE ) - @Serializable - data class Payload( - @JsonProperty("requestId") @SerialName("requestId") val requestId: Int, - @JsonProperty("codeVerifier") @SerialName("codeVerifier") val codeVerifier: String, + data class PayLoad( + val requestId: Int, + val codeVerifier: String ) override suspend fun login(redirectUrl: String, payload: String?): AuthToken? { - val payloadData = parseJson(payload!!) + val payloadData = parseJson(payload!!) val sanitizer = splitRedirectUrl(redirectUrl) val state = sanitizer["state"]!! @@ -78,13 +74,13 @@ class MALApi : SyncAPI() { "client_id" to key, "code" to currentCode, "code_verifier" to payloadData.codeVerifier, - "grant_type" to "authorization_code", + "grant_type" to "authorization_code" ) ).parsed() return AuthToken( - accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(), + accessTokenLifetime = unixTime + token.expiresIn.toLong(), refreshToken = token.refreshToken, - accessToken = token.accessToken, + accessToken = token.accessToken ) } @@ -98,13 +94,13 @@ class MALApi : SyncAPI() { return AuthUser( id = user.id, name = user.name, - profilePicture = user.picture, + profilePicture = user.picture ) } - override suspend fun search(auth: AuthData?, query: String): List? { + override suspend fun search(auth : AuthData?, query: String): List? { val auth = auth?.token?.accessToken ?: return null - val url = "$apiUrl/v2/anime?q=$query&limit=$MAL_MAX_SEARCH_LIMIT" + val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" val res = app.get( url, headers = mapOf( "Authorization" to "Bearer $auth", @@ -117,7 +113,7 @@ class MALApi : SyncAPI() { this.name, node.id.toString(), "$mainUrl/anime/${node.id}/", - node.mainPicture?.large ?: node.mainPicture?.medium, + node.mainPicture?.large ?: node.mainPicture?.medium ) } } @@ -126,7 +122,7 @@ class MALApi : SyncAPI() { Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() override suspend fun updateStatus( - auth: AuthData?, + auth : AuthData?, id: String, newStatus: SyncAPI.AbstractSyncStatus ): Boolean { @@ -139,89 +135,82 @@ class MALApi : SyncAPI() { ) } - @Serializable data class MalAnime( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("main_picture") @SerialName("main_picture") val mainPicture: MainPicture?, - @JsonProperty("alternative_titles") @SerialName("alternative_titles") val alternativeTitles: AlternativeTitles?, - @JsonProperty("start_date") @SerialName("start_date") val startDate: String?, - @JsonProperty("end_date") @SerialName("end_date") val endDate: String?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("mean") @SerialName("mean") val mean: Double?, - @JsonProperty("rank") @SerialName("rank") val rank: Int?, - @JsonProperty("popularity") @SerialName("popularity") val popularity: Int?, - @JsonProperty("num_list_users") @SerialName("num_list_users") val numListUsers: Int?, - @JsonProperty("num_scoring_users") @SerialName("num_scoring_users") val numScoringUsers: Int?, - @JsonProperty("nsfw") @SerialName("nsfw") val nsfw: String?, - @JsonProperty("created_at") @SerialName("created_at") val createdAt: String?, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String?, - @JsonProperty("media_type") @SerialName("media_type") val mediaType: String?, - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("genres") @SerialName("genres") val genres: ArrayList?, - @JsonProperty("my_list_status") @SerialName("my_list_status") val myListStatus: MyListStatus?, - @JsonProperty("num_episodes") @SerialName("num_episodes") val numEpisodes: Int?, - @JsonProperty("start_season") @SerialName("start_season") val startSeason: StartSeason?, - @JsonProperty("broadcast") @SerialName("broadcast") val broadcast: Broadcast?, - @JsonProperty("source") @SerialName("source") val source: String?, - @JsonProperty("average_episode_duration") @SerialName("average_episode_duration") val averageEpisodeDuration: Int?, - @JsonProperty("rating") @SerialName("rating") val rating: String?, - @JsonProperty("pictures") @SerialName("pictures") val pictures: ArrayList?, - @JsonProperty("background") @SerialName("background") val background: String?, - @JsonProperty("related_anime") @SerialName("related_anime") val relatedAnime: ArrayList?, - @JsonProperty("related_manga") @SerialName("related_manga") val relatedManga: ArrayList?, - @JsonProperty("recommendations") @SerialName("recommendations") val recommendations: ArrayList?, - @JsonProperty("studios") @SerialName("studios") val studios: ArrayList?, - @JsonProperty("statistics") @SerialName("statistics") val statistics: Statistics?, + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: String?, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("mean") val mean: Double?, + @JsonProperty("rank") val rank: Int?, + @JsonProperty("popularity") val popularity: Int?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("nsfw") val nsfw: String?, + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("status") val status: String?, + @JsonProperty("genres") val genres: ArrayList?, + @JsonProperty("my_list_status") val myListStatus: MyListStatus?, + @JsonProperty("num_episodes") val numEpisodes: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, + @JsonProperty("broadcast") val broadcast: Broadcast?, + @JsonProperty("source") val source: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, + @JsonProperty("rating") val rating: String?, + @JsonProperty("pictures") val pictures: ArrayList?, + @JsonProperty("background") val background: String?, + @JsonProperty("related_anime") val relatedAnime: ArrayList?, + @JsonProperty("related_manga") val relatedManga: ArrayList?, + @JsonProperty("recommendations") val recommendations: ArrayList?, + @JsonProperty("studios") val studios: ArrayList?, + @JsonProperty("statistics") val statistics: Statistics?, ) - @Serializable data class Recommendations( - @JsonProperty("node") @SerialName("node") val node: Node? = null, - @JsonProperty("num_recommendations") @SerialName("num_recommendations") val numRecommendations: Int? = null, + @JsonProperty("node") val node: Node? = null, + @JsonProperty("num_recommendations") val numRecommendations: Int? = null ) - @Serializable data class Studios( - @JsonProperty("id") @SerialName("id") val id: Int? = null, - @JsonProperty("name") @SerialName("name") val name: String? = null, + @JsonProperty("id") val id: Int? = null, + @JsonProperty("name") val name: String? = null ) - @Serializable data class MyListStatus( - @JsonProperty("status") @SerialName("status") val status: String? = null, - @JsonProperty("score") @SerialName("score") val score: Int? = null, - @JsonProperty("num_episodes_watched") @SerialName("num_episodes_watched") val numEpisodesWatched: Int? = null, - @JsonProperty("is_rewatching") @SerialName("is_rewatching") val isRewatching: Boolean? = null, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("score") val score: Int? = null, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int? = null, + @JsonProperty("is_rewatching") val isRewatching: Boolean? = null, + @JsonProperty("updated_at") val updatedAt: String? = null ) - @Serializable data class RelatedAnime( - @JsonProperty("node") @SerialName("node") val node: Node? = null, - @JsonProperty("relation_type") @SerialName("relation_type") val relationType: String? = null, - @JsonProperty("relation_type_formatted") @SerialName("relation_type_formatted") val relationTypeFormatted: String? = null, + @JsonProperty("node") val node: Node? = null, + @JsonProperty("relation_type") val relationType: String? = null, + @JsonProperty("relation_type_formatted") val relationTypeFormatted: String? = null ) - @Serializable data class Status( - @JsonProperty("watching") @SerialName("watching") val watching: String? = null, - @JsonProperty("completed") @SerialName("completed") val completed: String? = null, - @JsonProperty("on_hold") @SerialName("on_hold") val onHold: String? = null, - @JsonProperty("dropped") @SerialName("dropped") val dropped: String? = null, - @JsonProperty("plan_to_watch") @SerialName("plan_to_watch") val planToWatch: String? = null, + @JsonProperty("watching") val watching: String? = null, + @JsonProperty("completed") val completed: String? = null, + @JsonProperty("on_hold") val onHold: String? = null, + @JsonProperty("dropped") val dropped: String? = null, + @JsonProperty("plan_to_watch") val planToWatch: String? = null ) - @Serializable data class Statistics( - @JsonProperty("status") @SerialName("status") val status: Status? = null, - @JsonProperty("num_list_users") @SerialName("num_list_users") val numListUsers: Int? = null, + @JsonProperty("status") val status: Status? = null, + @JsonProperty("num_list_users") val numListUsers: Int? = null ) private fun parseDate(string: String?): Long? { return try { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -232,11 +221,11 @@ class MALApi : SyncAPI() { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.mainPicture?.large, + posterUrl = node.mainPicture?.large ) } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val auth = auth?.token?.accessToken ?: return null val internalId = id.toIntOrNull() ?: return null val url = @@ -258,7 +247,7 @@ class MALApi : SyncAPI() { airStatus = when (malAnime.status) { "finished_airing" -> ShowStatus.Completed "currently_airing" -> ShowStatus.Ongoing - // "not_yet_aired" + //"not_yet_aired" else -> null }, nextAiring = null, @@ -282,7 +271,7 @@ class MALApi : SyncAPI() { } } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val auth = auth?.token?.accessToken ?: return null // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get @@ -345,7 +334,7 @@ class MALApi : SyncAPI() { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -355,10 +344,12 @@ class MALApi : SyncAPI() { val codeVerifier = generateCodeVerifier() val requestId = ++requestIdCounter val codeChallenge = codeVerifier - val request = "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" + val request = + "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" + return AuthLoginPage( url = request, - payload = Payload(requestId, codeVerifier).toJson(), + payload = PayLoad(requestId, codeVerifier).toJson() ) } @@ -368,71 +359,69 @@ class MALApi : SyncAPI() { data = mapOf( "client_id" to key, "grant_type" to "refresh_token", - "refresh_token" to token.refreshToken!!, + "refresh_token" to token.refreshToken!! ) ).parsed() + return AuthToken( accessToken = res.accessToken, refreshToken = res.refreshToken, - accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong(), + accessTokenLifetime = unixTime + res.expiresIn.toLong() ) } private var requestIdCounter = 0 + + private val allTitles = hashMapOf() - @Serializable data class MalList( - @JsonProperty("data") @SerialName("data") val data: List, - @JsonProperty("paging") @SerialName("paging") val paging: Paging, + @JsonProperty("data") val data: List, + @JsonProperty("paging") val paging: Paging ) - @Serializable data class MainPicture( - @JsonProperty("medium") @SerialName("medium") val medium: String, - @JsonProperty("large") @SerialName("large") val large: String, + @JsonProperty("medium") val medium: String, + @JsonProperty("large") val large: String ) - @Serializable data class Node( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: String, - @JsonProperty("main_picture") @SerialName("main_picture") val mainPicture: MainPicture?, - @JsonProperty("alternative_titles") @SerialName("alternative_titles") val alternativeTitles: AlternativeTitles?, - @JsonProperty("media_type") @SerialName("media_type") val mediaType: String?, - @JsonProperty("num_episodes") @SerialName("num_episodes") val numEpisodes: Int?, - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("start_date") @SerialName("start_date") val startDate: String?, - @JsonProperty("end_date") @SerialName("end_date") val endDate: String?, - @JsonProperty("average_episode_duration") @SerialName("average_episode_duration") val averageEpisodeDuration: Int?, - @JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?, - @JsonProperty("mean") @SerialName("mean") val mean: Double?, - @JsonProperty("genres") @SerialName("genres") val genres: List?, - @JsonProperty("rank") @SerialName("rank") val rank: Int?, - @JsonProperty("popularity") @SerialName("popularity") val popularity: Int?, - @JsonProperty("num_list_users") @SerialName("num_list_users") val numListUsers: Int?, - @JsonProperty("num_favorites") @SerialName("num_favorites") val numFavorites: Int?, - @JsonProperty("num_scoring_users") @SerialName("num_scoring_users") val numScoringUsers: Int?, - @JsonProperty("start_season") @SerialName("start_season") val startSeason: StartSeason?, - @JsonProperty("broadcast") @SerialName("broadcast") val broadcast: Broadcast?, - @JsonProperty("nsfw") @SerialName("nsfw") val nsfw: String?, - @JsonProperty("created_at") @SerialName("created_at") val createdAt: String?, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String?, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("num_episodes") val numEpisodes: Int?, + @JsonProperty("status") val status: String?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("mean") val mean: Double?, + @JsonProperty("genres") val genres: List?, + @JsonProperty("rank") val rank: Int?, + @JsonProperty("popularity") val popularity: Int?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_favorites") val numFavorites: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, + @JsonProperty("broadcast") val broadcast: Broadcast?, + @JsonProperty("nsfw") val nsfw: String?, + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String? ) - @Serializable data class ListStatus( - @JsonProperty("status") @SerialName("status") val status: String?, - @JsonProperty("score") @SerialName("score") val score: Int, - @JsonProperty("num_episodes_watched") @SerialName("num_episodes_watched") val numEpisodesWatched: Int, - @JsonProperty("is_rewatching") @SerialName("is_rewatching") val isRewatching: Boolean, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String, + @JsonProperty("status") val status: String?, + @JsonProperty("score") val score: Int, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) - @Serializable data class Data( - @JsonProperty("node") @SerialName("node") val node: Node, - @JsonProperty("list_status") @SerialName("list_status") val listStatus: ListStatus?, + @JsonProperty("node") val node: Node, + @JsonProperty("list_status") val listStatus: ListStatus?, ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( @@ -463,37 +452,32 @@ class MALApi : SyncAPI() { } } - @Serializable data class Paging( - @JsonProperty("next") @SerialName("next") val next: String?, + @JsonProperty("next") val next: String? ) - @Serializable data class AlternativeTitles( - @JsonProperty("synonyms") @SerialName("synonyms") val synonyms: List, - @JsonProperty("en") @SerialName("en") val en: String, - @JsonProperty("ja") @SerialName("ja") val ja: String, + @JsonProperty("synonyms") val synonyms: List, + @JsonProperty("en") val en: String, + @JsonProperty("ja") val ja: String ) - @Serializable data class Genres( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String ) - @Serializable data class StartSeason( - @JsonProperty("year") @SerialName("year") val year: Int, - @JsonProperty("season") @SerialName("season") val season: String, + @JsonProperty("year") val year: Int, + @JsonProperty("season") val season: String ) - @Serializable data class Broadcast( - @JsonProperty("day_of_the_week") @SerialName("day_of_the_week") val dayOfTheWeek: String?, - @JsonProperty("start_time") @SerialName("start_time") val startTime: String?, + @JsonProperty("day_of_the_week") val dayOfTheWeek: 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 { convertToStatus(it.listStatus?.status ?: "").stringRes }?.mapValues { group -> @@ -521,7 +505,7 @@ class MALApi : SyncAPI() { ) } - private suspend fun getMalAnimeListSmart(auth: AuthData): Array? { + private suspend fun getMalAnimeListSmart(auth : AuthData): Array? { return if (requireLibraryRefresh) { val list = getMalAnimeList(auth.token) setKey(MAL_CACHED_LIST, auth.user.id.toString(), list) @@ -536,7 +520,7 @@ class MALApi : SyncAPI() { val fullList = mutableListOf() val offsetRegex = Regex("""offset=(\d+)""") while (true) { - val data: MalList = getMalAnimeListSlice(token, offset) + val data: MalList = getMalAnimeListSlice(token, offset) ?: break fullList.addAll(data.data) offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } @@ -545,17 +529,18 @@ class MALApi : SyncAPI() { return fullList.toTypedArray() } - private suspend fun getMalAnimeListSlice(token: AuthToken, offset: Int = 0): MalList { + private suspend fun getMalAnimeListSlice(token: AuthToken, offset: Int = 0): MalList? { val user = "@me" // Very lackluster docs // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get - val url = "$apiUrl/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" + val url = + "$apiUrl/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" val res = app.get( url, headers = mapOf( "Authorization" to "Bearer ${token.accessToken}", ), cacheTime = 0 ).text - return parseJson(res) + return res.toKotlinObject() } private suspend fun setScoreRequest( @@ -598,7 +583,7 @@ class MALApi : SyncAPI() { val data = mapOf( "status" to status, "score" to score?.toString(), - "num_watched_episodes" to numWatchedEpisodes?.toString(), + "num_watched_episodes" to numWatchedEpisodes?.toString() ).filterValues { it != null } as Map return app.put( @@ -610,74 +595,71 @@ class MALApi : SyncAPI() { ).text } - @Serializable + data class ResponseToken( - @JsonProperty("token_type") @SerialName("token_type") val tokenType: String, - @JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int, - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String, - @JsonProperty("refresh_token") @SerialName("refresh_token") val refreshToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) - @Serializable data class MalRoot( - @JsonProperty("data") @SerialName("data") val data: List, + @JsonProperty("data") val data: List, ) - @Serializable data class MalDatum( - @JsonProperty("node") @SerialName("node") val node: MalNode, - @JsonProperty("list_status") @SerialName("list_status") val listStatus: MalStatus, + @JsonProperty("node") val node: MalNode, + @JsonProperty("list_status") val listStatus: MalStatus, ) - @Serializable data class MalNode( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: String, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String, + /* + also, but not used + main_picture -> + public string medium; + public string large; + */ ) - @Serializable data class MalStatus( - @JsonProperty("status") @SerialName("status") val status: String, - @JsonProperty("score") @SerialName("score") val score: Int, - @JsonProperty("num_episodes_watched") @SerialName("num_episodes_watched") val numEpisodesWatched: Int, - @JsonProperty("is_rewatching") @SerialName("is_rewatching") val isRewatching: Boolean, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String, + @JsonProperty("status") val status: String, + @JsonProperty("score") val score: Int, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) - @Serializable data class MalUser( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("location") @SerialName("location") val location: String, - @JsonProperty("joined_at") @SerialName("joined_at") val joinedAt: String, - @JsonProperty("picture") @SerialName("picture") val picture: String?, + @JsonProperty("id") val id: Int, + @JsonProperty("name") val name: String, + @JsonProperty("location") val location: String, + @JsonProperty("joined_at") val joinedAt: String, + @JsonProperty("picture") val picture: String?, ) - @Serializable data class MalMainPicture( - @JsonProperty("large") @SerialName("large") val large: String?, - @JsonProperty("medium") @SerialName("medium") val medium: String?, + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String?, ) // Used for getDataAboutId() - @Serializable data class SmallMalAnime( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("num_episodes") @SerialName("num_episodes") val numEpisodes: Int, - @JsonProperty("my_list_status") @SerialName("my_list_status") val myListStatus: MalStatus?, - @JsonProperty("main_picture") @SerialName("main_picture") val mainPicture: MalMainPicture?, + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String?, + @JsonProperty("num_episodes") val numEpisodes: Int, + @JsonProperty("my_list_status") val myListStatus: MalStatus?, + @JsonProperty("main_picture") val mainPicture: MalMainPicture?, ) - @Serializable data class MalSearchNode( - @JsonProperty("node") @SerialName("node") val node: Node, + @JsonProperty("node") val node: Node, ) - @Serializable data class MalSearch( - @JsonProperty("data") @SerialName("data") val data: List, - // paging + @JsonProperty("data") val data: List, + //paging ) data class MalTitleHolder( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index fca3e5df8..4b17fdb29 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -2,11 +2,9 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthData import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement @@ -20,8 +18,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToOpenSubtitlesTag -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable class OpenSubtitlesApi : SubtitleAPI() { override val name = "OpenSubtitles" @@ -47,17 +43,17 @@ class OpenSubtitlesApi : SubtitleAPI() { } private fun canDoRequest(): Boolean { - return unixTimeMS > currentCoolDown + return unixTimeMs > currentCoolDown } private fun throwIfCantDoRequest() { if (!canDoRequest()) { - throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMS) / 1000L}s") + throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s") } } private fun throwGotTooManyRequests() { - currentCoolDown = unixTimeMS + COOLDOWN_DURATION + currentCoolDown = unixTimeMs + COOLDOWN_DURATION throw ErrorLoadingException("Too many requests") } @@ -93,7 +89,7 @@ class OpenSubtitlesApi : SubtitleAPI() { accessToken = response.token ?: throw ErrorLoadingException("Invalid password or username"), /// JWT token is valid 24 hours after successfully authentication of user - accessTokenLifetime = APIHolder.unixTime + 60 * 60 * 24, + accessTokenLifetime = unixTime + 60 * 60 * 24, payload = form.toJson() ) } @@ -101,7 +97,7 @@ class OpenSubtitlesApi : SubtitleAPI() { /** * Fetch subtitles using token authenticated on previous method (see authorize). * Returns list of Subtitles which user can select to download (see load). - */ + * */ override suspend fun search( auth : AuthData?, query: AbstractSubtitleEntities.SubtitleSearch @@ -181,15 +177,16 @@ class OpenSubtitlesApi : SubtitleAPI() { return results } - /** - * Process data returned from search. - * Returns string url for the subtitle file. - */ + /* + Process data returned from search. + Returns string url for the subtitle file. + */ + override suspend fun load( auth : AuthData?, subtitle: AbstractSubtitleEntities.SubtitleEntity ): String? { - if (auth == null) return null + if(auth == null) return null throwIfCantDoRequest() val req = app.post( @@ -221,64 +218,57 @@ class OpenSubtitlesApi : SubtitleAPI() { return null } - @Serializable data class OAuthToken( - @JsonProperty("token") @SerialName("token") var token: String? = null, - @JsonProperty("status") @SerialName("status") var status: Int? = null, + @JsonProperty("token") var token: String? = null, + @JsonProperty("status") var status: Int? = null ) - @Serializable data class Results( - @JsonProperty("data") @SerialName("data") var data: List? = listOf(), + @JsonProperty("data") var data: List? = listOf() ) - @Serializable data class ResultData( - @JsonProperty("id") @SerialName("id") var id: String? = null, - @JsonProperty("type") @SerialName("type") var type: String? = null, - @JsonProperty("attributes") @SerialName("attributes") var attributes: ResultAttributes? = ResultAttributes(), + @JsonProperty("id") var id: String? = null, + @JsonProperty("type") var type: String? = null, + @JsonProperty("attributes") var attributes: ResultAttributes? = ResultAttributes() ) - @Serializable data class ResultAttributes( - @JsonProperty("subtitle_id") @SerialName("subtitle_id") var subtitleId: String? = null, - @JsonProperty("language") @SerialName("language") var language: String? = null, - @JsonProperty("release") @SerialName("release") var release: String? = null, - @JsonProperty("url") @SerialName("url") var url: String? = null, - @JsonProperty("files") @SerialName("files") var files: List? = listOf(), - @JsonProperty("feature_details") @SerialName("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), - @JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Boolean? = null, + @JsonProperty("subtitle_id") var subtitleId: String? = null, + @JsonProperty("language") var language: String? = null, + @JsonProperty("release") var release: String? = null, + @JsonProperty("url") var url: String? = null, + @JsonProperty("files") var files: List? = listOf(), + @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), + @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, ) - @Serializable data class ResultFiles( - @JsonProperty("file_id") @SerialName("file_id") var fileId: Int? = null, - @JsonProperty("file_name") @SerialName("file_name") var fileName: String? = null, + @JsonProperty("file_id") var fileId: Int? = null, + @JsonProperty("file_name") var fileName: String? = null ) - @Serializable data class ResultDownloadLink( - @JsonProperty("link") @SerialName("link") var link: String? = null, - @JsonProperty("file_name") @SerialName("file_name") var fileName: String? = null, - @JsonProperty("requests") @SerialName("requests") var requests: Int? = null, - @JsonProperty("remaining") @SerialName("remaining") var remaining: Int? = null, - @JsonProperty("message") @SerialName("message") var message: String? = null, - @JsonProperty("reset_time") @SerialName("reset_time") var resetTime: String? = null, - @JsonProperty("reset_time_utc") @SerialName("reset_time_utc") var resetTimeUtc: String? = null, + @JsonProperty("link") var link: String? = null, + @JsonProperty("file_name") var fileName: String? = null, + @JsonProperty("requests") var requests: Int? = null, + @JsonProperty("remaining") var remaining: Int? = null, + @JsonProperty("message") var message: String? = null, + @JsonProperty("reset_time") var resetTime: String? = null, + @JsonProperty("reset_time_utc") var resetTimeUtc: String? = null ) - @Serializable data class ResultFeatureDetails( - @JsonProperty("year") @SerialName("year") var year: Int? = null, - @JsonProperty("title") @SerialName("title") var title: String? = null, - @JsonProperty("movie_name") @SerialName("movie_name") var movieName: String? = null, - @JsonProperty("imdb_id") @SerialName("imdb_id") var imdbId: Int? = null, - @JsonProperty("tmdb_id") @SerialName("tmdb_id") var tmdbId: Int? = null, - @JsonProperty("season_number") @SerialName("season_number") var seasonNumber: Int? = null, - @JsonProperty("episode_number") @SerialName("episode_number") var episodeNumber: Int? = null, - @JsonProperty("parent_imdb_id") @SerialName("parent_imdb_id") var parentImdbId: Int? = null, - @JsonProperty("parent_title") @SerialName("parent_title") var parentTitle: String? = null, - @JsonProperty("parent_tmdb_id") @SerialName("parent_tmdb_id") var parentTmdbId: Int? = null, - @JsonProperty("parent_feature_id") @SerialName("parent_feature_id") var parentFeatureId: Int? = null, + @JsonProperty("year") var year: Int? = null, + @JsonProperty("title") var title: String? = null, + @JsonProperty("movie_name") var movieName: String? = null, + @JsonProperty("imdb_id") var imdbId: Int? = null, + @JsonProperty("tmdb_id") var tmdbId: Int? = null, + @JsonProperty("season_number") var seasonNumber: Int? = null, + @JsonProperty("episode_number") var episodeNumber: Int? = null, + @JsonProperty("parent_imdb_id") var parentImdbId: Int? = null, + @JsonProperty("parent_title") var parentTitle: String? = null, + @JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null, + @JsonProperty("parent_feature_id") var parentFeatureId: Int? = null ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index a57ec6716..c4095e2d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -2,11 +2,10 @@ package com.lagradost.cloudstream3.syncproviders.providers import androidx.annotation.StringRes import androidx.core.net.toUri -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeys import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey @@ -17,6 +16,7 @@ import com.lagradost.cloudstream3.Score import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING @@ -30,14 +30,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear -import com.lagradost.cloudstream3.utils.serializers.NonEmptySerializer import com.lagradost.cloudstream3.utils.txt -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.math.BigInteger import java.security.SecureRandom import java.text.SimpleDateFormat @@ -50,18 +44,22 @@ import kotlin.time.DurationUnit import kotlin.time.toDuration class SimklApi : SyncAPI() { - override val name = "Simkl" + override var name = "Simkl" override val idPrefix = "simkl" + val key = "simkl-key" override val redirectUrlIdentifier = "simkl" override val hasOAuth2 = true override val hasPin = true override var requireLibraryRefresh = true - override val mainUrl = "https://api.simkl.com" + override var mainUrl = "https://api.simkl.com" override val icon = R.drawable.simkl_logo override val createAccountUrl = "$mainUrl/signup" override val syncIdName = SyncIdName.Simkl + /** Automatically adds simkl auth headers */ + // private val interceptor = HeaderInterceptor() + /** * This is required to override the reported last activity as simkl activites * may not always update based on testing. @@ -70,99 +68,68 @@ class SimklApi : SyncAPI() { private object SimklCache { private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE" + enum class CacheTimes(val value: String) { OneMonth("30d"), ThirtyMinutes("30m") } - @Serializable - private data class MediaObjectCacheEntry( - @JsonProperty("obj") @SerialName("obj") val obj: MediaObject?, - @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long = APIHolder.unixTime, - ) + private class SimklCacheWrapper( + @JsonProperty("obj") val obj: T?, + @JsonProperty("validUntil") val validUntil: Long, + @JsonProperty("cacheTime") val cacheTime: Long = unixTime, + ) { + /** Returns true if cache is newer than cacheDays */ + fun isFresh(): Boolean { + return validUntil > unixTime + } - @Serializable - private data class EpisodesCacheEntry( - @JsonProperty("obj") @SerialName("obj") val obj: Array?, - @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long = APIHolder.unixTime, - ) - - /** - * Minimal class used only to peek at an entry's expiry, without caring which of the - * concrete entry types above actually produced it. - */ - @Serializable - private data class CacheFreshness( - @JsonProperty("validUntil") @SerialName("validUntil") val validUntil: Long, - ) - - private fun Long.isFresh(): Boolean = this > APIHolder.unixTime - - private fun Long.remaining(): Duration { - val unixTime = APIHolder.unixTime - return if (this > unixTime) { - (this - unixTime).toDuration(DurationUnit.SECONDS) - } else { - Duration.ZERO + fun remainingTime(): Duration { + val unixTime = unixTime + return if (validUntil > unixTime) { + (validUntil - unixTime).toDuration(DurationUnit.SECONDS) + } else { + Duration.ZERO + } } } fun cleanOldCache() { getKeys(SIMKL_CACHE_KEY)?.forEach { - val isOld = getKey(it)?.validUntil?.isFresh() == false - if (isOld) removeKey(it) - } - } - - fun setMediaObject(path: String, value: MediaObject, cacheTime: Duration) { - debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } - setKey( - SIMKL_CACHE_KEY, - path, - MediaObjectCacheEntry(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson(), - ) - } - - /** Gets the cached [MediaObject], if it's not fresh returns null and removes it from cache */ - fun getMediaObject(path: String): MediaObject? { - val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson(it) - } - - return if (cache?.validUntil?.isFresh() == true) { - debugPrint { - "Cache hit at: $SIMKL_CACHE_KEY/$path. " + - "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." + val isOld = CloudStreamApp.getKey>(it)?.isFresh() == false + if (isOld) { + removeKey(it) } - cache.obj - } else { - debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" } - removeKey(SIMKL_CACHE_KEY, path) - null } } - fun setEpisodes(path: String, value: Array, cacheTime: Duration) { + fun setKey(path: String, value: T, cacheTime: Duration) { debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } setKey( SIMKL_CACHE_KEY, path, - EpisodesCacheEntry(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson(), + // Storing as plain sting is required to make generics work. + SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson() ) } - /** Gets the cached episode list, if it's not fresh returns null and removes it from cache */ - fun getEpisodes(path: String): Array? { + /** + * Gets cached object, if object is not fresh returns null and removes it from cache + */ + inline fun getKey(path: String): T? { + // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject" + val type = mapper.typeFactory.constructParametricType( + SimklCacheWrapper::class.java, + T::class.java + ) val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson(it) + mapper.readValue>(it, type) } - return if (cache?.validUntil?.isFresh() == true) { + return if (cache?.isFresh() == true) { debugPrint { "Cache hit at: $SIMKL_CACHE_KEY/$path. " + - "Remains fresh for ${cache.validUntil.remaining().inWholeDays} days or ${cache.validUntil.remaining().inWholeSeconds} seconds." + "Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds." } cache.obj } else { @@ -206,7 +173,7 @@ class SimklApi : SyncAPI() { ) ) ) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -222,7 +189,7 @@ class SimklApi : SyncAPI() { enum class SimklListStatusType( var value: Int, @StringRes val stringRes: Int, - val originalName: String?, + val originalName: String? ) { Watching(0, R.string.type_watching, "watching"), Completed(1, R.string.type_completed, "completed"), @@ -241,92 +208,83 @@ class SimklApi : SyncAPI() { } } + // ------------------- @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = TokenRequest.Serializer::class) data class TokenRequest( - @JsonProperty("code") @SerialName("code") val code: String, - @JsonProperty("client_id") @SerialName("client_id") val clientId: String = CLIENT_ID, - @JsonProperty("client_secret") @SerialName("client_secret") val clientSecret: String = CLIENT_SECRET, - @JsonProperty("redirect_uri") @SerialName("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", - @JsonProperty("grant_type") @SerialName("grant_type") val grantType: String = "authorization_code", - ) { - object Serializer : NonEmptySerializer(TokenRequest.generatedSerializer()) - } + @JsonProperty("code") val code: String, + @JsonProperty("client_id") val clientId: String = CLIENT_ID, + @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, + @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", + @JsonProperty("grant_type") val grantType: String = "authorization_code" + ) - @Serializable data class TokenResponse( /** No expiration date */ - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String, - @JsonProperty("token_type") @SerialName("token_type") val tokenType: String, - @JsonProperty("scope") @SerialName("scope") val scope: String, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("scope") val scope: String ) + // ------------------- /** https://simkl.docs.apiary.io/#reference/users/settings/receive-settings */ - @Serializable data class SettingsResponse( - @JsonProperty("user") @SerialName("user") val user: User, - @JsonProperty("account") @SerialName("account") val account: Account, + @JsonProperty("user") + val user: User, + @JsonProperty("account") + val account: Account, ) { - @Serializable data class User( - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("avatar") @SerialName("avatar") val avatar: String, // Url + @JsonProperty("name") + val name: String, + /** Url */ + @JsonProperty("avatar") + val avatar: String ) - @Serializable data class Account( - @JsonProperty("id") @SerialName("id") val id: Int, + @JsonProperty("id") + val id: Int, ) } - @Serializable data class PinAuthResponse( - @JsonProperty("result") @SerialName("result") val result: String, - @JsonProperty("device_code") @SerialName("device_code") val deviceCode: String, - @JsonProperty("user_code") @SerialName("user_code") val userCode: String, - @JsonProperty("verification_url") @SerialName("verification_url") val verificationUrl: String, - @JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int, - @JsonProperty("interval") @SerialName("interval") val interval: Int, + @JsonProperty("result") val result: String, + @JsonProperty("device_code") val deviceCode: String, + @JsonProperty("user_code") val userCode: String, + @JsonProperty("verification_url") val verificationUrl: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("interval") val interval: Int, ) - @Serializable data class PinExchangeResponse( - @JsonProperty("result") @SerialName("result") val result: String, - @JsonProperty("message") @SerialName("message") val message: String? = null, - @JsonProperty("access_token") @SerialName("access_token") val accessToken: String? = null, + @JsonProperty("result") val result: String, + @JsonProperty("message") val message: String? = null, + @JsonProperty("access_token") val accessToken: String? = null, ) - @Serializable + // ------------------- data class ActivitiesResponse( - @JsonProperty("all") @SerialName("all") val all: String?, - @JsonProperty("tv_shows") @SerialName("tv_shows") val tvShows: UpdatedAt, - @JsonProperty("anime") @SerialName("anime") val anime: UpdatedAt, - @JsonProperty("movies") @SerialName("movies") val movies: UpdatedAt, + @JsonProperty("all") val all: String?, + @JsonProperty("tv_shows") val tvShows: UpdatedAt, + @JsonProperty("anime") val anime: UpdatedAt, + @JsonProperty("movies") val movies: UpdatedAt, ) { - @Serializable data class UpdatedAt( - @JsonProperty("all") @SerialName("all") val all: String?, - @JsonProperty("removed_from_list") @SerialName("removed_from_list") val removedFromList: String?, - @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String?, + @JsonProperty("all") val all: String?, + @JsonProperty("removed_from_list") val removedFromList: String?, + @JsonProperty("rated_at") val ratedAt: String?, ) } /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = EpisodeMetadata.Serializer::class) data class EpisodeMetadata( - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int, - @JsonProperty("img") @SerialName("img") val img: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("season") val season: Int?, + @JsonProperty("episode") val episode: Int, + @JsonProperty("img") val img: String? ) { - object Serializer : NonEmptySerializer(EpisodeMetadata.generatedSerializer()) - companion object { fun convertToEpisodes(list: List?): List? { return list?.map { @@ -346,58 +304,40 @@ class SimklApi : SyncAPI() { /** * https://simkl.docs.apiary.io/#introduction/about-simkl-api/standard-media-objects - * Useful for finding shows from metadata. + * Useful for finding shows from metadata */ @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = MediaObject.Serializer::class) - data class MediaObject( - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("year") @SerialName("year") val year: Int?, - @JsonProperty("ids") @SerialName("ids") val ids: Ids?, - @JsonProperty("total_episodes") @SerialName("total_episodes") val totalEpisodes: Int? = null, - @JsonProperty("status") @SerialName("status") val status: String? = null, - @JsonProperty("poster") @SerialName("poster") val poster: String? = null, - @JsonProperty("type") @SerialName("type") val type: String? = null, - @JsonProperty("seasons") @SerialName("seasons") val seasons: List? = null, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, + open class MediaObject( + @JsonProperty("title") val title: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids?, + @JsonProperty("total_episodes") val totalEpisodes: Int? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("seasons") val seasons: List? = null, + @JsonProperty("episodes") val episodes: List? = null ) { - object Serializer : NonEmptySerializer(MediaObject.generatedSerializer()) - fun hasEnded(): Boolean { return status == "released" || status == "ended" } @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = Season.Serializer::class) data class Season( - @JsonProperty("number") @SerialName("number") val number: Int, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List, + @JsonProperty("number") val number: Int, + @JsonProperty("episodes") val episodes: List ) { - object Serializer : NonEmptySerializer(Season.generatedSerializer()) - - @Serializable - data class Episode( - @JsonProperty("number") @SerialName("number") val number: Int, - ) + data class Episode(@JsonProperty("number") val number: Int) } @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = Ids.Serializer::class) data class Ids( - @JsonProperty("simkl") @SerialName("simkl") val simkl: Int?, - @JsonProperty("imdb") @SerialName("imdb") val imdb: String? = null, - @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: String? = null, - @JsonProperty("mal") @SerialName("mal") val mal: String? = null, - @JsonProperty("anilist") @SerialName("anilist") val anilist: String? = null, + @JsonProperty("simkl") val simkl: Int?, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: String? = null, + @JsonProperty("mal") val mal: String? = null, + @JsonProperty("anilist") val anilist: String? = null, ) { - object Serializer : NonEmptySerializer(Ids.generatedSerializer()) - companion object { fun fromMap(map: Map): Ids { return Ids( @@ -405,21 +345,20 @@ class SimklApi : SyncAPI() { imdb = map[SimklSyncServices.Imdb], tmdb = map[SimklSyncServices.Tmdb], mal = map[SimklSyncServices.Mal], - anilist = map[SimklSyncServices.AniList], + anilist = map[SimklSyncServices.AniList] ) } } } fun toSyncSearchResult(): SyncAPI.SyncSearchResult? { - val currentIds = this.ids return SyncAPI.SyncSearchResult( this.title ?: return null, "Simkl", - currentIds?.simkl?.toString() ?: return null, - getUrlFromId(currentIds.simkl), + this.ids?.simkl?.toString() ?: return null, + getUrlFromId(this.ids.simkl), this.poster?.let { getPosterUrl(it) }, - if (this.type == "movie") TvType.Movie else TvType.TvSeries, + if (this.type == "movie") TvType.Movie else TvType.TvSeries ) } } @@ -434,7 +373,7 @@ class SimklApi : SyncAPI() { private var addEpisodes: Pair?, List?>? = null, private var removeEpisodes: Pair?, List?>? = null, // Required for knowing if the status should be overwritten - private var onList: Boolean = false, + private var onList: Boolean = false ) { fun token(token: AuthToken) = apply { this.headers = getHeaders(token) } fun apiUrl(url: String) = apply { this.url = url } @@ -448,7 +387,11 @@ class SimklApi : SyncAPI() { fun status(newStatus: Int?, oldStatus: Int?) = apply { onList = oldStatus != null // Only set status if its new - this.status = if (newStatus != oldStatus) newStatus else null + if (newStatus != oldStatus) { + this.status = newStatus + } else { + this.status = null + } } fun episodes( @@ -457,6 +400,7 @@ class SimklApi : SyncAPI() { oldEpisodes: Int?, ) = apply { if (allEpisodes == null || newEpisodes == null) return@apply + fun getEpisodes(rawEpisodes: List) = if (rawEpisodes.any { it.season != null }) { EpisodeMetadata.convertToSeasons(rawEpisodes) to null @@ -467,34 +411,36 @@ class SimklApi : SyncAPI() { // Do not add episodes if there is no change if (newEpisodes > (oldEpisodes ?: 0)) { this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) + // Set to watching if episodes are added and there is no current status if (!onList) { status = SimklListStatusType.Watching.value } } - if ((oldEpisodes ?: 0) > newEpisodes) { this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) } } suspend fun execute(): Boolean { - val time = getDateTime(APIHolder.unixTime) + val time = getDateTime(unixTime) val headers = this.headers ?: emptyMap() return if (this.status == SimklListStatusType.None.value) { app.post( "$url/sync/history/remove", - json = HistoryRequest( + json = StatusRequest( shows = listOf(HistoryMediaObject(ids = ids)), - movies = emptyList(), + movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful } else { val statusResponse = this.status?.let { setStatus -> - val newStatus = SimklListStatusType.entries.firstOrNull { - it.value == setStatus - }?.originalName ?: SimklListStatusType.Watching.originalName!! + val newStatus = + SimklListStatusType.entries + .firstOrNull { it.value == setStatus }?.originalName + ?: SimklListStatusType.Watching.originalName!! + app.post( "${this.url}/sync/add-to-list", json = StatusRequest( @@ -504,40 +450,41 @@ class SimklApi : SyncAPI() { null, ids, newStatus, - ), - ), - movies = emptyList(), + ) + ), movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful } ?: true val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) -> app.post( "${this.url}/sync/history/remove", - json = HistoryRequest( + json = StatusRequest( shows = listOf( HistoryMediaObject( ids = ids, seasons = seasons, - episodes = episodes, - ), + episodes = episodes + ) ), - movies = emptyList(), + movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful } ?: true // You cannot rate if you are planning to watch it. - val shouldRate = score != null && status != SimklListStatusType.Planning.value + val shouldRate = + score != null && status != SimklListStatusType.Planning.value val realScore = if (shouldRate) score else null + val historyResponse = // Only post if there are episodes or score to upload if (addEpisodes != null || shouldRate) { app.post( "${this.url}/sync/history", - json = HistoryRequest( + json = StatusRequest( shows = listOf( HistoryMediaObject( null, @@ -547,33 +494,34 @@ class SimklApi : SyncAPI() { addEpisodes?.second, realScore, realScore?.let { time }, - ), - ), - movies = emptyList(), + ) + ), movies = emptyList() ), - headers = headers, + headers = headers ).isSuccessful - } else true + } else { + true + } + statusResponse && episodeRemovalResponse && historyResponse } } } } - fun getHeaders(token: AuthToken): Map = mapOf( - "Authorization" to "Bearer ${token.accessToken}", - "simkl-api-key" to CLIENT_ID, - ) + fun getHeaders(token: AuthToken): Map = + mapOf("Authorization" to "Bearer ${token.accessToken}", "simkl-api-key" to CLIENT_ID) suspend fun getEpisodes( simklId: Int?, type: String?, episodes: Int?, - hasEnded: Boolean?, + hasEnded: Boolean? ): Array? { if (simklId == null) return null + val cacheKey = "Episodes/$simklId" - val cache = SimklCache.getEpisodes(cacheKey) + val cache = SimklCache.getKey>(cacheKey) // Return cached result if its higher or equal the amount of episodes. if (cache != null && cache.size >= (episodes ?: 0)) { @@ -590,7 +538,6 @@ class SimklApi : SyncAPI() { }.toTypedArray() } } - val url = when (type) { "anime" -> "https://api.simkl.com/anime/episodes/$simklId" "tv" -> "https://api.simkl.com/tv/episodes/$simklId" @@ -605,86 +552,57 @@ class SimklApi : SyncAPI() { if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value // 1 Month cache - SimklCache.setEpisodes(cacheKey, it, Duration.parse(cacheTime)) + SimklCache.setKey(cacheKey, it, Duration.parse(cacheTime)) } } @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = HistoryMediaObject.Serializer::class) - data class HistoryMediaObject( - @JsonProperty("title") @SerialName("title") val title: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, - @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, - @JsonProperty("seasons") @SerialName("seasons") val seasons: List? = null, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List? = null, - @JsonProperty("rating") @SerialName("rating") val rating: Int? = null, - @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = null, - ) { - object Serializer : NonEmptySerializer(HistoryMediaObject.generatedSerializer()) - } + class HistoryMediaObject( + @JsonProperty("title") title: String? = null, + @JsonProperty("year") year: Int? = null, + @JsonProperty("ids") ids: Ids? = null, + @JsonProperty("seasons") seasons: List? = null, + @JsonProperty("episodes") episodes: List? = null, + @JsonProperty("rating") val rating: Int? = null, + @JsonProperty("rated_at") val ratedAt: String? = null, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = RatingMediaObject.Serializer::class) - data class RatingMediaObject( - @JsonProperty("title") @SerialName("title") val title: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, - @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, - @JsonProperty("rating") @SerialName("rating") val rating: Int, - @JsonProperty("rated_at") @SerialName("rated_at") val ratedAt: String? = getDateTime(APIHolder.unixTime), - ) { - object Serializer : NonEmptySerializer(RatingMediaObject.generatedSerializer()) - } + class RatingMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("rating") val rating: Int, + @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = StatusMediaObject.Serializer::class) - data class StatusMediaObject( - @JsonProperty("title") @SerialName("title") val title: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, - @JsonProperty("ids") @SerialName("ids") val ids: MediaObject.Ids? = null, - @JsonProperty("to") @SerialName("to") val to: String, - @JsonProperty("watched_at") @SerialName("watched_at") val watchedAt: String? = getDateTime(APIHolder.unixTime), - ) { - object Serializer : NonEmptySerializer(StatusMediaObject.generatedSerializer()) - } + class StatusMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("to") val to: String, + @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = StatusRequest.Serializer::class) data class StatusRequest( - @JsonProperty("movies") @SerialName("movies") val movies: List, - @JsonProperty("shows") @SerialName("shows") val shows: List, - ) { - object Serializer : NonEmptySerializer(StatusRequest.generatedSerializer()) - } - - /** Same shape as [StatusRequest], for the endpoints that post [HistoryMediaObject]s instead. */ - @JsonInclude(JsonInclude.Include.NON_EMPTY) - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = HistoryRequest.Serializer::class) - data class HistoryRequest( - @JsonProperty("movies") @SerialName("movies") val movies: List, - @JsonProperty("shows") @SerialName("shows") val shows: List, - ) { - object Serializer : NonEmptySerializer(HistoryRequest.generatedSerializer()) - } + @JsonProperty("movies") val movies: List, + @JsonProperty("shows") val shows: List + ) /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ - @Serializable data class AllItemsResponse( - @JsonProperty("shows") @SerialName("shows") val shows: List = emptyList(), - @JsonProperty("anime") @SerialName("anime") val anime: List = emptyList(), - @JsonProperty("movies") @SerialName("movies") val movies: List = emptyList(), + @JsonProperty("shows") + val shows: List = emptyList(), + @JsonProperty("anime") + val anime: List = emptyList(), + @JsonProperty("movies") + val movies: List = emptyList(), ) { companion object { fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { + // Replace the first item with the same id, or add the new item fun MutableList.replaceOrAddItem(newItem: T, predicate: (T) -> Boolean) { for (i in this.indices) { @@ -693,10 +611,10 @@ class SimklApi : SyncAPI() { return } } - this.add(newItem) } + // fun merge( first: List?, second: List? @@ -730,17 +648,15 @@ class SimklApi : SyncAPI() { fun toLibraryItem(): SyncAPI.LibraryItem } - @Serializable data class MovieMetadata( - @JsonProperty("last_watched_at") @SerialName("last_watched_at") override val lastWatchedAt: String?, - @JsonProperty("status") @SerialName("status") override val status: String, - @JsonProperty("user_rating") @SerialName("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") @SerialName("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") @SerialName("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") @SerialName("total_episodes_count") override val totalEpisodesCount: Int?, - @JsonProperty("movie") @SerialName("movie") val movie: ShowMetadata.Show, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, + val movie: ShowMetadata.Show ) : Metadata { - @JsonIgnore override fun getIds(): ShowMetadata.Show.Ids { return this.movie.ids } @@ -760,22 +676,20 @@ class SimklApi : SyncAPI() { null, null, this.movie.year?.toYear(), - movie.ids.simkl, + movie.ids.simkl ) } } - @Serializable data class ShowMetadata( - @JsonProperty("last_watched_at") @SerialName("last_watched_at") override val lastWatchedAt: String?, - @JsonProperty("status") @SerialName("status") override val status: String, - @JsonProperty("user_rating") @SerialName("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") @SerialName("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") @SerialName("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") @SerialName("total_episodes_count") override val totalEpisodesCount: Int?, - @JsonProperty("show") @SerialName("show") val show: Show, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, + @JsonProperty("show") val show: Show ) : Metadata { - @JsonIgnore override fun getIds(): Show.Ids { return this.show.ids } @@ -795,30 +709,28 @@ class SimklApi : SyncAPI() { null, null, this.show.year?.toYear(), - show.ids.simkl, + show.ids.simkl ) } - @Serializable data class Show( - @JsonProperty("title") @SerialName("title") val title: String, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("year") @SerialName("year") val year: Int?, - @JsonProperty("ids") @SerialName("ids") val ids: Ids, + @JsonProperty("title") val title: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids, ) { - @Serializable data class Ids( - @JsonProperty("simkl") @SerialName("simkl") val simkl: Int, - @JsonProperty("slug") @SerialName("slug") val slug: String?, - @JsonProperty("imdb") @SerialName("imdb") val imdb: String?, - @JsonProperty("zap2it") @SerialName("zap2it") val zap2it: String?, - @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: String?, - @JsonProperty("offen") @SerialName("offen") val offen: String?, - @JsonProperty("tvdb") @SerialName("tvdb") val tvdb: String?, - @JsonProperty("mal") @SerialName("mal") val mal: String?, - @JsonProperty("anidb") @SerialName("anidb") val anidb: String?, - @JsonProperty("anilist") @SerialName("anilist") val anilist: String?, - @JsonProperty("traktslug") @SerialName("traktslug") val traktslug: String?, + @JsonProperty("simkl") val simkl: Int, + @JsonProperty("slug") val slug: String?, + @JsonProperty("imdb") val imdb: String?, + @JsonProperty("zap2it") val zap2it: String?, + @JsonProperty("tmdb") val tmdb: String?, + @JsonProperty("offen") val offen: String?, + @JsonProperty("tvdb") val tvdb: String?, + @JsonProperty("mal") val mal: String?, + @JsonProperty("anidb") val anidb: String?, + @JsonProperty("anilist") val anilist: String?, + @JsonProperty("traktslug") val traktslug: String? ) { fun matchesId(database: SimklSyncServices, id: String): Boolean { return when (database) { @@ -835,10 +747,27 @@ class SimklApi : SyncAPI() { } } + /** + * Appends api keys to the requests + **/ + /*private inner class HeaderInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + debugPrint { "${this@SimklApi.name} made request to ${chain.request().url}" } + return chain.proceed( + chain.request() + .newBuilder() + .addHeader("Authorization", "Bearer $token") + .addHeader("simkl-api-key", CLIENT_ID) + .build() + ) + } + }*/ + private suspend fun getUser(token: AuthToken): SettingsResponse = app.post("$mainUrl/users/settings", headers = getHeaders(token)) .parsed() + /** * Useful to get episodes on demand to prevent unnecessary requests. */ @@ -846,7 +775,7 @@ class SimklApi : SyncAPI() { private val simklId: Int?, private val type: String?, private val totalEpisodeCount: Int?, - private val hasEnded: Boolean?, + private val hasEnded: Boolean? ) { suspend fun getEpisodes(): Array? { return getEpisodes(simklId, type, totalEpisodeCount, hasEnded) @@ -861,12 +790,10 @@ class SimklApi : SyncAPI() { val episodeConstructor: SimklEpisodeConstructor, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, - /** - * Save seen episodes separately to know the change from old to new. - * Required to remove seen episodes if count decreases. - */ + /** Save seen episodes separately to know the change from old to new. + * Required to remove seen episodes if count decreases */ val oldEpisodes: Int, - val oldStatus: String?, + val oldStatus: String? ) : SyncAPI.AbstractSyncStatus() override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { @@ -876,23 +803,22 @@ class SimklApi : SyncAPI() { // Key which assumes all ids are the same each time :/ // This could be some sort of reference system to make multiple IDs // point to the same key. - val idKey = realIds.toList().map { - "${it.first.originalName}=${it.second}" - }.sorted().joinToString() + val idKey = + realIds.toList().map { "${it.first.originalName}=${it.second}" }.sorted().joinToString() - val cachedObject = SimklCache.getMediaObject(idKey) + val cachedObject = SimklCache.getKey(idKey) val searchResult: MediaObject = cachedObject ?: (searchByIds(realIds)?.firstOrNull()?.also { result -> val cacheTime = if (result.hasEnded()) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value - SimklCache.setMediaObject(idKey, result, Duration.parse(cacheTime)) + SimklCache.setKey(idKey, result, Duration.parse(cacheTime)) }) ?: return null val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, searchResult.totalEpisodes, - searchResult.hasEnded(), + searchResult.hasEnded() ) val foundItem = getSyncListSmart(auth)?.let { list -> @@ -907,16 +833,19 @@ class SimklApi : SyncAPI() { return SimklSyncStatus( status = foundItem.status?.let { SyncWatchType.fromInternalId( - SimklListStatusType.fromString(it)?.value + SimklListStatusType.fromString( + it + )?.value ) - } ?: return null, + } + ?: return null, score = Score.from10(foundItem.userRating), watchedEpisodes = foundItem.watchedEpisodesCount, maxEpisodes = searchResult.totalEpisodes, episodeConstructor = episodeConstructor, oldEpisodes = foundItem.watchedEpisodesCount ?: 0, oldScore = foundItem.userRating, - oldStatus = foundItem.status, + oldStatus = foundItem.status ) } else { return SimklSyncStatus( @@ -927,7 +856,7 @@ class SimklApi : SyncAPI() { episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, - oldScore = null, + oldScore = null ) } } @@ -935,11 +864,12 @@ class SimklApi : SyncAPI() { override suspend fun updateStatus( auth: AuthData?, id: String, - newStatus: AbstractSyncStatus, + newStatus: AbstractSyncStatus ): Boolean { - lastScoreTime = APIHolder.unixTime val parsedId = readIdFromString(id) + lastScoreTime = unixTime val simklStatus = newStatus as? SimklSyncStatus + val builder = SimklScoreBuilder.Builder() .apiUrl(this.mainUrl) .score(newStatus.score?.toInt(10), simklStatus?.oldScore) @@ -953,6 +883,7 @@ class SimklApi : SyncAPI() { .token(auth?.token ?: return false) .ids(MediaObject.Ids.fromMap(parsedId)) + // Get episodes only when required val episodes = simklStatus?.episodeConstructor?.getEpisodes() @@ -965,33 +896,38 @@ class SimklApi : SyncAPI() { } builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes) + requireLibraryRefresh = true return builder.execute() } + /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ private suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() + return app.get( "$mainUrl/search/id", params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> service.originalName to id } - ).parsedSafe>() + ).parsedSafe() } override suspend fun search(auth: AuthData?, query: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } override fun loginRequest(): AuthLoginPage? { val lastLoginState = BigInteger(130, SecureRandom()).toString(32) - val url = "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://$redirectUrlIdentifier&state=$lastLoginState" + val url = + "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrlIdentifier}&state=$lastLoginState" + return AuthLoginPage( url = url, - payload = lastLoginState, + payload = lastLoginState ) } @@ -1006,12 +942,12 @@ class SimklApi : SyncAPI() { return app.get( "$mainUrl/sync/all-items/", params = params, - headers = getHeaders(auth.token), - ).parsedSafe() + headers = getHeaders(auth.token) + ).parsedSafe() } private suspend fun getActivities(token: AuthToken): ActivitiesResponse? { - return app.post("$mainUrl/sync/activities", headers = getHeaders(token)).parsedSafe() + return app.post("$mainUrl/sync/activities", headers = getHeaders(token)).parsedSafe() } private fun getSyncListCached(auth: AuthData): AllItemsResponse? { @@ -1025,13 +961,18 @@ class SimklApi : SyncAPI() { val lastRemoval = listOf( activities?.tvShows?.removedFromList, activities?.anime?.removedFromList, - activities?.movies?.removedFromList, - ).maxOf { getUnixTime(it) ?: -1 } - val lastRealUpdate = listOf( - activities?.tvShows?.all, - activities?.anime?.all, - activities?.movies?.all, - ).maxOf { getUnixTime(it) ?: -1 } + activities?.movies?.removedFromList + ).maxOf { + getUnixTime(it) ?: -1 + } + val lastRealUpdate = + listOf( + activities?.tvShows?.all, + activities?.anime?.all, + activities?.movies?.all, + ).maxOf { + getUnixTime(it) ?: -1 + } debugPrint { "Cache times: lastCacheUpdate=$lastCacheUpdate, lastRemoval=$lastRemoval, lastRealUpdate=$lastRealUpdate" } val list = if (lastCacheUpdate == null || lastCacheUpdate < lastRemoval) { @@ -1043,33 +984,38 @@ class SimklApi : SyncAPI() { setKey(SIMKL_CACHED_LIST_TIME, userId, lastCacheUpdate) AllItemsResponse.merge( getSyncListCached(auth), - getSyncListSince(auth, lastCacheUpdate), + getSyncListSince(auth, lastCacheUpdate) ) } else { debugPrint { "Cached list update in ${this.name}." } getSyncListCached(auth) } - debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } + setKey(SIMKL_CACHED_LIST, userId, list) + return list } override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? { val list = getSyncListSmart(auth ?: return null) ?: return null - val baseMap = SimklListStatusType.entries - .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } - .associate { - it.stringRes to emptyList() - } + + val baseMap = + SimklListStatusType.entries + .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } + .associate { + it.stringRes to emptyList() + } val syncMap = listOf(list.anime, list.movies, list.shows) .flatten() - .groupBy { it.status } + .groupBy { + it.status + } .mapNotNull { (status, list) -> - val stringRes = status?.let { - SimklListStatusType.fromString(it)?.stringRes - } ?: return@mapNotNull null + val stringRes = + status?.let { SimklListStatusType.fromString(it)?.stringRes } + ?: return@mapNotNull null val libraryList = list.map { it.toLibraryItem() } stringRes to libraryList }.toMap() @@ -1095,14 +1041,15 @@ class SimklApi : SyncAPI() { override suspend fun pinRequest(): AuthPinData? { val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://$redirectUrlIdentifier" + "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrlIdentifier}" ).parsedSafe() ?: return null + return AuthPinData( deviceCode = pinAuthResp.deviceCode, userCode = pinAuthResp.userCode, verificationUrl = pinAuthResp.verificationUrl, expiresIn = pinAuthResp.expiresIn, - interval = pinAuthResp.interval, + interval = pinAuthResp.interval ) } @@ -1110,6 +1057,7 @@ class SimklApi : SyncAPI() { val pinAuthResp = app.get( "$mainUrl/oauth/pin/${payload.userCode}?client_id=$CLIENT_ID" ).parsedSafe() ?: return null + return AuthToken( accessToken = pinAuthResp.accessToken ?: return null, ) @@ -1125,6 +1073,7 @@ class SimklApi : SyncAPI() { val tokenResponse = app.post( "$mainUrl/oauth/token", json = TokenRequest(code) ).parsedSafe() ?: return null + return AuthToken( accessToken = tokenResponse.accessToken, ) @@ -1135,7 +1084,7 @@ class SimklApi : SyncAPI() { return AuthUser( id = user.account.id, name = user.user.name, - profilePicture = user.user.avatar, + profilePicture = user.user.avatar ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt index 317c3dc90..19122768e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -7,10 +7,9 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.SubtitleResource import com.lagradost.cloudstream3.syncproviders.AuthData import com.lagradost.cloudstream3.syncproviders.SubtitleAPI +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.SubtitleHelper -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.util.concurrent.TimeUnit class SubSourceApi : SubtitleAPI() { override val name = "SubSource" @@ -19,70 +18,77 @@ class SubSourceApi : SubtitleAPI() { override val requiresLogin = false companion object { - const val APIURL = "https://api.subsource.net/v1" + const val APIURL = "https://api.subsource.net/api" + const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub" } override suspend fun search( auth: AuthData?, query: AbstractSubtitleEntities.SubtitleSearch ): List? { + //Only supports Imdb Id search for now if (query.imdbId == null) return null val queryLang = SubtitleHelper.fromTagToEnglishLanguageName(query.lang) val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie - val searchResponse = app.post( - url = "$APIURL/movie/search", - json = mapOf( - "includeSeasons" to false, - "limit" to 15, - "query" to query.imdbId!!, - "signal" to "{}" - ), - cacheTime = 120, - cacheUnit = TimeUnit.MINUTES, - ).parsedSafe() ?: return null - - val firstResult = searchResponse.results.firstOrNull() ?: return null - - val apiResponse = app.get( - url = "$APIURL${firstResult.link.replace("series", "subtitles")}", - cacheTime = 120, - cacheUnit = TimeUnit.MINUTES, - ).parsedSafe() ?: return null - - val filteredSubtitles = apiResponse.subtitles.filter { sub -> - sub.releaseType != "trailer" && - sub.language.equals(queryLang, true) - } - - // api doesn't has episode number or lang filtering - val subtitles = if (type == TvType.Movie) { - filteredSubtitles - } else { - val shouldContain = String.format( - null, - "E%02d", - query.epNumber + val searchRes = app.post( + url = "$APIURL/searchMovie", + data = mapOf( + "query" to query.imdbId!! + ) + ).parsedSafe() ?: return null + + val postData = if (type == TvType.TvSeries) { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + "season" to "season-${query.seasonNumber}" + ) + } else { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, ) - filteredSubtitles.filter { sub -> - sub.releaseInfo.contains( - shouldContain - ) - } } - return subtitles.map { subtitle -> + val getMovieRes = app.post( + url = "$APIURL/getMovie", + data = postData + ).parsedSafe().let { + // api doesn't has episode number or lang filtering + if (type == TvType.Movie) { + it?.subs?.filter { sub -> + sub.lang == queryLang + } + } else { + it?.subs?.filter { sub -> + sub.releaseName!!.contains( + String.format( + null, + "E%02d", + query.epNumber + ) + ) && sub.lang == queryLang + } + } + } ?: return null + + return getMovieRes.map { subtitle -> AbstractSubtitleEntities.SubtitleEntity( idPrefix = this.idPrefix, - name = subtitle.releaseInfo, - lang = subtitle.language, - data = subtitle.link, + name = subtitle.releaseName!!, + lang = subtitle.lang!!, + data = SubData( + movie = subtitle.linkName!!, + lang = subtitle.lang, + id = subtitle.subId.toString(), + ).toJson(), type = type, source = this.name, epNumber = query.epNumber, seasonNumber = query.seasonNumber, - isHearingImpaired = subtitle.hearingImpaired == 1, + isHearingImpaired = subtitle.hi == 1, ) } } @@ -91,114 +97,71 @@ class SubSourceApi : SubtitleAPI() { auth: AuthData?, subtitle: AbstractSubtitleEntities.SubtitleEntity ) { - val data = app.get("$APIURL/subtitle/${subtitle.data}") - .parsedSafe() - ?: return + val parsedSub = parseJson(subtitle.data) + + val subRes = app.post( + url = "$APIURL/getSub", + data = mapOf( + "movie" to parsedSub.movie, + "lang" to subtitle.lang, + "id" to parsedSub.id + ) + ).parsedSafe() ?: return this.addZipUrl( - "$APIURL/subtitle/download/${data.subtitle.downloadToken}" + "$DOWNLOADENDPOINT/${subRes.sub.downloadToken}" ) { name, _ -> name } } - - @Serializable - data class SearchRoot( - @JsonProperty("success") @SerialName("success") var success: Boolean? = null, - @JsonProperty("results") @SerialName("results") var results: ArrayList = arrayListOf(), - @JsonProperty("users") @SerialName("users") var users: ArrayList = arrayListOf() + data class ApiSearch( + @JsonProperty("success") val success: Boolean, + @JsonProperty("found") val found: List, ) - @Serializable - data class Users( - - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("displayname") @SerialName("displayname") var displayname: String? = null, - @JsonProperty("avatar") @SerialName("avatar") var avatar: String? = null, - @JsonProperty("badges") @SerialName("badges") var badges: ArrayList = arrayListOf() - + data class Found( + @JsonProperty("id") val id: Long, + @JsonProperty("title") val title: String, + @JsonProperty("seasons") val seasons: Long, + @JsonProperty("type") val type: String, + @JsonProperty("releaseYear") val releaseYear: Long, + @JsonProperty("linkName") val linkName: String, ) - @Serializable - data class Results( - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("title") @SerialName("title") var title: String? = null, - @JsonProperty("type") @SerialName("type") var type: String? = null, - @JsonProperty("link") @SerialName("link") var link: String, - @JsonProperty("releaseYear") @SerialName("releaseYear") var releaseYear: Int? = null, - @JsonProperty("poster") @SerialName("poster") var poster: String? = null, - @JsonProperty("subtitleCount") @SerialName("subtitleCount") var subtitleCount: String? = null, - @JsonProperty("rating") @SerialName("rating") var rating: Double? = null, - @JsonProperty("cast") @SerialName("cast") var cast: ArrayList = arrayListOf(), - @JsonProperty("genres") @SerialName("genres") var genres: ArrayList = arrayListOf(), - @JsonProperty("score") @SerialName("score") var score: Double? = null + data class ApiResponse( + @JsonProperty("success") val success: Boolean, + @JsonProperty("movie") val movie: Movie, + @JsonProperty("subs") val subs: List, ) - @Serializable - - data class ItemRoot( - - // @SerialName("media_type" ) var mediaType : String? = null, - @JsonProperty("subtitles") @SerialName("subtitles") var subtitles: ArrayList, - //@SerialName("movie" ) var movie : Movie? = Movie() - + data class Movie( + @JsonProperty("id") val id: Long? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("year") val year: Long? = null, + @JsonProperty("fullName") val fullName: String? = null, ) - @Serializable - data class Subtitles( - - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("language") @SerialName("language") var language: String, - @JsonProperty("release_type") @SerialName("release_type") var releaseType: String? = null, - @JsonProperty("release_info") @SerialName("release_info") var releaseInfo: String, - @JsonProperty("upload_date") @SerialName("upload_date") var uploadDate: String? = null, - @JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Int? = null, - @JsonProperty("caption") @SerialName("caption") var caption: String? = null, - @JsonProperty("rating") @SerialName("rating") var rating: String? = null, - @JsonProperty("uploader_id") @SerialName("uploader_id") var uploaderId: Int? = null, - @JsonProperty("uploader_displayname") @SerialName("uploader_displayname") var uploaderDisplayname: String? = null, - @JsonProperty("uploader_badges") @SerialName("uploader_badges") var uploaderBadges: ArrayList = arrayListOf(), - @JsonProperty("link") @SerialName("link") var link: String, - @JsonProperty("production_type") @SerialName("production_type") var productionType: String? = null, - @JsonProperty("last_subtitle") @SerialName("last_subtitle") var lastSubtitle: Boolean? = null - + data class Sub( + @JsonProperty("hi") val hi: Int? = null, + @JsonProperty("fullLink") val fullLink: String? = null, + @JsonProperty("linkName") val linkName: String? = null, + @JsonProperty("lang") val lang: String? = null, + @JsonProperty("releaseName") val releaseName: String? = null, + @JsonProperty("subId") val subId: Long? = null, ) - @Serializable - data class DownloadRoot( - @JsonProperty("subtitle") @SerialName("subtitle") var subtitle: Subtitle, - //@SerializedName("movie" ) var movie : Movie? = Movie(), - //@SerializedName("donationLinks" ) var donationLinks : DonationLinks? = DonationLinks(), - //@SerializedName("isDownloaded" ) var isDownloaded : Boolean? = null, - //@SerializedName("user_rated" ) var userRated : String? = null + data class SubData( + @JsonProperty("movie") val movie: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("id") val id: String, ) - @Serializable - data class Subtitle( - - @JsonProperty("id") @SerialName("id") var id: Int? = null, - @JsonProperty("uploaded_at") @SerialName("uploaded_at") var uploadedAt: String? = null, - @JsonProperty("language") @SerialName("language") var language: String? = null, - @JsonProperty("rating") @SerialName("rating") var rating: String? = null, - //SerialName("rates" ) var rates : Rates? = Rates(), - @JsonProperty("uploaded_by") @SerialName("uploaded_by") var uploadedBy: Int? = null, - //@SerialName("contribs" ) var contribs : ArrayList = arrayListOf(), - @JsonProperty("release_info") @SerialName("release_info") var releaseInfo: ArrayList = arrayListOf(), - @JsonProperty("commentary") @SerialName("commentary") var commentary: String? = null, - @JsonProperty("files") @SerialName("files") var files: String? = null, - @JsonProperty("size") @SerialName("size") var size: String? = null, - @JsonProperty("downloads") @SerialName("downloads") var downloads: Int? = null, - @JsonProperty("comments") @SerialName("comments") var comments: Int? = null, - @JsonProperty("production_type") @SerialName("production_type") var productionType: String? = null, - @JsonProperty("release_type") @SerialName("release_type") var releaseType: String? = null, - @JsonProperty("episode") @SerialName("episode") var episode: String? = null, - @JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Int? = null, - @JsonProperty("foreign_parts") @SerialName("foreign_parts") var foreignParts: String? = null, - @JsonProperty("framerate") @SerialName("framerate") var framerate: String? = null, - @JsonProperty("preview") @SerialName("preview") var preview: String? = null, - @JsonProperty("user_uploaded") @SerialName("user_uploaded") var userUploaded: Boolean? = null, - @JsonProperty("download_token") @SerialName("download_token") var downloadToken: String - + data class SubTitleLink( + @JsonProperty("sub") val sub: SubToken, ) -} + + data class SubToken( + @JsonProperty("downloadToken") val downloadToken: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt index 19bd3b1a7..1f1e6de44 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -12,8 +12,6 @@ import com.lagradost.cloudstream3.syncproviders.AuthToken import com.lagradost.cloudstream3.syncproviders.AuthUser import com.lagradost.cloudstream3.syncproviders.SubtitleAPI import com.lagradost.cloudstream3.TvType -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable class SubDlApi : SubtitleAPI() { override val name = "SubDL" @@ -26,7 +24,7 @@ class SubDlApi : SubtitleAPI() { override val createAccountUrl = "https://subdl.com/panel/register" companion object { - const val APIURL = "https://api.subdl.com" + const val APIURL = "https://apiold.subdl.com" const val APIENDPOINT = "$APIURL/api/v1/subtitles" const val DOWNLOADENDPOINT = "https://dl.subdl.com" } @@ -124,80 +122,72 @@ class SubDlApi : SubtitleAPI() { } } - @Serializable data class SubtitleOAuthEntity( - @JsonProperty("userEmail") @SerialName("userEmail") var userEmail: String, - @JsonProperty("pass") @SerialName("pass") var pass: String, - @JsonProperty("name") @SerialName("name") var name: String? = null, - @JsonProperty("accessToken") @SerialName("accessToken") var accessToken: String? = null, - @JsonProperty("apiKey") @SerialName("apiKey") var apiKey: String? = null, + @JsonProperty("userEmail") var userEmail: String, + @JsonProperty("pass") var pass: String, + @JsonProperty("name") var name: String? = null, + @JsonProperty("accessToken") var accessToken: String? = null, + @JsonProperty("apiKey") var apiKey: String? = null, ) - @Serializable data class OAuthTokenResponse( - @JsonProperty("token") @SerialName("token") val token: String, - @JsonProperty("userData") @SerialName("userData") val userData: UserData? = null, - @JsonProperty("status") @SerialName("status") val status: Boolean? = null, - @JsonProperty("message") @SerialName("message") val message: String? = null, + @JsonProperty("token") val token: String, + @JsonProperty("userData") val userData: UserData? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("message") val message: String? = null, ) - @Serializable data class UserData( - @JsonProperty("email") @SerialName("email") val email: String, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("country") @SerialName("country") val country: String, - @JsonProperty("scStepCode") @SerialName("scStepCode") val scStepCode: String, - @JsonProperty("scVerified") @SerialName("scVerified") val scVerified: Boolean, - @JsonProperty("username") @SerialName("username") val username: String? = null, - @JsonProperty("scUsername") @SerialName("scUsername") val scUsername: String, + @JsonProperty("email") val email: String, + @JsonProperty("name") val name: String, + @JsonProperty("country") val country: String, + @JsonProperty("scStepCode") val scStepCode: String, + @JsonProperty("scVerified") val scVerified: Boolean, + @JsonProperty("username") val username: String? = null, + @JsonProperty("scUsername") val scUsername: String, ) - @Serializable data class ApiKeyResponse( - @JsonProperty("ok") @SerialName("ok") val ok: Boolean? = false, - @JsonProperty("api_key") @SerialName("api_key") val apiKey: String, - @JsonProperty("usage") @SerialName("usage") val usage: Usage? = null, + @JsonProperty("ok") val ok: Boolean? = false, + @JsonProperty("api_key") val apiKey: String, + @JsonProperty("usage") val usage: Usage? = null, ) - @Serializable data class Usage( - @JsonProperty("total") @SerialName("total") val total: Long? = 0, - @JsonProperty("today") @SerialName("today") val today: Long? = 0, + @JsonProperty("total") val total: Long? = 0, + @JsonProperty("today") val today: Long? = 0, ) - @Serializable data class ApiResponse( - @JsonProperty("status") @SerialName("status") val status: Boolean? = null, - @JsonProperty("results") @SerialName("results") val results: List? = null, - @JsonProperty("subtitles") @SerialName("subtitles") val subtitles: List? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("results") val results: List? = null, + @JsonProperty("subtitles") val subtitles: List? = null, ) - @Serializable data class Result( - @JsonProperty("sd_id") @SerialName("sd_id") val sdId: Int? = null, - @JsonProperty("type") @SerialName("type") val type: String? = null, - @JsonProperty("name") @SerialName("name") val name: String? = null, - @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null, - @JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long? = null, - @JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null, - @JsonProperty("year") @SerialName("year") val year: Int? = null, + @JsonProperty("sd_id") val sdId: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Long? = null, + @JsonProperty("first_air_date") val firstAirDate: String? = null, + @JsonProperty("year") val year: Int? = null, ) - @Serializable data class Subtitle( - @JsonProperty("release_name") @SerialName("release_name") val releaseName: String, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("lang") @SerialName("lang") val lang: String, // subdl language code - @JsonProperty("author") @SerialName("author") val author: String? = null, - @JsonProperty("url") @SerialName("url") val url: String? = null, - @JsonProperty("subtitlePage") @SerialName("subtitlePage") val subtitlePage: String? = null, - @JsonProperty("season") @SerialName("season") val season: Int? = null, - @JsonProperty("episode") @SerialName("episode") val episode: Int? = null, - @JsonProperty("language") @SerialName("language") val language: String? = null, // full language name - @JsonProperty("hi") @SerialName("hi") val hearingImpaired: Boolean? = null, + @JsonProperty("release_name") val releaseName: String, + @JsonProperty("name") val name: String, + @JsonProperty("lang") val lang: String, // subdl language code + @JsonProperty("author") val author: String? = null, + @JsonProperty("url") val url: String? = null, + @JsonProperty("subtitlePage") val subtitlePage: String? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + @JsonProperty("language") val language: String? = null, // full language name + @JsonProperty("hi") val hearingImpaired: Boolean? = null, ) - // https://subdl.com/api-files/language_list.json + // https://subdl.com/api-files/language_list.json // most of it is IETF BPC 47 conformant tag // but there are some exceptions private val langTagIETF2subdl = mapOf( @@ -207,63 +197,63 @@ class SubDlApi : SubtitleAPI() { "en-nl" to "NL_EN", // "Dutch_English" "pt-br" to "BR_PT", // "Brazillian Portuguese" "zh-hant" to "ZH_BG", // "Big 5 code" -> traditional Chinese (?_?) - // "ar" to "AR", // "Arabic" - // "az" to "AZ", // "Azerbaijani" - // "be" to "BE", // "Belarusian" - // "bg" to "BG", // "Bulgarian" - // "bn" to "BN", // "Bengali" - // "bs" to "BS", // "Bosnian" - // "ca" to "CA", // "Catalan" - // "cs" to "CS", // "Czech" - // "da" to "DA", // "Danish" - // "de" to "DE", // "German" - // "el" to "EL", // "Greek" - // "en" to "EN", // "English" - // "eo" to "EO", // "Esperanto" - // "es" to "ES", // "Spanish" - // "et" to "ET", // "Estonian" - // "fa" to "FA", // "Farsi_Persian" - // "fi" to "FI", // "Finnish" - // "fr" to "FR", // "French" - // "he" to "HE", // "Hebrew" - // "hi" to "HI", // "Hindi" - // "hr" to "HR", // "Croatian" - // "hu" to "HU", // "Hungarian" - // "id" to "ID", // "Indonesian" - // "is" to "IS", // "Icelandic" - // "it" to "IT", // "Italian" - // "ja" to "JA", // "Japanese" - // "ka" to "KA", // "Georgian" - // "kl" to "KL", // "Greenlandic" - // "ko" to "KO", // "Korean" - // "ku" to "KU", // "Kurdish" - // "lt" to "LT", // "Lithuanian" - // "lv" to "LV", // "Latvian" - // "mk" to "MK", // "Macedonian" - // "ml" to "ML", // "Malayalam" - // "mni" to "MNI", // "Manipuri" - // "ms" to "MS", // "Malay" - // "my" to "MY", // "Burmese" - // "nl" to "NL", // "Dutch" - // "no" to "NO", // "Norwegian" - // "pl" to "PL", // "Polish" - // "pt" to "PT", // "Portuguese" - // "ro" to "RO", // "Romanian" - // "ru" to "RU", // "Russian" - // "si" to "SI", // "Sinhala" - // "sk" to "SK", // "Slovak" - // "sl" to "SL", // "Slovenian" - // "sq" to "SQ", // "Albanian" - // "sr" to "SR", // "Serbian" - // "sv" to "SV", // "Swedish" - // "ta" to "TA", // "Tamil" - // "te" to "TE", // "Telugu" - // "th" to "TH", // "Thai" - // "tl" to "TL", // "Tagalog" - // "tr" to "TR", // "Turkish" - // "uk" to "UK", // "Ukranian" - // "ur" to "UR", // "Urdu" - // "vi" to "VI", // "Vietnamese" - // "zh" to "ZH", // "Chinese BG code" + // "ar" to "AR", // "Arabic" + // "az" to "AZ", // "Azerbaijani" + // "be" to "BE", // "Belarusian" + // "bg" to "BG", // "Bulgarian" + // "bn" to "BN", // "Bengali" + // "bs" to "BS", // "Bosnian" + // "ca" to "CA", // "Catalan" + // "cs" to "CS", // "Czech" + // "da" to "DA", // "Danish" + // "de" to "DE", // "German" + // "el" to "EL", // "Greek" + // "en" to "EN", // "English" + // "eo" to "EO", // "Esperanto" + // "es" to "ES", // "Spanish" + // "et" to "ET", // "Estonian" + // "fa" to "FA", // "Farsi_Persian" + // "fi" to "FI", // "Finnish" + // "fr" to "FR", // "French" + // "he" to "HE", // "Hebrew" + // "hi" to "HI", // "Hindi" + // "hr" to "HR", // "Croatian" + // "hu" to "HU", // "Hungarian" + // "id" to "ID", // "Indonesian" + // "is" to "IS", // "Icelandic" + // "it" to "IT", // "Italian" + // "ja" to "JA", // "Japanese" + // "ka" to "KA", // "Georgian" + // "kl" to "KL", // "Greenlandic" + // "ko" to "KO", // "Korean" + // "ku" to "KU", // "Kurdish" + // "lt" to "LT", // "Lithuanian" + // "lv" to "LV", // "Latvian" + // "mk" to "MK", // "Macedonian" + // "ml" to "ML", // "Malayalam" + // "mni" to "MNI", // "Manipuri" + // "ms" to "MS", // "Malay" + // "my" to "MY", // "Burmese" + // "nl" to "NL", // "Dutch" + // "no" to "NO", // "Norwegian" + // "pl" to "PL", // "Polish" + // "pt" to "PT", // "Portuguese" + // "ro" to "RO", // "Romanian" + // "ru" to "RU", // "Russian" + // "si" to "SI", // "Sinhala" + // "sk" to "SK", // "Slovak" + // "sl" to "SL", // "Slovenian" + // "sq" to "SQ", // "Albanian" + // "sr" to "SR", // "Serbian" + // "sv" to "SV", // "Swedish" + // "ta" to "TA", // "Tamil" + // "te" to "TE", // "Telugu" + // "th" to "TH", // "Thai" + // "tl" to "TL", // "Tagalog" + // "tr" to "TR", // "Turkish" + // "uk" to "UK", // "Ukranian" + // "ur" to "UR", // "Urdu" + // "vi" to "VI", // "Vietnamese" + // "zh" to "ZH", // "Chinese BG code" ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 8ec082520..93a79689e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.newSearchResponseList -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) { val hash: Pair ) - private val cache = atomicListOf() + private val cache = threadSafeListOf() private var cacheIndex: Int = 0 const val CACHE_SIZE = 20 @@ -66,7 +66,9 @@ class APIRepository(val api: MainAPI) { private fun afterPluginsLoaded(forceReload: Boolean) { if (forceReload) { - cache.clear() + synchronized(cache) { + cache.clear() + } } } @@ -89,25 +91,21 @@ class APIRepository(val api: MainAPI) { val fixedUrl = api.fixUrl(url) val lookingForHash = Pair(api.name, fixedUrl) - val cached = cache.withLock { - var found: LoadResponse? = null + synchronized(cache) { for (item in cache) { // 10 min save if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { - found = item.response - break + return@withTimeout item.response } } - found } - if (cached != null) return@withTimeout cached api.load(fixedUrl)?.also { response -> // Remove all blank tags as early as possible response.tags = response.tags?.filter { it.isNotBlank() } val add = SavedLoadResponse(unixTime, response, lookingForHash) - cache.withLock { + synchronized(cache) { if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache cacheIndex = (cacheIndex + 1) % CACHE_SIZE @@ -217,4 +215,4 @@ class APIRepository(val api: MainAPI) { return false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index f00fd8031..f91d40f28 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -12,7 +12,9 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListView import androidx.appcompat.app.AlertDialog -import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.google.android.gms.cast.MediaLoadOptions import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaSeekOptions @@ -35,24 +37,35 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import org.json.JSONObject +/*class SkipOpController(val view: ImageView) : UIController() { + init { + view.setImageResource(R.drawable.exo_controls_fastforward) + view.setOnClickListener { + remoteMediaClient?.let { + val options = MediaSeekOptions.Builder() + .setPosition(it.approximateStreamPosition + 85000) + it.seek(options.build()) + } + } + } +}*/ + private fun RemoteMediaClient.getItemIndex(): Int? { return try { val index = this.mediaQueue.itemIds.indexOf(this.currentItem?.itemId ?: 0) if (index < 0) null else index - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -79,41 +92,51 @@ class SkipNextEpisodeController(val view: ImageView) : UIController() { } } -@Serializable data class MetadataHolder( - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, - @JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("currentEpisodeIndex") @SerialName("currentEpisodeIndex") val currentEpisodeIndex: Int, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List, - @JsonProperty("currentLinks") @SerialName("currentLinks") val currentLinks: List, - @JsonProperty("currentSubtitles") @SerialName("currentSubtitles") val currentSubtitles: List, + val apiName: String, + val isMovie: Boolean, + val title: String?, + val poster: String?, + val currentEpisodeIndex: Int, + val episodes: List, + val currentLinks: List, + val currentSubtitles: List ) -class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { +class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : + UIController() { + private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() + init { view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setOnClickListener { + // lateinit var dialog: AlertDialog val holder = getCurrentMetaData() + if (holder != null) { val items = holder.currentLinks if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) { - val subTracks = remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT } - ?: ArrayList() + val subTracks = + remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT } + ?: ArrayList() - val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) + val bottomSheetDialogBuilder = + AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet) - val bottomSheetDialog = bottomSheetDialogBuilder.create() bottomSheetDialog.show() - - val providerList = bottomSheetDialog.findViewById(R.id.sort_providers)!! - val subtitleList = bottomSheetDialog.findViewById(R.id.sort_subtitles)!! + // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) + val providerList = + bottomSheetDialog.findViewById(R.id.sort_providers)!! + val subtitleList = + bottomSheetDialog.findViewById(R.id.sort_subtitles)!! if (subTracks.isEmpty()) { - bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = GONE + bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = + GONE } else { - val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + val arrayAdapter = + ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) arrayAdapter.add(view.context.getString(R.string.no_subtitles)) arrayAdapter.addAll(subTracks.mapNotNull { it.name }) @@ -121,8 +144,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi subtitleList.adapter = arrayAdapter val currentTracks = remoteMediaClient?.mediaStatus?.activeTrackIds - val subtitleIndex = if (currentTracks == null) 0 else subTracks.map { it.id } - .indexOfFirst { currentTracks.contains(it) } + 1 + + val subtitleIndex = + if (currentTracks == null) 0 else subTracks.map { it.id } + .indexOfFirst { currentTracks.contains(it) } + 1 subtitleList.setSelection(subtitleIndex) subtitleList.setItemChecked(subtitleIndex, true) @@ -134,7 +159,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ChromecastSubtitlesFragment.getCurrentSavedStyle().apply { val font = TextTrackStyle() font.setFontFamily(fontFamily ?: "Google Sans") - fontGenericFamily?.let { font.fontGenericFamily = it } + fontGenericFamily?.let { + font.fontGenericFamily = it + } font.windowColor = windowColor font.backgroundColor = backgroundColor @@ -151,7 +178,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi if (!it.status.isSuccess) { Log.e( "CHROMECAST", "Failed with status code:" + - it.status.statusCode + " > " + it.status.statusMessage + it.status.statusCode + " > " + it.status.statusMessage ) } } @@ -160,15 +187,17 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi } } - // https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation + //https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl ?: remoteMediaClient?.currentItem?.media?.contentId) - val sortingMethods = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } - .toTypedArray() + val sortingMethods = + items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } + .toTypedArray() val sotringIndex = items.indexOfFirst { it.url == contentUrl } - val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) + val arrayAdapter = + ArrayAdapter(view.context, R.layout.sort_bottom_single_choice) arrayAdapter.addAll(sortingMethods.toMutableList()) providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE @@ -178,8 +207,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi providerList.setOnItemClickListener { _, _, which, _ -> val epData = holder.episodes[holder.currentEpisodeIndex] + fun loadMirror(index: Int) { if (holder.currentLinks.size <= index) return + val mediaItem = getMediaInfo( epData, holder, @@ -189,21 +220,25 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ) val startAt = remoteMediaClient?.approximateStreamPosition ?: 0 + + //remoteMediaClient.load(mediaItem, true, startAt) try { // THIS IS VERY IMPORTANT BECAUSE WE NEVER WANT TO AUTOLOAD THE NEXT EPISODE val currentIdIndex = remoteMediaClient?.getItemIndex() + val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get( currentIdIndex?.plus(1) ?: 0 ) - if (currentIdIndex == null && nextId != null) { awaitLinks( remoteMediaClient?.queueInsertAndPlayItem( MediaQueueItem.Builder(mediaItem).build(), nextId, startAt, - JSONObject(), + JSONObject() ) - ) { loadMirror(index + 1) } + ) { + loadMirror(index + 1) + } } else { val mediaLoadOptions = MediaLoadOptions.Builder() @@ -215,9 +250,11 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi mediaItem, mediaLoadOptions ) - ) { loadMirror(index + 1) } + ) { + loadMirror(index + 1) + } } - } catch (_: Exception) { + } catch (e: Exception) { val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(startAt) @@ -228,8 +265,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi } } } - loadMirror(which) + bottomSheetDialog.dismissSafe(activity) } } @@ -239,19 +276,23 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi private fun getCurrentMetaData(): MetadataHolder? { return try { - val data = remoteMediaClient?.mediaInfo?.customData?.toString() ?: return null - parseJson(data) - } catch (_: Exception) { + val data = remoteMediaClient?.mediaInfo?.customData?.toString() + data?.toKotlinObject() + } catch (e: Exception) { null } } var isLoadingMore = false + + override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() - view.visibility = if ((meta?.currentLinks?.size ?: 0) > 1) VISIBLE else INVISIBLE + view.visibility = if ((meta?.currentLinks?.size + ?: 0) > 1 + ) VISIBLE else INVISIBLE try { if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) { val currentIdIndex = remoteMediaClient?.getItemIndex() ?: return @@ -268,7 +309,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi currentPosition, currentDuration, epData, - meta.episodes.getOrNull(index + 1), + meta.episodes.getOrNull(index + 1) ) } catch (t: Throwable) { logError(t) @@ -279,7 +320,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ioSafe { val currentLinks = mutableSetOf() val currentSubs = mutableSetOf() + val generator = RepoLinkGenerator(listOf(epData)) + val isSuccessful = safeApiCall { generator.generateLinks( clearCache = false, @@ -292,7 +335,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi currentSubs.add(it) }, offset = 0, - isCasting = true, + isCasting = true ) } @@ -303,18 +346,32 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val jsonCopy = meta.copy( currentLinks = sortedLinks, currentSubtitles = sortedSubs, - currentEpisodeIndex = index, + currentEpisodeIndex = index ) - val done = JSONObject(jsonCopy.toJson()) + val done = + JSONObject(jsonCopy.toJson()) + val mediaInfo = getMediaInfo( epData, jsonCopy, 0, done, - sortedSubs, + sortedSubs ) + /*fun loadIndex(index: Int) { + println("LOAD INDEX::::: $index") + if (meta.currentLinks.size <= index) return + val info = getMediaInfo( + epData, + meta, + index, + done) + awaitLinks(remoteMediaClient?.load(info, true, 0)) { + loadIndex(index + 1) + } + }*/ activity.runOnUiThread { awaitLinks( remoteMediaClient?.queueAppendItem( @@ -323,6 +380,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi ) ) { println("FAILED TO LOAD NEXT ITEM") + // loadIndex(1) } isLoadingMore = false } @@ -345,7 +403,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi class SkipTimeController(val view: ImageView, forwards: Boolean) : UIController() { init { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences() + //val time = settingsManager?.getInt("chromecast_tap_time", 30) ?: 30 val time = 30 + //view.setImageResource(if (forwards) R.drawable.netflix_skip_forward else R.drawable.netflix_skip_back) view.setImageResource(if (forwards) R.drawable.go_forward_30 else R.drawable.go_back_30) view.setOnClickListener { remoteMediaClient?.let { @@ -388,4 +449,4 @@ class ControllerActivity : ExpandedControllerActivity() { SkipNextEpisodeController(skipOpButton) ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index 7725bad91..1d6b41e5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -37,10 +37,8 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod -import com.lagradost.cloudstream3.utils.UIHelper.showProgress object AccountHelper { fun showAccountEditDialog( @@ -166,7 +164,7 @@ object AccountHelper { canSetPin = true - binding.editProfilePhotoButton.setOnClickListener { + binding.editProfilePhotoButton.setOnClickListener({ val bottomSheetDialog = BottomSheetDialog(context) val sheetBinding = BottomInputDialogBinding.inflate(LayoutInflater.from(context)) bottomSheetDialog.setContentView(sheetBinding.root) @@ -176,46 +174,42 @@ object AccountHelper { text1.text = context.getString(R.string.edit_profile_image_title) nginxTextInput.hint = context.getString(R.string.edit_profile_image_hint) - applyBtt.setOnClickListener { + applyBtt.setOnClickListener({ val url = sheetBinding.nginxTextInput.text.toString() - if (url.isEmpty()) { + if (url.isNotEmpty()) { + val imageLoader = ImageLoader(context) + val request = ImageRequest.Builder(context) + .data(url) + .allowHardware(false) + .listener( + onSuccess = { _, _ -> + currentEditAccount = currentEditAccount.copy(customImage = url) + binding.accountImage.loadImage(url) + showToast( + R.string.edit_profile_image_success, + Toast.LENGTH_SHORT + ) + bottomSheetDialog.dismiss() + }, + onError = { _, _ -> + showToast( + R.string.edit_profile_image_error_invalid, + Toast.LENGTH_SHORT + ) + } + ) + .build() + imageLoader.enqueue(request) + } else { showToast(R.string.edit_profile_image_error_empty, Toast.LENGTH_SHORT) - return@setOnClickListener } - applyBtt.showProgress() - val imageLoader = ImageLoader(context) - val request = ImageRequest.Builder(context) - .data(url) - .allowHardware(false) - .listener( - onSuccess = { _, _ -> - currentEditAccount = currentEditAccount.copy(customImage = url) - binding.accountImage.loadImage(url) - showToast( - R.string.edit_profile_image_success, - Toast.LENGTH_SHORT - ) - bottomSheetDialog.dismissSafe() - }, - onError = { _, _ -> - showToast( - R.string.edit_profile_image_error_invalid, - Toast.LENGTH_SHORT - ) - applyBtt.hideProgress() - }, - onCancel = { - applyBtt.hideProgress() - } - ) - .build() - imageLoader.enqueue(request) - } - sheetBinding.cancelBtt.setOnClickListener { bottomSheetDialog.dismissSafe() - } + }) + sheetBinding.cancelBtt.setOnClickListener({ + bottomSheetDialog.dismissSafe() + }) } - } + }) } fun showPinInputDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 19c006b28..0d35d5670 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -10,7 +10,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.api.Log -import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.Resource @@ -37,7 +36,6 @@ import com.lagradost.cloudstream3.utils.ResourceLiveData import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.deleteFilesAndUpdateSettings -import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.downloadDeleteEvent import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -69,17 +67,6 @@ class DownloadViewModel : ViewModel() { private val _selectedItemIds = ConsistentLiveData?>(null) val selectedItemIds: LiveData?> = _selectedItemIds - init { - // Keep the Downloads list in sync when a download is deleted/cancelled from - // anywhere in the app (result page button, queue, notification, etc.). See - // onDownloadDeleted for the rationale (issue #1227). - downloadDeleteEvent += ::onDownloadDeleted - } - - override fun onCleared() { - downloadDeleteEvent -= ::onDownloadDeleted - super.onCleared() - } fun cancelSelection() { updateSelectedItems { null } @@ -402,18 +389,6 @@ class DownloadViewModel : ViewModel() { postChildren(_childCards.success?.filter { it.data.id !in idsToRemove }) } - /** - * Refreshes the Downloads screen in real time when a download is deleted/cancelled. - */ - private fun onDownloadDeleted(id: Int) { - // Keep multi-select state consistent: forget the removed id if it was selected. - updateSelectedItems { it?.minus(id) } - - val context = CloudStreamApp.context ?: return - updateHeaderList(context) - postChildren(_childCards.success?.filterNot { it.data.id == id }) - } - private fun updateStorageStats(visual: List) { try { val stat = StatFs(Environment.getExternalStorageDirectory().path) @@ -609,4 +584,4 @@ class DownloadViewModel : ViewModel() { val names: List, val parentName: String? ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 450dc9a61..382a770cd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -164,11 +164,9 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } } - fun downloadDeleteEvent(data: Int) { - if (data == persistentId) { - resetView() - } - } + /*fun downloadDeleteEvent(data: Int) { + + }*/ /*fun downloadEvent(data: Pair) { val (id, action) = data @@ -187,7 +185,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onAttachedToWindow() { VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent - VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent + // VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent // VideoDownloadManager.downloadEvent += ::downloadEvent VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent @@ -202,7 +200,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onDetachedFromWindow() { VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent - VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent + // VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent // VideoDownloadManager.downloadEvent -= ::downloadEvent VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 0e52baff6..b68ef5962 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -24,8 +24,6 @@ import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip @@ -45,7 +43,6 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable -import com.lagradost.cloudstream3.plugins.Plugin import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.BaseFragment @@ -427,30 +424,11 @@ class HomeFragment : BaseFragment( .inflate(R.layout.sort_bottom_single_provider_choice, parent, false) val titleText = view.findViewById(R.id.text1) val pinIcon = view.findViewById(R.id.pinicon) - val settingsIcon = view.findViewById(R.id.action_settings) - val name = getItem(position) titleText?.text = name - val providerApi = currentValidApis[position] val isPinned = - pinnedphashset.contains(providerApi.name) + pinnedphashset.contains(currentValidApis[position].name) pinIcon.visibility = if (isPinned) View.VISIBLE else View.GONE - - val pluginInstance = providerApi.sourcePlugin?.let { PluginManager.plugins[it] } as? Plugin - val isDownloadedPluginWithSettings = pluginInstance?.openSettings != null && !isLayout(TV) - - settingsIcon.visibility = if (isDownloadedPluginWithSettings) View.VISIBLE else View.GONE - if (isDownloadedPluginWithSettings) { - settingsIcon.setOnClickListener { - try { - val activityContext = it.context.getActivity() ?: it.context - pluginInstance.openSettings?.invoke(activityContext) - } catch (e: Throwable) { - logError(e) - } - } - } - return view } } @@ -473,14 +451,7 @@ class HomeFragment : BaseFragment( arrayAdapter.clear() val sortedApis = validAPIs .filter { - val isPinned = pinnedphashset.contains(it.name) - - // Hide pinned NSFW when NSFW not selected. NSFW is distracting when not chosen. - if (isPinned && !preSelectedTypes.contains(TvType.NSFW)) { - if (it.supportedTypes.all { type -> type == TvType.NSFW }) return@filter false - } - - it.hasMainPage && (isPinned || it.supportedTypes.any( + it.hasMainPage && (pinnedphashset.contains(it.name) || it.supportedTypes.any( preSelectedTypes::contains )) } @@ -694,6 +665,7 @@ class HomeFragment : BaseFragment( fromUI = true ) showToast(R.string.action_reload, Toast.LENGTH_SHORT) + true } homePreviewSearchButton.setOnClickListener { _ -> @@ -701,23 +673,6 @@ class HomeFragment : BaseFragment( homeViewModel.queryTextSubmit("") } - // Load value for toggling Tv layout real time clock. Hide by default at startup - // set visibility first, to apply a scroll effect later - context?.let { - if (isLayout(TV)) { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) - val toggleClock = - settingsManager.getBoolean( - getString(R.string.tv_layout_clock_key), - false - ) - binding.homeClock.isVisible = toggleClock - } else { - binding.homeClock.isVisible = false - } - } - - homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (isLayout(PHONE)) { @@ -757,17 +712,6 @@ class HomeFragment : BaseFragment( view.getLocationInWindow(rect) scrollParent.isVisible = true scrollParent.translationY = rect[1].toFloat() - 60.toPx - - // Move the TV layout real time clock out of the way too - // We check if we have the correct layout and if the clock is enabled - if(isLayout(TV) && binding.homeClock.isVisible) { - val scrollParent = binding.homeClock - - val rect = IntArray(2) - view.getLocationInWindow(rect) - scrollParent.isVisible = true - scrollParent.translationY = rect[1].toFloat() - 60.toPx - } } } super.onScrolled(recyclerView, dx, dy) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index 1020205ac..e42e774b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -58,7 +58,7 @@ class HomeScrollAdapter( when (binding) { is HomeScrollViewBinding -> { - binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders) + binding.homeScrollPreview.loadImage(posterUrl) binding.homeScrollPreviewTags.apply { text = item.tags?.joinToString(" • ") ?: "" isGone = item.tags.isNullOrEmpty() @@ -79,8 +79,8 @@ class HomeScrollAdapter( binding.homeScrollPreview.setOnClickListener { view -> callback.invoke(view ?: return@setOnClickListener, position, item) } - binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders) + binding.homeScrollPreview.loadImage(posterUrl) } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 8d48f5a68..e0609c0e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(apis.withLock { apis.first { it.hasMainPage } }) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index e73a8c75a..6e28c128d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -20,7 +20,6 @@ import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView -import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder @@ -53,13 +52,12 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs const val LIBRARY_FOLDER = "library_folder" + enum class LibraryOpenerType(@StringRes val stringRes: Int) { Default(R.string.action_default), Provider(R.string.none), @@ -69,15 +67,13 @@ enum class LibraryOpenerType(@StringRes val stringRes: Int) { } /** Used to store how the user wants to open said poster */ -@Serializable data class LibraryOpener( - @JsonProperty("openType") @SerialName("openType") val openType: LibraryOpenerType, - @JsonProperty("providerData") @SerialName("providerData") val providerData: ProviderLibraryData?, + val openType: LibraryOpenerType, + val providerData: ProviderLibraryData?, ) -@Serializable data class ProviderLibraryData( - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, + val apiName: String ) class LibraryFragment : BaseFragment( @@ -214,13 +210,14 @@ class LibraryFragment : BaseFragment( syncId: SyncIdName, apiName: String? = null, ) { - val availableProviders = allProviders.filter { - it.supportedSyncNames.contains(syncId) - }.map { it.name } + - // Add the api if it exists - (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } - ?: emptyList()) - + val availableProviders = synchronized(allProviders) { + allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } + ?: emptyList()) + } val baseOptions = listOf( LibraryOpenerType.Default, LibraryOpenerType.None, @@ -572,4 +569,4 @@ class LibraryFragment : BaseFragment( } } -class MenuSearchView(context: Context) : SearchView(context) +class MenuSearchView(context: Context) : SearchView(context) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index d316f28cd..aa44b9235 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -96,7 +96,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus -import com.lagradost.cloudstream3.utils.CLEARKEY_DRM_UUID +import com.lagradost.cloudstream3.utils.CLEARKEY_UUID import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount @@ -104,9 +104,9 @@ import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.PLAYREADY_DRM_UUID +import com.lagradost.cloudstream3.utils.PLAYREADY_UUID import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName -import com.lagradost.cloudstream3.utils.WIDEVINE_DRM_UUID +import com.lagradost.cloudstream3.utils.WIDEVINE_UUID import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp import kotlinx.coroutines.delay import okhttp3.Interceptor @@ -118,7 +118,6 @@ import java.util.concurrent.Executors import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession -import kotlin.uuid.toJavaUuid const val TAG = "CS3ExoPlayer" const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language" @@ -719,7 +718,7 @@ class CS3IPlayer : IPlayer { **/ var preferredAudioTrackLanguage: String? = null get() { - return field ?: getKey( + return field ?: getKey( "$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field )?.also { @@ -1279,7 +1278,7 @@ class CS3IPlayer : IPlayer { item.drm?.let { drm -> when (drm.uuid) { - CLEARKEY_DRM_UUID.toJavaUuid() -> { + CLEARKEY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1300,8 +1299,8 @@ class CS3IPlayer : IPlayer { .createMediaSource(item.mediaItem) } - WIDEVINE_DRM_UUID.toJavaUuid(), - PLAYREADY_DRM_UUID.toJavaUuid() -> { + WIDEVINE_UUID, + PLAYREADY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1915,7 +1914,7 @@ class CS3IPlayer : IPlayer { drm = DrmMetadata( kid = link.kid, key = link.key, - uuid = link.uuid.toJavaUuid(), + uuid = link.uuid, kty = link.kty, licenseUrl = link.licenseUrl, keyRequestParameters = link.keyRequestParameters, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index a086cc16f..7a42cea93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -58,23 +58,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { enableEdgeToEdgeCompat() setContentView(R.layout.empty_layout) Log.i(TAG, "onCreate") - handleIntent(intent) - /** - * Use moveTaskToBack instead of finish() so there is always exactly one task - * entry in recents, always reflecting the current file. - * - * finish() destroys the Activity but may leave the task in recents. Each new file - * open can create a new task entry, so recents accumulates stale entries for old - * files. The user then taps a stale entry and gets the wrong file. - * - * moveTaskToBack keeps the Activity alive in the background. There is only ever - * one task entry in recents. New files opened from the file manager arrive via - * onNewIntent on the live instance, updating the player immediately. The single - * recents entry always reflects the current state, ensuring we load the - * correct file. - */ - attachBackPressedCallback("DownloadedPlayerActivity") { moveTaskToBack(true) } + handleIntent(intent) + attachBackPressedCallback("DownloadedPlayerActivity") { finish() } } private fun handleIntent(intent: Intent) { @@ -97,11 +83,11 @@ class DownloadedPlayerActivity : AppCompatActivity() { url != null -> playLink(this, url) data != null -> playUri(this, data) extraText != null -> playLink(this, extraText) - else -> finishAndRemoveTask() + else -> { finish(); return } } } else if (data?.scheme == "content") { playUri(this, data) - } else finishAndRemoveTask() + } else finish() } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index d90b6043f..26706699b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -267,72 +267,84 @@ open class FullScreenPlayer : AbstractPlayerFragment( // The lib uses Invisible instead of Gone for no reason binding.previewFrameLayout.height - binding.bottomPlayerBar.height ) else -sStyle.elevation.toPx - - sView.animateY(move.toFloat()) + ObjectAnimator.ofFloat(sView, "translationY", move.toFloat()).apply { + duration = 200 + start() + } } - private fun View.animateY(value: Float) { - ObjectAnimator.ofFloat(this, "translationY", value).apply { - duration = 200 - start() - } - } - - private fun View.animateX(value: Float) { - ObjectAnimator.ofFloat(this, "translationX", value).apply { - duration = 200 - start() - } - } - protected fun animateLayoutChanges() { + if (isLayout(PHONE)) { // isEnabled also disables the onKeyDown + playerBinding?.exoProgress?.isEnabled = isShowing // Prevent accidental clicks/drags + } + + if (isShowing) { + updateUIVisibility() + } else { + toggleEpisodesOverlay(false) + playerBinding?.playerHolder?.postDelayed({ updateUIVisibility() }, 200) + } + + val titleMove = if (isShowing) 0f else -50.toPx.toFloat() + playerBinding?.playerVideoTitleHolder?.let { + ObjectAnimator.ofFloat(it, "translationY", titleMove).apply { + duration = 200 + start() + } + } + playerBinding?.playerVideoTitleRez?.let { + ObjectAnimator.ofFloat(it, "translationY", titleMove).apply { + duration = 200 + start() + } + } + playerBinding?.playerVideoInfo?.let { + ObjectAnimator.ofFloat(it, "translationY", titleMove).apply { + duration = 200 + start() + } + } + playerBinding?.playerMetadataScrim?.let { + ObjectAnimator.ofFloat(it, "translationY", 1f).apply { + duration = 200 + start() + } + } + + val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat() + playerBinding?.bottomPlayerBar?.let { + ObjectAnimator.ofFloat(it, "translationY", playerBarMove).apply { + duration = 200 + start() + } + } + if (isLayout(PHONE)) { + playerBinding?.playerEpisodesButton?.let { + ObjectAnimator.ofFloat(it, "translationX", if (isShowing) 0f else 50.toPx.toFloat()) + .apply { + duration = 200 + start() + } + } + } + val fadeTo = if (isShowing) 1f else 0f + val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo) + + fadeAnimation.duration = 100 + fadeAnimation.fillAfter = true + + animateLayoutChangesForSubtitles() + + val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() playerBinding?.apply { - - if (isLayout(PHONE)) { // isEnabled also disables the onKeyDown - exoProgress.isEnabled = isShowing // Prevent accidental clicks/drags + playerOpenSource.let { + ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply { + duration = 200 + start() + } } - if (isShowing) { - updateUIVisibility() - } else { - toggleEpisodesOverlay(false) - playerHolder.postDelayed({ updateUIVisibility() }, 200) - } - - val titleMove = if (isShowing) 0f else -50.toPx.toFloat() - - listOfNotNull( - playerVideoTitleHolder, - playerVideoTitleRez, - playerVideoInfo, - playerGoBackHolder, - playerVideoClock, - ).forEach { - it.animateY(titleMove) - } - - playerMetadataScrim.animateY(1f) - - val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat() - bottomPlayerBar.animateY(playerBarMove) - - if (isLayout(PHONE)) { - playerEpisodesButton.animateX(if (isShowing) 0f else 50.toPx.toFloat()) - } - - val fadeTo = if (isShowing) 1f else 0f - val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo) - - fadeAnimation.duration = 100 - fadeAnimation.fillAfter = true - - animateLayoutChangesForSubtitles() - - val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() - - playerOpenSource.animateY(playerSourceMove) - if (!isLocked) { playerHostView?.gestureHelper?.animateCenterControls(fadeTo) shadowOverlay.isVisible = true @@ -519,22 +531,9 @@ open class FullScreenPlayer : AbstractPlayerFragment( var currentOffset = subtitleDelay binding.apply { - var subtitleAdapter: SubtitleOffsetItemAdapter? = null - subtitleOffsetInput.doOnTextChanged { text, _, _, _ -> text?.toString()?.toLongOrNull()?.let { time -> currentOffset = time - - // Scroll to the first active subtitle - val playerPosition = player.getPosition() ?: 0 - val totalPosition = playerPosition - currentOffset - subtitleAdapter?.updateTime(totalPosition) - - subtitleAdapter?.getLatestActiveItem(totalPosition) - ?.let { subtitlePos -> - subtitleOffsetRecyclerview.scrollToPosition(subtitlePos) - } - val str = when { time > 0L -> { txt(R.string.subtitle_offset_extra_hint_later_format, time) @@ -560,7 +559,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( noSubtitlesLoadedNotice.isVisible = subtitles.isEmpty() val initialSubtitlePosition = (player.getPosition() ?: 0) - currentOffset - subtitleAdapter = + val subtitleAdapter = SubtitleOffsetItemAdapter(initialSubtitlePosition) { subtitleCue -> val playerPosition = player.getPosition() ?: 0 subtitleOffsetInput.text = Editable.Factory.getInstance() @@ -773,7 +772,7 @@ open class FullScreenPlayer : AbstractPlayerFragment( val showPlayerEpisodes = !isGone && isThereEpisodes() playerEpisodesButtonRoot.isVisible = showPlayerEpisodes playerEpisodesButton.isVisible = showPlayerEpisodes - playerVideoTitleHolder.isGone = togglePlayerTitleGone || playerVideoTitle.text.isBlank() + playerVideoTitleHolder.isGone = togglePlayerTitleGone playerVideoTitleRez.isGone = isGone || playerVideoTitleRez.text.isBlank() playerEpisodeFiller.isGone = isGone playerCenterMenu.isGone = isGone @@ -946,18 +945,12 @@ open class FullScreenPlayer : AbstractPlayerFragment( player.handleEvent(CSPlayerEvent.SkipCurrentChapter) } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER -> { // space is not captured due to navigation + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation player.handleEvent(CSPlayerEvent.PlayPauseToggle) } - // KEYCODE_DPAD_CENTER and KEYCODE_ENTER both act as a "select/confirm" button. - // Some remotes (e.g. LG Magic Remote) send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER. - // When the player UI or a dialog is visible, we let the event pass through (return null) - // so the focused button/item can handle the click normally, rather than always toggling - // play/pause. Only when the UI is hidden do we treat it as a play/pause toggle. - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER -> { - if (isShowing || isDialogOpen()) { + KeyEvent.KEYCODE_DPAD_CENTER -> { + if (isShowing) { return null } // If UI is not shown make click instantly skip to next chapter even if locked @@ -1014,7 +1007,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( } toggleEpisodesOverlay(true) } - else -> return null // Avoid capturing all input } return true @@ -1203,10 +1195,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( } skipChapterButton.setOnClickListener { - // Switch focus for a better UX, as otherwise it is reset to a random button like "back button" - if(skipChapterButton.hasFocus()) { - playerPausePlay.requestFocus() - } player.handleEvent(CSPlayerEvent.SkipCurrentChapter) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 4495a5602..9ee85a941 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -8,7 +8,6 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap -import android.graphics.Typeface import android.os.Build import android.os.Bundle import android.text.Spanned @@ -80,7 +79,6 @@ import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioT import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.ui.player.source_priority.LinkSource -import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog @@ -119,10 +117,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.downloader.DownloadUtils.getImageBitmapFromUrl import com.lagradost.cloudstream3.utils.setText @@ -134,7 +130,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.io.Serializable -import java.lang.ref.WeakReference import java.util.Calendar import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -148,11 +143,7 @@ class GeneratorPlayer : FullScreenPlayer() { const val STOP_ACTION = "stopcs3" private val generators = ConcurrentHashMap>() - fun newInstance( - generator: VideoGenerator<*>, - index: Int, - syncData: HashMap? = null - ): Bundle { + fun newInstance(generator: VideoGenerator<*>, index : Int, syncData: HashMap? = null): Bundle { Log.i(TAG, "newInstance = $syncData") val uuid = UUID.randomUUID().toString() generators[uuid] = generator @@ -187,14 +178,12 @@ class GeneratorPlayer : FullScreenPlayer() { private var isNextEpisode: Boolean = false // this is used to reset the watch time private var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none - private val allMeta: List? - get() = viewModel.state.generatorState?.allMeta?.filterIsInstance() - ?.map { episode -> - // Refresh all the episodes watch duration - getViewPos(episode.id)?.let { data -> - episode.copy(position = data.position, duration = data.duration) - } ?: episode - } + private val allMeta: List? get() = viewModel.state.generatorState?.allMeta?.filterIsInstance()?.map { episode -> + // Refresh all the episodes watch duration + getViewPos(episode.id)?.let { data -> + episode.copy(position = data.position, duration = data.duration) + } ?: episode + } private fun setSubtitles(subtitle: SubtitleData?, userInitiated: Boolean): Boolean { // If subtitle is changed and user initiated -> Save the language @@ -512,8 +501,7 @@ class GeneratorPlayer : FullScreenPlayer() { showDownloadProgress(DownloadEvent(0, 0, 0, null)) - // uiReset() // Removed due to UX - + uiReset() currentSelectedLink = link // setEpisodes(viewModel.getAllMeta() ?: emptyList()) setPlayerDimen(null) @@ -796,58 +784,47 @@ class GeneratorPlayer : FullScreenPlayer() { } binding.applyBtt.setOnClickListener { - val currentSubtitle = currentSubtitle - if (currentSubtitle == null) { - dialog.dismissSafe() - return@setOnClickListener - } + currentSubtitle?.let { currentSubtitle -> + providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api -> + ioSafe { + when (val apiResource = + Resource.fromResult(api.resource(currentSubtitle))) { + is Resource.Success -> { + val subtitles = apiResource.value.getSubtitles().map { resource -> + SubtitleData( + originalName = resource.name ?: getName( + currentSubtitle, + true + ), + nameSuffix = "", + url = resource.url, + origin = resource.origin, + mimeType = resource.url.toSubtitleMimeType(), + headers = currentSubtitle.headers, + languageCode = currentSubtitle.lang + ) + } + if (subtitles.isEmpty()) { + showToast(R.string.no_subtitles) + return@ioSafe + } + runOnMainThread { + addAndSelectSubtitles(*subtitles.toTypedArray()) + } + } - val api = providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix } - if (api == null) { - dialog.dismissSafe() - return@setOnClickListener - } + is Resource.Failure -> { + showToast(apiResource.errorString) + } - binding.applyBtt.showProgress() - ioSafe { - val apiResource = - Resource.fromResult(api.resource(currentSubtitle)) - binding.applyBtt.hideProgress() - when (apiResource) { - is Resource.Success -> { - val subtitles = apiResource.value.getSubtitles().map { resource -> - SubtitleData( - originalName = resource.name ?: getName( - currentSubtitle, - true - ), - nameSuffix = "", - url = resource.url, - origin = resource.origin, - mimeType = resource.url.toSubtitleMimeType(), - headers = currentSubtitle.headers, - languageCode = currentSubtitle.lang - ) + is Resource.Loading -> { + // not possible + } } - if (subtitles.isEmpty()) { - showToast(R.string.no_subtitles) - return@ioSafe - } - dialog.dismissSafe() - runOnMainThread { - addAndSelectSubtitles(*subtitles.toTypedArray()) - } - } - - is Resource.Failure -> { - showToast(apiResource.errorString) - } - - is Resource.Loading -> { - // not possible } } } + dialog.dismissSafe() } dialog.setOnDismissListener { @@ -1115,29 +1092,21 @@ class GeneratorPlayer : FullScreenPlayer() { var sourceIndex = 0 var startSource = 0 - // Filtered and sorted links - var currentHiddenFooter: View? = null - var filteredLinks: List = emptyList() + var sortedUrls = emptyList>() fun refreshLinks(qualityProfile: Int) { - val currentLinkUsed = currentSelectedLink - // Always display current linkFooter - val sortedLinks = viewModel.state.sortLinks(qualityProfile) - - filteredLinks = sortedLinks.filter { it.shouldUseLink || it.link == currentLinkUsed } - - if (sortedLinks.isEmpty()) { + sortedUrls = viewModel.state.sortLinks(qualityProfile) + if (sortedUrls.isEmpty()) { sourceDialog.findViewById(R.id.sort_sources_holder)?.isGone = true } else { - startSource = filteredLinks.indexOfFirst { it.link == currentLinkUsed } + startSource = sortedUrls.indexOf(currentSelectedLink) sourceIndex = startSource val sourcesArrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - sourcesArrayAdapter.addAll(filteredLinks.map { displayLink -> - val (link, uri) = displayLink.link + sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) -> val name = link?.name ?: uri?.name ?: "NULL" "$name ${Qualities.getStringByInt(link?.quality)}" }) @@ -1153,7 +1122,7 @@ class GeneratorPlayer : FullScreenPlayer() { } providerList.setOnItemLongClickListener { _, _, position, _ -> - sortedLinks.getOrNull(position)?.link?.first?.url?.let { + sortedUrls.getOrNull(position)?.first?.url?.let { clipboardHelper( txt(R.string.video_source), it @@ -1161,25 +1130,6 @@ class GeneratorPlayer : FullScreenPlayer() { } true } - - val hiddenLinks = sortedLinks.size - filteredLinks.size - providerList.removeFooterView(currentHiddenFooter) - - if (hiddenLinks > 0) { - val hiddenLinksFooter: TextView = layoutInflater.inflate( - R.layout.sort_bottom_footer_add_choice, null - ) as TextView - - providerList.addFooterView(hiddenLinksFooter, null, false) - currentHiddenFooter = hiddenLinksFooter - - val hiddenLinksText = - ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) - .format(hiddenLinks) - hiddenLinksFooter.text = hiddenLinksText - hiddenLinksFooter.setCompoundDrawables(null, null, null, null) - hiddenLinksFooter.setTypeface(null, Typeface.ITALIC) - } } } @@ -1393,8 +1343,8 @@ class GeneratorPlayer : FullScreenPlayer() { } } if (init) { - filteredLinks.getOrNull(sourceIndex)?.let { - loadLink(it.link, true) + sortedUrls.getOrNull(sourceIndex)?.let { + loadLink(it, true) } } sourceDialog.dismissSafe(activity) @@ -1562,10 +1512,6 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerError(exception: Throwable) { - currentSelectedLink?.let { link -> - viewModel.modifyState { this.addError(link) } - } - val currentUrl = currentSelectedLink?.let { it.first?.url ?: it.second?.uri?.toString() } ?: "unknown" val headers = currentSelectedLink?.first?.headers?.toString() ?: "none" @@ -1573,7 +1519,7 @@ class GeneratorPlayer : FullScreenPlayer() { Log.e( TAG, "playerError: $currentSelectedLink, " + - "type=${exception::class.qualifiedName}, " + + "type=${exception::class.java.canonicalName}, " + "message=${exception.message}, url=$currentUrl, headers=$headers, " + "referer=$referer, position=${player.getPosition() ?: "unknown"}, " + "duration=${player.getDuration() ?: "unknown"}, " + @@ -1588,35 +1534,19 @@ class GeneratorPlayer : FullScreenPlayer() { private fun noLinksFound() { viewModel.forceClearCache = true - val hiddenLinks = viewModel.state.sortLinks(currentQualityProfile).count { !it.shouldUseLink } - - context?.let { ctx -> - // Display that there are hidden links to the user. - if (hiddenLinks > 0) { - val noLinksString = ctx.getString(R.string.no_links_found_toast) - val hiddenString = - ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) - .format(hiddenLinks) - val toastText = "$noLinksString\n($hiddenString)" - showToast(toastText, Toast.LENGTH_SHORT) - } else { - showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) - } - } + showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) activity?.popCurrentPage() } private fun startPlayer() { // We don't want double load when you skip loading - if (isPlayerActive.get()) { + if(isPlayerActive.get()) { return } val links = viewModel.state.sortLinks(currentQualityProfile) - - val firstAvailableLink = links.firstOrNull { it.shouldUseLink }?.link - if (firstAvailableLink == null) { + if (links.isEmpty()) { noLinksFound() return } @@ -1624,7 +1554,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (!isPlayerActive.compareAndSet(false, true)) { return } - loadLink(firstAvailableLink, false) + loadLink(links.first(), false) showPlayerMetadata() } @@ -1648,7 +1578,7 @@ class GeneratorPlayer : FullScreenPlayer() { ) val meta = arrayOf( - load.tags?.takeIf { it.isNotEmpty() }?.take(6)?.joinToString(", "), + load.tags?.takeIf { it.isNotEmpty() }?.joinToString(", "), load.year?.toString(), if (!load.type.isMovieType()) context?.getShortSeasonText( @@ -1668,7 +1598,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (!description.isNullOrBlank()) { descView.isVisible = true - descView.text = description.html() + descView.text = description } else { descView.isVisible = false @@ -1691,26 +1621,25 @@ class GeneratorPlayer : FullScreenPlayer() { } } - private fun getNextLink(): DisplayLink? { - val links = viewModel.state.sortLinks(currentQualityProfile) - val currentIndex = links.indexOfFirst { it.link == currentSelectedLink } - val nextPotentialLink = - links.withIndex().firstOrNull { it.index > currentIndex && it.value.shouldUseLink } - return nextPotentialLink?.value - } - override fun hasNextMirror(): Boolean { - return getNextLink() != null + val links = viewModel.state.sortLinks(currentQualityProfile) + return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size } override fun nextMirror() { - val nextLink = getNextLink() - if (nextLink == null) { + val links = viewModel.state.sortLinks(currentQualityProfile) + if (links.isEmpty()) { noLinksFound() return } - loadLink(nextLink.link, true) + val newIndex = links.indexOf(currentSelectedLink) + 1 + if (newIndex >= links.size) { + noLinksFound() + return + } + + loadLink(links[newIndex], true) } override fun onDestroy() { @@ -1763,10 +1692,8 @@ class GeneratorPlayer : FullScreenPlayer() { if (settingsManager.getBoolean( ctx.getString(R.string.episode_sync_enabled_key), true ) - ) { - maxEpisodeSet = meta.episode - sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode) - } + ) maxEpisodeSet = meta.episode + sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode) } } @@ -1799,11 +1726,11 @@ class GeneratorPlayer : FullScreenPlayer() { ): SubtitleData? { val langCode = preferredAutoSelectSubtitles ?: return null if (downloads) { - sortSubs(subtitles).firstOrNull { + return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode( langCode ) - }?.let { return it } + } } if (!settings) return null @@ -2213,11 +2140,9 @@ class GeneratorPlayer : FullScreenPlayer() { fun releasePlayer() { player.release() currentSelectedSubtitles = null - currentSelectedLink = null isPlayerActive.set(false) binding?.overlayLoadingSkipButton?.isVisible = false binding?.playerLoadingOverlay?.isVisible = true - viewModel.modifyState { setError(emptyList()) } uiReset() } @@ -2227,31 +2152,19 @@ class GeneratorPlayer : FullScreenPlayer() { activity?.popCurrentPage() } - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt("index", viewModel.episodeIndex) - super.onSaveInstanceState(outState) - } - override fun onBindingCreated(binding: FragmentPlayerBinding, savedInstanceState: Bundle?) { viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] val uuid = savedInstanceState?.getString("uuid") ?: arguments?.getString("uuid") val index = savedInstanceState?.getInt("index") ?: arguments?.getInt("index") - val generator = generators[uuid] + + viewModel.attachGenerator(generators[uuid], index) unwrapBundle(savedInstanceState) unwrapBundle(arguments) super.onBindingCreated(binding, savedInstanceState) - - // Avoid showing no links found - if (generator == null || index == null) { - exitPlayer() - return - } - viewModel.attachGenerator(generator, index) - context?.let { ctx -> val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) showName = settingsManager.getBoolean(ctx.getString(R.string.show_name_key), true) @@ -2271,14 +2184,6 @@ class GeneratorPlayer : FullScreenPlayer() { fromTagToEnglishLanguageName(it)?.lowercase() ?: return@mapNotNull null } ?: listOf() } - - // Set up TV clock visibility - if (isLayout(TV)) { - val showTvClock = settingsManager.getBoolean(ctx.getString(R.string.tv_layout_clock_key), false) - playerBinding?.playerVideoClock?.isVisible = showTvClock - } else { - playerBinding?.playerVideoClock?.isVisible = false - } } unwrapBundle(savedInstanceState) @@ -2288,18 +2193,14 @@ class GeneratorPlayer : FullScreenPlayer() { preferredAutoSelectSubtitles = getAutoSelectLanguageTagIETF() - val selectedLink = currentSelectedLink - if (selectedLink == null) { + if (currentSelectedLink == null) { viewModel.loadLinks() - } else { - // Recreated view, so we need to recreate the - loadLink(selectedLink, true) } binding.overlayLoadingSkipButton.setOnClickListener { // Mark as "success" early viewModel.modifyState { - copy(loading = Resource.Success(Unit)) + copy(loading = Resource.Success(true)) } } @@ -2317,13 +2218,11 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentStamps) { (stamps, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe + observe(viewModel.currentStamps) { stamps -> player.addTimeStamps(stamps) } - observe(viewModel.currentSubtitles) { (subtitles, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe + observe(viewModel.currentSubtitles) { subtitles -> player.setActiveSubtitles(subtitles) // If the file is downloaded then do not select auto select the subtitles @@ -2334,9 +2233,7 @@ class GeneratorPlayer : FullScreenPlayer() { autoSelectSubtitles() } } - observe(viewModel.loadingLinks) { (loading, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe - + observe(viewModel.loadingLinks) { loading -> when (loading) { is Resource.Loading -> { releasePlayer() @@ -2357,28 +2254,22 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentLinks) { (_, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe - - val sortedLinks = viewModel.state.sortLinks(currentQualityProfile) - val usableLinks = sortedLinks.count { link -> link.shouldUseLink } - - val turnVisible = usableLinks > 0 && viewModel.generator?.canSkipLoading == true + observe(viewModel.currentLinks) { links -> + val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true val wasGone = binding.overlayLoadingSkipButton.isGone binding.overlayLoadingSkipButton.apply { isVisible = turnVisible - - if (usableLinks == 0) { + if (links.isEmpty()) { setText(R.string.skip_loading) } else { @SuppressLint("SetTextI18n") - text = "${context.getString(R.string.skip_loading)} (${usableLinks})" + text = "${context.getString(R.string.skip_loading)} (${links.size})" } } safe { - if (!isPlayerActive.get() && viewModel.state.links.any { link -> + if (viewModel.state.links.any { link -> getLinkPriority(currentQualityProfile, link.first) >= QualityDataHelper.AUTO_SKIP_PRIORITY } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index dcf976612..ac25347b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.net.Uri import androidx.core.content.ContextCompat.getString -import androidx.navigation.NavOptions import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.actions.temp.CloudStreamPackage import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -13,15 +12,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile object OfflinePlaybackHelper { - /** - * Pop any existing player off the nav back stack before pushing the new one, - * keeping the stack flat (at most one player at a time). This prevents an - * OOM when many files are opened in sequence via DownloadedPlayerActivity. - */ - private val replacePlayerNavOptions = NavOptions.Builder() - .setPopUpTo(R.id.navigation_player, inclusive = true, saveState = false) - .build() - fun playLink(activity: Activity, url: String) { activity.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( @@ -30,8 +20,7 @@ object OfflinePlaybackHelper { BasicLink(url) ), id = url.hashCode() ), 0 - ), - replacePlayerNavOptions + ) ) } @@ -63,8 +52,7 @@ object OfflinePlaybackHelper { subs, if (id != -1) id else null, ), 0 - ), - replacePlayerNavOptions + ) ) return true } @@ -88,8 +76,7 @@ object OfflinePlaybackHelper { ) ) ), 0 - ), - replacePlayerNavOptions + ) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index cb8cf8bff..049ed06d6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -10,8 +10,6 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings -import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -42,76 +40,34 @@ data class GeneratorState( val id: Int?, ) -data class DisplayLink( - val link: VideoLink, - // If the link should be displayed and used by the player - val shouldUseLink: Boolean, - val priority: Int -) - /** Immutable state of all current links relevant to displaying the video */ // @MustUseReturnValues // @Immutable data class VideoState( val subtitles: PersistentSet = persistentSetOf(), val links: PersistentSet = persistentSetOf(), - val erroredLinks: PersistentSet = persistentSetOf(), val stamps: PersistentList = persistentListOf(), - val loading: Resource = Resource.Loading(), + val loading: Resource = Resource.Loading(), val generatorState: GeneratorState? = null, - val instance: Int, ) { /** * This acts as a local cache for sorted links that are not copied over by the copy constructor. * * sortedBy is not exactly expensive, but each hasNextMirror does it again, so this alleviates unnecessary recomputation * */ - private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() + private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() - /** - * The cache is guaranteed to be up to date link-wise due to the immutable links. - * However, hideNegativeSources and hideErrorSources could be updated, which requires clearing the cache. - */ fun clearSortedLinksCache() = sortedLinks.clear() - private fun hasLinkErrored(link: VideoLink): Boolean { - return erroredLinks.any { it == link } - } - - private fun VideoLink.toDisplayLink( - qualityProfile: Int, - hideNegativeSources: Boolean, - hideErrorSources: Boolean - ): DisplayLink { - val priority = getLinkPriority(qualityProfile, this.first) - val shouldHideLink = - (hideNegativeSources && priority < 0) || (hideErrorSources && hasLinkErrored(this)) - val displayLink = DisplayLink(this, !shouldHideLink, priority) - - return displayLink - } - // Modifying sortedLinks is not considered a "visible" side effect, and rerunning it does not change the result // It is by all standards, idempotent and by extension also pure as it has no "visible" side effect /** Returns .links in the sorted order according to the qualityProfile. * Use .links if order is not needed */ @Contract(pure = true) - fun sortLinks(qualityProfile: Int): List { - sortedLinks[qualityProfile]?.let { - return it - } - - val hideNegativeSources = - QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideNegativeSources) - val hideErrorSources = - QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources) - - return links.map { link -> + fun sortLinks(qualityProfile: Int): List { + return sortedLinks[qualityProfile] ?: links.sortedBy { link -> // negative because we want to sort highest quality first - link.toDisplayLink(qualityProfile, hideNegativeSources, hideErrorSources) - }.sortedBy { - // negative because we want to sort highest quality first - -it.priority + -getLinkPriority(qualityProfile, link.first) }.also { value -> sortedLinks[qualityProfile] = value } } @@ -156,19 +112,8 @@ data class VideoState( @JvmName("setVideoSkipStamp") @Contract(pure = true) fun set(items: Collection): VideoState = copy(stamps = items.toPersistentList()) - - @Contract(pure = true) - fun addError(item: VideoLink): VideoState = copy(erroredLinks = erroredLinks.add(item)) - - @Contract(pure = true) - fun setError(items: Collection): VideoState = copy(erroredLinks = items.toPersistentSet()) } -data class VideoLive( - val value: T, - val instance: Int, -) - class PlayerGeneratorViewModel : ViewModel() { companion object { const val TAG = "PlayViewGen" @@ -178,7 +123,7 @@ class PlayerGeneratorViewModel : ViewModel() { var generator: VideoGenerator<*>? = null @Volatile - var episodeIndex: Int = 0 + private var episodeIndex: Int = 0 /** * The state of the video player, only modify it by modifyState to make sure observe is called, @@ -187,20 +132,20 @@ class PlayerGeneratorViewModel : ViewModel() { * This value can be used without Synchronized or locking when reading, as all fields are immutable. * */ @Volatile - var state = VideoState(instance = 0) + var state = VideoState() private set - private val _currentLinks = MutableLiveData>>(null) - val currentLinks: LiveData>> = _currentLinks + private val _currentLinks = MutableLiveData>>(setOf()) + val currentLinks: LiveData>> = _currentLinks - private val _currentSubtitles = MutableLiveData>>(null) - val currentSubtitles: LiveData>> = _currentSubtitles + private val _currentSubtitles = MutableLiveData>(setOf()) + val currentSubtitles: LiveData> = _currentSubtitles - private val _loadingLinks = MutableLiveData>>() - val loadingLinks: LiveData>> = _loadingLinks + private val _loadingLinks = MutableLiveData>() + val loadingLinks: LiveData> = _loadingLinks - private val _currentStamps = MutableLiveData>>(null) - val currentStamps: LiveData>> = _currentStamps + private val _currentStamps = MutableLiveData>(emptyList()) + val currentStamps: LiveData> = _currentStamps /** * Modifies the `state` variable safely, and with the correct observe behavior. @@ -213,15 +158,6 @@ class PlayerGeneratorViewModel : ViewModel() { val oldState = state state = op.invoke(oldState) - /** New instance, always push state */ - if (state.instance != oldState.instance) { - _currentSubtitles.postValue(VideoLive(state.subtitles, state.instance)) - _currentStamps.postValue(VideoLive(state.stamps, state.instance)) - _currentLinks.postValue(VideoLive(state.links, state.instance)) - _loadingLinks.postValue(VideoLive(state.loading, state.instance)) - return - } - /** * Only post the changed values, this makes sure we do not invoke the "observe" * @@ -229,15 +165,15 @@ class PlayerGeneratorViewModel : ViewModel() { * to avoid comparing the entire set or list as "Persistent" classes will hold the same reference if they are unchanged. * */ if (state.links !== oldState.links) - _currentLinks.postValue(VideoLive(state.links, state.instance)) + _currentLinks.postValue(state.links) if (state.stamps !== oldState.stamps) - _currentStamps.postValue(VideoLive(state.stamps, state.instance)) + _currentStamps.postValue(state.stamps) if (state.subtitles !== oldState.subtitles) - _currentSubtitles.postValue(VideoLive(state.subtitles, state.instance)) + _currentSubtitles.postValue(state.subtitles) /** Normal equality here as it is not a collection */ if (state.loading != oldState.loading) - _loadingLinks.postValue(VideoLive(state.loading, state.instance)) + _loadingLinks.postValue(state.loading) } private val _currentSubtitleYear = MutableLiveData(null) @@ -316,10 +252,13 @@ class PlayerGeneratorViewModel : ViewModel() { loadLinks() } - fun attachGenerator(newGenerator: VideoGenerator<*>, index: Int) { - Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index") - generator = newGenerator - episodeIndex = index + fun attachGenerator(newGenerator: VideoGenerator<*>?, index: Int?) { + if (generator == null) { + generator = newGenerator + if (index != null) { + episodeIndex = index + } + } } /** @@ -382,49 +321,45 @@ class PlayerGeneratorViewModel : ViewModel() { } fun loadLinks(sourceTypes: Set = LOADTYPE_INAPP) { - Log.i(TAG, "loadLinks with generator=$generator and index=$episodeIndex") + Log.i(TAG, "loadLinks") currentJob?.cancel() val index = episodeIndex - // Clear old data and reset the state - modifyState { - VideoState( - loading = Resource.Loading(), - generatorState = generator?.let { gen -> - GeneratorState( - meta = gen.videos.getOrNull(index), - nextMeta = gen.videos.getOrNull(index + 1), - id = gen.getId(index), - response = (gen as? RepoLinkGenerator)?.page, - index = index, - allMeta = gen.videos - ) - }, - instance = instance + 1 - ) - } - currentJob = viewModelScope.launchSafe { + // Clear old data and reset the state + modifyState { + VideoState( + generatorState = generator?.let { gen -> + GeneratorState( + meta = gen.videos.getOrNull(index), + nextMeta = gen.videos.getOrNull(index + 1), + id = gen.getId(index), + response = (gen as? RepoLinkGenerator)?.page, + index = index, + allMeta = gen.videos + ) + } + ) + } + // Load more data val loadingState = safeApiCall { generator?.generateLinks( sourceTypes = sourceTypes, clearCache = forceClearCache, callback = { link -> - if (isActive) - modifyState { - add(link) - } + modifyState { + add(link) + } }, isCasting = false, offset = index, subtitleCallback = { link -> - if (isActive && isValidSubtitle(link)) + if (isValidSubtitle(link)) modifyState { add(link) } }) - Unit } if (!isActive) { @@ -433,13 +368,9 @@ class PlayerGeneratorViewModel : ViewModel() { /** Only mark as success if we have not skipped loading */ modifyState { - if (!isActive) { - this - } else { - when (loading) { - is Resource.Loading -> copy(loading = loadingState) - else -> this - } + when (loading) { + is Resource.Loading -> copy(loading = loadingState) + else -> this } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index f62bad58d..ee6170aa5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -8,14 +8,11 @@ import androidx.annotation.OptIn import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView -import com.fasterxml.jackson.annotation.JsonIgnore import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable enum class SubtitleStatus { IS_ACTIVE, @@ -35,19 +32,17 @@ enum class SubtitleOrigin { * @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend id * @param headers if empty it will use the base onlineDataSource headers else only the specified headers * @param languageCode usually, tags such as "en", "es-mx", or "zh-hant-TW". But it could be something like "English 4" - */ -@Serializable + * */ data class SubtitleData( - @SerialName("originalName") val originalName: String, - @SerialName("nameSuffix") val nameSuffix: String, - @SerialName("url") val url: String, - @SerialName("origin") val origin: SubtitleOrigin, - @SerialName("mimeType") val mimeType: String, - @SerialName("headers") val headers: Map, - @SerialName("languageCode") val languageCode: String?, + val originalName: String, + val nameSuffix: String, + val url: String, + val origin: SubtitleOrigin, + val mimeType: String, + val headers: Map, + val languageCode: String?, ) { - /** Internal ID for media3, unique for each link. */ - @JsonIgnore + /** Internal ID for exoplayer, unique for each link*/ fun getId(): String { return if (origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) url else "$url|$name" @@ -59,22 +54,22 @@ data class SubtitleData( } /** Tries hard to figure out a valid IETF tag based on language code and name. Will return null if not found. */ - @JsonIgnore fun getIETF_tag(): String? { return fromLanguageToTagIETF(this.languageCode) ?: fromLanguageToTagIETF(this.originalName, halfMatch = true) } - @SerialName("name") val name = "$originalName $nameSuffix" + val name = "$originalName $nameSuffix" /** * Gets the URL, but tries to fix it if it is malformed. */ - @JsonIgnore fun getFixedUrl(): String { // Some extensions fail to include the protocol, this helps with that. val fixedSubUrl = if (this.url.startsWith("//")) { "https:${this.url}" - } else this.url + } else { + this.url + } return fixedSubUrl } } @@ -147,4 +142,4 @@ class PlayerSubtitleHelper { setSubStyle(it) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt index 7e5d04eb0..2e554f75e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/Torrent.kt @@ -9,8 +9,6 @@ import com.lagradost.cloudstream3.mvvm.logError 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 import torrServer.TorrServer import java.io.File import java.net.ConnectException @@ -34,14 +32,14 @@ object Torrent { /** Returns true if the server is up */ private suspend fun echo(): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { app.get( "$TORRENT_SERVER_URL/echo", ).text.isNotEmpty() - } catch (_: ConnectException) { + } catch (e: ConnectException) { // `Failed to connect to /127.0.0.1:8090` if the server is down false } catch (t: Throwable) { @@ -54,7 +52,7 @@ object Torrent { /** Gracefully shutdown the server. * should not be used because I am unable to start it again, and the stopTorrentServer() crashes the app */ suspend fun shutdown(): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -70,7 +68,7 @@ object Torrent { /** Lists all torrents by the server */ @Throws private suspend fun list(): Array { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -85,7 +83,7 @@ object Torrent { /** Drops a single torrent, (I think) this means closing the stream. Returns returns if it is successful */ private suspend fun drop(hash: String): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -106,7 +104,7 @@ object Torrent { /** Removes a single torrent from the server registry */ private suspend fun rem(hash: String): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return false } return try { @@ -128,7 +126,7 @@ object Torrent { /** Removes all torrents from the server, and returns if it is successful */ suspend fun clearAll(): Boolean { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { return true } return try { @@ -166,8 +164,10 @@ object Torrent { /** Gets all the metadata of a torrent, will throw if that hash does not exists * https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L126 */ @Throws - suspend fun get(hash: String): TorrentStatus { - if (TORRENT_SERVER_URL.isEmpty()) { + suspend fun get( + hash: String, + ): TorrentStatus { + if(TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -184,7 +184,7 @@ object Torrent { /** Adds a torrent to the server, this is needed for us to get the hash for further modification, as well as start streaming it*/ @Throws private suspend fun add(url: String): TorrentStatus { - if (TORRENT_SERVER_URL.isEmpty()) { + if(TORRENT_SERVER_URL.isEmpty()) { throw ErrorLoadingException("Not initialized") } return app.post( @@ -204,7 +204,7 @@ object Torrent { return true } val port = TorrServer.startTorrentServer(dir, 0) - if (port < 0) { + if(port < 0) { return false } TORRENT_SERVER_URL = "http://127.0.0.1:$port" @@ -278,55 +278,94 @@ object Torrent { // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L18 // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/main/web/api/route.go#L7 - @Serializable data class TorrentRequest( - @JsonProperty("action") @SerialName("action") val action: String, - @JsonProperty("hash") @SerialName("hash") val hash: String = "", - @JsonProperty("link") @SerialName("link") val link: String = "", - @JsonProperty("title") @SerialName("title") val title: String = "", - @JsonProperty("poster") @SerialName("poster") val poster: String = "", - @JsonProperty("data") @SerialName("data") val data: String = "", - @JsonProperty("save_to_db") @SerialName("save_to_db") val saveToDB: Boolean = false, + @JsonProperty("action") + val action: String, + @JsonProperty("hash") + val hash: String = "", + @JsonProperty("link") + val link: String = "", + @JsonProperty("title") + val title: String = "", + @JsonProperty("poster") + val poster: String = "", + @JsonProperty("data") + val data: String = "", + @JsonProperty("save_to_db") + val saveToDB: Boolean = false, ) // https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/torr/state/state.go#L33 // omitempty = nullable - @Serializable data class TorrentStatus( - @JsonProperty("title") @SerialName("title") var title: String, - @JsonProperty("poster") @SerialName("poster") var poster: String, - @JsonProperty("data") @SerialName("data") var data: String?, - @JsonProperty("timestamp") @SerialName("timestamp") var timestamp: Long, - @JsonProperty("name") @SerialName("name") var name: String?, - @JsonProperty("hash") @SerialName("hash") var hash: String?, - @JsonProperty("stat") @SerialName("stat") var stat: Int, - @JsonProperty("stat_string") @SerialName("stat_string") var statString: String, - @JsonProperty("loaded_size") @SerialName("loaded_size") var loadedSize: Long?, - @JsonProperty("torrent_size") @SerialName("torrent_size") var torrentSize: Long?, - @JsonProperty("preloaded_bytes") @SerialName("preloaded_bytes") var preloadedBytes: Long?, - @JsonProperty("preload_size") @SerialName("preload_size") var preloadSize: Long?, - @JsonProperty("download_speed") @SerialName("download_speed") var downloadSpeed: Double?, - @JsonProperty("upload_speed") @SerialName("upload_speed") var uploadSpeed: Double?, - @JsonProperty("total_peers") @SerialName("total_peers") var totalPeers: Int?, - @JsonProperty("pending_peers") @SerialName("pending_peers") var pendingPeers: Int?, - @JsonProperty("active_peers") @SerialName("active_peers") var activePeers: Int?, - @JsonProperty("connected_seeders") @SerialName("connected_seeders") var connectedSeeders: Int?, - @JsonProperty("half_open_peers") @SerialName("half_open_peers") var halfOpenPeers: Int?, - @JsonProperty("bytes_written") @SerialName("bytes_written") var bytesWritten: Long?, - @JsonProperty("bytes_written_data") @SerialName("bytes_written_data") var bytesWrittenData: Long?, - @JsonProperty("bytes_read") @SerialName("bytes_read") var bytesRead: Long?, - @JsonProperty("bytes_read_data") @SerialName("bytes_read_data") var bytesReadData: Long?, - @JsonProperty("bytes_read_useful_data") @SerialName("bytes_read_useful_data") var bytesReadUsefulData: Long?, - @JsonProperty("chunks_written") @SerialName("chunks_written") var chunksWritten: Long?, - @JsonProperty("chunks_read") @SerialName("chunks_read") var chunksRead: Long?, - @JsonProperty("chunks_read_useful") @SerialName("chunks_read_useful") var chunksReadUseful: Long?, - @JsonProperty("chunks_read_wasted") @SerialName("chunks_read_wasted") var chunksReadWasted: Long?, - @JsonProperty("pieces_dirtied_good") @SerialName("pieces_dirtied_good") var piecesDirtiedGood: Long?, - @JsonProperty("pieces_dirtied_bad") @SerialName("pieces_dirtied_bad") var piecesDirtiedBad: Long?, - @JsonProperty("duration_seconds") @SerialName("duration_seconds") var durationSeconds: Double?, - @JsonProperty("bit_rate") @SerialName("bit_rate") var bitRate: String?, - @JsonProperty("file_stats") @SerialName("file_stats") var fileStats: List?, - @JsonProperty("trackers") @SerialName("trackers") var trackers: List?, + @JsonProperty("title") + var title: String, + @JsonProperty("poster") + var poster: String, + @JsonProperty("data") + var data: String?, + @JsonProperty("timestamp") + var timestamp: Long, + @JsonProperty("name") + var name: String?, + @JsonProperty("hash") + var hash: String?, + @JsonProperty("stat") + var stat: Int, + @JsonProperty("stat_string") + var statString: String, + @JsonProperty("loaded_size") + var loadedSize: Long?, + @JsonProperty("torrent_size") + var torrentSize: Long?, + @JsonProperty("preloaded_bytes") + var preloadedBytes: Long?, + @JsonProperty("preload_size") + var preloadSize: Long?, + @JsonProperty("download_speed") + var downloadSpeed: Double?, + @JsonProperty("upload_speed") + var uploadSpeed: Double?, + @JsonProperty("total_peers") + var totalPeers: Int?, + @JsonProperty("pending_peers") + var pendingPeers: Int?, + @JsonProperty("active_peers") + var activePeers: Int?, + @JsonProperty("connected_seeders") + var connectedSeeders: Int?, + @JsonProperty("half_open_peers") + var halfOpenPeers: Int?, + @JsonProperty("bytes_written") + var bytesWritten: Long?, + @JsonProperty("bytes_written_data") + var bytesWrittenData: Long?, + @JsonProperty("bytes_read") + var bytesRead: Long?, + @JsonProperty("bytes_read_data") + var bytesReadData: Long?, + @JsonProperty("bytes_read_useful_data") + var bytesReadUsefulData: Long?, + @JsonProperty("chunks_written") + var chunksWritten: Long?, + @JsonProperty("chunks_read") + var chunksRead: Long?, + @JsonProperty("chunks_read_useful") + var chunksReadUseful: Long?, + @JsonProperty("chunks_read_wasted") + var chunksReadWasted: Long?, + @JsonProperty("pieces_dirtied_good") + var piecesDirtiedGood: Long?, + @JsonProperty("pieces_dirtied_bad") + var piecesDirtiedBad: Long?, + @JsonProperty("duration_seconds") + var durationSeconds: Double?, + @JsonProperty("bit_rate") + var bitRate: String?, + @JsonProperty("file_stats") + var fileStats: List?, + @JsonProperty("trackers") + var trackers: List?, ) { fun streamUrl(url: String): String { val fileName = @@ -342,10 +381,12 @@ object Torrent { } } - @Serializable data class TorrentFileStat( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("path") @SerialName("path") val path: String?, - @JsonProperty("length") @SerialName("length") val length: Long?, + @JsonProperty("id") + val id: Int?, + @JsonProperty("path") + val path: String?, + @JsonProperty("length") + val length: Long?, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index d8d47bd10..02470484e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -12,16 +12,12 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import java.util.EnumMap -import java.util.concurrent.ConcurrentHashMap -import kotlin.also import kotlin.math.abs object QualityDataHelper { private const val VIDEO_SOURCE_PRIORITY = "video_source_priority" private const val VIDEO_PROFILE_NAME = "video_profile_name" private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority" - const val VIDEO_PROFILE_SETTINGS = "video_profile_settings" // Old key only supporting one type per profile @Deprecated("Changed to support multiple types per profile") @@ -57,21 +53,13 @@ object QualityDataHelper { val types: Set ) - - // Map profile and name to priority - val sourcePriorityCache: ConcurrentHashMap> = ConcurrentHashMap() - fun getSourcePriority(profile: Int, name: String?): Int { if (name == null) return DEFAULT_SOURCE_PRIORITY - - return sourcePriorityCache[profile]?.get(name) ?: (getKey( + return getKey( "$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, DEFAULT_SOURCE_PRIORITY - ) ?: DEFAULT_SOURCE_PRIORITY).also { - sourcePriorityCache.getOrPut(profile) { hashMapOf() } - sourcePriorityCache[profile]?.set(name, it) - } + ) ?: DEFAULT_SOURCE_PRIORITY } fun getAllSourcePriorityNames(profile: Int): List { @@ -89,8 +77,6 @@ object QualityDataHelper { } else { setKey(folder, name, priority) } - - sourcePriorityCache[profile]?.set(name, priority) } fun setProfileName(profile: Int, name: String?) { @@ -107,17 +93,12 @@ object QualityDataHelper { ?: txt(R.string.profile_number, profile) } - // Map profile and quality to priority - val qualityPriorityCache: ConcurrentHashMap> = ConcurrentHashMap() fun getQualityPriority(profile: Int, quality: Qualities): Int { - return qualityPriorityCache[profile]?.get(quality) ?: (getKey( + return getKey( "$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile", quality.value.toString(), quality.defaultPriority - )?.also { - qualityPriorityCache.getOrPut(profile) { EnumMap(Qualities::class.java) } - qualityPriorityCache[profile]?.set(quality, it) - }) ?: quality.defaultPriority + ) ?: quality.defaultPriority } fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) { @@ -126,24 +107,8 @@ object QualityDataHelper { quality.value.toString(), priority ) - qualityPriorityCache[profile]?.set(quality, priority) } - fun setProfileSetting(profile: Int, setting: ProfileSettings, value: T) { - val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" - // Prevent unnecessary keys - if (value == setting.defaultValue) { - removeKey(folder, setting.key) - } else { - setKey(folder, setting.key, value) - } - } - - inline fun getProfileSetting(profile: Int, setting: ProfileSettings): T { - val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" - val value = getKey(folder, setting.key) - return value ?: setting.defaultValue - } @Suppress("DEPRECATION") fun getQualityProfileTypes(profile: Int): Set { @@ -258,9 +223,4 @@ object QualityDataHelper { if (target == null) return Qualities.Unknown return Qualities.entries.minBy { abs(it.value - target) } } -} - -sealed class ProfileSettings(val key: String, val defaultValue: T) { - object HideErrorSources : ProfileSettings("hide_error_sources", false) - object HideNegativeSources : ProfileSettings("hide_negative_sources", false) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index 604ec3149..c8ac96ebb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding class SourcePriorityDialog( val ctx: Context, - @StyleRes val themeRes: Int, + @StyleRes themeRes: Int, val links: List, private val profile: QualityDataHelper.QualityProfile, /** @@ -28,79 +28,76 @@ class SourcePriorityDialog( PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false) setContentView(binding.root) fixSystemBarsPadding(binding.root) + val sourcesRecyclerView = binding.sortSources + val qualitiesRecyclerView = binding.sortQualities + val profileText = binding.profileTextEditable + val saveBtt = binding.saveBtt + val exitBtt = binding.closeBtt + val helpBtt = binding.helpBtt - binding.apply { - profileTextEditable.setText( - QualityDataHelper.getProfileName(profile.id).asString(context) - ) - profileTextEditable.hint = txt(R.string.profile_number, profile.id).asString(context) + profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) + profileText.hint = txt(R.string.profile_number, profile.id).asString(context) - sortSources.adapter = PriorityAdapter( - ).apply { - val sortedLinks = links.map { link -> - SourcePriority( - null, - link.source, - QualityDataHelper.getSourcePriority(profile.id, link.source) - ) - }.distinctBy { it.name }.sortedBy { -it.priority } - - submitList(sortedLinks) - } - - sortQualities.adapter = PriorityAdapter( - ).apply { - submitList(Qualities.entries.mapNotNull { - SourcePriority( - it, - Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, - QualityDataHelper.getQualityPriority(profile.id, it) - ) - }.sortedBy { -it.priority }) - } - - @Suppress("UNCHECKED_CAST") // We know the types - saveBtt.setOnClickListener { - val qualityAdapter = sortQualities.adapter as? PriorityAdapter - val sourcesAdapter = sortSources.adapter as? PriorityAdapter - - val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() - val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() - - qualities.forEach { - QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) - } - - sources.forEach { - QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) - } - - qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) - sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) - - val savedProfileName = profileTextEditable.text.toString() - if (savedProfileName.isBlank()) { - QualityDataHelper.setProfileName(profile.id, null) - } else { - QualityDataHelper.setProfileName(profile.id, savedProfileName) - } - updatedCallback.invoke() - } - - closeBtt.setOnClickListener { - dismissSafe() - } - - helpBtt.setOnClickListener { - AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setMessage(R.string.quality_profile_help) - }.show() - } - - settingsBtt.setOnClickListener { - SourceProfileSettingsDialog(ctx, themeRes, profile.id).show() - } + sourcesRecyclerView.adapter = PriorityAdapter( + ).apply { + submitList(links.map { link -> + SourcePriority( + null, + link.source, + QualityDataHelper.getSourcePriority(profile.id, link.source) + ) + }.distinctBy { it.name }.sortedBy { -it.priority }) } + + qualitiesRecyclerView.adapter = PriorityAdapter( + ).apply { + submitList(Qualities.entries.mapNotNull { + SourcePriority( + it, + Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, + QualityDataHelper.getQualityPriority(profile.id, it) + ) + }.sortedBy { -it.priority }) + } + + @Suppress("UNCHECKED_CAST") // We know the types + saveBtt.setOnClickListener { + val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter + val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter + + val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() + val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() + + qualities.forEach { + QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) + } + + sources.forEach { + QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) + } + + qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) + sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) + + val savedProfileName = profileText.text.toString() + if (savedProfileName.isBlank()) { + QualityDataHelper.setProfileName(profile.id, null) + } else { + QualityDataHelper.setProfileName(profile.id, savedProfileName) + } + updatedCallback.invoke() + } + + exitBtt.setOnClickListener { + this.dismissSafe() + } + + helpBtt.setOnClickListener { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setMessage(R.string.quality_profile_help) + }.show() + } + super.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt deleted file mode 100644 index c26955b5e..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.lagradost.cloudstream3.ui.player.source_priority - -import android.app.Dialog -import android.content.Context -import android.view.LayoutInflater -import androidx.annotation.StyleRes -import com.lagradost.cloudstream3.databinding.SourceProfileSettingsDialogBinding -import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding - -class SourceProfileSettingsDialog( - val ctx: Context, - @StyleRes themeRes: Int, - val profile: Int -) : Dialog(ctx, themeRes) { - override fun show() { - val binding = - SourceProfileSettingsDialogBinding.inflate(LayoutInflater.from(ctx), null, false) - setContentView(binding.root) - fixSystemBarsPadding(binding.root) - - binding.apply { - var hideErrorSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideErrorSources) - var hideNegativeSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideNegativeSources) - - profileHideErrorSources.isChecked = hideErrorSources - profileHideErrorSources.setOnCheckedChangeListener { _, bool -> - hideErrorSources = bool - } - - profileHideNegativeSources.isChecked = hideNegativeSources - profileHideNegativeSources.setOnCheckedChangeListener { _, bool -> - hideNegativeSources = bool - } - - applyBtt.setOnClickListener { - QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideErrorSources, hideErrorSources) - QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideNegativeSources, hideNegativeSources) - dismissSafe() - } - - cancelBtt.setOnClickListener { - dismissSafe() - } - } - super.show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index fe8c617f2..056588d0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -5,9 +5,6 @@ import android.content.Intent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.animation.Animation -import android.view.animation.OvershootInterpolator -import android.view.animation.ScaleAnimation import androidx.core.view.isVisible import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole @@ -49,24 +46,6 @@ class ActorAdaptor( } } - override fun onUpdateContent(holder: ViewHolderState, item: ActorData, position: Int) { - when (val binding = holder.view) { - is CastItemBinding -> { - val anim: Animation = ScaleAnimation( - 0.8f, 1f, - 0.8f, 1f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f - ) - anim.fillAfter = true - anim.duration = 200 - anim.interpolator = OvershootInterpolator() - binding.voiceActorImageHolder2.startAnimation(anim) - } - } - super.onUpdateContent(holder, item, position) - } - override fun onBindContent(holder: ViewHolderState, item: ActorData, position: Int) { when (val binding = holder.view) { is CastItemBinding -> { @@ -158,4 +137,4 @@ class ActorAdaptor( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 71909c5d3..cbf94fd97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -21,8 +21,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.UiImage -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable const val START_ACTION_RESUME_LATEST = 1 const val START_ACTION_LOAD_EP = 2 @@ -36,32 +34,33 @@ enum class VideoWatchState { Watched } -@Serializable data class ResultEpisode( - @SerialName("headerName") val headerName: String, - @SerialName("name") val name: String?, - @SerialName("poster") val poster: String?, - @SerialName("episode") val episode: Int, - @SerialName("seasonIndex") val seasonIndex: Int?, // this is the "season" index used season names - @SerialName("season") val season: Int?, // this is the display - @SerialName("data") val data: String, - @SerialName("apiName") val apiName: String, - @SerialName("id") val id: Int, - @SerialName("index") val index: Int, - @SerialName("position") val position: Long, // time in MS - @SerialName("duration") val duration: Long, // duration in MS - @SerialName("score") val score: Score?, - @SerialName("description") val description: String?, - @SerialName("isFiller") val isFiller: Boolean?, - @SerialName("tvType") val tvType: TvType, - @SerialName("parentId") val parentId: Int, - /** Conveys if the episode itself is marked as watched. */ - @SerialName("videoWatchState") val videoWatchState: VideoWatchState, - /** Sum of all previous season episode counts + episode. */ - @SerialName("totalEpisodeIndex") val totalEpisodeIndex: Int? = null, - @SerialName("airDate") val airDate: Long? = null, - @SerialName("runTime") val runTime: Int? = null, - @SerialName("seasonData") val seasonData: SeasonData? = null, + val headerName: String, + val name: String?, + val poster: String?, + val episode: Int, + val seasonIndex: Int?, // this is the "season" index used season names + val season: Int?, // this is the display + val data: String, + val apiName: String, + val id: Int, + val index: Int, + val position: Long, // time in MS + val duration: Long, // duration in MS + val score: Score?, + val description: String?, + val isFiller: Boolean?, + val tvType: TvType, + val parentId: Int, + /** + * Conveys if the episode itself is marked as watched + **/ + val videoWatchState: VideoWatchState, + /** Sum of all previous season episode counts + episode */ + val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, + val runTime: Int? = null, + val seasonData: SeasonData? = null, ) fun ResultEpisode.getRealPosition(): Long { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index ce67763d0..cfbacc5d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -877,7 +877,7 @@ class ResultFragmentTv : BaseFragment( resultCastText.setText(d.actorsText) resultNextAiring.setText(d.nextAiringEpisode) resultNextAiringTime.setText(d.nextAiringDate) - resultPoster.loadImage(d.posterImage, headers = d.posterHeaders) + resultPoster.loadImage(d.posterImage) var isExpanded = false resultDescription.apply { @@ -910,7 +910,7 @@ class ResultFragmentTv : BaseFragment( R.drawable.profile_bg_teal ).random() - backgroundPoster.loadImage(d.posterBackgroundImage, headers = d.posterHeaders) { + backgroundPoster.loadImage(d.posterBackgroundImage) { error { getImageFromDrawable(context ?: return@error null, error) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index c519e0de2..7dfe3cf59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -83,7 +83,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -1325,7 +1324,7 @@ class ResultViewModel2 : ViewModel() { episodeIds: Array, watchState: VideoWatchState ) { - val watchStateString = watchState.toJson() + val watchStateString = DataStore.mapper.writeValueAsString(watchState) episodeIds.forEach { if (getVideoWatchState(it.toInt()) != watchState) { editor.setKeyRaw( @@ -1686,13 +1685,14 @@ class ResultViewModel2 : ViewModel() { } val realRecommendations = ArrayList() - val apiNames = apis.filter { - it.name.contains("gogoanime", true) || - it.name.contains("9anime", true) - }.map { - it.name + val apiNames = synchronized(apis) { + apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name + } } - meta.recommendations?.forEach { rec -> apiNames.forEach { name -> realRecommendations.add(rec.copy(apiName = name)) @@ -2706,4 +2706,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 3161a1204..6c5c64ff8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -182,7 +182,6 @@ class SyncViewModel : ViewModel() { fun publishUserData() = ioSafe { Log.i(TAG, "publishUserData") val user = userData.value - _userDataResponse.postValue(Resource.Loading()) if (user is Resource.Success) { syncs.forEach { (prefix, id) -> repos.firstOrNull { it.idPrefix == prefix }?.updateStatus(id, user.value) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 793d96734..4868abb3d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -13,15 +13,12 @@ import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -@Serializable data class SearchHistoryItem( - @JsonProperty("searchedAt") @SerialName("searchedAt") val searchedAt: Long, - @JsonProperty("searchText") @SerialName("searchText") val searchText: String, - @JsonProperty("type") @SerialName("type") val type: List, - @JsonProperty("key") @SerialName("key") val key: String, + @JsonProperty("searchedAt") val searchedAt: Long, + @JsonProperty("searchText") val searchText: String, + @JsonProperty("type") val type: List, + @JsonProperty("key") val key: String, ) data class SearchHistoryCallback( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt index a22c2fcef..8dbd78178 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchSuggestionApi.kt @@ -4,8 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.nicehttp.NiceResponse -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable /** * API for fetching search suggestions from external sources. @@ -15,18 +13,16 @@ object SearchSuggestionApi { private const val TMDB_API_URL = "https://api.themoviedb.org/3/search/multi" private const val TMDB_API_KEY = "e6333b32409e02a4a6eba6fb7ff866bb" - @Serializable data class TmdbSearchResult( - @JsonProperty("results") @SerialName("results") val results: List?, + @JsonProperty("results") val results: List? ) - @Serializable data class TmdbSearchItem( - @JsonProperty("media_type") @SerialName("media_type") val mediaType: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("original_title") @SerialName("original_title") val originalTitle: String?, - @JsonProperty("original_name") @SerialName("original_name") val originalName: String?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("name") val name: String?, + @JsonProperty("original_title") val originalTitle: String?, + @JsonProperty("original_name") val originalName: String? ) /** @@ -61,7 +57,7 @@ object SearchSuggestionApi { * Parses the TMDB search response and extracts movie/TV show titles. * Filters to only include movies, TV shows, and anime. */ - private suspend fun parseSuggestions(response: NiceResponse): List { + private fun parseSuggestions(response: NiceResponse): List { return try { val parsed = response.parsed() parsed.results diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index f60588e35..27db8d1ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() { private var suggestionJob: Job? = null - private var repos = apis.withLock { apis.map { APIRepository(it) } } + private var repos = synchronized(apis) { apis.map { APIRepository(it) } } fun clearSearch() { _searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false))) @@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun reloadRepos() { - repos = apis.withLock { apis.map { APIRepository(it) } } + repos = synchronized(apis) { apis.map { APIRepository(it) } } } fun searchAndCancel( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 75ba55674..8d96a6b14 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -65,8 +65,6 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogTe import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.txt import qrcode.QRCode @@ -350,7 +348,6 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { email = if (req.email) binding.loginEmailInput.text?.toString() else null, server = if (req.server) binding.loginServerInput.text?.toString() else null, ) - binding.applyBtt.showProgress() ioSafe { try { if (api.login(loginData)) { @@ -380,8 +377,6 @@ class SettingsAccount : BasePreferenceFragmentCompat(), BiometricCallback { api.name ) ) - } finally { - binding.applyBtt.hideProgress() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 6943e5cf4..e41109b59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -249,7 +249,7 @@ class SettingsFragment : BaseFragment( val appVersion = BuildConfig.VERSION_NAME val commitHash = activity?.currentCommitHash() ?: "" - val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, + val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.getDefault() ).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") @@ -262,4 +262,4 @@ class SettingsFragment : BaseFragment( true } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index b04f9a4a2..dbf2ff1dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -10,7 +10,6 @@ import androidx.core.content.edit import androidx.core.os.ConfigurationCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager -import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.CloudStreamApp @@ -49,10 +48,6 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getBasePath import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames import java.util.Locale // Change local language settings in the app. @@ -77,7 +72,6 @@ val appLanguages = arrayListOf( Pair("Azərbaycan dili", "az"), Pair("Bahasa Indonesia", "in"), Pair("Bahasa Melayu", "ms"), - Pair("català", "ca"), Pair("Deutsch", "de"), Pair("English", "en"), Pair("Español", "es"), @@ -98,7 +92,6 @@ val appLanguages = arrayListOf( Pair("Português", "pt"), Pair("Português (Brasil)", "pt-BR"), Pair("Română", "ro"), - Pair("Shqip мова", "sq"), Pair("Slovenčina", "sk"), Pair("Soomaaliga", "so"), Pair("Svenska", "sv"), @@ -108,7 +101,6 @@ val appLanguages = arrayListOf( Pair("Wikang Filipino", "fil"), Pair("Čeština", "cs"), Pair("Ελληνικά", "el"), - Pair("беларуская мова", "be"), Pair("български", "bg"), Pair("македонски", "mk"), Pair("русский", "ru"), @@ -142,7 +134,7 @@ fun Pair.nameNextToFlagEmoji(): String { // fallback to [A][A] -> [?] question mak flag val flag = SubtitleHelper.getFlagFromIso(this.second) ?: "\ud83c\udde6\ud83c\udde6" - return "$flag\u00a0${this.first}" // \u00a0 non-breaking space + return "$flag\u00a0${this.first}" // \u00a0 non-breaking space } class SettingsGeneral : BasePreferenceFragmentCompat() { @@ -153,15 +145,15 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { setToolBarScrollFlags() } - @OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now - @Serializable data class CustomSite( - @JsonProperty("parentClassName") @JsonAlias("parentJavaClass") - @SerialName("parentClassName") @JsonNames("parentJavaClass") - val parentClassName: String, // ::class.simpleName - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("url") @SerialName("url") val url: String, - @JsonProperty("lang") @SerialName("lang") val lang: String, + @JsonProperty("parentJavaClass") // javaClass.simpleName + val parentJavaClass: String, + @JsonProperty("name") + val name: String, + @JsonProperty("url") + val url: String, + @JsonProperty("lang") + val lang: String, ) companion object { @@ -227,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { } fun showAdd() { - val providers = allProviders.distinctBy { it::class }.sortedBy { it.name } + val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( providers.map { "${it.name} (${it.mainUrl})" }, -1, @@ -251,14 +243,13 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { val url = binding.siteUrlInput.text?.toString() val lang = binding.siteLangInput.text?.toString() val realLang = if (lang.isNullOrBlank()) provider.lang else lang - val simpleName = provider::class.simpleName - if (url.isNullOrBlank() || name.isNullOrBlank() || simpleName == null) { + if (url.isNullOrBlank() || name.isNullOrBlank()) { showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT) return@setOnClickListener } val current = getCurrent() - val newSite = CustomSite(simpleName, name, url, realLang) + val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang) current.add(newSite) setKey(USER_PROVIDER_API, current.toTypedArray()) // reload apis @@ -362,7 +353,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { } ?: emptyList() } - settingsManager.edit { putBoolean(getString(R.string.jsdelivr_proxy_key), getKey(getString(R.string.jsdelivr_proxy_key), false) ?: false) } + settingsManager.edit { putBoolean(getString(R.string.jsdelivr_proxy_key), getKey(getString(R.string.jsdelivr_proxy_key), false) ?: false) } getPref(R.string.jsdelivr_proxy_key)?.setOnPreferenceChangeListener { _, newValue -> setKey(getString(R.string.jsdelivr_proxy_key), newValue) return@setOnPreferenceChangeListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 294928115..0a0fb33c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -205,35 +205,29 @@ class SettingsPlayer : BasePreferenceFragmentCompat() { } getPref(R.string.player_default_key)?.setOnPreferenceClickListener { - // Pair each player with its display name, dropping any with none, - // which would mean something is definitely wrong. - val players = VideoClickActionHolder.getPlayers(activity).mapNotNull { player -> - (player.name.asStringNull(activity) ?: player::class.simpleName)?.let { player to it } - } - + val players = VideoClickActionHolder.getPlayers(activity) val prefNames = buildList { - add(getString(R.string.player_settings_play_in_app)) // built-in player display name - addAll(players.map { (_, name) -> name }) + add(getString(R.string.player_settings_play_in_app)) + addAll(players.map { it.name.asStringNull(activity) ?: it.javaClass.simpleName }) } - val prefValues = buildList { - add("") // "" = built-in player, matches default - addAll(players.map { (player, _) -> player.uniqueId() }) + add("") + addAll(players.map { it.uniqueId() }) } + val current = + settingsManager.getString(getString(R.string.player_default_key), "") ?: "" - val current = settingsManager.getString(getString(R.string.player_default_key), "") ?: "" activity?.showBottomDialog( prefNames.toList(), - prefValues.indexOf(current), // finds index of currently selected player + prefValues.indexOf(current), getString(R.string.player_pref), true, - {}, + {} ) { settingsManager.edit { putString(getString(R.string.player_default_key), prefValues[it]) } } - return@setOnPreferenceClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index c8478a840..076f17a0a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() { getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { currentLangTags -> - val languagesTagName = APIHolder.apis.withLock { - listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) + - APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } - .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } + val languagesTagName = synchronized(APIHolder.apis) { + listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) + + APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } + .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji } val currentIndexList = currentLangTags.map { langTag -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 5f86a3a5a..f4c522bf9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -228,8 +228,6 @@ class SettingsUI : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - getPref(R.string.tv_layout_clock_key)?.hideOn(PHONE or EMULATOR) - getPref(R.string.confirm_exit_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.confirm_exit) val prefValues = resources.getIntArray(R.array.confirm_exit_values) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 107227497..af0d3dfe7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -5,12 +5,10 @@ import android.content.Context import android.content.DialogInterface import android.os.Build import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.SearchView import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.marginBottom @@ -28,7 +26,6 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.ui.BaseFragment import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setSystemBarsPadding @@ -39,8 +36,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.hideProgress -import com.lagradost.cloudstream3.utils.UIHelper.showProgress import com.lagradost.cloudstream3.utils.setText class ExtensionsFragment : BaseFragment( @@ -48,7 +43,6 @@ class ExtensionsFragment : BaseFragment( ) { private val extensionViewModel: ExtensionsViewModel by activityViewModels() - private val pluginViewModel: PluginsViewModel by activityViewModels() private fun View.setLayoutWidth(weight: Int) { val param = LinearLayout.LayoutParams( @@ -116,7 +110,11 @@ class ExtensionsFragment : BaseFragment( adapter = RepoAdapter(false, { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newInstance(it) + PluginsFragment.newInstance( + it.name, + it.url, + false + ) ) }, { repo -> // Prompt user before deleting repo @@ -128,10 +126,7 @@ class ExtensionsFragment : BaseFragment( when (which) { DialogInterface.BUTTON_POSITIVE -> { ioSafe { - RepositoryManager.removeRepository( - uiContext.applicationContext, - repo - ) + RepositoryManager.removeRepository(uiContext.applicationContext, repo) extensionViewModel.loadStats() extensionViewModel.loadRepositories() } @@ -150,11 +145,10 @@ class ExtensionsFragment : BaseFragment( }) } - observe(extensionViewModel.repositories) { repos -> - binding.repoRecyclerView.isVisible = repos.isNotEmpty() - binding.blankRepoScreen.isVisible = repos.isEmpty() - (binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(repos.toList()) - pluginViewModel.updatePluginList(binding.root.context, repos.toList()) + observe(extensionViewModel.repositories) { + binding.repoRecyclerView.isVisible = it.isNotEmpty() + binding.blankRepoScreen.isVisible = it.isEmpty() + (binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(it.toList()) } observeNullable(extensionViewModel.pluginStats) { value -> @@ -183,75 +177,14 @@ class ExtensionsFragment : BaseFragment( binding.pluginStorageAppbar.setOnClickListener { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newLocalInstance( + PluginsFragment.newInstance( getString(R.string.extensions), + "", + true ) ) } - binding.pluginRecyclerView.apply { - setLinearListLayout( - isHorizontal = false, - nextDown = FOCUS_SELF, - nextRight = FOCUS_SELF, - ) - setRecycledViewPool(PluginAdapter.sharedPool) - adapter = - PluginAdapter(true) { - val urls = extensionViewModel.repositories.value?.toList() ?: emptyList() - pluginViewModel.handlePluginAction(activity, urls, it, false) - } - } - - observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) -> - (binding.pluginRecyclerView.adapter as? PluginAdapter)?.submitList(list) - if (scrollToTop) { - binding.pluginRecyclerView.scrollToPosition(0) - } - } - - binding.settingsToolbar.apply { - val searchItem = menu?.findItem(R.id.search_button) - val searchView = searchItem?.actionView as? SearchView - - searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionCollapse(p0: MenuItem): Boolean { - binding.pluginRecyclerView.isVisible = false - binding.repoRecyclerView.isVisible = true - return true - - } - - override fun onMenuItemActionExpand(p0: MenuItem): Boolean { - binding.pluginRecyclerView.isVisible = true - binding.repoRecyclerView.isVisible = false - return true - } - }) - - // Don't go back if active query - setNavigationOnClickListener { - if (searchView?.isIconified == false) { - searchView.isIconified = true - } else { - dispatchBackPressed() - } - } - - searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean { - pluginViewModel.search(query) - return true - } - - override fun onQueryTextChange(newText: String?): Boolean { - pluginViewModel.search(newText) - return true - } - }) - } - - val addRepositoryClick = View.OnClickListener { val ctx = context ?: return@OnClickListener val binding = AddRepoInputBinding.inflate(LayoutInflater.from(ctx), null, false) @@ -266,10 +199,7 @@ class ExtensionsFragment : BaseFragment( )?.text?.toString()?.let { copiedText -> if (copiedText.contains(RepoAdapter.SHAREABLE_REPO_SEPARATOR)) { // text is of format : - val (name, url) = copiedText.split( - RepoAdapter.SHAREABLE_REPO_SEPARATOR, - limit = 2 - ) + val (name, url) = copiedText.split(RepoAdapter.SHAREABLE_REPO_SEPARATOR, limit = 2) binding.repoUrlInput.setText(url.trim()) binding.repoNameInput.setText(name.trim()) } else { @@ -280,18 +210,13 @@ class ExtensionsFragment : BaseFragment( binding.applyBtt.setOnClickListener secondListener@{ val name = binding.repoNameInput.text?.toString() val urlInput = binding.repoUrlInput.text?.toString() - if (urlInput.isNullOrEmpty()) { - showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) - return@secondListener - } - binding.applyBtt.showProgress() ioSafe { - try { - val url = RepositoryManager.parseRepoUrl(urlInput) - if (url.isNullOrBlank()) { + val url = urlInput?.let { it1 -> RepositoryManager.parseRepoUrl(it1) } + if (url.isNullOrBlank()) { + main { showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT) - return@ioSafe } + } else { val repository = RepositoryManager.parseRepository(url) // Exit if wrong repository @@ -302,33 +227,29 @@ class ExtensionsFragment : BaseFragment( val fixedName = if (!name.isNullOrBlank()) name else repository.name - val newRepo = RepositoryData(repository.iconUrl, fixedName, url) + val newRepo = RepositoryData(repository.iconUrl,fixedName, url) RepositoryManager.addRepository(newRepo) extensionViewModel.loadStats() extensionViewModel.loadRepositories() - dialog.dismissSafe(activity) // Only dismiss if the repo was added - - val plugins = RepositoryManager.getRepoPlugins(newRepo) + val plugins = RepositoryManager.getRepoPlugins(url) if (plugins.isNullOrEmpty()) { showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) - return@ioSafe + } else { + this@ExtensionsFragment.activity?.addRepositoryDialog( + fixedName, + url, + ) } - - this@ExtensionsFragment.activity?.addRepositoryDialog( - newRepo - ) - } finally { - binding.applyBtt.hideProgress() } } + dialog.dismissSafe(activity) } binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } - val isTv = isLayout(TV) binding.apply { addRepoButton.isGone = isTv diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt index d30962598..482251b78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt @@ -15,16 +15,13 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIE import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -@Serializable data class RepositoryData( - @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("url") @SerialName("url") val url: String, -) { - constructor(name: String, url: String): this(null, name, url) + @JsonProperty("iconUrl") val iconUrl: String?, + @JsonProperty("name") val name: String, + @JsonProperty("url") val url: String +){ + constructor(name: String,url: String):this(null,name,url) } const val REPOSITORIES_KEY = "REPOSITORIES_KEY" @@ -55,16 +52,16 @@ class ExtensionsViewModel : ViewModel() { ?: emptyArray()) + PREBUILT_REPOSITORIES val onlinePlugins = urls.toList().amap { - RepositoryManager.getRepoPlugins(it)?.toList() ?: emptyList() - }.flatten().distinctBy { it.plugin.url } + RepositoryManager.getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten().distinctBy { it.second.url } // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated - val outdatedPlugins = getPluginsOnline().flatMap { savedData -> - onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.plugin.internalName } + val outdatedPlugins = getPluginsOnline().map { savedData -> + onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName } .map { onlineData -> PluginManager.OnlinePluginData(savedData, onlineData) } - }.distinctBy { it.onlineData.plugin.url } + }.flatten().distinctBy { it.onlineData.second.url } val total = onlinePlugins.count() val disabled = outdatedPlugins.count { it.isDisabled } @@ -93,4 +90,4 @@ class ExtensionsViewModel : ViewModel() { val urls = repos() _repositories.postValue(urls) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 2ff06cd6a..d0f9ff565 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.PluginWrapper import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState @@ -35,7 +34,7 @@ import kotlin.math.log10 import kotlin.math.pow data class PluginViewData( - val pluginWrapper: PluginWrapper, + val plugin: Plugin, val isDownloaded: Boolean, ) @@ -45,9 +44,9 @@ class RepositoryViewHolderState(view: ViewBinding) : ViewHolderState(view) } class PluginAdapter( - val showRepositoryNames: Boolean = false, val iconClickCallback: (PluginWrapper) -> Unit, + val iconClickCallback: (Plugin) -> Unit ) : NoStateAdapter(diffCallback = BaseDiffCallback(itemSame = { a, b -> - a.pluginWrapper.plugin.internalName == b.pluginWrapper.plugin.internalName && a.pluginWrapper.repositoryData.url == b.pluginWrapper.repositoryData.url + a.plugin.second.internalName == b.plugin.second.internalName && a.plugin.first == b.plugin.first })) { override fun onCreateContent(parent: ViewGroup): ViewHolderState { val layout = if (isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item @@ -74,22 +73,14 @@ class PluginAdapter( val binding = holder.view as? RepositoryItemBinding ?: return val itemView = holder.itemView - val metadata = item.pluginWrapper.plugin + val metadata = item.plugin.second val disabled = metadata.status == PROVIDER_STATUS_DOWN val name = metadata.name.removeSuffix("Provider") val alpha = if (disabled) 0.6f else 1f - val isLocal = !item.pluginWrapper.plugin.url.startsWith("http") + val isLocal = !item.plugin.second.url.startsWith("http") binding.mainText.alpha = alpha binding.subText.alpha = alpha - binding.repositoryNameText.isVisible = showRepositoryNames - if (showRepositoryNames) { - val name = item.pluginWrapper.repositoryData.name - binding.repositoryNameText.text = name - } else { - binding.repositoryNameText.text = "" - } - val drawableInt = if (item.isDownloaded) R.drawable.ic_baseline_delete_outline_24 else R.drawable.netflix_download @@ -98,7 +89,7 @@ class PluginAdapter( binding.actionButton.setImageResource(drawableInt) binding.actionButton.setOnClickListener { - iconClickCallback.invoke(item.pluginWrapper) + iconClickCallback.invoke(item.plugin) } itemView.setOnClickListener { if (isLocal) return@setOnClickListener diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt index 06a95d527..0dcbece6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt @@ -54,7 +54,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra } override fun onBindingCreated(binding: FragmentPluginDetailsBinding) { - val metadata = data.pluginWrapper.plugin + val metadata = data.plugin.second binding.apply { pluginIcon.loadImage(metadata.iconUrl?.replace("%size%", "$iconSize") ?.replace("%exact_size%", "$iconSizeExact")) { @@ -135,7 +135,7 @@ class PluginDetailsFragment(val data: PluginViewData) : BaseBottomSheetDialogFra } private fun updateVoting(value: Int) { - val metadata = data.pluginWrapper.plugin + val metadata = data.plugin.second binding?.apply { pluginVotes.text = value.toString() if (metadata.hasVoted()) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index a7b24d3ae..534ffa62a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -4,13 +4,13 @@ import android.os.Bundle import android.view.View import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.BaseFragment import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.result.FOCUS_SELF @@ -23,19 +23,19 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setSyst import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji import com.lagradost.cloudstream3.utils.UIHelper.toPx -const val PLUGINS_BUNDLE_DATA = "data" +const val PLUGINS_BUNDLE_NAME = "name" +const val PLUGINS_BUNDLE_URL = "url" const val PLUGINS_BUNDLE_LOCAL = "isLocal" class PluginsFragment : BaseFragment( BaseFragment.BindingCreator.Inflate(FragmentPluginsBinding::inflate) ) { - private lateinit var pluginViewModel: PluginsViewModel + + private val pluginViewModel: PluginsViewModel by activityViewModels() override fun onDestroyView() { pluginViewModel.clear() // clear for the next observe @@ -47,8 +47,6 @@ class PluginsFragment : BaseFragment( } override fun onBindingCreated(binding: FragmentPluginsBinding) { - pluginViewModel = ViewModelProvider(this)[PluginsViewModel::class.java] - // Since the ViewModel is getting reused the tvTypes must be cleared between uses pluginViewModel.tvTypes.clear() pluginViewModel.selectedLanguages = listOf() @@ -62,25 +60,24 @@ class PluginsFragment : BaseFragment( } } - val repositoryData = arguments?.getString(PLUGINS_BUNDLE_DATA)?.let { data -> - tryParseJson(data) - } + val name = arguments?.getString(PLUGINS_BUNDLE_NAME) + val url = arguments?.getString(PLUGINS_BUNDLE_URL) val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true // download all extensions button val downloadAllButton = binding.settingsToolbar.menu?.findItem(R.id.download_all) - if (repositoryData == null) { + if (url == null || name == null) { dispatchBackPressed() return } setToolBarScrollFlags() - setUpToolbar(repositoryData.name) + setUpToolbar(name) binding.settingsToolbar.apply { setOnMenuItemClickListener { menuItem -> when (menuItem?.itemId) { R.id.download_all -> { - PluginsViewModel.downloadAll(activity, repositoryData, pluginViewModel) + PluginsViewModel.downloadAll(activity, url, pluginViewModel) } R.id.lang_filter -> { @@ -133,6 +130,9 @@ class PluginsFragment : BaseFragment( dispatchBackPressed() } } + searchView?.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (!hasFocus) pluginViewModel.search(null) + } searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { @@ -161,7 +161,7 @@ class PluginsFragment : BaseFragment( setRecycledViewPool(PluginAdapter.sharedPool) adapter = PluginAdapter { - pluginViewModel.handlePluginAction(activity, listOf(repositoryData), it, isLocal) + pluginViewModel.handlePluginAction(activity, url, it, isLocal) } } @@ -185,7 +185,7 @@ class PluginsFragment : BaseFragment( binding.tvtypesChipsScroll.root.isVisible = false } else { - pluginViewModel.updatePluginList(context, listOf(repositoryData)) + pluginViewModel.updatePluginList(context, url) binding.tvtypesChipsScroll.root.isVisible = true // not needed for users but may be useful for devs downloadAllButton?.isVisible = BuildConfig.DEBUG @@ -206,17 +206,21 @@ class PluginsFragment : BaseFragment( } companion object { - fun newInstance(repositoryData: RepositoryData): Bundle { + fun newInstance(name: String, url: String, isLocal: Boolean): Bundle { return Bundle().apply { - putString(PLUGINS_BUNDLE_DATA, repositoryData.toJson()) - putBoolean(PLUGINS_BUNDLE_LOCAL, false) - } - } - fun newLocalInstance(name: String): Bundle { - return Bundle().apply { - putString(PLUGINS_BUNDLE_DATA, RepositoryData("", name, "").toJson()) - putBoolean(PLUGINS_BUNDLE_LOCAL, true) + putString(PLUGINS_BUNDLE_NAME, name) + putString(PLUGINS_BUNDLE_URL, url) + putBoolean(PLUGINS_BUNDLE_LOCAL, isLocal) } } + +// class RepoSearchView(context: Context) : android.widget.SearchView(context) { +// var onActionViewCollapsed = {} +// +// override fun onActionViewCollapsed() { +// onActionViewCollapsed() +// } +// } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 09c202d43..dfc61eba5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -17,15 +17,17 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath -import com.lagradost.cloudstream3.plugins.PluginWrapper import com.lagradost.cloudstream3.plugins.RepositoryManager +import com.lagradost.cloudstream3.plugins.SitePlugin import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread -import com.lagradost.cloudstream3.utils.Levenshtein +import me.xdrop.fuzzywuzzy.FuzzySearch import java.io.File +// String => repository url +typealias Plugin = Pair /** * The boolean signifies if the plugin list should be scrolled to the top, used for searching. * */ @@ -38,7 +40,7 @@ class PluginsViewModel : ViewModel() { set(value) { // Also set all the plugin languages for easier filtering value.map { pluginViewData -> - val language = pluginViewData.pluginWrapper.plugin.language?.lowercase() + val language = pluginViewData.plugin.second.language?.lowercase() pluginLanguages.add( when { language.isNullOrBlank() -> "none" @@ -60,7 +62,7 @@ class PluginsViewModel : ViewModel() { private var currentQuery: String? = null companion object { - private val repositoryCache: MutableMap> = mutableMapOf() + private val repositoryCache: MutableMap> = mutableMapOf() const val TAG = "PLG" private fun isDownloaded( @@ -72,33 +74,32 @@ class PluginsViewModel : ViewModel() { } private suspend fun getPlugins( - repository: RepositoryData, + repositoryUrl: String, canUseCache: Boolean = true - ): List { - Log.i(TAG, "getPlugins = $repository") - if (canUseCache && repositoryCache.containsKey(repository.url)) { - repositoryCache[repository.url]?.let { + ): List { + Log.i(TAG, "getPlugins = $repositoryUrl") + if (canUseCache && repositoryCache.containsKey(repositoryUrl)) { + repositoryCache[repositoryUrl]?.let { return it } } - - return RepositoryManager.getRepoPlugins(repository) - ?.also { repositoryCache[repository.url] = it } ?: emptyList() + return RepositoryManager.getRepoPlugins(repositoryUrl) + ?.also { repositoryCache[repositoryUrl] = it } ?: emptyList() } /** * @param viewModel optional, updates the plugins livedata for that viewModel if included * */ - fun downloadAll(activity: Activity?, repository: RepositoryData, viewModel: PluginsViewModel?) = + fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = ioSafe { if (activity == null) return@ioSafe - val plugins = getPlugins(repository) + val plugins = getPlugins(repositoryUrl) - plugins.filter { pluginWrapper -> + plugins.filter { plugin -> !isDownloaded( activity, - pluginWrapper.plugin.internalName, - repository.url + plugin.second.internalName, + repositoryUrl ) }.also { list -> main { @@ -123,13 +124,13 @@ class PluginsViewModel : ViewModel() { Toast.LENGTH_SHORT ) } - }.amap { (_, repo, metadata) -> + }.amap { (repo, metadata) -> PluginManager.downloadPlugin( activity, metadata.url, metadata.fileHash, metadata.internalName, - repo.url, + repo, metadata.status != PROVIDER_STATUS_DOWN ) }.main { list -> @@ -142,7 +143,7 @@ class PluginsViewModel : ViewModel() { ), Toast.LENGTH_SHORT ) - viewModel?.updatePluginListPrivate(activity, listOf(repository)) + viewModel?.updatePluginListPrivate(activity, repositoryUrl) } else if (list.isNotEmpty()) { showToast(R.string.download_failed, Toast.LENGTH_SHORT) } @@ -156,32 +157,32 @@ class PluginsViewModel : ViewModel() { * */ fun handlePluginAction( activity: Activity?, - repositoryUrls: List, - pluginWrapper: PluginWrapper, + repositoryUrl: String, + plugin: Plugin, isLocal: Boolean ) = ioSafe { - Log.i(TAG, "handlePluginAction = ${repositoryUrls}, $pluginWrapper, $isLocal") + Log.i(TAG, "handlePluginAction = $repositoryUrl, $plugin, $isLocal") if (activity == null) return@ioSafe - val (_, repositoryData, metadata) = pluginWrapper + val (repo, metadata) = plugin - val file = if (isLocal) File(pluginWrapper.plugin.url) else getPluginPath( + val file = if (isLocal) File(plugin.second.url) else getPluginPath( activity, - pluginWrapper.plugin.internalName, - pluginWrapper.repositoryData.url + plugin.second.internalName, + plugin.first ) val (success, message) = if (file.exists()) { PluginManager.deletePlugin(file) to R.string.plugin_deleted } else { - val isEnabled = pluginWrapper.plugin.status != PROVIDER_STATUS_DOWN + val isEnabled = plugin.second.status != PROVIDER_STATUS_DOWN val message = if (isEnabled) R.string.plugin_loaded else R.string.plugin_downloaded PluginManager.downloadPlugin( activity, metadata.url, metadata.fileHash, metadata.internalName, - repositoryData.url, + repo, isEnabled ) to message } @@ -197,23 +198,20 @@ class PluginsViewModel : ViewModel() { if (isLocal) updatePluginListLocal() else - updatePluginListPrivate(activity, repositoryUrls) + updatePluginListPrivate(activity, repositoryUrl) } - private suspend fun updatePluginListPrivate(context: Context, repositories: List) { + private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { val isAdult = PreferenceManager.getDefaultSharedPreferences(context) .getStringSet(context.getString(R.string.prefer_media_type_key), emptySet()) ?.contains(TvType.NSFW.ordinal.toString()) == true - val plugins = repositories.flatMap { repositoryUrl -> - getPlugins(repositoryUrl) - } - + val plugins = getPlugins(repositoryUrl) val list = plugins.filter { // Show all non-nsfw plugins or all if nsfw is enabled - it.plugin.tvTypes?.contains(TvType.NSFW.name) != true || isAdult + it.second.tvTypes?.contains(TvType.NSFW.name) != true || isAdult }.map { plugin -> - PluginViewData(plugin, isDownloaded(context, plugin.plugin.internalName, plugin.repositoryData.url)) + PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } this.plugins = list @@ -226,8 +224,8 @@ class PluginsViewModel : ViewModel() { private fun List.filterTvTypes(): List { if (tvTypes.isEmpty()) return this return this.filter { - (it.pluginWrapper.plugin.tvTypes?.any { type -> tvTypes.contains(type) } == true) || - (tvTypes.contains(TvType.Others.name) && (it.pluginWrapper.plugin.tvTypes + (it.plugin.second.tvTypes?.any { type -> tvTypes.contains(type) } == true) || + (tvTypes.contains(TvType.Others.name) && (it.plugin.second.tvTypes ?: emptyList()).isEmpty()) } } @@ -235,36 +233,24 @@ class PluginsViewModel : ViewModel() { private fun List.filterLang(): List { if (selectedLanguages.isEmpty()) return this // do not filter return this.filter { - if (it.pluginWrapper.plugin.language == null) { + if (it.plugin.second.language == null) { return@filter selectedLanguages.contains("none") } - selectedLanguages.contains(it.pluginWrapper.plugin.language.lowercase()) + selectedLanguages.contains(it.plugin.second.language?.lowercase()) } } private fun List.sortByQuery(query: String?): List { - return if (query.isNullOrBlank()) { + return if (query == null) { // Return list to base state if no query - this.sortedBy { it.pluginWrapper.plugin.name } + this.sortedBy { it.plugin.second.name } } else { - this.mapNotNull { - // Try matching name - val score = Levenshtein.partialRatio( - it.pluginWrapper.plugin.name.lowercase(), + this.sortedBy { + -FuzzySearch.partialRatio( + it.plugin.second.name.lowercase(), query.lowercase() - ).takeIf { score -> score > 80 } ?: - // Fallback to description, but limit characters to reduce lag - it.pluginWrapper.plugin.description?.lowercase()?.take(64) - ?.let { description -> - Levenshtein.partialRatio( - description, - query.lowercase() - ) - }?.takeIf { score -> score > 80 } ?: return@mapNotNull null - it to score - }.sortedBy { - -it.second - }.map { it.first } + ) + } } } @@ -281,17 +267,16 @@ class PluginsViewModel : ViewModel() { ) } - fun updatePluginList(context: Context?, repositories: List) = - viewModelScope.launchSafe { - if (context == null) return@launchSafe - Log.i(TAG, "updatePluginList = $repositories") - updatePluginListPrivate(context, repositories) - } + fun updatePluginList(context: Context?, repositoryUrl: String) = viewModelScope.launchSafe { + if (context == null) return@launchSafe + Log.i(TAG, "updatePluginList = $repositoryUrl") + updatePluginListPrivate(context, repositoryUrl) + } fun search(query: String?) { currentQuery = query _filteredPlugins.postValue( - true to plugins.filterTvTypes().filterLang().sortByQuery(query) + true to (filteredPlugins.value?.second?.sortByQuery(query) ?: emptyList()) ) } @@ -304,7 +289,7 @@ class PluginsViewModel : ViewModel() { val downloadedPlugins = (PluginManager.getPluginsOnline() + PluginManager.getPluginsLocal()) .distinctBy { it.filePath } .map { - PluginViewData(PluginWrapper.getLocalPluginWrapper(it.toSitePlugin()), true) + PluginViewData("" to it.toSitePlugin(), true) } plugins = downloadedPlugins diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 22500d931..818f1fd79 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -40,7 +40,7 @@ class TestViewModel : ViewModel() { get() = scope != null private var filter = ProviderFilter.All - private val providers = atomicListOf>() + private val providers = threadSafeListOf>() private var passed = 0 private var failed = 0 private var total = 0 @@ -51,9 +51,9 @@ class TestViewModel : ViewModel() { } private fun postProviders() { - providers.withLock { + synchronized(providers) { val filtered = when (filter) { - ProviderFilter.All -> providers.toList() + ProviderFilter.All -> providers ProviderFilter.Passed -> providers.filter { it.second.success } ProviderFilter.Failed -> providers.filter { !it.second.success } } @@ -68,7 +68,7 @@ class TestViewModel : ViewModel() { } private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) { - providers.withLock { + synchronized(providers) { val index = providers.indexOfFirst { it.first == api } if (index == -1) { providers.add(api to results) @@ -81,14 +81,14 @@ class TestViewModel : ViewModel() { } fun init() { - total = APIHolder.allProviders.withLock { APIHolder.allProviders.size } + total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size } updateProgress() } fun startTest() { scope = CoroutineScope(Dispatchers.Default) - val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() } + val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() } total = apis.size failed = 0 passed = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 544dd9335..501ee0eef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -56,7 +56,7 @@ class SetupFragmentExtensions : BaseFragment( if (hasRepos) { binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, { - PluginsViewModel.downloadAll(activity, it, null) + PluginsViewModel.downloadAll(activity, it.url, null) }).apply { submitList(repositories.toList()) } } // else { @@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment( if (isSetup) if ( // If any available languages - apis.distinctBy { it.lang }.size > 1 + synchronized(apis) { apis.distinctBy { it.lang }.size > 1 } ) { findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index c18be8a2f..3c4a09ade 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index ab64e9993..f9b1cb1fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -38,21 +38,18 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings" -@Serializable data class SaveChromeCaptionStyle( - @JsonProperty("fontFamily") @SerialName("fontFamily") var fontFamily: String? = null, - @JsonProperty("fontGenericFamily") @SerialName("fontGenericFamily") var fontGenericFamily: Int? = null, - @JsonProperty("backgroundColor") @SerialName("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent - @JsonProperty("edgeColor") @SerialName("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") @SerialName("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, - @JsonProperty("foregroundColor") @SerialName("foregroundColor") var foregroundColor: Int = Color.WHITE, - @JsonProperty("fontScale") @SerialName("fontScale") var fontScale: Float = 1.05f, - @JsonProperty("windowColor") @SerialName("windowColor") var windowColor: Int = Color.TRANSPARENT, + @JsonProperty("fontFamily") var fontFamily: String? = null, + @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, + @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent + @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK + @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, + @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, + @JsonProperty("fontScale") var fontScale: Float = 1.05f, + @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, ) class ChromecastSubtitlesFragment : BaseFragment( @@ -101,7 +98,7 @@ class ChromecastSubtitlesFragment : BaseFragment(CHROME_SUBTITLE_KEY) ?: defaultState + return getKey(CHROME_SUBTITLE_KEY) ?: defaultState } private val defaultState = SaveChromeCaptionStyle() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index bf77ef247..5f716cca3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -54,39 +54,40 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.File const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" -@Serializable data class SaveCaptionStyle( - @JsonProperty("foregroundColor") @SerialName("foregroundColor") var foregroundColor: Int, - @JsonProperty("backgroundColor") @SerialName("backgroundColor") var backgroundColor: Int, - @JsonProperty("windowColor") @SerialName("windowColor") var windowColor: Int, + @JsonProperty("foregroundColor") var foregroundColor: Int, + @JsonProperty("backgroundColor") var backgroundColor: Int, + @JsonProperty("windowColor") var windowColor: Int, @OptIn(UnstableApi::class) - @JsonProperty("edgeType") @SerialName("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int, - @JsonProperty("edgeColor") @SerialName("edgeColor") var edgeColor: Int, - @FontRes @JsonProperty("typeface") @SerialName("typeface") var typeface: Int?, - @JsonProperty("typefaceFilePath") @SerialName("typefaceFilePath") var typefaceFilePath: String?, - @JsonProperty("elevation") @SerialName("elevation") var elevation: Int, // in dp - @JsonProperty("fixedTextSize") @SerialName("fixedTextSize") var fixedTextSize: Float?, // in sp - @Px @JsonProperty("edgeSize") @SerialName("edgeSize") var edgeSize: Float? = null, - @JsonProperty("removeCaptions") @SerialName("removeCaptions") var removeCaptions: Boolean = false, - @JsonProperty("removeBloat") @SerialName("removeBloat") var removeBloat: Boolean = true, - /** Apply caps lock to the text */ - @JsonProperty("upperCase") @SerialName("upperCase") var upperCase: Boolean = false, - /** Apply bold to the text */ - @JsonProperty("bold") @SerialName("bold") var bold: Boolean = false, - /** Apply italic to the text */ - @JsonProperty("italic") @SerialName("italic") var italic: Boolean = false, - /** in px, background radius, aka how round the background (backgroundColor) on each row is */ - @JsonProperty("backgroundRadius") @SerialName("backgroundRadius") var backgroundRadius: Float? = null, + @JsonProperty("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int, + @JsonProperty("edgeColor") var edgeColor: Int, + @FontRes + @JsonProperty("typeface") var typeface: Int?, + @JsonProperty("typefaceFilePath") var typefaceFilePath: String?, + /**in dp**/ + @JsonProperty("elevation") var elevation: Int, + /**in sp**/ + @JsonProperty("fixedTextSize") var fixedTextSize: Float?, + @Px + @JsonProperty("edgeSize") var edgeSize: Float? = null, + @JsonProperty("removeCaptions") var removeCaptions: Boolean = false, + @JsonProperty("removeBloat") var removeBloat: Boolean = true, + /** Apply caps lock to the text **/ + @JsonProperty("upperCase") var upperCase: Boolean = false, + /** Apply bold to the text **/ + @JsonProperty("bold") var bold: Boolean = false, + /** Apply italic to the text **/ + @JsonProperty("italic") var italic: Boolean = false, + /** in px, background radius, aka how round the background (backgroundColor) on each row is **/ + @JsonProperty("backgroundRadius") var backgroundRadius: Float? = null, /** The SSA_ALIGNMENT */ - @JsonProperty("alignment") @SerialName("alignment") var alignment: Int? = null, + @JsonProperty("alignment") var alignment: Int? = null, ) const val DEF_SUBS_ELEVATION = 20 @@ -115,9 +116,6 @@ class SubtitlesFragment : BaseDialogFragment( ) } - view.clipToPadding = false - view.clipChildren = false - // we default to 25sp, this is needed as RoundedBackgroundColorSpan breaks on override sizes val size = data.fixedTextSize ?: 25.0f view.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, size) @@ -264,7 +262,7 @@ class SubtitlesFragment : BaseDialogFragment( } fun getCurrentSavedStyle(): SaveCaptionStyle { - return cachedSubtitleStyle ?: (getKey(SUBTITLE_KEY) ?: SaveCaptionStyle( + return cachedSubtitleStyle ?: (getKey(SUBTITLE_KEY) ?: SaveCaptionStyle( foregroundColor = getDefColor(0), backgroundColor = getDefColor(2), windowColor = getDefColor(3), @@ -296,11 +294,11 @@ class SubtitlesFragment : BaseDialogFragment( } fun getDownloadSubsLanguageTagIETF(): List { - return getKey>(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en") + return getKey(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en") } fun getAutoSelectLanguageTagIETF(): String { - return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" + return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 8d322fd04..1377ccd08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -90,9 +90,12 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.Cache import java.io.File +import java.net.URL +import java.net.URLDecoder import java.util.concurrent.Executor import java.util.concurrent.Executors + object AppContextUtils { fun RecyclerView.isRecyclerScrollable(): Boolean { val layoutManager = @@ -144,7 +147,6 @@ object AppContextUtils { text.toSpanned() } } - /** Get channel ID by name */ @SuppressLint("RestrictedApi") private fun buildWatchNextProgramUri( @@ -362,22 +364,33 @@ object AppContextUtils { } } - /** Sort subtitles by names */ fun sortSubs(subs: Set): List { - // Be aware, sorting by "$originalName $nameSuffix" causes "a (b) 1" < "a 1", - // where "originalName then nameSuffix" preserves "a 1" < "a (b) 1", because we do not compare '(' and '1'. - return subs - .sortedWith( - compareBy { subtitle: SubtitleData -> subtitle.originalName } - .thenBy { subtitle: SubtitleData -> subtitle.nameSuffix }) + return subs.sortedBy { it.name } } fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) } + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list return hashSet } @@ -468,8 +481,9 @@ object AppContextUtils { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = + val allApis = synchronized(apis) { apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } return if (currentPrefMedia.isEmpty()) { allApis } else { @@ -523,12 +537,13 @@ object AppContextUtils { fun Activity.loadRepository(url: String) { ioSafe { val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe - val data = RepositoryData( - repo.iconUrl ?: "", - repo.name, - url + RepositoryManager.addRepository( + RepositoryData( + repo.iconUrl ?: "", + repo.name, + url + ) ) - RepositoryManager.addRepository(data) main { showToast( getString(R.string.player_loaded_subtitles, repo.name), @@ -536,12 +551,13 @@ object AppContextUtils { ) } afterRepositoryLoadedEvent.invoke(true) - addRepositoryDialog(data) + addRepositoryDialog(repo.name, url) } } fun Activity.addRepositoryDialog( - repositoryData: RepositoryData + repositoryName: String, + repositoryURL: String, ) { val repos = RepositoryManager.getRepositories() @@ -551,7 +567,9 @@ object AppContextUtils { navigate( R.id.global_to_navigation_settings_plugins, PluginsFragment.newInstance( - repositoryData, + repositoryName, + repositoryURL, + false, ) ) } @@ -559,7 +577,7 @@ object AppContextUtils { runOnUiThread { AlertDialog.Builder(this).apply { - setTitle(repositoryData.name) + setTitle(repositoryName) setMessage(R.string.download_all_plugins_from_repo) setPositiveButton(R.string.open_downloaded_repo) { _, _ -> openAddedRepo() @@ -632,17 +650,16 @@ object AppContextUtils { } } - // Deprecate after next stable - /* @Deprecated( - message = "Use splitUrlParameters instead.", - replaceWith = ReplaceWith( - expression = "splitUrlParameters(url.toString())", - imports = ["com.lagradost.cloudstream3.splitUrlParameters"], - ), - level = DeprecationLevel.WARNING, - ) */ - fun splitQuery(url: java.net.URL): Map { - return com.lagradost.cloudstream3.splitUrlParameters(url.toString()) + fun splitQuery(url: URL): Map { + val queryPairs: MutableMap = LinkedHashMap() + val query: String = url.query + val pairs = query.split("&").toTypedArray() + for (pair in pairs) { + val idx = pair.indexOf("=") + queryPairs[URLDecoder.decode(pair.substring(0, idx), "UTF-8")] = + URLDecoder.decode(pair.substring(idx + 1), "UTF-8") + } + return queryPairs } /**| S1:E2 Hello World @@ -687,7 +704,7 @@ object AppContextUtils { "$seasonNameShort${rSeason}:$episodeNameShort${rEpisode}" } else if (rEpisode != null) { "$episodeNameShort$rEpisode" - } else null + }else null } fun Activity?.loadCache() { @@ -710,7 +727,7 @@ object AppContextUtils { fun loadResult( url: String, apiName: String, - name: String, + name : String, startAction: Int = 0, startValue: Int = 0 ) { @@ -720,7 +737,7 @@ object AppContextUtils { fun FragmentActivity.loadResult( url: String, apiName: String, - name: String, + name : String, startAction: Int = 0, startValue: Int = 0 ) { @@ -846,8 +863,7 @@ object AppContextUtils { } fun Context.isUsingMobileData(): Boolean { - val connectionManager = - getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val connectionManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val activeNetwork: Network? = connectionManager.activeNetwork val networkCapabilities = connectionManager.getNetworkCapabilities(activeNetwork) @@ -900,4 +916,4 @@ object AppContextUtils { } else null return currentAudioFocusRequest } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 0a561b471..88cb7481c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -10,6 +10,7 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R @@ -20,12 +21,11 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi.Companion.KITSU_CACHED_LIST -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs +import com.lagradost.cloudstream3.utils.DataStore.mapper import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream @@ -36,8 +36,6 @@ import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESU import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESUME_PACKAGES import com.lagradost.safefile.MediaFileContentType import com.lagradost.safefile.SafeFile -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import okhttp3.internal.closeQuietly import java.io.IOException import java.io.OutputStream @@ -51,7 +49,7 @@ object BackupUtils { /** * No sensitive or breaking data in the backup - */ + * */ private val nonTransferableKeys = listOf( ANILIST_CACHED_LIST, MAL_CACHED_LIST, @@ -96,8 +94,8 @@ object BackupUtils { // Download headers are unintuitively used in the resume watching system. // We can therefore not prune download headers in backups. - // DOWNLOAD_HEADER_CACHE_BACKUP, - // DOWNLOAD_HEADER_CACHE, + //DOWNLOAD_HEADER_CACHE_BACKUP, + //DOWNLOAD_HEADER_CACHE, // This may overwrite valid local data with invalid data @@ -120,24 +118,24 @@ object BackupUtils { private var restoreFileSelector: ActivityResultLauncher>? = null // Kinda hack, but I couldn't think of a better way - @Serializable data class BackupVars( - @JsonProperty("_Bool") @SerialName("_Bool") val bool: Map?, - @JsonProperty("_Int") @SerialName("_Int") val int: Map?, - @JsonProperty("_String") @SerialName("_String") val string: Map?, - @JsonProperty("_Float") @SerialName("_Float") val float: Map?, - @JsonProperty("_Long") @SerialName("_Long") val long: Map?, - @JsonProperty("_StringSet") @SerialName("_StringSet") val stringSet: Map?>?, + @JsonProperty("_Bool") val bool: Map?, + @JsonProperty("_Int") val int: Map?, + @JsonProperty("_String") val string: Map?, + @JsonProperty("_Float") val float: Map?, + @JsonProperty("_Long") val long: Map?, + @JsonProperty("_StringSet") val stringSet: Map?>?, ) - @Serializable data class BackupFile( - @JsonProperty("datastore") @SerialName("datastore") val datastore: BackupVars, - @JsonProperty("settings") @SerialName("settings") val settings: BackupVars, + @JsonProperty("datastore") val datastore: BackupVars, + @JsonProperty("settings") val settings: BackupVars ) @Suppress("UNCHECKED_CAST") - private fun getBackup(context: Context): BackupFile { + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } @@ -147,7 +145,7 @@ object BackupUtils { allData.filter { it.value is String } as? Map, allData.filter { it.value is Float } as? Map, allData.filter { it.value is Long } as? Map, - allData.filter { it.value as? Set != null } as? Map>, + allData.filter { it.value as? Set != null } as? Map> ) val allSettingsSorted = BackupVars( @@ -156,12 +154,12 @@ object BackupUtils { allSettings.filter { it.value is String } as? Map, allSettings.filter { it.value is Float } as? Map, allSettings.filter { it.value is Long } as? Map, - allSettings.filter { it.value as? Set != null } as? Map>, + allSettings.filter { it.value as? Set != null } as? Map> ) return BackupFile( allDataSorted, - allSettingsSorted, + allSettingsSorted ) } @@ -170,7 +168,7 @@ object BackupUtils { context: Context?, backupFile: BackupFile, restoreSettings: Boolean, - restoreDataStore: Boolean, + restoreDataStore: Boolean ) { if (context == null) return if (restoreSettings) { @@ -199,9 +197,9 @@ object BackupUtils { fun backup(context: Context?) = ioSafe { if (context == null) return@ioSafe + var fileStream: OutputStream? = null var printStream: PrintWriter? = null - try { if (!context.checkWrite()) { showToast(R.string.backup_failed, Toast.LENGTH_LONG) @@ -216,14 +214,18 @@ object BackupUtils { fileStream = stream.openNew() printStream = PrintWriter(fileStream) - printStream.print(backupFile.toJson()) - showToast(R.string.backup_success, Toast.LENGTH_LONG) + printStream.print(mapper.writeValueAsString(backupFile)) + + showToast( + R.string.backup_success, + Toast.LENGTH_LONG + ) } catch (e: Exception) { logError(e) try { showToast( txt(R.string.backup_failed_error_format, e.toString()), - Toast.LENGTH_LONG, + Toast.LENGTH_LONG ) } catch (e: Exception) { logError(e) @@ -242,7 +244,7 @@ object BackupUtils { name, folder = null, extension = ext, - tryResume = false, + tryResume = false ) } @@ -257,14 +259,14 @@ object BackupUtils { val input = activity.contentResolver.openInputStream(uri) ?: return@ioSafe - val text = input.bufferedReader().readText() - val restoredValue = parseJson(text) + val restoredValue = + mapper.readValue(input) restore( activity, restoredValue, restoreSettings = true, - restoreDataStore = true, + restoreDataStore = true ) activity.runOnUiThread { activity.recreate() } } catch (e: Exception) { @@ -305,7 +307,7 @@ object BackupUtils { private fun Context.restoreMap( map: Map?, - isEditingAppSettings: Boolean = false, + isEditingAppSettings: Boolean = false ) { val editor = DataStore.editor(this, isEditingAppSettings) map?.forEach { @@ -317,27 +319,21 @@ object BackupUtils { } /** - * Copy of [com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getDefaultDir], - * modified for backup-specific paths. - */ + * Copy of [com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.basePathToFile], [com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDefaultDir] and [com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getBasePath] + * modded for backup specific paths + * */ + fun getDefaultBackupDir(context: Context): SafeFile? { return SafeFile.fromMedia(context, MediaFileContentType.Downloads) } - /** - * Copy of [com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getBasePath], - * modified for backup-specific paths. - */ fun getCurrentBackupDir(context: Context): Pair { val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) - val basePathSetting = settingsManager.getString(context.getString(R.string.backup_path_key), null) + val basePathSetting = + settingsManager.getString(context.getString(R.string.backup_path_key), null) return baseBackupPathToFile(context, basePathSetting) to basePathSetting } - /** - * Copy of [com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.basePathToFile], - * modified for backup-specific paths. - */ private fun baseBackupPathToFile(context: Context, path: String?): SafeFile? { return when { path.isNullOrBlank() -> getDefaultBackupDir(context) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 68f262d8d..0a1db85fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -2,16 +2,17 @@ package com.lagradost.cloudstream3.utils import android.content.Context import android.content.SharedPreferences -import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral import kotlin.reflect.KClass import kotlin.reflect.KProperty +import androidx.core.content.edit /** Used to display metadata about downloads and resume watching */ const val DOWNLOAD_HEADER_CACHE = "download_header_cache" @@ -87,18 +88,8 @@ data class Editor( } object DataStore { - // Extensions shouldn't have really been using this version of it, but it seems - // some have. Since there has always been a very easy alternative, we won't - // need to deprecate it that long, and should be able to fully remove it - // once extensions at least use the other version. - @Deprecated( - "Please do not use the mapper version from DataStore. Preferably use methods from AppUtils " + - "to parse JSON. However, you can use the stable-API version of the mapper at " + - "com.lagradost.cloudstream3.mapper to access the mapper directly if necessary.", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("com.lagradost.cloudstream3.mapper"), - ) - val mapper = com.lagradost.cloudstream3.mapper + val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() private fun getPreferences(context: Context): SharedPreferences { return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) @@ -108,6 +99,7 @@ object DataStore { return getPreferences(this) } + fun getFolderName(folder: String, path: String): String { return "${folder}/${path}" } @@ -173,19 +165,19 @@ object DataStore { fun Context.setKey(path: String, value: T) { try { getSharedPrefs().edit { - putString(path, value?.toJsonLiteral()) + putString(path, mapper.writeValueAsString(value)) } } catch (e: Exception) { logError(e) } } - fun Context.getKey(path: String, valueType: Class): T? { - return try { + fun Context.getKey(path: String, valueType: Class): T? { + try { val json: String = getSharedPrefs().getString(path, null) ?: return null - parseJson(json, valueType.kotlin) - } catch (_: Exception) { - null + return json.toKotlinObject(valueType) + } catch (e: Exception) { + return null } } @@ -193,37 +185,21 @@ object DataStore { setKey(getFolderName(folder, path), value) } - @Deprecated( - message = "Use parseJson(this) directly instead.", - level = DeprecationLevel.WARNING, - replaceWith = ReplaceWith( - expression = "parseJson(this)", - imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], - ), - ) inline fun String.toKotlinObject(): T { - return parseJson(this) + return mapper.readValue(this, T::class.java) } - @Deprecated( - message = "Use parseJson(this) directly instead.", - level = DeprecationLevel.WARNING, - replaceWith = ReplaceWith( - expression = "parseJson(this)", - imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"], - ), - ) - fun String.toKotlinObject(valueType: Class): T { - return parseJson(this, valueType.kotlin) + fun String.toKotlinObject(valueType: Class): T { + return mapper.readValue(this, valueType) } // GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR inline fun Context.getKey(path: String, defVal: T?): T? { - return try { + try { val json: String = getSharedPrefs().getString(path, null) ?: return defVal - parseJson(json) - } catch (_: Exception) { - null + return json.toKotlinObject() + } catch (e: Exception) { + return null } } @@ -238,4 +214,4 @@ object DataStore { inline fun Context.getKey(folder: String, path: String, defVal: T?): T? { return getKey(getFolderName(folder, path), defVal) ?: defVal } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 340266f6c..19caead21 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.utils import android.content.Context -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.CloudStreamApp.Companion.context @@ -32,12 +31,6 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia import com.lagradost.cloudstream3.utils.downloader.DownloadObjects -import com.lagradost.cloudstream3.utils.serializers.WriteOnlySerializer -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient import java.util.Calendar import java.util.Date import java.util.GregorianCalendar @@ -50,18 +43,17 @@ const val RESULT_WATCH_STATE = "result_watch_state" const val RESULT_WATCH_STATE_DATA = "result_watch_state_data" const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data" const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data" -const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // Changed due to id changes +const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching" const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated" const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" const val KEY_RESULT_SORT = "result_sort" -const val USER_PINNED_PROVIDERS = "user_pinned_providers" // Key for pinned user set +const val USER_PINNED_PROVIDERS = "user_pinned_providers" //key for pinned user set class UserPreferenceDelegate( - private val key: String, - private val default: T, + private val key: String, private val default: T //, private val klass: KClass ) { private val klass: KClass = default::class private val realKey get() = "${DataStoreHelper.currentAccount}/$key" @@ -71,7 +63,7 @@ class UserPreferenceDelegate( operator fun setValue( self: Any?, property: KProperty<*>, - t: T?, + t: T? ) { if (t == null) { removeKey(realKey) @@ -90,7 +82,7 @@ object DataStoreHelper { R.drawable.profile_bg_pink, R.drawable.profile_bg_purple, R.drawable.profile_bg_red, - R.drawable.profile_bg_teal, + R.drawable.profile_bg_teal ) private var searchPreferenceProvidersStrings: List by UserPreferenceDelegate( @@ -120,17 +112,16 @@ object DataStoreHelper { private var searchPreferenceTagsStrings: List by UserPreferenceDelegate( "search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) - var searchPreferenceTags: List get() = deserializeTv(searchPreferenceTagsStrings) set(value) { searchPreferenceTagsStrings = serializeTv(value) } + private var homePreferenceStrings: List by UserPreferenceDelegate( "home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) - var homePreference: List get() = deserializeTv(homePreferenceStrings) set(value) { @@ -141,38 +132,38 @@ object DataStoreHelper { "home_bookmarked_last_list", IntArray(0) ) - var playBackSpeed: Float by UserPreferenceDelegate("playback_speed", 1.0f) var resizeMode: Int by UserPreferenceDelegate("resize_mode", 0) var librarySortingMode: Int by UserPreferenceDelegate( "library_sorting_mode", ListSorting.AlphabeticalA.ordinal ) - private var _resultsSortingMode: Int by UserPreferenceDelegate( "results_sorting_mode", EpisodeSortType.NUMBER_ASC.ordinal ) - var resultsSortingMode: EpisodeSortType get() = EpisodeSortType.entries.getOrNull(_resultsSortingMode) ?: EpisodeSortType.NUMBER_ASC set(value) { _resultsSortingMode = value.ordinal } - @Serializable data class Account( - @JsonProperty("keyIndex") @SerialName("keyIndex") val keyIndex: Int, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("customImage") @SerialName("customImage") val customImage: String? = null, - @JsonProperty("defaultImageIndex") @SerialName("defaultImageIndex") val defaultImageIndex: Int, - @JsonProperty("lockPin") @SerialName("lockPin") val lockPin: String? = null, + @JsonProperty("keyIndex") + val keyIndex: Int, + @JsonProperty("name") + val name: String, + @JsonProperty("customImage") + val customImage: String? = null, + @JsonProperty("defaultImageIndex") + val defaultImageIndex: Int, + @JsonProperty("lockPin") + val lockPin: String? = null, ) { - @get:JsonIgnore - val image get() = customImage?.let { UiImage.Image(it) } ?: - profileImages.getOrNull(defaultImageIndex)?.let { - UiImage.Drawable(it) - } ?: UiImage.Drawable(profileImages.first()) + val image + get() = customImage?.let { UiImage.Image(it) } ?: profileImages.getOrNull( + defaultImageIndex + )?.let { UiImage.Drawable(it) } ?: UiImage.Drawable(profileImages.first()) } const val TAG = "data_store_helper" @@ -185,7 +176,7 @@ object DataStoreHelper { * Setting this does not automatically reload the homepage. */ var currentHomePage: String? - get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") set(value) { val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" if (value == null) { @@ -197,6 +188,7 @@ object DataStoreHelper { fun setAccount(account: Account) { val homepage = currentHomePage + selectedKeyIndex = account.keyIndex AccountManager.updateAccountIds() showToast(context?.getString(R.string.logged_account, account.name) ?: account.name) @@ -214,7 +206,7 @@ object DataStoreHelper { currentAccounts.getOrNull(currentAccounts.indexOfFirst { it.keyIndex == 0 }) ?: Account( keyIndex = 0, name = context.getString(R.string.default_account), - defaultImageIndex = 0, + defaultImageIndex = 0 ) } } @@ -240,21 +232,18 @@ object DataStoreHelper { } } - @Serializable data class PosDur( - @JsonProperty("position") @SerialName("position") val position: Long, - @JsonProperty("duration") @SerialName("duration") val duration: Long, + @JsonProperty("position") val position: Long, + @JsonProperty("duration") val duration: Long ) fun PosDur.fixVisual(): PosDur { if (duration <= 0) return PosDur(0, duration) val percentage = position * 100 / duration - return when { - percentage <= 1 -> PosDur(0, duration) - percentage <= 5 -> PosDur(5 * duration / 100, duration) - percentage >= 95 -> PosDur(duration, duration) - else -> this - } + if (percentage <= 1) return PosDur(0, duration) + if (percentage <= 5) return PosDur(5 * duration / 100, duration) + if (percentage >= 95) return PosDur(duration, duration) + return this } fun Int.toYear(): Date = @@ -262,38 +251,28 @@ object DataStoreHelper { /** * Used to display notifications on new episodes and posters in library. - */ - @Serializable + **/ abstract class LibrarySearchResponse( - /** - * These fields are marked @Transient because this class is only ever serialized through - * through its subclasses, which redeclare each property with their own @SerialName - * annotations. Without @Transient here, kotlinx.serialization would try to - * generate a serializer for the abstract base class itself (or double-serialize - * these fields), which fails/conflicts since these are meant to be overridden, - * not serialized directly from the parent. - */ - @Transient override var id: Int? = null, - @Transient open val latestUpdatedTime: Long = 0L, - @Transient override val name: String = "", - @Transient override val url: String = "", - @Transient override val apiName: String = "", - @Transient override var type: TvType? = null, - @Transient override var posterUrl: String? = null, - @Transient open val year: Int? = null, - @Transient open val syncData: Map? = null, - @Transient override var quality: SearchQuality? = null, - @Transient override var posterHeaders: Map? = null, - @Transient open val plot: String? = null, - @Transient override var score: Score? = null, - @Transient open val tags: List? = null, + @JsonProperty("id") override var id: Int?, + @JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long, + @JsonProperty("name") override val name: String, + @JsonProperty("url") override val url: String, + @JsonProperty("apiName") override val apiName: String, + @JsonProperty("type") override var type: TvType?, + @JsonProperty("posterUrl") override var posterUrl: String?, + @JsonProperty("year") open val year: Int?, + @JsonProperty("syncData") open val syncData: Map?, + @JsonProperty("quality") override var quality: SearchQuality?, + @JsonProperty("posterHeaders") override var posterHeaders: Map?, + @JsonProperty("plot") open val plot: String? = null, + @JsonProperty("score") override var score: Score? = null, + @JsonProperty("tags") open val tags: List? = null, ) : SearchResponse { @JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) - @SerialName("rating") @Deprecated( "`rating` is the old scoring system, use score instead", replaceWith = ReplaceWith("score"), - level = DeprecationLevel.ERROR, + level = DeprecationLevel.ERROR ) var rating: Int? = null set(value) { @@ -304,26 +283,23 @@ object DataStoreHelper { } } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = SubscribedData.Serializer::class) data class SubscribedData( - @JsonProperty("subscribedTime") @SerialName("subscribedTime") val subscribedTime: Long, - @JsonProperty("lastSeenEpisodeCount") @SerialName("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType?, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("year") @SerialName("year") override val year: Int?, - @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, - @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + @JsonProperty("subscribedTime") val subscribedTime: Long, + @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null, + override val plot: String? = null, + override var score: Score? = null, + override val tags: List? = null, ) : LibrarySearchResponse( id, latestUpdatedTime, @@ -338,13 +314,8 @@ object DataStoreHelper { posterHeaders, plot, score, - tags, + tags ) { - object Serializer : WriteOnlySerializer( - SubscribedData.generatedSerializer(), - setOf("rating"), - ) - fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -363,30 +334,27 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags, + tags = this.tags ) } } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = BookmarkedData.Serializer::class) data class BookmarkedData( - @JsonProperty("bookmarkedTime") @SerialName("bookmarkedTime") val bookmarkedTime: Long, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType?, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("year") @SerialName("year") override val year: Int?, - @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, - @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + @JsonProperty("bookmarkedTime") val bookmarkedTime: Long, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null, + override val plot: String? = null, + override var score: Score? = null, + override val tags: List? = null, ) : LibrarySearchResponse( id, latestUpdatedTime, @@ -399,13 +367,8 @@ object DataStoreHelper { syncData, quality, posterHeaders, - plot, + plot ) { - object Serializer : WriteOnlySerializer( - BookmarkedData.generatedSerializer(), - setOf("rating"), - ) - fun toLibraryItem(id: String): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( name, @@ -424,30 +387,27 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags, + tags = this.tags ) } } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = FavoritesData.Serializer::class) data class FavoritesData( - @JsonProperty("favoritesTime") @SerialName("favoritesTime") val favoritesTime: Long, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("latestUpdatedTime") @SerialName("latestUpdatedTime") override val latestUpdatedTime: Long, - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType?, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("year") @SerialName("year") override val year: Int?, - @JsonProperty("syncData") @SerialName("syncData") override val syncData: Map? = null, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("plot") @SerialName("plot") override val plot: String? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, - @JsonProperty("tags") @SerialName("tags") override val tags: List? = null, + @JsonProperty("favoritesTime") val favoritesTime: Long, + override var id: Int?, + override val latestUpdatedTime: Long, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override val year: Int?, + override val syncData: Map? = null, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null, + override val plot: String? = null, + override var score: Score? = null, + override val tags: List? = null, ) : LibrarySearchResponse( id, latestUpdatedTime, @@ -460,13 +420,8 @@ object DataStoreHelper { syncData, quality, posterHeaders, - plot, + plot ) { - object Serializer : WriteOnlySerializer( - FavoritesData.generatedSerializer(), - setOf("rating"), - ) - fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -485,32 +440,31 @@ object DataStoreHelper { this.id, plot = this.plot, score = this.score, - tags = this.tags, + tags = this.tags ) } } - @Serializable data class ResumeWatchingResult( - @JsonProperty("name") @SerialName("name") override val name: String, - @JsonProperty("url") @SerialName("url") override val url: String, - @JsonProperty("apiName") @SerialName("apiName") override val apiName: String, - @JsonProperty("type") @SerialName("type") override var type: TvType? = null, - @JsonProperty("posterUrl") @SerialName("posterUrl") override var posterUrl: String?, - @JsonProperty("watchPos") @SerialName("watchPos") val watchPos: PosDur?, - @JsonProperty("id") @SerialName("id") override var id: Int?, - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("isFromDownload") @SerialName("isFromDownload") val isFromDownload: Boolean, - @JsonProperty("quality") @SerialName("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") @SerialName("posterHeaders") override var posterHeaders: Map? = null, - @JsonProperty("score") @SerialName("score") override var score: Score? = null, + @JsonProperty("name") override val name: String, + @JsonProperty("url") override val url: String, + @JsonProperty("apiName") override val apiName: String, + @JsonProperty("type") override var type: TvType? = null, + @JsonProperty("posterUrl") override var posterUrl: String?, + @JsonProperty("watchPos") val watchPos: PosDur?, + @JsonProperty("id") override var id: Int?, + @JsonProperty("parentId") val parentId: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("season") val season: Int?, + @JsonProperty("isFromDownload") val isFromDownload: Boolean, + @JsonProperty("quality") override var quality: SearchQuality? = null, + @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, + @JsonProperty("score") override var score: Score? = null, ) : SearchResponse /** * A datastore wide account for future implementations of a multiple account system - */ + **/ fun getAllWatchStateIds(): List? { val folder = "$currentAccount/$RESULT_WATCH_STATE" @@ -546,7 +500,7 @@ object DataStoreHelper { } fun migrateResumeWatching() { - // if (getKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, false) != true) { + // if (getKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, false) != true) { setKey(RESULT_RESUME_WATCHING_HAS_MIGRATED, true) getAllResumeStateIdsOld()?.forEach { id -> getLastWatchedOld(id)?.let { @@ -556,12 +510,12 @@ object DataStoreHelper { it.episode, it.season, it.isFromDownload, - it.updateTime, + it.updateTime ) removeLastWatchedOld(it.parentId) } } - // } + //} } fun setLastWatched( @@ -582,7 +536,7 @@ object DataStoreHelper { episode, season, updateTime ?: System.currentTimeMillis(), - isFromDownload, + isFromDownload ) ) } @@ -599,7 +553,7 @@ object DataStoreHelper { fun getLastWatched(id: Int?): DownloadObjects.ResumeWatching? { if (id == null) return null - return getKey( + return getKey( "$currentAccount/$RESULT_RESUME_WATCHING", id.toString(), ) @@ -607,7 +561,7 @@ object DataStoreHelper { private fun getLastWatchedOld(id: Int?): DownloadObjects.ResumeWatching? { if (id == null) return null - return getKey( + return getKey( "$currentAccount/$RESULT_RESUME_WATCHING_OLD", id.toString(), ) @@ -621,18 +575,18 @@ object DataStoreHelper { fun getBookmarkedData(id: Int?): BookmarkedData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) } fun getAllBookmarkedData(): List { return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } fun getAllSubscriptions(): List { return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } @@ -644,12 +598,12 @@ object DataStoreHelper { /** * Set new seen episodes and update time - */ + **/ fun updateSubscribedData(id: Int?, data: SubscribedData?, episodeResponse: EpisodeResponse?) { if (id == null || data == null || episodeResponse == null) return val newData = data.copy( latestUpdatedTime = unixTimeMS, - lastSeenEpisodeCount = episodeResponse.getLatestEpisodes(), + lastSeenEpisodeCount = episodeResponse.getLatestEpisodes() ) setKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString(), newData) } @@ -662,12 +616,12 @@ object DataStoreHelper { fun getSubscribedData(id: Int?): SubscribedData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) } fun getAllFavorites(): List { return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull { - getKey(it) + getKey(it) } ?: emptyList() } @@ -685,7 +639,7 @@ object DataStoreHelper { fun getFavoritesData(id: Int?): FavoritesData? { if (id == null) return null - return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) + return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) } fun setViewPos(id: Int?, pos: Long, dur: Long) { @@ -694,10 +648,10 @@ object DataStoreHelper { setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur)) } - /** - * Sets the position, duration, and resume data of an episode/movie, - * If nextEpisode is not specified it will not be able to set the next episode as resumable if progress > NEXT_WATCH_EPISODE_PERCENTAGE - */ + /** Sets the position, duration, and resume data of an episode/movie, + * + * if nextEpisode is not specified it will not be able to set the next episode as resumable if progress > NEXT_WATCH_EPISODE_PERCENTAGE + * */ fun setViewPosAndResume(id: Int?, position: Long, duration: Long, currentEpisode: Any?, nextEpisode: Any?) { setViewPos(id, position, duration) if (id != null) { @@ -733,7 +687,7 @@ object DataStoreHelper { resumeMeta.id, resumeMeta.episode, resumeMeta.season, - isFromDownload = false, + isFromDownload = false ) } @@ -743,7 +697,7 @@ object DataStoreHelper { resumeMeta.id, resumeMeta.episode, resumeMeta.season, - isFromDownload = true, + isFromDownload = true ) } } @@ -752,16 +706,17 @@ object DataStoreHelper { fun getViewPos(id: Int?): PosDur? { if (id == null) return null - return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null) + return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null) } fun getVideoWatchState(id: Int?): VideoWatchState? { if (id == null) return null - return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null) + return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null) } fun setVideoWatchState(id: Int?, watchState: VideoWatchState) { if (id == null) return + // None == No key if (watchState == VideoWatchState.None) { removeKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString()) @@ -772,7 +727,7 @@ object DataStoreHelper { fun getDub(id: Int): DubStatus? { return DubStatus.entries - .getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1) + .getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1) } fun setDub(id: Int, status: DubStatus) { @@ -793,13 +748,13 @@ object DataStoreHelper { getKey( "$currentAccount/$RESULT_WATCH_STATE", id.toString(), - null, + null ) ) } fun getResultSeason(id: Int): Int? { - return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null) + return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null) } fun setResultSeason(id: Int, value: Int?) { @@ -807,7 +762,7 @@ object DataStoreHelper { } fun getResultEpisode(id: Int): Int? { - return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null) + return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null) } fun setResultEpisode(id: Int, value: Int?) { @@ -820,11 +775,12 @@ object DataStoreHelper { fun getSync(id: Int, idPrefixes: List): List { return idPrefixes.map { idPrefix -> - getKey("${idPrefix}_sync", id.toString()) + getKey("${idPrefix}_sync", id.toString()) } } var pinnedProviders: Array - get() = getKey>(USER_PINNED_PROVIDERS) ?: emptyArray() + get() = getKey(USER_PINNED_PROVIDERS) ?: emptyArray() set(value) = setKey(USER_PINNED_PROVIDERS, value) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt index 8ec46cbe5..8456094d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt @@ -10,14 +10,12 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.getId -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.Coroutines.main -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.io.InputStream import java.lang.Thread.sleep import java.util.* import kotlin.concurrent.thread +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import java.io.InputStream import kotlin.let object FillerEpisodeCheck { @@ -27,45 +25,66 @@ object FillerEpisodeCheck { return q + "cache" + z } - @Serializable data class Show( - @JsonProperty("slug") @SerialName("slug") val slug: String, - @JsonProperty("title") @SerialName("title") val title: String, - @JsonProperty("filler") @SerialName("filler") val filler: ArrayList, - @JsonProperty("mixedCanon") @SerialName("mixedCanon") val mixedCanon: ArrayList, - @JsonProperty("mangaCanon") @SerialName("mangaCanon") val mangaCanon: ArrayList, - @JsonProperty("animeCanon") @SerialName("animeCanon") val animeCanon: ArrayList, + @JsonProperty("slug") + val slug: String, + @JsonProperty("title") + val title: String, + @JsonProperty("filler") + val filler: ArrayList, + @JsonProperty("mixedCanon") + val mixedCanon: ArrayList, + @JsonProperty("mangaCanon") + val mangaCanon: ArrayList, + @JsonProperty("animeCanon") + val animeCanon: ArrayList, ) - @Serializable data class MappingRoot( - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("anidb_id") @SerialName("anidb_id") val anidbId: Long?, - @JsonProperty("anilist_id") @SerialName("anilist_id") val anilistId: Long?, - @JsonProperty("animecountdown_id") @SerialName("animecountdown_id") val animecountdownId: Long?, - @JsonProperty("animenewsnetwork_id") @SerialName("animenewsnetwork_id") val animenewsnetworkId: Long?, - @JsonProperty("anime-planet_id") @SerialName("anime-planet_id") val animePlanetId: String?, - @JsonProperty("anisearch_id") @SerialName("anisearch_id") val anisearchId: Long?, - @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String?, - @JsonProperty("kitsu_id") @SerialName("kitsu_id") val kitsuId: Long?, - @JsonProperty("livechart_id") @SerialName("livechart_id") val livechartId: Long?, - @JsonProperty("mal_id") @SerialName("mal_id") val malId: Long?, - @JsonProperty("simkl_id") @SerialName("simkl_id") val simklId: Long?, - @JsonProperty("themoviedb_id") @SerialName("themoviedb_id") val themoviedbId: Long?, - @JsonProperty("tvdb_id") @SerialName("tvdb_id") val tvdbId: Long?, - @JsonProperty("season") @SerialName("season") val season: Season?, + @JsonProperty("type") + val type: String?, + @JsonProperty("anidb_id") + val anidbId: Long?, + @JsonProperty("anilist_id") + val anilistId: Long?, + @JsonProperty("animecountdown_id") + val animecountdownId: Long?, + @JsonProperty("animenewsnetwork_id") + val animenewsnetworkId: Long?, + @JsonProperty("anime-planet_id") + val animePlanetId: String?, + @JsonProperty("anisearch_id") + val anisearchId: Long?, + @JsonProperty("imdb_id") + val imdbId: String?, + @JsonProperty("kitsu_id") + val kitsuId: Long?, + @JsonProperty("livechart_id") + val livechartId: Long?, + @JsonProperty("mal_id") + val malId: Long?, + @JsonProperty("simkl_id") + val simklId: Long?, + @JsonProperty("themoviedb_id") + val themoviedbId: Long?, + @JsonProperty("tvdb_id") + val tvdbId: Long?, + @JsonProperty("season") + val season: Season?, ) - @Serializable data class Season( - @JsonProperty("tvdb") @SerialName("tvdb") val tvdb: Long?, - @JsonProperty("tmdb") @SerialName("tmdb") val tmdb: Long?, + @JsonProperty("tvdb") + val tvdb: Long?, + @JsonProperty("tmdb") + val tmdb: Long?, ) - @Serializable data class CombinedMedia( - @JsonProperty("mapping") @SerialName("mapping") val mapping: MappingRoot?, - @JsonProperty("show") @SerialName("show") val show: Show, + @JsonProperty("mapping") + val mapping: MappingRoot?, + @JsonProperty("show") + val show: Show ) data class Database( @@ -161,4 +180,4 @@ object FillerEpisodeCheck { return counter } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt index 96193fe45..9d5c75289 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri -import android.os.Build import android.os.Build.VERSION.SDK_INT import android.util.Log import android.widget.ImageView @@ -12,7 +11,6 @@ import coil3.EventListener import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader -import coil3.decode.BitmapFactoryDecoder import coil3.disk.DiskCache import coil3.dispose import coil3.load @@ -24,86 +22,82 @@ import coil3.request.CachePolicy import coil3.request.ErrorResult import coil3.request.ImageRequest import coil3.request.allowHardware -import coil3.request.bitmapConfig import coil3.request.crossfade import coil3.util.DebugLogger import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.network.buildDefaultClient +import okhttp3.HttpUrl import okio.Path.Companion.toOkioPath import java.io.File import java.nio.ByteBuffer object ImageLoader { + private const val TAG = "CoilImgLoader" - internal fun buildImageLoader(context: PlatformContext): ImageLoader { - val isBrokenHardware = hasPotentialBrokenHardware() - return ImageLoader.Builder(context) + + internal fun buildImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context) .crossfade(200) - .allowHardware(SDK_INT >= 28 && !isBrokenHardware) + .allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder .diskCachePolicy(CachePolicy.ENABLED) .networkCachePolicy(CachePolicy.ENABLED) .memoryCache { - MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache - .strongReferencesEnabled(false) + MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching .build() } .diskCache { DiskCache.Builder() .directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath()) .maxSizeBytes(512L * 1024 * 1024) // 512 MB - .maxSizePercent(0.04) // max 4% of storage for disk caching + .maxSizePercent(0.04) // Use 4 % of the device's storage space for disk caching .build() } /** Pass interceptors with care, unnecessary passing tokens to servers or image hosting services causes unauthorized exceptions **/ - .components { - add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) - if (isBrokenHardware) { - add(BitmapFactoryDecoder.Factory()) - } // sw decoder - } - .apply { - if (isBrokenHardware) { // coil will auto choose optimal config on modern device - bitmapConfig(Bitmap.Config.ARGB_8888) - } - setupCoilLogger() + .components { add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) } + .also { + it.setupCoilLogger() + Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.") } .build() - } - /** DebugLogger on debug builds which won't slow down release builds & use EventListener for + /** Use DebugLogger on debug builds which won't slow down release builds & use EventListener for Errors on release builds. **/ internal fun ImageLoader.Builder.setupCoilLogger() { if (BuildConfig.DEBUG) { logger(DebugLogger()) + Log.d(TAG, "setupCoilLogger: Activated DEBUG_LOGGER FOR COIL") } else { eventListener(object : EventListener() { override fun onError(request: ImageRequest, result: ErrorResult) { super.onError(request, result) - Log.e(TAG, "Image load error: ${result.throwable.message ?: result.throwable}") - Log.e(TAG, " URL: ${request.data}") - Log.e(TAG, " allowHardware: ${request.allowHardware}") - Log.e(TAG, " hardware: ${Build.HARDWARE}, board: ${Build.BOARD}") + Log.e(TAG, "Error loading image: ${result.throwable}") } }) + Log.d(TAG, "setupCoilLogger: Activated EVENT_LISTENER FOR COIL") } } - /** coil's built in loader attached w/ global synchronized instance **/ + /** we use coil's built in loader with our global synchronized instance, this way we achieve + latest and complete functionality as well as stability **/ private fun ImageView.loadImageInternal( imageData: Any?, headers: Map? = null, builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations ) { - // clear image to avoid loading & flickering issue at fast scrolling (~recycler view/lazy column) + // clear image to avoid loading & flickering issue at fast scrolling (e.g, an image recycler) this.dispose() - if (imageData == null) return + + if(imageData == null) return // Just in case + // setImageResource is better than coil3 on resources due to attr - if (imageData is Int) { - this.setImageResource(imageData); return + if(imageData is Int) { + this.setImageResource(imageData) + return } - // headers can be overridden by extensions. + + // Use Coil's built-in load method but with our custom module & a decent USER-AGENT always + // which can be overridden by extensions. this.load(imageData, SingletonImageLoader.get(context)) { this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder -> headerBuilder["User-Agent"] = USER_AGENT @@ -111,22 +105,11 @@ object ImageLoader { headerBuilder[key] = value } }.build()) + builder() // if passed } } - private fun hasPotentialBrokenHardware(): Boolean { - val hardware = Build.HARDWARE?.lowercase() ?: "" - val board = Build.BOARD?.lowercase() ?: "" - val model = Build.MODEL?.lowercase() ?: "" - val manufacturer = Build.MANUFACTURER?.lowercase() ?: "" - val allwinnerPatterns = listOf("sun50iw9", "h713", "allwinner", "sunxi") - val problematicModels = - listOf("hy320", "hy300", "a10plus", "magcubic", "sinoy", "android tv box") - return allwinnerPatterns.any { it in hardware || it in board || it in manufacturer } || - problematicModels.any { it in model } - } - /** TYPE_SAFE_LOADERS **/ fun ImageView.loadImage( imageData: UiImage?, @@ -155,6 +138,12 @@ object ImageLoader { builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( + imageData: HttpUrl?, + headers: Map? = null, + builder: ImageRequest.Builder.() -> Unit = {} + ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( imageData: File?, builder: ImageRequest.Builder.() -> Unit = {} @@ -184,4 +173,4 @@ object ImageLoader { imageData: ByteBuffer?, builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, builder = builder) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index f4d6b4271..8bcd1b88e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -27,8 +27,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import okio.BufferedSink import okio.buffer import okio.sink @@ -44,43 +42,38 @@ object InAppUpdater { private const val PRERELEASE_PACKAGE_NAME = "com.lagradost.cloudstream3.prerelease" private const val LOG_TAG = "InAppUpdater" - @Serializable private data class GithubAsset( - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("size") @SerialName("size") val size: Int, // Size in bytes - @JsonProperty("browser_download_url") @SerialName("browser_download_url") val browserDownloadUrl: String, - @JsonProperty("content_type") @SerialName("content_type") val contentType: String, // application/vnd.android.package-archive + @JsonProperty("name") val name: String, + @JsonProperty("size") val size: Int, // Size in bytes + @JsonProperty("browser_download_url") val browserDownloadUrl: String, + @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive ) - @Serializable private data class GithubRelease( - @JsonProperty("tag_name") @SerialName("tag_name") val tagName: String, // Version code - @JsonProperty("body") @SerialName("body") val body: String, // Description - @JsonProperty("assets") @SerialName("assets") val assets: List, - @JsonProperty("target_commitish") @SerialName("target_commitish") val targetCommitish: String, // Branch - @JsonProperty("prerelease") @SerialName("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") @SerialName("node_id") val nodeId: String, + @JsonProperty("tag_name") val tagName: String, // Version code + @JsonProperty("body") val body: String, // Description + @JsonProperty("assets") val assets: List, + @JsonProperty("target_commitish") val targetCommitish: String, // Branch + @JsonProperty("prerelease") val prerelease: Boolean, + @JsonProperty("node_id") val nodeId: String, ) - @Serializable private data class GithubObject( - @JsonProperty("sha") @SerialName("sha") val sha: String, // SHA-256 hash - @JsonProperty("type") @SerialName("type") val type: String, - @JsonProperty("url") @SerialName("url") val url: String, + @JsonProperty("sha") val sha: String, // SHA-256 hash + @JsonProperty("type") val type: String, + @JsonProperty("url") val url: String, ) - @Serializable private data class GithubTag( - @JsonProperty("object") @SerialName("object") val githubObject: GithubObject, + @JsonProperty("object") val githubObject: GithubObject, ) - @Serializable private data class Update( - @JsonProperty("shouldUpdate") @SerialName("shouldUpdate") val shouldUpdate: Boolean, - @JsonProperty("updateURL") @SerialName("updateURL") val updateURL: String?, - @JsonProperty("updateVersion") @SerialName("updateVersion") val updateVersion: String?, - @JsonProperty("changelog") @SerialName("changelog") val changelog: String?, - @JsonProperty("updateNodeId") @SerialName("updateNodeId") val updateNodeId: String?, + @JsonProperty("shouldUpdate") val shouldUpdate: Boolean, + @JsonProperty("updateURL") val updateURL: String?, + @JsonProperty("updateVersion") val updateVersion: String?, + @JsonProperty("changelog") val changelog: String?, + @JsonProperty("updateNodeId") val updateNodeId: String?, ) private suspend fun Activity.getAppUpdate(installPrerelease: Boolean): Update { @@ -100,9 +93,9 @@ object InAppUpdater { private suspend fun Activity.getReleaseUpdate(): Update { val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(url, headers = headers).text - ).toList() + ) val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""") val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""") @@ -110,7 +103,9 @@ object InAppUpdater { !rel.prerelease }.sortedWith(compareBy { release -> release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> - versionRegex.find(it1)?.groupValues?.let { + versionRegex.find( + it1 + )?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } } @@ -155,9 +150,9 @@ object InAppUpdater { "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/git/ref/tags/pre-release" val releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(releaseUrl, headers = headers).text - ).toList() + ) val found = response.lastOrNull { rel -> rel.prerelease || rel.tagName == "pre-release" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 43e61ad88..351e77c8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -5,11 +5,10 @@ package com.lagradost.cloudstream3.utils import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder.apis +//import com.lagradost.cloudstream3.animeproviders.AniflixProvider import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import java.util.concurrent.TimeUnit object SyncUtil { @@ -25,16 +24,18 @@ object SyncUtil { private const val NINE_ANIME = "9anime" private const val TWIST_MOE = "Twistmoe" - private val matchList = mapOf( - "9anime" to NINE_ANIME, - "gogoanime" to GOGOANIME, - "gogoanimes" to GOGOANIME, - "twist.moe" to TWIST_MOE, - ) + private val matchList = + mapOf( + "9anime" to NINE_ANIME, + "gogoanime" to GOGOANIME, + "gogoanimes" to GOGOANIME, + "twist.moe" to TWIST_MOE + ) suspend fun getIdsFromUrl(url: String?): Pair? { if (url == null) return null Log.i(TAG, "getIdsFromUrl $url") + for (regex in regexs) { regex.find(url)?.let { match -> if (match.groupValues.size == 3) { @@ -55,120 +56,117 @@ object SyncUtil { } } } - return null } - /** - * first. Mal, second. Anilist, - * Valid sites are: Gogoanime, Twistmoe and 9anime - */ + /** first. Mal, second. Anilist, + * valid sites are: Gogoanime, Twistmoe and 9anime*/ private suspend fun getIdsFromSlug( slug: String, - site: String = "Gogoanime", + site: String = "Gogoanime" ): Pair? { Log.i(TAG, "getIdsFromSlug $slug $site") try { - // Gogoanime, Twistmoe and 9anime - val url = "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" + //Gogoanime, Twistmoe and 9anime + val url = + "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text - val mapped = tryParseJson(response) + val mapped = parseJson(response) val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id + if (overrideMal != null) { return overrideMal.toString() to overrideAnilist?.toString() } - return null } catch (e: Exception) { logError(e) } - return null } suspend fun getUrlsFromId(id: String, type: String = "anilist"): List { - val url = "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" + val url = + "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed() val pages = response.pages ?: return emptyList() - val current = pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values) - .mapNotNull { it.url }.toMutableList() + val current = + pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values) + .mapNotNull { it.url }.toMutableList() if (type == "anilist") { // TODO MAKE BETTER - apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { - current.add("${it.mainUrl}/anime/$id") + synchronized(apis) { + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } } } - return current } - @Serializable data class SyncPage( - @JsonProperty("Pages") @SerialName("Pages") val pages: SyncPages?, + @JsonProperty("Pages") val pages: SyncPages?, ) - @Serializable data class SyncPages( - @JsonProperty("9anime") @SerialName("9anime") val nineanime: Map = emptyMap(), - @JsonProperty("Gogoanime") @SerialName("Gogoanime") val gogoanime: Map = emptyMap(), - @JsonProperty("Twistmoe") @SerialName("Twistmoe") val twistmoe: Map = emptyMap(), + @JsonProperty("9anime") val nineanime: Map = emptyMap(), + @JsonProperty("Gogoanime") val gogoanime: Map = emptyMap(), + @JsonProperty("Twistmoe") val twistmoe: Map = emptyMap(), ) - @Serializable data class ProviderPage( - @JsonProperty("url") @SerialName("url") val url: String?, + @JsonProperty("url") val url: String?, ) - @Serializable data class MalSyncPage( - @JsonProperty("identifier") @SerialName("identifier") val identifier: String?, - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("page") @SerialName("page") val page: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("image") @SerialName("image") val image: String?, - @JsonProperty("hentai") @SerialName("hentai") val hentai: Boolean?, - @JsonProperty("sticky") @SerialName("sticky") val sticky: Boolean?, - @JsonProperty("active") @SerialName("active") val active: Boolean?, - @JsonProperty("actor") @SerialName("actor") val actor: String?, - @JsonProperty("malId") @SerialName("malId") val malId: Int?, - @JsonProperty("aniId") @SerialName("aniId") val aniId: Int?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("deletedAt") @SerialName("deletedAt") val deletedAt: String?, - @JsonProperty("Mal") @SerialName("Mal") val mal: Mal?, - @JsonProperty("Anilist") @SerialName("Anilist") val anilist: Anilist?, - @JsonProperty("malUrl") @SerialName("malUrl") val malUrl: String?, + @JsonProperty("identifier") val identifier: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("page") val page: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("url") val url: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("hentai") val hentai: Boolean?, + @JsonProperty("sticky") val sticky: Boolean?, + @JsonProperty("active") val active: Boolean?, + @JsonProperty("actor") val actor: String?, + @JsonProperty("malId") val malId: Int?, + @JsonProperty("aniId") val aniId: Int?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("deletedAt") val deletedAt: String?, + @JsonProperty("Mal") val mal: Mal?, + @JsonProperty("Anilist") val anilist: Anilist?, + @JsonProperty("malUrl") val malUrl: String? ) - @Serializable data class Anilist( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("malId") @SerialName("malId") val malId: Int?, - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("image") @SerialName("image") val image: String?, - @JsonProperty("category") @SerialName("category") val category: String?, - @JsonProperty("hentai") @SerialName("hentai") val hentai: Boolean?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("deletedAt") @SerialName("deletedAt") val deletedAt: String?, +// @JsonProperty("altTitle") val altTitle: List?, +// @JsonProperty("externalLinks") val externalLinks: List?, + @JsonProperty("id") val id: Int?, + @JsonProperty("malId") val malId: Int?, + @JsonProperty("type") val type: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("url") val url: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("category") val category: String?, + @JsonProperty("hentai") val hentai: Boolean?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("deletedAt") val deletedAt: String? ) - @Serializable data class Mal( - @JsonProperty("id") @SerialName("id") val id: Int?, - @JsonProperty("type") @SerialName("type") val type: String?, - @JsonProperty("title") @SerialName("title") val title: String?, - @JsonProperty("url") @SerialName("url") val url: String?, - @JsonProperty("image") @SerialName("image") val image: String?, - @JsonProperty("category") @SerialName("category") val category: String?, - @JsonProperty("hentai") @SerialName("hentai") val hentai: Boolean?, - @JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?, - @JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?, - @JsonProperty("deletedAt") @SerialName("deletedAt") val deletedAt: String?, +// @JsonProperty("altTitle") val altTitle: List?, + @JsonProperty("id") val id: Int?, + @JsonProperty("type") val type: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("url") val url: String?, + @JsonProperty("image") val image: String?, + @JsonProperty("category") val category: String?, + @JsonProperty("hentai") val hentai: Boolean?, + @JsonProperty("createdAt") val createdAt: String?, + @JsonProperty("updatedAt") val updatedAt: String?, + @JsonProperty("deletedAt") val deletedAt: String? ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 5b784e05e..91c8a2fc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* +import org.junit.Assert import kotlin.random.Random object TestingUtils { - open class TestResult(val success: Boolean) { companion object { val Pass = TestResult(true) @@ -48,10 +48,6 @@ object TestingUtils { messageLog.add(Message(LogLevel.Error, message)) } } - - private fun fail(message: String): Nothing = throw AssertionError(message) - private fun assertTrue(message: String, condition: Boolean) { if (!condition) fail(message) } - private fun assertNotNull(message: String, value: Any?) { if (value == null) fail(message) } class TestResultList(val results: List) : TestResult(true) class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) @@ -91,7 +87,7 @@ object TestingUtils { } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider marked as hasMainPage, while in reality is has not been implemented") + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") } is CancellationException -> { @@ -119,7 +115,7 @@ object TestingUtils { api.search(query, 1)?.items?.takeIf { it.isNotEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented search()") + Assert.fail("Provider has not implemented search()") } else if (e is CancellationException) { throw e } @@ -129,7 +125,7 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - fail("Api ${api.name} did not return any search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { TestResultList(searchResults) @@ -200,7 +196,7 @@ object TestingUtils { } else -> { - logger.error("Unknown load response: ${loadResponse::class.qualifiedName}") + logger.error("Unknown load response: ${loadResponse.javaClass.name}") return TestResult.Fail } } ?: return TestResult.Fail @@ -220,7 +216,7 @@ object TestingUtils { // return TestResult(validResults) } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented load()") + Assert.fail("Provider has not implemented load()") } throw e } @@ -232,14 +228,14 @@ object TestingUtils { url: String?, logger: Logger ): TestResult { - assertNotNull("Api ${api.name} has invalid url on episode", url) + Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> logger.log("Video loaded: ${link.name}") - assertTrue( + Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 ) @@ -249,12 +245,12 @@ object TestingUtils { logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { - fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") + Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") } } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider has not implemented loadLinks()") + Assert.fail("Provider has not implemented loadLinks()") } else -> { @@ -280,7 +276,7 @@ object TestingUtils { // Test Homepage val homepage = testHomepage(api, logger) - assertTrue("Homepage failed to load", homepage.success) + Assert.assertTrue("Homepage failed to load", homepage.success) val homePageList = (homepage as? TestResultList)?.results ?: emptyList() // Test Search Results @@ -291,7 +287,7 @@ object TestingUtils { listOf("over", "iron", "guy")).take(3) val searchResults = testSearch(api, searchQueries, logger) - assertTrue("Failed to get search results", searchResults.success) + Assert.assertTrue("Failed to get search results", searchResults.success) searchResults as TestResultList // Test Load and LoadLinks @@ -325,4 +321,4 @@ object TestingUtils { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt index b5b367262..feecbe312 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TvChannelUtils.kt @@ -23,15 +23,15 @@ const val PROGRAM_ID_LIST_KEY = "persistent_program_ids" object TvChannelUtils { fun Context.saveProgramId(programId: Long) { - val existing: List = getKey>(PROGRAM_ID_LIST_KEY) ?: emptyList() + val existing: List = getKey(PROGRAM_ID_LIST_KEY) ?: emptyList() val updated = (existing + programId).distinct() setKey(PROGRAM_ID_LIST_KEY, updated) } fun Context.getStoredProgramIds(): List { - return getKey>(PROGRAM_ID_LIST_KEY) ?: emptyList() + return getKey(PROGRAM_ID_LIST_KEY) ?: emptyList() } fun Context.removeProgramId(programId: Long) { - val existing: List = getKey>(PROGRAM_ID_LIST_KEY) ?: emptyList() + val existing: List = getKey(PROGRAM_ID_LIST_KEY) ?: emptyList() val updated = existing.filter { it != programId } setKey(PROGRAM_ID_LIST_KEY, updated) } @@ -149,12 +149,10 @@ object TvChannelUtils { .setInputId(inputId) .build() - val channelUri = runCatching { - context.contentResolver.insert( - TvContractCompat.Channels.CONTENT_URI, - channel.toContentValues() - ) - }.getOrNull() + val channelUri = context.contentResolver.insert( + TvContractCompat.Channels.CONTENT_URI, + channel.toContentValues() + ) channelUri?.let { val channelId = ContentUris.parseId(it) @@ -163,4 +161,4 @@ object TvChannelUtils { } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index a848bee26..c12674816 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -65,12 +65,9 @@ import androidx.navigation.fragment.NavHostFragment import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup -import com.google.android.material.progressindicator.CircularProgressIndicatorSpec -import com.google.android.material.progressindicator.IndeterminateDrawable import com.lagradost.cloudstream3.CloudStreamApp.Companion.context import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast @@ -586,43 +583,6 @@ object UIHelper { } } - /** - * Source: https://stackoverflow.com/questions/70954321/circular-progress-indicator-inside-buttons-android-material-design - * - * Shows indeterminate progress bar on this button in place of where icon would be. - * By default the tint of progress bar is the same as iconTint. - * - * @param tintColor (@ColorInt Int) Sets custom progress bar tint color. - */ - fun MaterialButton.showProgress(@ColorInt tintColor: Int = this.iconTint.defaultColor) = - // Use runOnMainThreadNative to allow process on io threads, to make the code a bit cleaner - runOnMainThreadNative { - // No need to set it again, as then it will reset the animation - if(this.icon is IndeterminateDrawable<*>) { - return@runOnMainThreadNative - } - val spec = CircularProgressIndicatorSpec( - context, null, 0, - com.google.android.material.R.style.Widget_Material3_CircularProgressIndicator_ExtraSmall - ) - - spec.indicatorColors = intArrayOf(tintColor) - - val progressIndicatorDrawable = - IndeterminateDrawable.createCircularDrawable(context, spec) - - this.icon = progressIndicatorDrawable - if (this.getTag(R.id.text1) == null) - this.setTag(R.id.text1, this.text) - this.text = "" - } - - fun MaterialButton.hideProgress() = - runOnMainThreadNative { - this.text = this.getTag(R.id.text1) as? String - this.icon = null - } - /**id, stringRes */ @SuppressLint("RestrictedApi") fun View.popupMenuNoIcons( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt index adefedd20..12fcc0c33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt @@ -804,7 +804,6 @@ object VideoDownloadManager { private suspend fun resolve( startByte: Long, endByte: Long?, - buffer: ByteArray, callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) ): Long = withContext(Dispatchers.IO) { var currentByte: Long = startByte @@ -823,6 +822,7 @@ object VideoDownloadManager { ) val requestStream = request.body.byteStream() + val buffer = ByteArray(bufferSize) var read: Int try { @@ -853,7 +853,6 @@ object VideoDownloadManager { suspend fun resolveSafe( index: Int, retries: Int = 3, - buffer: ByteArray, callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) ): Boolean { var start = chuckStartByte.getOrNull(index) ?: return false @@ -862,7 +861,7 @@ object VideoDownloadManager { for (i in 0 until retries) { try { // in case - start = resolve(start, end, buffer, callback) + start = resolve(start, end, callback) // no end defined, so we don't care exactly where it ended if (end == null) return true // we have download more or exactly what we needed @@ -1159,29 +1158,13 @@ object VideoDownloadManager { } } - // Reuse a download buffer to decrease unnecessary alloc - val buffer = ByteArray(items.bufferSize) - - // This will take up the first available job and resolve + // this will take up the first available job and resolve while (true) { if (!isActive) return@launch - - var isTooFarAhead = false fileMutex.withLock { if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed ) return@launch - - // Limit RAM usage by throttling if too much data is downloaded but not yet written to disk - // 50MB limit - if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) { - isTooFarAhead = true - } - } - - if (isTooFarAhead) { - delay(500) - continue } // mutex just in case, we never want this to fail due to multithreading @@ -1192,7 +1175,7 @@ object VideoDownloadManager { // in case something has gone wrong set to failed if the fail is not caused by // user cancellation - if (!items.resolveSafe(index, buffer = buffer, callback = callback)) { + if (!items.resolveSafe(index, callback = callback)) { fileMutex.withLock { if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed @@ -1350,23 +1333,10 @@ object VideoDownloadManager { launch(Dispatchers.IO) { while (true) { if (!isActive) return@launch - - var isTooFarAhead = false fileMutex.withLock { if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed ) return@launch - - // Limit RAM usage by throttling if too much data is downloaded but not yet written to disk - // 50MB limit - if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) { - isTooFarAhead = true - } - } - - if (isTooFarAhead) { - delay(500) - continue } // mutex just in case, we never want this to fail due to multithreading @@ -1640,11 +1610,11 @@ object VideoDownloadManager { } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { - return context.getKey(KEY_RESUME_PACKAGES, id.toString()) + return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } fun getDownloadQueuePackage(context: Context, id: Int): DownloadQueueWrapper? { - return context.getKey(KEY_RESUME_IN_QUEUE, id.toString()) + return context.getKey(KEY_RESUME_IN_QUEUE, id.toString()) } fun getDownloadEpisodeMetadata( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt index 946e7ecf9..25a9fdf2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt @@ -1,32 +1,23 @@ package com.lagradost.cloudstream3.utils.downloader import android.net.Uri -import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.Score -import com.lagradost.cloudstream3.SkipSerializationTest import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.services.DownloadQueueService import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.serializers.UriSerializer -import com.lagradost.cloudstream3.utils.serializers.WriteOnlySerializer import com.lagradost.safefile.SafeFile -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.io.IOException import java.io.OutputStream import java.util.Objects object DownloadObjects { /** An item can either be something to resume or something new to start */ - @Serializable data class DownloadQueueWrapper( - @JsonProperty("resumePackage") @SerialName("resumePackage") val resumePackage: DownloadResumePackage?, - @JsonProperty("downloadItem") @SerialName("downloadItem") val downloadItem: DownloadQueueItem?, + @JsonProperty("resumePackage") val resumePackage: DownloadResumePackage?, + @JsonProperty("downloadItem") val downloadItem: DownloadQueueItem?, ) { init { assert(resumePackage != null || downloadItem != null) { @@ -35,66 +26,56 @@ object DownloadObjects { } /** Loop through the current download instances to see if it is currently downloading. Also includes link loading. */ - @JsonIgnore fun isCurrentlyDownloading(): Boolean { return DownloadQueueService.downloadInstances.value.any { it.downloadQueueWrapper.id == this.id } } - @JsonProperty("id") @SerialName("id") + @JsonProperty("id") val id = resumePackage?.item?.ep?.id ?: downloadItem!!.episode.id - @JsonProperty("parentId") @SerialName("parentId") + @JsonProperty("parentId") val parentId = resumePackage?.item?.ep?.parentId ?: downloadItem!!.episode.parentId } /** General data about the episode and show to start a download from. */ - @Serializable data class DownloadQueueItem( - @JsonProperty("episode") @SerialName("episode") val episode: ResultEpisode, - @JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean, - @JsonProperty("resultName") @SerialName("resultName") val resultName: String, - @JsonProperty("resultType") @SerialName("resultType") val resultType: TvType, - @JsonProperty("resultPoster") @SerialName("resultPoster") val resultPoster: String?, - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, - @JsonProperty("resultId") @SerialName("resultId") val resultId: Int, - @JsonProperty("resultUrl") @SerialName("resultUrl") val resultUrl: String, - @JsonProperty("links") @SerialName("links") val links: List? = null, - @JsonProperty("subs") @SerialName("subs") val subs: List? = null, + @JsonProperty("episode") val episode: ResultEpisode, + @JsonProperty("isMovie") val isMovie: Boolean, + @JsonProperty("resultName") val resultName: String, + @JsonProperty("resultType") val resultType: TvType, + @JsonProperty("resultPoster") val resultPoster: String?, + @JsonProperty("apiName") val apiName: String, + @JsonProperty("resultId") val resultId: Int, + @JsonProperty("resultUrl") val resultUrl: String, + @JsonProperty("links") val links: List? = null, + @JsonProperty("subs") val subs: List? = null, ) { fun toWrapper(): DownloadQueueWrapper { return DownloadQueueWrapper(null, this) } } - interface DownloadCached { - val id: Int - } - @OptIn(ExperimentalSerializationApi::class) // KeepGeneratedSerializer is an experimental annotation for now - @KeepGeneratedSerializer - @Serializable(with = DownloadEpisodeCached.Serializer::class) + abstract class DownloadCached( + @JsonProperty("id") open val id: Int, + ) + data class DownloadEpisodeCached( - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("episode") @SerialName("episode") val episode: Int, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, - @JsonProperty("score") @SerialName("score") var score: Score? = null, - @JsonProperty("description") @SerialName("description") val description: String?, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long, - @JsonProperty("id") @SerialName("id") override val id: Int, - ) : DownloadCached { - object Serializer : WriteOnlySerializer( - DownloadEpisodeCached.generatedSerializer(), - setOf("rating"), - ) - + @JsonProperty("name") val name: String?, + @JsonProperty("poster") val poster: String?, + @JsonProperty("episode") val episode: Int, + @JsonProperty("season") val season: Int?, + @JsonProperty("parentId") val parentId: Int, + @JsonProperty("score") var score: Score? = null, + @JsonProperty("description") val description: String?, + @JsonProperty("cacheTime") val cacheTime: Long, + override val id: Int, + ) : DownloadCached(id) { @JsonProperty("rating", access = JsonProperty.Access.WRITE_ONLY) - @SerialName("rating") @Deprecated( "`rating` is the old scoring system, use score instead", replaceWith = ReplaceWith("score"), - level = DeprecationLevel.ERROR, + level = DeprecationLevel.ERROR ) var rating: Int? = null set(value) { @@ -106,81 +87,74 @@ object DownloadObjects { } /** What to display to the user for a downloaded show/movie. Includes info such as name, poster and url */ - @Serializable data class DownloadHeaderCached( - @JsonProperty("apiName") @SerialName("apiName") val apiName: String, - @JsonProperty("url") @SerialName("url") val url: String, - @JsonProperty("type") @SerialName("type") val type: TvType, - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("cacheTime") @SerialName("cacheTime") val cacheTime: Long, - @JsonProperty("id") @SerialName("id") override val id: Int, - ) : DownloadCached + @JsonProperty("apiName") val apiName: String, + @JsonProperty("url") val url: String, + @JsonProperty("type") val type: TvType, + @JsonProperty("name") val name: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("cacheTime") val cacheTime: Long, + override val id: Int, + ) : DownloadCached(id) - @Serializable data class DownloadResumePackage( - @JsonProperty("item") @SerialName("item") val item: DownloadItem, + @JsonProperty("item") val item: DownloadItem, /** Tills which link should get resumed */ - @JsonProperty("linkIndex") @SerialName("linkIndex") val linkIndex: Int?, + @JsonProperty("linkIndex") val linkIndex: Int?, ) { fun toWrapper(): DownloadQueueWrapper { return DownloadQueueWrapper(this, null) } } - @Serializable data class DownloadItem( - @JsonProperty("source") @SerialName("source") val source: String?, - @JsonProperty("folder") @SerialName("folder") val folder: String?, - @JsonProperty("ep") @SerialName("ep") val ep: DownloadEpisodeMetadata, - @JsonProperty("links") @SerialName("links") val links: List, + @JsonProperty("source") val source: String?, + @JsonProperty("folder") val folder: String?, + @JsonProperty("ep") val ep: DownloadEpisodeMetadata, + @JsonProperty("links") val links: List, ) /** Metadata for a specific episode and how to display it. */ - @Serializable data class DownloadEpisodeMetadata( - @JsonProperty("id") @SerialName("id") val id: Int, - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, - @JsonProperty("mainName") @SerialName("mainName") val mainName: String, - @JsonProperty("sourceApiName") @SerialName("sourceApiName") val sourceApiName: String?, - @JsonProperty("poster") @SerialName("poster") val poster: String?, - @JsonProperty("name") @SerialName("name") val name: String?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("type") @SerialName("type") val type: TvType?, + @JsonProperty("id") val id: Int, + @JsonProperty("parentId") val parentId: Int, + @JsonProperty("mainName") val mainName: String, + @JsonProperty("sourceApiName") val sourceApiName: String?, + @JsonProperty("poster") val poster: String?, + @JsonProperty("name") val name: String?, + @JsonProperty("season") val season: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("type") val type: TvType?, ) - @Serializable + data class DownloadedFileInfo( - @JsonProperty("totalBytes") @SerialName("totalBytes") val totalBytes: Long, - @JsonProperty("relativePath") @SerialName("relativePath") val relativePath: String, - @JsonProperty("displayName") @SerialName("displayName") val displayName: String, - @JsonProperty("extraInfo") @SerialName("extraInfo") val extraInfo: String? = null, - @JsonProperty("basePath") @SerialName("basePath") val basePath: String? = null, // null is for legacy downloads. See getBasePath() + @JsonProperty("totalBytes") val totalBytes: Long, + @JsonProperty("relativePath") val relativePath: String, + @JsonProperty("displayName") val displayName: String, + @JsonProperty("extraInfo") val extraInfo: String? = null, + @JsonProperty("basePath") val basePath: String? = null, // null is for legacy downloads. See getBasePath() // Hash of the link associated with this DownloadFile, used so not override old data in the DownloadedFileInfo - @JsonProperty("linkHash") @SerialName("linkHash") val linkHash: Int? = null, + @JsonProperty("linkHash") val linkHash : Int? = null ) - @Serializable - @SkipSerializationTest // Uri has issues with Jackson data class DownloadedFileInfoResult( - @JsonProperty("fileLength") @SerialName("fileLength") val fileLength: Long, - @JsonProperty("totalBytes") @SerialName("totalBytes") val totalBytes: Long, - @JsonProperty("path") @SerialName("path") - @Serializable(with = UriSerializer::class) - val path: Uri, + @JsonProperty("fileLength") val fileLength: Long, + @JsonProperty("totalBytes") val totalBytes: Long, + @JsonProperty("path") val path: Uri, ) - @Serializable + data class ResumeWatching( - @JsonProperty("parentId") @SerialName("parentId") val parentId: Int, - @JsonProperty("episodeId") @SerialName("episodeId") val episodeId: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("updateTime") @SerialName("updateTime") val updateTime: Long, - @JsonProperty("isFromDownload") @SerialName("isFromDownload") val isFromDownload: Boolean, + @JsonProperty("parentId") val parentId: Int, + @JsonProperty("episodeId") val episodeId: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("season") val season: Int?, + @JsonProperty("updateTime") val updateTime: Long, + @JsonProperty("isFromDownload") val isFromDownload: Boolean, ) + data class DownloadStatus( /** if you should retry with the same args and hope for a better result */ val retrySame: Boolean, @@ -190,19 +164,20 @@ object DownloadObjects { val success: Boolean, ) + data class CreateNotificationMetadata( val type: VideoDownloadManager.DownloadType, val bytesDownloaded: Long, val bytesTotal: Long, val hlsProgress: Long? = null, val hlsTotal: Long? = null, - val bytesPerSecond: Long, + val bytesPerSecond: Long ) data class StreamData( private val fileLength: Long, val file: SafeFile, - // val fileStream: OutputStream, + //val fileStream: OutputStream, ) { @Throws(IOException::class) fun open(): OutputStream { @@ -223,11 +198,9 @@ object DownloadObjects { val exists: Boolean get() = file.exists() == true } - /** - * Bytes have the size end-start where the byte range is [start,end) - * note that ByteArray is a pointer and therefore can't be stored - * without cloning it. - */ + + /** bytes have the size end-start where the byte range is [start,end) + * note that ByteArray is a pointer and therefore cant be stored without cloning it */ data class LazyStreamDownloadResponse( val bytes: ByteArray, val startByte: Long, @@ -248,4 +221,4 @@ object DownloadObjects { return Objects.hash(startByte, endByte) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt deleted file mode 100644 index adceede42..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import android.net.Uri -import com.lagradost.cloudstream3.InternalAPI -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, - * ) - */ -@InternalAPI -object UriSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Uri) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Uri { - return Uri.parse(decoder.decodeString()) - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt index 4bb78a1b0..0db90afea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt @@ -1,14 +1,12 @@ package com.lagradost.cloudstream3.utils.videoskip -import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.lagradost.cloudstream3.AnimeLoadResponse import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ui.result.ResultEpisode -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable // taken from https://github.com/saikou-app/saikou/blob/3803f8a7a59b826ca193664d46af3a22bbc989f7/app/src/main/java/ani/saikou/others/AniSkip.kt // the following is GPLv3 code https://github.com/saikou-app/saikou/blob/main/LICENSE.md @@ -49,25 +47,22 @@ class AniSkip : SkipAPI() { } } - @Serializable data class AniSkipResponse( - @JsonProperty("found") @SerialName("found") val found: Boolean, - @JsonProperty("results") @SerialName("results") val results: List?, - @JsonProperty("message") @SerialName("message") val message: String?, - @JsonProperty("statusCode") @SerialName("statusCode") val statusCode: Int, + @JsonSerialize val found: Boolean, + @JsonSerialize val results: List?, + @JsonSerialize val message: String?, + @JsonSerialize val statusCode: Int ) - @Serializable data class Stamp( - @JsonProperty("interval") @SerialName("interval") val interval: AniSkipInterval, - @JsonProperty("skipType") @SerialName("skipType") val skipType: String, - @JsonProperty("skipId") @SerialName("skipId") val skipId: String, - @JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Double, + @JsonSerialize val interval: AniSkipInterval, + @JsonSerialize val skipType: String, + @JsonSerialize val skipId: String, + @JsonSerialize val episodeLength: Double ) - @Serializable data class AniSkipInterval( - @JsonProperty("startTime") @SerialName("startTime") val startTime: Double, - @JsonProperty("endTime") @SerialName("endTime") val endTime: Double, + @JsonSerialize val startTime: Double, + @JsonSerialize val endTime: Double ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt index df9d56217..f9254576b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AnimeSkip.kt @@ -17,8 +17,6 @@ import com.lagradost.cloudstream3.syncproviders.PlainAuthRepo import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.math.BigInteger import java.util.concurrent.ConcurrentHashMap import java.security.MessageDigest @@ -36,51 +34,58 @@ class AnimeSkipAuth : AuthAPI() { return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') } - @Serializable data class LoginRoot( - @JsonProperty("data") @SerialName("data") val data: LoginData, + @JsonProperty("data") + val data: LoginData, ) - @Serializable data class LoginData( - @JsonProperty("login") @SerialName("login") val login: Login, + @JsonProperty("login") + val login: Login, ) - @Serializable data class Login( - @JsonProperty("authToken") @SerialName("authToken") val authToken: String, - @JsonProperty("refreshToken") @SerialName("refreshToken") val refreshToken: String, - @JsonProperty("account") @SerialName("account") val account: Account, + @JsonProperty("authToken") + val authToken: String, + @JsonProperty("refreshToken") + val refreshToken: String, + @JsonProperty("account") + val account: Account, ) - @Serializable data class ApiRoot( - @JsonProperty("data") @SerialName("data") val data: ApiData, + @JsonProperty("data") + val data: ApiData, ) - @Serializable data class ApiData( - @JsonProperty("myApiClients") @SerialName("myApiClients") val myApiClients: List, + @JsonProperty("myApiClients") + val myApiClients: List, ) - @Serializable data class MyApiClient( - @JsonProperty("id") @SerialName("id") val id: String, + @JsonProperty("id") + val id: String, ) - @Serializable data class Account( - @JsonProperty("profileUrl") @SerialName("profileUrl") val profileUrl: String, - @JsonProperty("username") @SerialName("username") val username: String, - @JsonProperty("email") @SerialName("email") val email: String, + @JsonProperty("profileUrl") + val profileUrl: String, + @JsonProperty("username") + val username: String, + @JsonProperty("email") + val email: String, ) - @Serializable data class Payload( - @JsonProperty("profileUrl") @SerialName("profileUrl") val profileUrl: String, - @JsonProperty("username") @SerialName("username") val username: String, - @JsonProperty("email") @SerialName("email") val email: String, - @JsonProperty("clientId") @SerialName("clientId") val clientId: String, + @JsonProperty("profileUrl") + val profileUrl: String, + @JsonProperty("username") + val username: String, + @JsonProperty("email") + val email: String, + @JsonProperty("clientId") + val clientId: String, ) override suspend fun user(token: AuthToken?): AuthUser? { @@ -182,43 +187,52 @@ class AnimeSkip : SkipAPI() { name?.replace(asciiRegex, "")?.lowercase() } - @Serializable data class Root( - @JsonProperty("data") @SerialName("data") val data: Data, + @JsonProperty("data") + val data: Data, ) - @Serializable data class Data( - @JsonProperty("searchShows") @SerialName("searchShows") val searchShows: List, + @JsonProperty("searchShows") + val searchShows: List, ) - @Serializable data class SearchShow( - @JsonProperty("name") @SerialName("name") val name: String, - @JsonProperty("originalName") @SerialName("originalName") val originalName: String?, - @JsonProperty("seasonCount") @SerialName("seasonCount") val seasonCount: Long, - @JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Long, - @JsonProperty("baseDuration") @SerialName("baseDuration") val baseDuration: Double, - @JsonProperty("episodes") @SerialName("episodes") val episodes: List, + @JsonProperty("name") + val name: String, + @JsonProperty("originalName") + val originalName: String?, + @JsonProperty("seasonCount") + val seasonCount: Long, + @JsonProperty("episodeCount") + val episodeCount: Long, + @JsonProperty("baseDuration") + val baseDuration: Double, + @JsonProperty("episodes") + val episodes: List, ) - @Serializable data class Episode( - @JsonProperty("number") @SerialName("number") val number: String?, - @JsonProperty("absoluteNumber") @SerialName("absoluteNumber") val absoluteNumber: String?, - @JsonProperty("season") @SerialName("season") val season: String?, - @JsonProperty("timestamps") @SerialName("timestamps") val timestamps: List, + @JsonProperty("number") + val number: String?, + @JsonProperty("absoluteNumber") + val absoluteNumber: String?, + @JsonProperty("season") + val season: String?, + @JsonProperty("timestamps") + val timestamps: List, ) - @Serializable data class Timestamp( - @JsonProperty("at") @SerialName("at") val at: Double, - @JsonProperty("type") @SerialName("type") val type: Type, + @JsonProperty("at") + val at: Double, + @JsonProperty("type") + val type: Type, ) - @Serializable data class Type( - @JsonProperty("name") @SerialName("name") val name: String, + @JsonProperty("name") + val name: String, ) val cache: ConcurrentHashMap = ConcurrentHashMap() @@ -353,3 +367,4 @@ class AnimeSkip : SkipAPI() { return result } } + diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt index 75e22a15f..869515f43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt @@ -6,8 +6,6 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.ui.result.ResultEpisode -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable class IntroDbSkip : SkipAPI() { override val name = "IntroDb" @@ -57,24 +55,23 @@ class IntroDbSkip : SkipAPI() { ) } - @Serializable + data class IntroDbResponse( - @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String?, - @JsonProperty("season") @SerialName("season") val season: Int?, - @JsonProperty("episode") @SerialName("episode") val episode: Int?, - @JsonProperty("intro") @SerialName("intro") val intro: Segment?, - @JsonProperty("recap") @SerialName("recap") val recap: Segment?, - @JsonProperty("outro") @SerialName("outro") val outro: Segment?, + @JsonProperty("imdb_id") val imdbId: String?, + val season: Int?, + val episode: Int?, + val intro: Segment?, + val recap: Segment?, + val outro: Segment?, ) - @Serializable data class Segment( - @JsonProperty("start_sec") @SerialName("start_sec") val startSec: Double?, - @JsonProperty("end_sec") @SerialName("end_sec") val endSec: Double?, - @JsonProperty("start_ms") @SerialName("start_ms") val startMs: Long?, - @JsonProperty("end_ms") @SerialName("end_ms") val endMs: Long?, - @JsonProperty("confidence") @SerialName("confidence") val confidence: Double?, - @JsonProperty("submission_count") @SerialName("submission_count") val submissionCount: Int?, - @JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String?, + @JsonProperty("start_sec") val startSec: Double?, + @JsonProperty("end_sec") val endSec: Double?, + @JsonProperty("start_ms") val startMs: Long?, + @JsonProperty("end_ms") val endMs: Long?, + val confidence: Double?, + @JsonProperty("submission_count") val submissionCount: Int?, + @JsonProperty("updated_at") val updatedAt: String?, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt index 3fd050f1a..cc2661cb0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt @@ -8,8 +8,6 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.app -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable /** https://theintrodb.org/docs */ class TheIntroDBSkip : SkipAPI() { @@ -54,19 +52,25 @@ class TheIntroDBSkip : SkipAPI() { }.flatten() } - @Serializable data class Root( - @JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long, - @JsonProperty("type") @SerialName("type") val type: String, - @JsonProperty("intro") @SerialName("intro") val intro: List = emptyList(), - @JsonProperty("recap") @SerialName("recap") val recap: List = emptyList(), - @JsonProperty("credits") @SerialName("credits") val credits: List = emptyList(), - @JsonProperty("preview") @SerialName("preview") val preview: List = emptyList(), + @JsonProperty("tmdb_id") + val tmdbId: Long, + @JsonProperty("type") + val type: String, + @JsonProperty("intro") + val intro: List = emptyList(), + @JsonProperty("recap") + val recap: List = emptyList(), + @JsonProperty("credits") + val credits: List = emptyList(), + @JsonProperty("preview") + val preview: List = emptyList(), ) - @Serializable data class Stamp( - @JsonProperty("start_ms") @SerialName("start_ms") val startMs: Long?, - @JsonProperty("end_ms") @SerialName("end_ms") val endMs: Long?, + @JsonProperty("start_ms") + val startMs: Long?, + @JsonProperty("end_ms") + val endMs: Long?, ) -} +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_clock_24.xml b/app/src/main/res/drawable/ic_baseline_clock_24.xml deleted file mode 100644 index 3436c6208..000000000 --- a/app/src/main/res/drawable/ic_baseline_clock_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout-port/player_select_source_priority.xml b/app/src/main/res/layout-port/player_select_source_priority.xml index 926227c58..2cba9c869 100644 --- a/app/src/main/res/layout-port/player_select_source_priority.xml +++ b/app/src/main/res/layout-port/player_select_source_priority.xml @@ -1,5 +1,6 @@ - - + @@ -32,21 +33,14 @@ android:textSize="20sp" android:textStyle="bold" /> - - + android:src="@drawable/baseline_help_outline_24" + android:contentDescription="@string/help" /> - + + tools:text="@string/profile_number" + tools:ignore="LabelFor" /> + android:text="@string/sort_save" + style="@style/WhiteButton" /> + android:text="@string/sort_close" + style="@style/BlackButton" /> diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index 610b4e3e4..4f7bdf74d 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -47,7 +47,6 @@ + app:tint="?attr/textColor"> @@ -306,7 +306,7 @@ android:paddingBottom="100dp" android:clipToPadding="false" android:descendantFocusability="afterDescendants" - android:nextFocusUp="@id/download_stream_button_tv" + android:nextFocusUp="@id/download_appbar" android:nextFocusLeft="@id/navigation_downloads" android:nextFocusDown="@id/download_queue_button" android:tag="@string/tv_no_focus_tag" diff --git a/app/src/main/res/layout/fragment_extensions.xml b/app/src/main/res/layout/fragment_extensions.xml index 6dd2d8662..b7cf4b6cd 100644 --- a/app/src/main/res/layout/fragment_extensions.xml +++ b/app/src/main/res/layout/fragment_extensions.xml @@ -4,39 +4,20 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/extensions_root" + android:background="?attr/primaryGrayBackground" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/primaryGrayBackground" android:orientation="vertical"> - - - - - - - + - - @@ -97,14 +64,14 @@ android:layout_height="80dp" android:layout_gravity="bottom" android:background="?attr/primaryGrayBackground" - android:baselineAligned="false" android:focusable="true" android:foreground="@drawable/outline_drawable" android:nextFocusRight="@id/add_repo_button_imageview" android:nextFocusUp="@id/repo_recycler_view" android:orientation="horizontal" android:padding="10dp" - android:visibility="visible"> + android:visibility="visible" + android:baselineAligned="false"> diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 8d78c01b3..99a764dee 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -227,12 +227,6 @@ tools:listitem="@layout/homepage_parent" tools:visibility="gone" /> - - - - - - - @@ -1085,8 +1075,6 @@ diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index c6445d80f..3a3076943 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -12,7 +12,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="680dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -352,22 +351,6 @@ android:layout_marginEnd="32dp" android:orientation="vertical"> - - diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 0d825de77..182cd1861 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -160,13 +160,6 @@ - - - - - diff --git a/app/src/main/res/layout/sort_bottom_single_provider_choice.xml b/app/src/main/res/layout/sort_bottom_single_provider_choice.xml index ea778c488..657e2f2b8 100644 --- a/app/src/main/res/layout/sort_bottom_single_provider_choice.xml +++ b/app/src/main/res/layout/sort_bottom_single_provider_choice.xml @@ -10,31 +10,13 @@ style="@style/CheckLabel" android:layout_gravity="center" tools:text="hello" /> - - - - - - + android:src="@drawable/pin_ic" + android:visibility="gone" + tools:visibility="visible" + tools:ignore="RtlHardcoded" /> \ No newline at end of file diff --git a/app/src/main/res/layout/source_profile_settings_dialog.xml b/app/src/main/res/layout/source_profile_settings_dialog.xml deleted file mode 100644 index 3f1f39fed..000000000 --- a/app/src/main/res/layout/source_profile_settings_dialog.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index b1dc96479..76231a2d3 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -11,7 +11,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="640dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" @@ -102,8 +101,6 @@ @@ -171,12 +168,6 @@ android:layout_marginEnd="32dp" android:orientation="vertical"> - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-arz/strings.xml b/app/src/main/res/values-arz/strings.xml index 4a4cb755f..55344e519 100644 --- a/app/src/main/res/values-arz/strings.xml +++ b/app/src/main/res/values-arz/strings.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-b+af/strings.xml b/app/src/main/res/values-b+af/strings.xml index b81848db6..81d7a96ae 100644 --- a/app/src/main/res/values-b+af/strings.xml +++ b/app/src/main/res/values-b+af/strings.xml @@ -24,7 +24,8 @@ Rand tipe Klaar Afgelaai Kyk verder - Nuwe opdatering gevind! \n%1$s -> %2$s + Nuwe opdatering gevind! +\n%1$s -> %2$s Laai Tale af Soek deur verskaffers te gebruik Gaan terug diff --git a/app/src/main/res/values-b+am/strings.xml b/app/src/main/res/values-b+am/strings.xml index 56b71f5a0..7fd3274b9 100644 --- a/app/src/main/res/values-b+am/strings.xml +++ b/app/src/main/res/values-b+am/strings.xml @@ -8,7 +8,8 @@ %1$dሰዓት %2$dደቂቃ ፖስተር የወረዱ - አዲስ ማሻሻያ ተገኝቷል! \n%1$s -> %2$s + አዲስ ማሻሻያ ተገኝቷል! +\n%1$s -> %2$s ተመለስ ተጨማሪ አማራጮች በማየት ላይ diff --git a/app/src/main/res/values-b+apc/strings.xml b/app/src/main/res/values-b+apc/strings.xml index 10ba0a88c..3bf88ce13 100644 --- a/app/src/main/res/values-b+apc/strings.xml +++ b/app/src/main/res/values-b+apc/strings.xml @@ -29,7 +29,8 @@ فرجي الـLogcat 🐈 +30 كفي حضر - في أپدايت جديدة! \n%1$s ← %2$s + في أپدايت جديدة! +\n%1$s ← %2$s نزل الترجمات مع الڤيديو عوزو المصادر لَ تنبّشو رجاع @@ -95,7 +96,8 @@ لون الكتيبة مخلص عوز قوة ضوّ الشاشة تبع السيستام بدل من تغميئ الڤيديو - فشل ترجيع النسخة الإحتياطية من ملف \n%s + فشل ترجيع النسخة الإحتياطية من ملف +\n%s مشّي المقطع الدعائي مشّي البث المباشر م لقينا ولا حلقة @@ -164,7 +166,8 @@ طفي الترجمة القصة مستعمل - %dد \nباقي + %dد +\nباقي عم ينعرض حاليًا بلايحة النَطر حالة @@ -191,7 +194,7 @@ في مشكلة بجهاز العرض (Renderer error) العِنوان پروكسي \"گِت هَب\" - فرجي معلومات مشغل الڤيديو + جودة مشغل الڤيديو ملصق الترجمة أوڤا نَزِل من مصادر وجودات مختلفة @@ -358,7 +361,11 @@ العشوائي يللي بعده خيال عم نجدِد المثلثلات يللي مشتركينلها - مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن: \n \n%s \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟ + مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن: +\n +\n%s +\n +\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟ نزلت %1$d %2$s معرف مش صالح أفّي %s @@ -366,7 +373,9 @@ حطو الأرقام السرية لـ\"%s\" الطريقة القديمة معلى - \"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. \n \nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. + \"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. +\n +\nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. زبد تتبع 3G/4G… نفَتح %s @@ -379,7 +388,8 @@ دايمًا كتوب ب أحرف كاپيتال، A بدل a مشغل الڤيديو المفضل 4K - بَلَش تنزيل %1$d %2$s \n… + بَلَش تنزيل %1$d %2$s +\n… الوصف شوف الريپويات تبع مجتمع \"كلاود ستريم\" إنت هلّق بال وضع الآمن @@ -409,7 +419,9 @@ أفّى الإعداد فتت ع أكونت \"%s\" تبعك حدود خطية - في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا \n(UI was unable to be created correctly) \n%s + في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا +\n(UI was unable to be created correctly) +\n%s عَدِل تجَدَد (من الجديد للقديم) TC @@ -465,7 +477,8 @@ SD الإضافات شيل الإعلانات من الترجمة - رفّكن فاضي ☹ \nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. + رفّكن فاضي ☹ +\nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. اسم الريپو (مش ضروري) الجودات بيانات مش صالحة @@ -492,7 +505,8 @@ رايتينگ (من الواطي للعالي) فتاح من ملف طفي - لقينا ملف الوضع الآمن! \nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف. + لقينا ملف الوضع الآمن! +\nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف. مش مغير وقت الترجمة مشكلة مصدر @@ -517,7 +531,7 @@ إضافات م قدرنا نفتح %s رايتينگ: %s - تحزير: \"كلاود ستريم\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا! + تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا! الحالة محي الريپو مشغل الڤيديو @@ -526,7 +540,10 @@ إنتو أصلًا مصوتين كاميرا م لقينا ولا إضافة بال ريپو - مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: \n\"%s\" \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟ + مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: +\n\"%s\" +\n +\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟ رايط مش صالح 1000 مللي ثانية إصدار @@ -546,7 +563,13 @@ @string/home_play شيلو من لايحة المحتوى الحاضرينو الإعتمادات - فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n \nمتلًا: \nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). \nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). \n \nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! + فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. +\n +\nمتلًا: +\nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). +\nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). +\n +\nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! حطو الأرقام السرية الحالية صوت حط كبسة لبرم إتجاه الشاشة @@ -566,14 +589,16 @@ رمز/كلمة مرور للمصادقة فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، أو الپاسورد. بعد كذا محاولة فاشلة، هيدا الشباك رح يسكر. بكل بساطة، سكر الآپ ورجاع فتحه حتى تجرب بعد مرة. - %s \nباقي + %s +\nباقي المصادقة البيومترية مش مدعومة ع هالجهاز شيله من المفضل اسم وعنوان الريپو نتسخ! فيه ارور بال وصول ل الكليپ-بورد. پليز جرب مرة أخرى. فيه ارور بال نسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. - هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. + هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. +\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. أوكي وقف اپتميزايشن بطارية جهازك بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" @@ -609,13 +634,21 @@ نقي الإشيا اللي بدك تمحيها موجود لينحضر بلا إنترنت محي الفايلات - متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟ \n \n%s - رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟ \n \n%s + متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟ +\n +\n%s + رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟ +\n +\n%s نقي كل شي شيل التنقاية محي (%1$d | %2$s) - متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟ \n \n%2$s - متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟ \n \n%s + متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟ +\n +\n%2$s + متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟ +\n +\n%s صورة زغيرة مع التقريب وال تبعيد بت حط صورة زغير من الڤيديو إنت و عم بت قرب أو ترجع بال ڤيديو بعد مش معمول لود لولا ترجمة @@ -706,33 +739,4 @@ فوق، عال شمال فوق، بال نُص فوق، عال يمين - ليستة التنزيلات - مافي شي عم يتنزّل هلّق. - قوة ضو إضافية - بت حط فلتر للبرايتنس لمّا تعلي قوة الضو ل أكتر من 100% - extra_brightness_enabled - اقتراحات التنبيش - بت فرجي اقتراحات إنتا و عم بت نَبّش - مساح الاقتراحات - فرجي ميتا-ديتا فوق الڤيديو - فرجي ليستة الممثلين - ڤيديو - معلومات الڤيديو - أولوية المصدر - حدد ترتيب المصادر بال مشغل - اسم المصدر - نزلن كلن - لغين كلن - بدك تنزل الحلقة %s؟ - بدك تلغي كل شي عم يتنَزَّل؟ - - م شي عم يتنزل - شي واحد عم يتنزل - - - مافي شي بعد بده يبلش يتنزل - فيه شي واحد بعد بده يبلش يتنزل - - لايڤ - پريڤيو diff --git a/app/src/main/res/values-b+ar/strings.xml b/app/src/main/res/values-b+ar/strings.xml index 65fcc149d..f7186c0ef 100644 --- a/app/src/main/res/values-b+ar/strings.xml +++ b/app/src/main/res/values-b+ar/strings.xml @@ -12,7 +12,8 @@ سرعة (%.2fx) تقييم: %.1f - يوجد تحديث جديد! \n%1$s -> %2$s + يوجد تحديث جديد! +\n%1$s -> %2$s %d دقيقة CloudStream تشغيل بواسطة CloudStream @@ -175,8 +176,10 @@ إستئناف -٣٠ +٣٠ - سوف يتم الحذف نهائيا %s \nهل أنت متأكد? - %dm \nمتبقية + سوف يتم الحذف نهائيا %s +\nهل أنت متأكد? + %dm +\nمتبقية جاري التنفيذ اكتمل الحالة @@ -398,7 +401,9 @@ تم تحميل: %d مُعطل %d غير مُحمل: %d - لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات. \n \nانضم إلى ديسكورد أو ابحث عبر الإنترنت. + لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات. +\n +\nانضم إلى ديسكورد أو ابحث عبر الإنترنت. عرض مستودعات المجتمع قائمة عامة جميع الترجمات حروف كبيرة @@ -488,13 +493,15 @@ التقييم (من الأعلى إلى الأدنى) التقييم (من الأدنى إلى الأعلى) الترتيب الأبجدي (من ي إلى أ) - مكتبتك فارغة :( \nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية. + مكتبتك فارغة :( +\nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية. محدث (من القديم إلى الجديد) فرز حسب افرز فتح بواسطة المكتبة - تم العثور على ملف الوضع الآمن! \nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. + تم العثور على ملف الوضع الآمن! +\nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. مدة التقديم عنما يكون المشغل مخفيا مدة التقديم - المشغل مخفي تلفزيون أندرويد @@ -526,7 +533,13 @@ تعديل الملفات التعريفية مساعدة - ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. \n \nالمصدر أ: 3 \nالجودة ب: 7 \nسيكون لها أولوية فيديو مجمعة تبلغ 10. \n \nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! + ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nسيكون لها أولوية فيديو مجمعة تبلغ 10. +\n +\nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! النوعيات خلفية الملف الشخصي تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s @@ -539,7 +552,11 @@ تمت إزالة %s من المفضلة المفضلة تمت إضافة %s إلى المفضلة - احتمال وجود تكرارات في مكتبتك. \n \n%s \n \nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟ + احتمال وجود تكرارات في مكتبتك. +\n +\n%s +\n +\nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟ احتمال أن يكون موجود بالفعل قفل الحساب اضافة الى المفضلة @@ -552,7 +569,9 @@ إشترك إزالة من المفضلة اختار حساب - يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'. \n \nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟ + يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'. +\n +\nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟ ادخال ال PIN PIN أدخل ال PIN الحالي @@ -580,7 +599,8 @@ مصادقة كلمة المرور/رقم التعريف الشخصي بعد عدة محاولات فاشلة، سيتم إغلاق المطالبة. ما عليك سوى إعادة تشغيل التطبيق للمحاولة مرة أخرى. لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا. - %s \nمتبقي + %s +\nمتبقي المفضلة إزالة من المفضلة اسم و عنوان المخزن @@ -622,13 +642,21 @@ الرجاء تحديد العناصر للحذف تحديد الكل حذف الملفات - هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟ \n \n%2$s - ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا: \n \n%s + هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟ +\n +\n%2$s + ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا: +\n +\n%s حذف (%1$d | %2$s) متاح للمشاهدة في وضع عدم الاتصال إلغاء تحديد الكل - هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟ \n \n%s - هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟ \n \n%s + هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟ +\n +\n%s + هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟ +\n +\n%s معاينة شريط البحث تمكين معاينة الصورة المصغرة على شريط البحث لم يتم تحميل أي ترجمات بعد @@ -721,7 +749,7 @@ اقتراحات البحث عرض اقتراحات البحث أثناء الكتابة مسح الاقتراحات - عرض لوحة فريق التمثيل + عرض لوحة البث تثبيت الإصدار التجريبي تم تثبيت الإصدار التجريبي بالفعل. فشل تثبيت الإصدار التجريبي. @@ -753,12 +781,4 @@ %d تنزيل قيد الانتظار عرض واجهة منبثقة للبيانات الوصفية للمشغِّل - مقطع - استعراض - البث قائم - أظهر ساعة الوقت الحقيقي - أظهر شاشة الوقت الحقيقي في قمة الشاشة. ينطبق على الصفحة الرئيسية و المشغل - اعدادات الملف الشخصي - أخفي المصادر صاحبة الأولوية السلبية - أخفي المصادر التي تحتوي أخطاء diff --git a/app/src/main/res/values-b+ars/strings.xml b/app/src/main/res/values-b+ars/strings.xml index cd3830a01..3104e6a9a 100644 --- a/app/src/main/res/values-b+ars/strings.xml +++ b/app/src/main/res/values-b+ars/strings.xml @@ -35,7 +35,8 @@ توقف التنزيل خطط للمشاهدة إعادة المشاهدة - !تم العثور على تحديث جديد \n%1$s -> %2$s + !تم العثور على تحديث جديد +\n%1$s -> %2$s %.1f:قدر %dاقل كلاودستريم @@ -156,15 +157,23 @@ تم التحديث (من الجديد إلى القديم) تم التحديث (القديم إلى الجديد) أبجديًا (من الألف إلى الياء) - مكتبتك فارغة :( \nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. - !تم العثور على ملف الوضع الآمن \n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف + مكتبتك فارغة :( +\nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. + !تم العثور على ملف الوضع الآمن +\n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف ارجع تحديث العروض المشتركة الوضع العادي حرر ملفات تعريفية مساعدة - .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو \n \nالمصدر أ: 3 \nالجودة ب: 7 \nستكون أولوية الفيديو المدمجة .10 \n \n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط + .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nستكون أولوية الفيديو المدمجة .10 +\n +\n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط لقد صوت بالفعل أبجديًا (ياء إلى ألف) ترتيب حسب @@ -218,7 +227,8 @@ قدم وصف يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى - نهائيا %sسيؤدي هذا الى حذف \nهل أنت متأكد؟ + نهائيا %sسيؤدي هذا الى حذف +\nهل أنت متأكد؟ الخط حجم الخط زيل @@ -245,7 +255,8 @@ 🐈عرض لوجكات سجل صور في صور - %d \nباقي + %d +\nباقي مصدر اللاعب مخفي - ابحث عن المبلغ تكرار النسخ الاحتياطي diff --git a/app/src/main/res/values-b+as/strings.xml b/app/src/main/res/values-b+as/strings.xml index b7efb3341..16be7e313 100644 --- a/app/src/main/res/values-b+as/strings.xml +++ b/app/src/main/res/values-b+as/strings.xml @@ -252,9 +252,12 @@ পুনৰ আৰম্ভ কৰক -৩০ +৩০ - এইটো স্থায়ীভাৱে %s ডিলিট কৰিব। \nআপুনি নিশ্চিত নেকি? - %dm \nবাকী - %s \nবাকী + এইটো স্থায়ীভাৱে %s ডিলিট কৰিব। +\nআপুনি নিশ্চিত নেকি? + %dm +\nবাকী + %s +\nবাকী চলমান সম্পূৰ্ণ স্থিতি @@ -453,7 +456,9 @@ %d প্লাগইন আপডেট কৰা হ\'ল নিষ্ক্ৰিয় কৰা: %d ডাউনলোড কৰা নহয়: %d - CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব। \n \nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক। + CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব। +\n +\nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক। সম্প্ৰদায়ৰ ৰিপ\'জিট\'ৰিসমূহ চাওক সকলো চাবটাইটল মুকলি আখৰত সতর্কতা: CloudStream 3 কোৱা নাই যে তৃতীয় পক্ষৰ বৃদ্ধিসমূহ ব্যৱহাৰ কৰিবলৈ আপুনি সম্পূৰ্ণ দায়িত্ব ল\'ব আৰু কোনো সহায় নাপাব! @@ -518,9 +523,11 @@ বৰ্ণানুক্ৰমিক (A ৰ পৰা Z) পুথিভঁৰালী বাছক ইয়াৰ সহায়ত খুলক - আপোনাৰ পুথিভঁৰালী খালি আছে :( \nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক। + আপোনাৰ পুথিভঁৰালী খালি আছে :( +\nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক। এই তালিকা খালি। অন্য এটি তালিকালৈ সলনি কৰি চাওক। - নিরাপদ ম\'ড ফাইল পোৱা গৈছে! \nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে। + নিরাপদ ম\'ড ফাইল পোৱা গৈছে! +\nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে। ঘূৰাই দিয়া সদস্যতা গ্ৰহণ কৰা শ্ব\'সমূহ আপডেট কৰিছে %s-ত সদস্যতা গ্ৰহণ কৰা হৈছে @@ -532,7 +539,13 @@ সম্পাদনা কৰক প্ৰ\'ফাইলসমূহ সহায় - ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ। \n \nউৎস A: 3 \nগুণ B: 7 \nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10। \n \nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব! + ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ। +\n +\nউৎস A: 3 +\nগুণ B: 7 +\nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10। +\n +\nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব! প্ৰ\'ফাইলৰ পটভূমি UI সঠিকভাৱে সৃষ্টি কৰিব পৰা নগ\'ল, ই এটা গুৰুত্বপূৰ্ণ সমস্যা আৰু তাক অবিলম্বে জনোৱা উচিত %s আপুনি ইতিমধ্যে ভোট দিছে @@ -543,8 +556,14 @@ প্ৰিয় তালিকাৰ পৰা আঁতৰ কৰক সম্ভাৱ্য নকল বস্ত্ত পোৱা গৈছে সকলো প্ৰতিস্থাপন কৰক - আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\' \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? - আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে: \n \n%s \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? + আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\' +\n +\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? + আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে: +\n +\n%s +\n +\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? %s ৰ বাবে পিন সন্নিবিষ্ট কৰক বৰ্তমান পিন সন্নিবিষ্ট কৰক প্ৰফাইল লক কৰক @@ -569,7 +588,8 @@ ডাউনলোড এপ্‌ আৰম্ভণিৰ পিছত নতুন আপডেটৰ সন্ধান কৰক। একেই ডেভেলপাৰৰ দ্বাৰা এনিম এপ্‌ - নতুন আপডেট পোৱা গ’ল! \n%1$s -> %2$s + নতুন আপডেট পোৱা গ’ল! +\n%1$s -> %2$s ফিলাৰ CloudStreamৰে প্লে কৰক সন্ধান @@ -589,10 +609,18 @@ প্ৰয়োগ কৰক ফাইলসমূহ ডিলিট কৰক ডিলিট (%1$d | %2$s) - আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s - %1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি? \n \n%2$s - আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব: \n \n%s - আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s + আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি? +\n +\n%s + %1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি? +\n +\n%2$s + আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব: +\n +\n%s + আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি? +\n +\n%s মুক্তিৰ তাৰিখ (পুৰণাৰ পৰা নতুন) সতৰ্কবাৰ্তা স্থানীয়ভাৱে প্ৰমাণীকৰণ কৰক diff --git a/app/src/main/res/values-b+bg/strings.xml b/app/src/main/res/values-b+bg/strings.xml index 4fb7757fd..e59f19462 100644 --- a/app/src/main/res/values-b+bg/strings.xml +++ b/app/src/main/res/values-b+bg/strings.xml @@ -16,7 +16,8 @@ Визуализация на фона Скорост (%.2fx) Оценка: %.1f - Намерена е нова актуализация! \n%1$s -> %2$s + Намерена е нова актуализация! +\n%1$s -> %2$s Шаблон %d мин CloudStream @@ -182,8 +183,10 @@ Продължи -30 30 - Това ще изтрие за постоянно %s \nСигурни ли сте? - %dm \nостава + Това ще изтрие за постоянно %s +\nСигурни ли сте? + %dm +\nостава Продължава Завършен Статус @@ -402,7 +405,9 @@ Деактивирано: %d Не е изтеглено: %d Актуализирани %d плъгини - CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища. \n \nПрисъединете се към нашия Дискорд или потърсете онлайн. + CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища. +\n +\nПрисъединете се към нашия Дискорд или потърсете онлайн. Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви @@ -514,10 +519,12 @@ Профил %d По азбучен ред (A до Z) Отваряне с - Вашата библиотека е празна :( \nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека. + Вашата библиотека е празна :( +\nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека. Използване Епизод %d е публикуван! - Намерен е файл за безопасен режим! \nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат. + Намерен е файл за безопасен режим! +\nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат. Вече сте гласували Задаване по подразбиране ПИН трябва да е 4 символа @@ -544,11 +551,23 @@ Скрит играч - сума за търсене Сумата за търсене, използвана, когато играчът е скрит Актуализирано (от ново към старо) - Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото. \n \nИзточник A: 3 \nКачество B: 7 \nЩе има комбиниран видео приоритет от 10. \n \nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди! + Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото. +\n +\nИзточник A: 3 +\nКачество B: 7 +\nЩе има комбиниран видео приоритет от 10. +\n +\nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди! Замени Замени Всички - Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“. \n \nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието? - Във вашата библиотека са намерени потенциални дублиращи се елементи: \n \n%s \n \nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието? + Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“. +\n +\nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието? + Във вашата библиотека са намерени потенциални дублиращи се елементи: +\n +\n%s +\n +\nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието? Заключи Профил Вкарай Сегашен ПИН Управлявай Профили diff --git a/app/src/main/res/values-b+bn/strings.xml b/app/src/main/res/values-b+bn/strings.xml index b6edbd1cd..87aa8e7eb 100644 --- a/app/src/main/res/values-b+bn/strings.xml +++ b/app/src/main/res/values-b+bn/strings.xml @@ -3,7 +3,7 @@ পোস্টার ক্লাউডস্ট্রিম দিয়ে চালান হোম - অভিনয়ে: %s + অভিনয়েঃ %s %1$dদিন %2$dঘন্টা %3$dমিনিট %1$dঘন্টা %2$dমিনিট %d মিনিট @@ -15,14 +15,15 @@ ব্যাকগ্রাউন্ড দেখান গতি (%.2f গুণ) মূল্যায়নঃ %.1f - নতুন আপডেট এসেছে! \n%1$s -> %2$s + নতুন আপডেট এসেছে! +\n%1$s -> %2$s ফিলার %d মিনিট ক্লাউডস্ট্রিম খুঁজুন ডাউনলোডসমূহ সেটিংস - %1$s পর্ব %2$d + %1$s এপি %2$d পর্ব %d মুক্তির তারিখ শেয়ার ব্রাউজারে খুলুন @@ -131,8 +132,8 @@ স্টোরেজ এর অনুমতি অনুপস্থিত। দয়া করে আবার চেষ্টা করুন। তথ্য ট্রেইলার প্রদর্শন করুন - Kitsu থেকে পোস্টারসমূহ প্রদর্শন করুন - স্বয়ংক্রিয় প্লাগইন আপডেট + কিটসু হতে পোস্টারসমূহ প্রদর্শন করুন + স্বয়ংক্রিয়ভাবে প্লাগিন এর হালনাগাদ স্বয়ংক্রিয়ভাবে প্লাগিনসমুহের ডাউনলোড হালনাগাদ ও ব্যাকআপ অ্যাপ এর হালনাগাদ দেখান @@ -148,9 +149,9 @@ সামনে বা পিছনের দিকে যেতে ডান বা বাম দিকে দুবার আলতো চাপুন ফাইল ডিলিট মান ডিফল্ট এ রিসেট করুন - স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন যোগ। + স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন সিনেমা - অ্যানিমে ফিলার পর্ব প্রদর্শন + এনিমে এর ফিলার পর্ব দেখায় টিভি সিরিজ লিংক পাওয়া যায়নি চা খাওয়ানো হয়েছে @@ -158,7 +159,8 @@ সিনেমা ডিসকর্ডে যোগ দিন টরেন্টস - এটি স্থায়ীভাবে মুছে ফেলা হবে %s \nআপনি কি নিশ্চিত? + এটি স্থায়ীভাবে মুছে ফেলা হবে %s +\nআপনি কি নিশ্চিত? থামুন -৩০ গিটহাব @@ -174,7 +176,7 @@ ডিলিট শুরু কার্টুন - Some devices do not support the new package installer. Try the legacy option if updates do not install। + কিছু ফোন নতুন প্যাকেজ ইনস্টলার সাপোর্ট করে না। যদি আপডেটগুলি ইনস্টল না হয় তবে পুরোনো পদ্ধতি ব্যবহার করে দেখুন। সাবটাইটেল নেই এই প্রোভাইডার ক্রোমকাস্ট সাপোর্ট করে না উন্নত অনুসন্ধান @@ -182,7 +184,8 @@ ব্যবহৃত লাইব্রেরী আমাদের তৈরি ছোট উপন্যাস পড়ার অ্যাপ্লিকেশন - %d মি \nবাকি + %d মি +\nবাকি অন্যান্য চলমান এশিয়ান নাটক @@ -204,7 +207,7 @@ আমাদের তৈরি Anime দেখার অ্যাপ্লিকেশন 18+ এনিমে - অনুসন্ধানের ফলে নির্বাচিত ভিডিওর মান লুকান + অনুসন্ধান ফলাফলে নির্বাচিত ভিডিও কুয়ালিটি লুকান অ্যাপ লাইভস্ট্রিম APK ইনস্টলার @@ -305,7 +308,8 @@ password123 আসছে %s সময়ের মধ্যে বাতিল করুন - %s \nঅবশিষ্ট + %s +\nঅবশিষ্ট লাইভ স্ট্রিম সোর্স সমস্যা রিমোট সমস্যা @@ -353,38 +357,4 @@ শুরু থেকে চালু করুন স্পিচ রিকগনিশন উপলব্ধ নেই কথা বলা শুরু করুন… - ডাউনলোড তালিকা - সম্পূর্ণ সিরিজ প্লে করুন - এই ভিডিওটি একটি টরেন্ট, অর্থাৎ আপনার ভিডিও অ্যাক্টিভিটি ট্র্যাক করা সম্ভব।\nএগোনোর আগে টরেন্টিং সম্পর্কে ভালোভাবে বুঝে নিন। - ডিলিট করার জন্য আইটেম সিলেক্ট করুন - বর্তমানে কোনো ডাউনলোড নেই। - বর্তমানে কোনো ডাউনলোড কিউতে নেই। - অফলাইনে দেখার জন্য উপলব্ধ - সব সিলেক্ট করুন - সব ডিসিলেক্ট করুন - লোকাল ভিডিও খুলুন - অতিরিক্ত ব্রাইটনেস - ডিসপ্লে ব্রাইটনেস ১০০% ছাড়িয়ে গেলে ব্রাইটনেস ফিল্টার চালু করুন - সার্চ সাজেশন - টাইপ করার সময় সার্চ সাজেশন দেখান - সাজেশন মুছে ফেলুন - প্লেয়ার মেটাডেটা ওভারলে প্রদর্শন - কাস্ট প্যানেল প্রদর্শন - প্রাক-মুক্তি সংস্করণ ইনস্টলেশন - প্রাক-মুক্তি সংস্করণ ইতোমধ্যে ইনস্টল রয়েছে। - প্রাক-মুক্তি সংস্করণ ইনস্টল করতে সমস্যা হয়েছে। - (%1$d | %2$s) মুছুন - সাবধান - আপনি কি নিম্নলিখিত আইটেমগুলো স্থায়ীভাবে মুছতে চান?\n\n%s - ফাইল মুছুন - আপনি কি নিশ্চিত যে আপনি %1$s-এ নিচের এপিসোডগুলো স্থায়ীভাবে মুছে ফেলতে চান?\n\n%2$s - আপনি নিচের সিরিজগুলোর সব এপিসোডও স্থায়ীভাবে মুছে ফেলবেন:\n\n%s - আপনি কি নিশ্চিত যে আপনি নিচের সিরিজটির সব এপিসোড স্থায়ীভাবে মুছে ফেলতে চান?\n\n%s - সঙ্গীত - অডিও বই - মিডিয়া - অডিও - পডকাস্ট - ভিডিও - এনকোডিং ত্রুটি diff --git a/app/src/main/res/values-b+cs/strings.xml b/app/src/main/res/values-b+cs/strings.xml index e03da1d6f..2ee23c1c5 100644 --- a/app/src/main/res/values-b+cs/strings.xml +++ b/app/src/main/res/values-b+cs/strings.xml @@ -16,7 +16,8 @@ Rychlost (%.2fx) Hodnocení: %.1f - Nalezena nová aktualizace! \n%1$s -> %2$s + Nalezena nová aktualizace! +\n%1$s -> %2$s Výplň %d min CloudStream @@ -171,8 +172,10 @@ Pokračovat -30 +30 - Toto nevratně smaže %s \nJste si jisti? - %dm \nzbývá + Toto nevratně smaže %s +\nJste si jisti? + %dm +\nzbývá Probíhající Dokončena Stav @@ -413,7 +416,9 @@ Doplněk stažen 18+ Spuštěno stahování %1$d %2$s… - CloudStream nemá ve výchozím nastavení nainstalované žádné zdroje. Je třeba je nainstalovat z repozitářů. \n \nPřipojte se na náš Discord nebo hledejte na internetu. + CloudStream nemá ve výchozím nastavení nainstalované žádné zdroje. Je třeba je nainstalovat z repozitářů. +\n +\nPřipojte se na náš Discord nebo hledejte na internetu. Zakázáno: %d Aktualizováno %d doplňků Zobrazit informace o pádu @@ -441,7 +446,8 @@ Nepodařilo se nainstalovat novou verzi aplikace Původní Aplikace bude po ukončení aktualizována - Vaše knihovna je prázdná :( \nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny. + Vaše knihovna je prázdná :( +\nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny. Vybrat knihovnu Hodnocení (od nejvyššího) Hodnocení (od nejnižšího) @@ -449,7 +455,8 @@ Seřadit podle Řazení Tento seznam je prázdný. Zkuste přepnout na jiný. - Nalezen soubor bezpečného režimu! \nDo odebrání souboru nebudeme načítat žádná rozšíření. + Nalezen soubor bezpečného režimu! +\nDo odebrání souboru nebudeme načítat žádná rozšíření. Aktualizováno (od nejnovějšího) Aktualizováno (od nejstaršího) Abecedně (od A do Z) @@ -530,7 +537,13 @@ Nápověda Kvality Pozadí profilu - Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. \n \nZdroj A: 3 \nKvalita B: 7 \nBudou mít celkovou prioritu videa 10. \n \nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! + Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. +\n +\nZdroj A: 3 +\nKvalita B: 7 +\nBudou mít celkovou prioritu videa 10. +\n +\nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! Nepodařilo se správně vytvořit rozhraní. Toto je VÁŽNÁ CHYBA, kterou je potřeba ihned nahlásit %s Vypnout Výběr režimu pro filtrování stahování doplňků @@ -540,7 +553,11 @@ %s odebráno z oblíbených Oblíbené %s přidáno do oblíbených - Ve vaší knihovně byl nalezen potenciální duplikát: \n \n%s \n \nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? + Ve vaší knihovně byl nalezen potenciální duplikát: +\n +\n%s +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? Frekvence záloh Nalezena potenciální duplicita Zamknout profil @@ -554,7 +571,9 @@ Odebírat Odebrat z oblíbených Vyberte účet - Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%s“. \n \nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? + Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%s“. +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? Zadejte PIN PIN Zadejte současný PIN @@ -583,7 +602,8 @@ Po několika nezdařilých pokusech se okno zavře. Pro opětovný pokus restartujte aplikaci. Vaše data z aplikace CloudStream byla nyní zálohována. Ačkoli je tato možnost velmi malá, různá zařízení se mohou chovat různě. Ve výjimečném případě, že se vám přístup k aplikaci zablokuje, data aplikace zcela vymažte a obnovte je ze zálohy. Velmi se omlouváme za případné nepříjemnosti z toho plynoucí. Odebrat z oblíbených - %s \nzbývá + %s +\nzbývá Přidat do oblíbených Název a adresa repozitáře Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. @@ -624,12 +644,20 @@ Zvolte položky k odstranění Dostupné pro sledování offline Vybrat vše - Opravdu chcete trvale odstranit následující položky? \n \n%s - Opravdu chcete trvale odstranit následující epizody v %1$s? \n \n%2$s - Opravdu chcete trvale odstranit všechny epizody v následujících sériích? \n \n%s + Opravdu chcete trvale odstranit následující položky? +\n +\n%s + Opravdu chcete trvale odstranit následující epizody v %1$s? +\n +\n%2$s + Opravdu chcete trvale odstranit všechny epizody v následujících sériích? +\n +\n%s Zrušit výběr všeho Odstranit soubory - Také trvale odstraníte všechny epizody v následujících sériích: \n \n%s + Také trvale odstraníte všechny epizody v následujících sériích: +\n +\n%s Odstranit (%1$d | %2$s) Náhled v liště přehrávače Povolit náhled miniatur na liště přehrávače @@ -751,18 +779,4 @@ Priorita zdrojů Rozhodněte, jak mají být řazeny zdroje videí v přehrávači Zobrazit překrytí metadat v přehrávači - Video - Náhled - Živě - Nastavení profilu - Skrýt zdroje s negativní prioritou - Skrýt zdroje s chybami - - %d skrytý odkaz - %d skryté odkazy - %d skrytých odkazů - %d skrytých odkazů - - Zobrazit hodiny s reálným časem - Zobrazit hodiny s reálným časem v horní části obrazovky. Platí pro domovskou obrazovku a přehrávač diff --git a/app/src/main/res/values-b+de/strings.xml b/app/src/main/res/values-b+de/strings.xml index e674fafd6..eb939ec5e 100644 --- a/app/src/main/res/values-b+de/strings.xml +++ b/app/src/main/res/values-b+de/strings.xml @@ -27,7 +27,8 @@ Hintergrundbildvorschau Geschwindigkeit (%.2fx) Bewertung: %.1f - Neues Update gefunden! \n%1$s -> %2$s + Neues Update gefunden! +\n%1$s -> %2$s Füller %d Min CloudStream @@ -187,8 +188,10 @@ Fortsetzen -30 +30 - Dadurch wird %s permanent gelöscht \nBist du dir sicher? - %dm \nverbleibend + Dadurch wird %s permanent gelöscht +\nBist du dir sicher? + %dm +\nverbleibend Laufend Abgeschlossen Status @@ -252,7 +255,7 @@ Update Bevorzugte Videoqualität (WLAN) Videoplayertitel max. Zeichen - Zeige Playerinformationen + Playerinformationen anzeigen Videopuffergröße Videopufferlänge Video-Cache in Speicher @@ -398,7 +401,9 @@ Heruntergeladen: %d Deaktiviert: %d Nicht heruntergeladen: %d - CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden. \n \nTrete unserem Discord Server bei oder suche online. + CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden. +\n +\nTrete unserem Discord Server bei oder suche online. Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben @@ -479,9 +484,11 @@ Alphabetisch (Z zu A) Bibliothek auswählen Öffnen mit - Deine Bibliothek ist leer :( \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. + Deine Bibliothek ist leer :( +\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln. - Datei für den abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. + Datei für den abgesicherten Modus gefunden! +\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. Player ausgeblendet - Betrag zum vor- und zurückspulen Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist Der Betrag, welcher verwendet wird, wenn der Player ausgeblendet ist @@ -515,7 +522,13 @@ Hilfe Qualitäten Profil-Hintergrund - Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. \n \nQuelle A: 3 \nQualität B: 7 \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! + Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. +\n +\nQuelle A: 3 +\nQualität B: 7 +\nWerden eine kombinierte Videopriorität von 10 haben. +\n +\nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! Filtermodus für Plugin-Downloads auswählen Es wurde bereits abgestimmt Keine Plugins im Repository gefunden @@ -547,8 +560,14 @@ Kontoauswahl beim Starten überspringen Konten verwalten Konto bearbeiten - Es wurden potentielle Duplikate in deiner Bibliothek gefunden: \n \n%s \n \nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? - Deine Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\' \n \nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? + Es wurden potentielle Duplikate in deiner Bibliothek gefunden: +\n +\n%s +\n +\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? + Deine Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\' +\n +\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? Links wurden neu geladen Drehen Zeige einen Umschalter für Bildschirmorientierung an @@ -568,7 +587,8 @@ kein Favorit Dieser Bildschirm wurde nach einigen Fehlversuchen geschlossen. Starte die App neu. Ihre CloudStream-Daten wurden gesichert. Obwohl die Wahrscheinlichkeit dieses seltenen Falles sehr gering ist, verhalten sich alle Geräte unterschiedlich. Falls Sie im schlimmsten Fall den Zugriff zur App verlieren, löschen Sie die App-Daten vollständig und stellen Sie die Sicherung wieder her. Jegliche Unannehmlichkeiten, die Ihnen dadurch entstehen, bedauern wir sehr. - %s \nausstehend + %s +\nausstehend Favorit Kopiert! Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. @@ -587,7 +607,7 @@ Sicherheit Konten Repository öffnen - Besuche %s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein + Besuche%s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung Zur Zeit sind keine Downloads verfügbar. Lokales Video öffnen @@ -610,10 +630,18 @@ Vom Beginn an spielen Elemente zum Löschen auswählen Zum Offline-Ansehen verfügbar - Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst? \n \n%s - Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst? \n \n%2$s - Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen: \n \n%s - Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst? \n \n%s + Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst? +\n +\n%s + Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst? +\n +\n%2$s + Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen: +\n +\n%s + Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst? +\n +\n%s Veröffentlichungsdatum (von neu nach alt) Veröffentlichungsdatum (von alt nach neu) Suchleisten Vorschau @@ -712,8 +740,8 @@ Zusätzliche Helligkeit Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist Erhöhte Helligkeit aktiviert - Zeige Cast-Panel - Mediainfo + Cast-Panel zeigen + Medieninfo Quellname Alle herunterladen Möchtest du Episode %s herunter laden? @@ -731,8 +759,4 @@ Es befinden sich keine Downloads in der Warteschlange. Quellpriorität Entscheide, wie Videoquellen im Player sortiert werden sollen - Zeige Player-Metadaten - Video - Vorschau - Live diff --git a/app/src/main/res/values-b+el/strings.xml b/app/src/main/res/values-b+el/strings.xml index a38ee2682..4a53c4e58 100644 --- a/app/src/main/res/values-b+el/strings.xml +++ b/app/src/main/res/values-b+el/strings.xml @@ -110,7 +110,8 @@ Μπανάνα δόθηκε Ταχύτητα (%.2fx) Βαθμολογία: %.1f - Νέα διαθέσιμη ενημέρωση! \n%1$s -> %2$s + Νέα διαθέσιμη ενημέρωση! +\n%1$s -> %2$s Πατήστε δύο φορές στη μέση για παύση Χρήση φωτεινότητας συστήματος Χρήση φωτεινότητας συστήματος στο ενσωματωμένο πρόγραμμα αναπαραγωγής, αντί εφαρμογής προεπιλεγμένου σκούρου επικαλύμματος @@ -148,8 +149,10 @@ Ακύρωση Παύση Συνέχιση - Αυτό θα διαγράψει μόνιμα το %s \nΕίστε σίγουροι πως θέλετε να προχωρήσετε; - %dm \nαπομένουν + Αυτό θα διαγράψει μόνιμα το %s +\nΕίστε σίγουροι πως θέλετε να προχωρήσετε; + %dm +\nαπομένουν Σε εξέλιξη Κατάσταση Έτος @@ -320,7 +323,9 @@ Απενεργοποιήθηκε: %d Δεν κατέβηκε: %d Ενημερώθηκαν %d πρόσθετα - Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. \n \nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. + Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. +\n +\nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. Προβολή αποθετηρίων κοινότητας Δημόσια λίστα Κεφαλοποίηση υποτίτλων @@ -482,15 +487,23 @@ Αφαίρεση από παρακολουθημένα Περιηγητής Άνοιγμα με - Η βιβλιοθήκη σας είναι άδεια :( \nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας. - Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! \nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. + Η βιβλιοθήκη σας είναι άδεια :( +\nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας. + Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! +\nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. Αρχείο Καταγραφής Απέτυχε Πέτυχε Εκκίνηση Δε βρέθηκαν επεκτάσεις στο αποθετήριο Δε βρέθηκε αποθετήριο, ελέγξτε την URL και δοκιμάστε VPN - Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. \n \nΠηγή Α: 3 \nΠοιότητα Β: 7 \nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. \n \nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! + Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. +\n +\nΠηγή Α: 3 +\nΠοιότητα Β: 7 +\nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. +\n +\nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! Δοκιμή παρόχου Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου) Διακομιστής μεσολάβησης GitHub @@ -536,7 +549,9 @@ Εντάξει Απενεργοποιήση της εξοικονόμησης της μπαταρίας Έχετε ήδη ψηφίσει - Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' \n \nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; + Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' +\n +\nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; Εισαγωγή Τρέχον Κωδικού Κλείδωμα Προφίλ Ξεκλείδωμα Cloudstream @@ -563,7 +578,11 @@ Πιθανό αντίγραφο βρέθηκε Προσθήκη Αντικατάσταση - Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: \n \n%s \n \nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? + Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: +\n +\n%s +\n +\nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? Εισαγωγή Κωδικού για %s Κωδικός Εσφαλμένος Κωδικός. Προσπαθήστε ξανά. @@ -576,7 +595,8 @@ Παράκαμψη απαγόρευσης από raw github URLs χρησιμοποιώντας jsDelivr. Μπορεί να καθυστερήσει τις ενημερώσεις για μερικές μέρες. Εμφάνιση κουμπιού για περιστροφή οθόνης Αγαπημένο - %s \nαπομένουν + %s +\nαπομένουν Βιομετρική αυθεντικοποίηση δεν υποστηρίζεται από τη συσκευή Καστ ταινίας Για να εξασφαλιστούν αδιάκοπες λήψεις και ειδοποιήσεις για αναγραφόμενες τηλεοπτικές εκπομπές, το CloudStream χρειάζεται άδεια για να τρέξει στο παρασκήνιο. Πατώντας OK, θα εμφανιστεί ένας διάλογος αιτήματος. Παρακαλώ πατήστε \\\"Επιτρέπω\\\".\n\nΠαρακαλώ σημειώστε, αυτή η άδεια δεν σημαίνει ότι το CS3 θα αποστραγγίσει την μπαταρία σας. Θα λειτουργεί στο παρασκήνιο μόνο όταν είναι απαραίτητο, όπως κατά τη λήψη ειδοποιήσεων ή τη λήψη βίντεο από επίσημες επεκτάσεις. diff --git a/app/src/main/res/values-b+eo/strings.xml b/app/src/main/res/values-b+eo/strings.xml index 5dea7b24d..ccd18eae3 100644 --- a/app/src/main/res/values-b+eo/strings.xml +++ b/app/src/main/res/values-b+eo/strings.xml @@ -83,7 +83,8 @@ %1$dt %2$dh %3$dm %1$dh %2$dm %dm - Nova ĝisdatigo trovita! \n%1$s -> %2$s + Nova ĝisdatigo trovita! +\n%1$s -> %2$s Speciala epizodo CloudStream Elŝuto Komencite diff --git a/app/src/main/res/values-b+es/strings.xml b/app/src/main/res/values-b+es/strings.xml index 1287c7c09..1801d39a2 100644 --- a/app/src/main/res/values-b+es/strings.xml +++ b/app/src/main/res/values-b+es/strings.xml @@ -7,8 +7,8 @@ Descargado %1$d %2$s Borrar repositorio El episodio %d se lanzará en - %1$d h %2$d m - %d m + %1$d h %2$d m + %d m Póster Extensiones Archivo descargado @@ -80,7 +80,8 @@ Recargar enlaces /?? /%d - Esto eliminará %s permanentemente \nEstá seguro? + Esto eliminará %s permanentemente +\nEstá seguro? ¿Seguro que quieres salir? Continuar Descarga Código de idioma (es_ES) @@ -103,7 +104,7 @@ Velocidad (%.2f×) Omitir carga %1$s Ep. %2$d - %1$d d %2$d h %3$d m + %1$d d %2$d h %3$d m Reparto: %s Relleno %d min @@ -248,7 +249,8 @@ Continuar -30 +30 - %dm \nfaltante + %dm +\nfaltante En curso Completado Estado @@ -480,8 +482,10 @@ Alfabéticamente (Z a A) Seleccionar biblioteca Abrir con - Tu biblioteca está vacía :( \nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. - ¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. + Tu biblioteca está vacía :( +\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. + ¡Se encontró un archivo en modo seguro! +\nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. Reproductor visible - buscar cantidad Reproductor oculto - buscar cantidad Android TV @@ -506,7 +510,13 @@ ISP Bypasses Calidad de visualización preferida (Datos móviles) Ayuda - Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. \n \nFuente A: 3 \nCalidad B: 7 \nTendrá una prioridad en el vídeo combinada de 10. \n \nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! + Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. +\n +\nFuente A: 3 +\nCalidad B: 7 +\nTendrá una prioridad en el vídeo combinada de 10. +\n +\nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! Perfil %d Wifi Editar @@ -526,7 +536,11 @@ %s eliminado de favoritos Favoritos %s añadido a favoritos - Se han encontrado posibles elementos duplicados en su biblioteca: \n \n%s \n \n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción? + Se han encontrado posibles elementos duplicados en su biblioteca: +\n +\n%s +\n +\n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción? Posible duplicado encontrado Bloquear perfil Añadido a favoritos @@ -569,7 +583,8 @@ Ahora se ha realizado una copia de seguridad de sus datos de CloudStream. Aunque la posibilidad de que esto ocurra es muy baja, todos los dispositivos pueden comportarse de forma diferente. En el raro caso de que no puedas acceder a la aplicación, borra completamente los datos de la aplicación y restaura desde una copia de seguridad. Sentimos mucho las molestias que esto pueda ocasionarte. Favorito Eliminar de favoritos - %s \nrestante + %s +\nrestante Nombre y URL del repositorio ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. @@ -594,7 +609,7 @@ Imagen del código QR Descartar Abrir repositorio - Visita %s en tu smartphone o equipo e introduce el código anterior + Visita %s en tu smartphone o ordenador e introduce el código anterior ¡El código PIN ya ha caducado! El código caduca en %1$d mín y %2$d s No puedo obtener el código PIN del dispositivo; intente con la autenticación local @@ -606,16 +621,24 @@ Ocultar los nombres de los controles del reproductor Fecha de lanzamiento (antigua a nueva) Fecha de lanzamiento (de nueva a antigua) - ¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie? \n \n%s + ¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie? +\n +\n%s Seleccionar elementos para eliminar Disponible para visualizar sin conexión Seleccionar todo Deseleccionar todo Borrar archivos Borrar (%1$d | %2$s) - ¿Seguro que quieres borrar de forma permanente los siguientes elementos? \n \n%s - ¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s? \n \n%2$s - También borrará permanentemente todos los episodios de las siguientes series: \n \n%s + ¿Seguro que quieres borrar de forma permanente los siguientes elementos? +\n +\n%s + ¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s? +\n +\n%2$s + También borrará permanentemente todos los episodios de las siguientes series: +\n +\n%s Activar la previsualización para las miniaturas en la barra de búsqueda Previsualización de Seekbar Aún no hay subtítulos cargados @@ -672,9 +695,9 @@ Sobreexploración Cambios en el tamaño de los pósteres Tamaño del póster - %1$d h %2$d m %3$d s - %1$d m %2$d s - %1$d s + %1$d h %2$d m %3$d s + %1$d m %2$d s + %1$d s Etiqueta de valoración Mantenga presionado para duplicar la velocidad Sin cuenta @@ -733,13 +756,4 @@ %d descargas encoladas %d descargas encoladas - Mostrar superposición de metadatos del jugador - Vídeo - Vista previa - En Vivo - Mostrar reloj en tiempo real - Muestra un reloj en tiempo real en la parte superior de la pantalla. Se aplica a la página de inicio y al reproductor - Configuración del perfil - Ocultar fuentes con prioridad negativa - Ocultar fuentes con errores diff --git a/app/src/main/res/values-b+fa/strings.xml b/app/src/main/res/values-b+fa/strings.xml index 1018e9a82..5fa557f08 100644 --- a/app/src/main/res/values-b+fa/strings.xml +++ b/app/src/main/res/values-b+fa/strings.xml @@ -113,7 +113,8 @@ در حال تماشا بارگیری‌ها سرعت (%.2f برابر) - بروزرسانی جدید پیدا شد! \n%1$s -> %2$s + بروزرسانی جدید پیدا شد! +\n%1$s -> %2$s پخش فیلم مرورگر پخش قسمت @@ -129,7 +130,8 @@ برای بازنشانی به پیشفرض نگه‌دارید کتابخانه در ادامه - این فرآیند بطور کامل %s را حذف می‌کند \nآیا از این کار اطمینان دارید؟ + این فرآیند بطور کامل %s را حذف می‌کند +\nآیا از این کار اطمینان دارید؟ نام مخزن و نشانی کپی شد! درباره @@ -178,11 +180,13 @@ حذف پرونده نمایش تریلر ها قسمت‌ها - %dد \nباقی‌مانده + %dد +\nباقی‌مانده گیتهاب پنهان کردن ویدیو مشخص شده از نتایج جستجو لغو - %s \nباقی‌مانده + %s +\nباقی‌مانده پیش‌فرض کارتون تورنت diff --git a/app/src/main/res/values-b+fr/strings.xml b/app/src/main/res/values-b+fr/strings.xml index bf401b20a..34bf1393d 100644 --- a/app/src/main/res/values-b+fr/strings.xml +++ b/app/src/main/res/values-b+fr/strings.xml @@ -79,7 +79,8 @@ Annuler Pause Reprendre - Cela va supprimer définitivement %s \nÊtes-vous sûr ? + Cela va supprimer définitivement %s +\nÊtes-vous sûr ? En cours Terminé Statut @@ -121,7 +122,8 @@ Mettre à jour Utile pour contourner les bloquages des FAI Emplacement de téléchargement - Nouvelle mise à jour trouvée ! \n%1$s -> %2$s + Nouvelle mise à jour trouvée ! +\n%1$s -> %2$s Épisode spécial Qualité de visionnage préférée (WiFi) Taille de la mémoire cache @@ -135,7 +137,7 @@ Afficher les animés en Anglais (Dub) / sous-titrés Disposition en mode téléphone %1$s Ep %2$d - Note : %.1f + Note : %.1f Zoom Adapter à l\'écran Disposition de l\'application @@ -143,7 +145,7 @@ Langues des extensions Médias préférées Auto - Distribution : %s + Distribution : %s %d min Rechercher sur %s… À re-regarder @@ -289,8 +291,8 @@ Application Light Novel par les mêmes devs Anime app by the same devs Rejoignez le Discord - %1$d h %2$d min - %d min + %1$d h %2$d min + %d min Lire avec CloudStream Lire en direct Fin @@ -304,9 +306,9 @@ Intro Effacer l\'historique Oui - %1$d j %2$d h %3$d min + %1$d j %2$d h %3$d min Stream - Êtes-vous sûr·e de vouloir quitter ? + Êtes-vous sûr·e de vouloir quitter ? Non Téléchargement de la mise à jour… L\'épisode %d sera publié dans @@ -319,7 +321,8 @@ Nouveau Nom du site ID invalide Installer automatiquement les plugins qui sont dans les repository mais qui n\'ont pas encore été installés. - %dm \nrestant + %dm +\nrestant En direct Autres En direct @@ -360,7 +363,7 @@ NSFW 127.0.0.1 %d / 10 - / ?? + /?? /%d SD UHD @@ -406,14 +409,14 @@ Téléchargé %1$d %2$s Tous les %s déjà téléchargés Télécharger la liste de sites que vous voulez utiliser - Téléchargé : %d + Téléchargé : %d Pistes vidéo Redémarrez l\'application pour voir les changements. Toutes les extensions ont été désactivé à cause d\'un crash pour vous aider à trouver l\'extension causant le problème. Mode sans échec activé Taille Version - Note : %s + Note : %s Description Status Installer l\'extension d\'abord @@ -429,10 +432,10 @@ Nom de dépôt (optionnel) plugin Supprimer le repository - Désactivé : %d - Non téléchargé : %d + Désactivé : %d + Non téléchargé : %d %d plugins mis-à-jour - Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci ! + Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci ! %s (Désactivé) Pistes Pistes audio @@ -445,7 +448,9 @@ Installateur de paquet plugins Cela supprimera également tous les plugins du repository - CloudStream n\'a aucun site installé par défaut. Vous devez installer les sites à partir de dépôts. \n \nRejoignez notre Discord ou cherchez en ligne. + CloudStream n\'a aucun site installé par défaut. Vous devez installer les sites à partir de dépôts. +\n +\nRejoignez notre Discord ou cherchez en ligne. Langage Afficher les popups skip pour les intro / fins Ancienne méthode d\'installation @@ -474,7 +479,8 @@ Note (basse à haute) Note (haut à bas) Alphabétique (A à Z) - Votre bibliothèque est vide :( \nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale. + Votre bibliothèque est vide :( +\nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale. Cette liste est vide. Essayez d\'en changer. Android TV Trier par @@ -483,7 +489,8 @@ Ouvrir avec Mis à jour (Nouveau vers ancien) Mis à jour (ancien vers nouveau) - Fichier du mode sans échec trouvé ! \nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. + Fichier du mode sans échec trouvé ! +\nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. Arrêter Annuler Enregistrer @@ -508,7 +515,13 @@ Impossible d\'atteindre GitHub. Activation du proxy jsDelivr… Vous avez déjà voté Désactivé - Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. \n \nSource A : 3 \nQualité B : 7 \nLa priorité vidéo combinée sera de 10. \n \nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! + Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. +\n +\nSource A : 3 +\nQualité B : 7 +\nLa priorité vidéo combinée sera de 10. +\n +\nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! Aucun plugin trouvé dans ce dossier Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN Données mobiles @@ -541,14 +554,20 @@ PIN Favoris Connecté en tant que %s - Des doublons potentiels ont été trouvés dans votre bibliothèque : \n \n%s \n \nSouhaitez-vous ajouter cet élément, remplacer les éléments existants ou annuler l\'action ? + Des doublons potentiels ont été trouvés dans votre bibliothèque : +\n +\n%s +\n +\nSouhaitez-vous ajouter cet élément, remplacer les éléments existants ou annuler l\'action ? Saisir le code PIN pour %s Doublon potentiel trouvé Verrouiller le profil Ignorer la sélection de compte au démarrage Se désabonner S\'abonner - Il semble qu\'un élément potentiellement dupliqué existe déjà dans votre bibliothèque : \'%s. \n \nSouhaitez-vous ajouter cet élément, remplacer l\'élément existant ou annuler l\'action ? + Il semble qu\'un élément potentiellement dupliqué existe déjà dans votre bibliothèque : \'%s. +\n +\nSouhaitez-vous ajouter cet élément, remplacer l\'élément existant ou annuler l\'action ? Saisir le code PIN actuel Pivoter Les liens ont été rechargés @@ -561,7 +580,7 @@ Testez toutes les extensions Afficher les recommandations Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. - Copié ! + Copié ! Nom du dépôt et adresse internet Favori Vos données CloudStream viennent d\'être sauvegardées. Bien que cette éventualité soit très faible, tous les appareils peuvent se comporter différemment. Dans le rare cas où l\'accès à l\'application est bloqué, effacez complètement les données de l\'application et restaurez à partir d\'une sauvegarde. Nous sommes sincèrement désolés pour les désagréments occasionnés par cette situation. @@ -600,10 +619,10 @@ Ouvrir le dépôt Code expire dans %1$dm %2$ds Wiki de CloudStream - Le code PIN est maintenant expiré ! + Le code PIN est maintenant expiré ! Image du code QR Supprimer l\'extension - Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s + Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s Authentification locale Date de sortie (du plus ancien au plus récent) Date de sortie (du plus récent au plus ancien) @@ -619,9 +638,9 @@ Comptes Cette vidéo est un torrent, ce qui signifie que votre activité vidéo peut être suivie.\nAssurez-vous de comprendre le fonctionnement des torrents avant de continuer. Ignorer - Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s - Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s - Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s + Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s + Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s + Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s Recopier l’écran Aucun sous-titre n’a encore été chargé Emplacement du dossier de sauvegarde @@ -641,7 +660,7 @@ Note %s Date %s Mettre à jour les plugins - %d plugin(s) mis à jour avec succès ! + %d plugin(s) mis à jour avec succès ! Aucun plugin n\'a été mis à jour. Note (Plus Haute) Mettre à jour les plugins manuellement @@ -652,7 +671,7 @@ La notification du lecteur pour contrôler la lecture en arrière-plan Date (Plus Récent) Note (Plus Basse) - Démarrage du processus de mise à jour du plugin ! + Démarrage du processus de mise à jour du plugin ! Intégré En ligne La reconnaissance vocale n\'est pas disponible @@ -722,8 +741,8 @@ Déterminez comment les sources vidéo seront triées dans le lecteur Télécharger tout Tout annuler - Voulez-vous télécharger l\'épisode %s ? - Vous voulez annuler tous les téléchargements en file d\'attente ? + Voulez-vous télécharger l\'épisode %s ? + Vous voulez annuler tous les téléchargements en file d\'attente ? %d téléchargement actif %d téléchargements actifs @@ -735,7 +754,4 @@ %d téléchargements en attente Afficher les métadata de l\'overlay du lecteur vidéo - Vidéo - Prévisualisation - Direct diff --git a/app/src/main/res/values-b+gl/strings.xml b/app/src/main/res/values-b+gl/strings.xml index e3775b091..ede5fd0dc 100644 --- a/app/src/main/res/values-b+gl/strings.xml +++ b/app/src/main/res/values-b+gl/strings.xml @@ -13,7 +13,8 @@ Póster do Episodio Regresar Cambiar provedor - Nova actualización atopada! \n%1$s -> %2$s + Nova actualización atopada! +\n%1$s -> %2$s Recheo %d min Configuración diff --git a/app/src/main/res/values-b+hi/strings.xml b/app/src/main/res/values-b+hi/strings.xml index 4e013ab36..c9030a00a 100644 --- a/app/src/main/res/values-b+hi/strings.xml +++ b/app/src/main/res/values-b+hi/strings.xml @@ -2,7 +2,8 @@ स्पीड (%.2fx) - नया अपडेट आया है! \n%1$s -> %2$s + नया अपडेट आया है! +\n%1$s -> %2$s होम खोजें डाउनलोडस @@ -86,7 +87,8 @@ रद्द करें रोकें फिर से चलाएं - इससे %s स्थायी रूप से हट जाएगा \nक्या आपका निर्णय निश्चित है ? + इससे %s स्थायी रूप से हट जाएगा +\nक्या आपका निर्णय निश्चित है ? अभी चालू है मुकम्मल हुया स्थिति @@ -151,7 +153,11 @@ %d मिनट क्लाउडस्ट्रीम क्लाउडस्ट्रीम के साथ चलाएं - आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं: \n \n%s \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? + आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं: +\n +\n%s +\n +\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? %s के लिए पिन दर्ज करें संभावित डुप्लिकेट मिला अपडेट शुरू हुआ @@ -171,7 +177,9 @@ अकाउंट चुनिये लोडिंग स्किप करे लोडिंग… - ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\' \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? + ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\' +\n +\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? पिन दर्ज करें पिन लिंक पुन्ह खुली diff --git a/app/src/main/res/values-b+hr/strings.xml b/app/src/main/res/values-b+hr/strings.xml index 7927a15c4..c2c0379f5 100644 --- a/app/src/main/res/values-b+hr/strings.xml +++ b/app/src/main/res/values-b+hr/strings.xml @@ -19,7 +19,8 @@ Brzina (%.2f×) Ocjena: %.1f - Pronađeno je novo ažuriranje! \n%1$s -> %2$s + Pronađeno je novo ažuriranje! +\n%1$s -> %2$s Umetak %d min CloudStream @@ -185,8 +186,10 @@ Nastavi −30 +30 - Ovo će trajno izbrisati %s \nJeste li sigurni? - %dmin \npreostalo + Ovo će trajno izbrisati %s +\nJeste li sigurni? + %dmin +\npreostalo U tijeku Završeno Status @@ -408,7 +411,9 @@ Preuzeto: %d Onemogućeno: %d Nepreuzeto: %d - CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. \n \nPridružite se našem Discordu ili tražite online. + CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. +\n +\nPridružite se našem Discordu ili tražite online. Prikaži repozitorije zajednice Javni popis Koristi velika slova za sve titlove @@ -493,9 +498,11 @@ Abecedno (Ž do A) Odaberite biblioteku Otvori sa - Vaša je biblioteka prazna :( \nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. + Vaša je biblioteka prazna :( +\nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu. - Pronađena je datoteka sigurnog načina rada! \nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. + Pronađena je datoteka sigurnog načina rada! +\nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. Prikazan player – Količina pomicanja Količina pomicanja koja se koristi kada je player vidljiv Player skriven – Količina pomicanja @@ -534,13 +541,23 @@ Onemogući U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen. Provjeri URL i pokušaj VPN - Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. \n \nIzvor A: 3 \nKvaliteta B: 7 \nImat će kombinirani prioritet videa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. +\n +\nIzvor A: 3 +\nKvaliteta B: 7 +\nImat će kombinirani prioritet videa od 10. +\n +\nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! Već si glasao/la Učestalost spremanja sigurnosne kopije %s uklonjeno iz favorita Favoriti %s dodano u favorite - Potencijalni duplikati pronađeni su u vašoj biblioteci: \n \n%s \n \nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeće ili poništiti radnju? + Potencijalni duplikati pronađeni su u vašoj biblioteci: +\n +\n%s +\n +\nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeće ili poništiti radnju? Pronađen potencijalni duplikat Zaključaj profil Dodaj u favorite @@ -553,7 +570,9 @@ Pretplata Ukloni iz favorita Odaberite račun - Čini se da potencijalno duplicirana stavka već postoji u vašoj biblioteci: \'%s.\' \n \nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeću ili poništiti radnju? + Čini se da potencijalno duplicirana stavka već postoji u vašoj biblioteci: \'%s.\' +\n +\nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeću ili poništiti radnju? Unesite PIN PIN Unesite trenutni PIN @@ -577,7 +596,8 @@ Ime repozitorija i URL kopirano! Zaključaj s biometrijskim podatcima - %s \npreostalo + %s +\npreostalo Pogreška pri pristupanju međuspremnika. Pokušaj ponovo. Otključaj CloudStream Lozinka/PIN autentifikacija @@ -596,7 +616,7 @@ Otključaj aplikaciju pomoću otiska prsta, ID-a lica, PIN-a, uzorka i lozinke. Sljedeća u %s Pogreška pri kopiranju. Kopirajte zapisnik i kontaktirajte podršku aplikacije. - Da biste osigurali neprekinuta preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom na \"U redu\" prikazat će vam se dijaloški okvir s zahtjevom. Molimo vas da pritisnete \"Dopusti\". \n\nNapominjemo da ovo dopuštenje ne znači da će CS3 trošiti vašu bateriju. Aplikacija će raditi u pozadini samo kada je to potrebno, primjerice prilikom primanja obavijesti ili preuzimanja videozapisa iz službenih proširenja. + Da biste osigurali neprekinuta preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom na \"U redu\" prikazat će vam se dijaloški okvir s zahtjevom. Molimo vas da pritisnete \"Dopusti\". \n\nNapominjemo da ovo dopuštenje ne znači da će CS3 trošiti vašu bateriju. Aplikacija će raditi u pozadini samo kada je to potrebno, primjerice prilikom primanja obavijesti ili preuzimanja videozapisa iz službenih proširenja. Vaši CloudStream podaci su sada spremljeni u sigurnosnu kopiju. Iako je vjerojatnost mala, neki se uređaji mogu ponašati drugačije. Ako izgubite pristup aplikaciji, potpuno izbrišite podatke aplikacije i obnovite ih pomoću sigurnosne kopije. Ispričavamo se zbog mogućih neugodnosti. Sezona %1$d epizoda %2$d izlazi za Cast duplikat @@ -622,14 +642,22 @@ Izbriši dodatak Dostupno za gledanje offline Označi sve - Stvarno želite trajno izbrisati sljedeće stavke? \n \n%s - Stvarno želite trajno izbrisati sljedeće epizode u %1$s? \n \n%2$s - Trajno ćete izbrisati i sve epizode u sljedećim serijama: \n \n%s + Stvarno želite trajno izbrisati sljedeće stavke? +\n +\n%s + Stvarno želite trajno izbrisati sljedeće epizode u %1$s? +\n +\n%2$s + Trajno ćete izbrisati i sve epizode u sljedećim serijama: +\n +\n%s Odaberi stavke za brisanje Odznači sve Izbriši (%1$d | %2$s) Izbriši datoteke - Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji? \n \n%s + Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji? +\n +\n%s Još nije učitan nijedan titl Pretpregled trake za traženje Omogući minijaturu pregleda na traci za pretraživanje @@ -721,7 +749,7 @@ Gore u sredini Gore desno Dodatna svjetlina - Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana + Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana dodatna_svjetlina_uključena Prijedlozi za pretraživanje Prikaži prijedloge za pretraživanje tijekom tipkanja @@ -747,8 +775,4 @@ Želiš li preuzeti epizodu %s? Želiš li otkazati sva preuzimanja u redu čekanja? Prikaži ploču glumačke postave - Video - Pregled - Uživo - Prikaži sloj metapodataka playera diff --git a/app/src/main/res/values-b+hu/strings.xml b/app/src/main/res/values-b+hu/strings.xml index b753eadb2..c50b18133 100644 --- a/app/src/main/res/values-b+hu/strings.xml +++ b/app/src/main/res/values-b+hu/strings.xml @@ -10,7 +10,8 @@ Szolgáltató Váltás Sebesség (%.2fx) Értékelés: %.1f - Új frissítés található! \n%1$s -> %2$s + Új frissítés található! +\n%1$s -> %2$s %d perc %1$sEp%2$d CloudStream @@ -148,7 +149,8 @@ Ep Nem található epizód Fájl törlése - %dp \nhátra + %dp +\nhátra Időtartam Elérhető Használatban @@ -204,7 +206,8 @@ %1$s %2$d%3$s Nincs évad +30 - Ezzel véglegesen törli a %s \nBiztosan törli? + Ezzel véglegesen törli a %s +\nBiztosan törli? Folyamatban levő Év Webhely @@ -320,7 +323,8 @@ Támogatott Alkalmazásfrissítés letöltése… Frissítve (újabbtól a régebbihez) - Úgy tűnik, a könyvtárad üres :( \nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. + Úgy tűnik, a könyvtárad üres :( +\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani. Max 4K @@ -412,7 +416,9 @@ Zárt feliratok eltávolítása a feliratokból 18+ Ez az összes tároló bővítményt is törli - A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. \n \nCsatlakozz a Discord-unkhoz vagy keress online. + A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. +\n +\nCsatlakozz a Discord-unkhoz vagy keress online. Verzió Megjelölés megtekintettként Eltávolítás a megnézettek közül @@ -466,7 +472,8 @@ Közreműködők Betűrendben (Z-től az A-ig) Könyvtár kiválasztása - Biztonságos módú fájlba ütköztünk! \nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. + Biztonságos módú fájlba ütköztünk! +\nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. Normál %s betöltve Beállítás kihagyása @@ -529,8 +536,18 @@ Profilok Eltávolítás kedvencekből Adja meg a jelenlegi PIN-t - Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. \n \nForrás A: 3 \nMinőség B: 7 \nEzek összértéke egy 10-es videó prioritást eredményez. \n \nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! - Potenciálisan dupla elemek a könyvtárjában: \n \n%s \n \nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? + Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. +\n +\nForrás A: 3 +\nMinőség B: 7 +\nEzek összértéke egy 10-es videó prioritást eredményez. +\n +\nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! + Potenciálisan dupla elemek a könyvtárjában: +\n +\n%s +\n +\nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? Fiók választás kihagyása belépéskor Használjon alapértelmezett fiókot Elforgatás @@ -539,7 +556,9 @@ %s hozzáadva a kedvencekhez %s eltávolítva a kedvencekből Hozzáadás a kedvencekhez - Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' \n \nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? + Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' +\n +\nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? Adja meg a PIN-t Profil Zárolása Válasszon egy fiókot diff --git a/app/src/main/res/values-b+in/strings.xml b/app/src/main/res/values-b+in/strings.xml index 7d6475f31..9c4c7fc11 100644 --- a/app/src/main/res/values-b+in/strings.xml +++ b/app/src/main/res/values-b+in/strings.xml @@ -169,8 +169,10 @@ Lanjutkan -30 +30 - Ini akan secara permanen menghapus %s \nApakah anda yakin? - %dm \ntersisa + Ini akan secara permanen menghapus %s +\nApakah anda yakin? + %dm +\ntersisa Masih Berlanjut Tamat Status @@ -388,7 +390,9 @@ %d plugin diperbarui Lihat repositori komunitas Daftar publik - CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. \n \nBergabunglah dengan Discord kami atau cari secara online. + CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. +\n +\nBergabunglah dengan Discord kami atau cari secara online. URL Repositori atau Kode Pendek Buat Akun Error @@ -483,7 +487,8 @@ Hapus dari tontonan Peramban Pilih pustaka - Yahh daftar pustaka kamu kosong :( \nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu. + Yahh daftar pustaka kamu kosong :( +\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu. Pustaka Urutkan berdasarkan Urutkan @@ -495,7 +500,8 @@ Abjad (Z ke A) Buka dengan Yahh daftar ini kosong. Coba ganti ke yang lain. - Mode aman file ditemukan! \nTidak memuat ekstensi pada startup sampai berkas dihapus. + Mode aman file ditemukan! +\nTidak memuat ekstensi pada startup sampai berkas dihapus. Sembunyikan Pemutaran - Geser Pemutar terlihat - Geser Geser untuk menghilangkan @@ -521,7 +527,13 @@ Kualitas nonton yang diinginkan (Data Seluler) Data seluler Bantuan - Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. \n \nSumber A: 3 \nKualitas B: 7 \nAkan memiliki prioritas video yang digabung 10. \n \nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! + Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. +\n +\nSumber A: 3 +\nKualitas B: 7 +\nAkan memiliki prioritas video yang digabung 10. +\n +\nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! Profil %d Wifi Pengaturan default @@ -581,7 +593,8 @@ Setelah beberapa kali gagal, perintah akan ditutup. Cukup mulai ulang aplikasi untuk mencoba lagi. Batalkan favorit Buka kunci CloudStream - %s \ntersisa + %s +\ntersisa Favorit Kunci dengan Biometrik Nama dan URL repositori @@ -625,10 +638,18 @@ Pilih Semua Batal Pilih Semua Hapus File - Apakah Anda yakin ingin menghapus item berikut secara permanen? \n \n%s - Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen? \n \n%2$s - Anda juga akan menghapus semua episode dalam seri berikut secara permanen: \n \n%s - Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen? \n \n%s + Apakah Anda yakin ingin menghapus item berikut secara permanen? +\n +\n%s + Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen? +\n +\n%2$s + Anda juga akan menghapus semua episode dalam seri berikut secara permanen: +\n +\n%s + Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen? +\n +\n%s Hapus (%1$d | %2$s) Hapus plugin Tidak bisa mendapatkan kode PIN perangkat, coba autentikasi lokal @@ -744,7 +765,4 @@ Apakah kamu ingin mengunduh episode %s? Apakah kamu ingin membatalkan semua unduhan dalam antrean? Tampilkan overlay metadata pemutar - Live - Video - Pratinjau diff --git a/app/src/main/res/values-b+it/strings.xml b/app/src/main/res/values-b+it/strings.xml index 2672f39b1..55da49687 100644 --- a/app/src/main/res/values-b+it/strings.xml +++ b/app/src/main/res/values-b+it/strings.xml @@ -6,7 +6,7 @@ L\'episodio %d uscirà in %1$dd %2$dh %3$dm %1$dh %2$dm - %d min + %d min Poster Poster @@ -19,7 +19,8 @@ Velocità (%.2fx) Valutato: %.1f - Nuovo aggiornamento trovato! \n%1$s -> %2$s + Nuovo aggiornamento trovato! +\n%1$s -> %2$s Filler %d min @@ -185,8 +186,10 @@ Riprendi -30 +30 - Stai per eliminare permanentemente %s \nSei sicuro? - %dm \nrimanenti + Stai per eliminare permanentemente %s +\nSei sicuro? + %dm +\nrimanenti In corso Completato Stato @@ -225,7 +228,7 @@ Livestream NSFW Altro - Errore fonte + Errore sorgente Errore remoto Errore del renderer Errore imprevisto del lettore @@ -247,7 +250,7 @@ Controlla aggiornamenti Blocca Ridimensiona - Fonte + Sorgente Salta OP Non mostrare di nuovo Salta questo aggiornamento @@ -346,7 +349,7 @@ Protagonista Supporto Secondario - Fonte + Sorgente Casuale Prossimamente… Cam @@ -409,7 +412,9 @@ Disabilitato: %d Non scaricato: %d Aggiornati %d plugin - CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. \n \nUnisciti al nostro Discord o cerca online. + CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. +\n +\nUnisciti al nostro Discord o cerca online. Visualizza i repository della comunità Lista pubblica Tutti i sottotitoli in maiuscolo @@ -497,13 +502,15 @@ Aggiornato (Da vecchio a nuovo) Alfabetico (A - Z) Alfabetico (Z - A) - La tua libreria è vuota :( \nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale. + La tua libreria è vuota :( +\nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale. Seleziona libreria Apri con Libreria Ordina Questo elenco è vuoto. Prova a passare a un altro. - File \"safe mode\" trovato! \nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. + File \"safe mode\" trovato! +\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. Intervallo di ricerca utilizzato quando il lettore è nascosto TV Android Intervallo di ricerca utilizzato quando il lettore è visibile @@ -527,7 +534,13 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. \n \nFonte A: 3 \nQualità B: 7 \nAvranno una priorità video combinata di 10. \n \nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! + Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. +\n +\nFonte A: 3 +\nQualità B: 7 +\nAvranno una priorità video combinata di 10. +\n +\nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! Profilo %d Wi-Fi Imposta predefinito @@ -547,7 +560,11 @@ %s rimosso dai preferiti Preferiti %s aggiunto ai preferiti - Dei possibili duplicati sono stati trovati nella tua libreria: \n \n%s \n \nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione? + Dei possibili duplicati sono stati trovati nella tua libreria: +\n +\n%s +\n +\nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione? Frequenza di backup Trovato Possibile Duplicato Aggiungi ai preferiti @@ -560,7 +577,9 @@ Iscriviti Rimuovi dai preferiti Seleziona un account - Sembra che nella tua libreria esista già un elemento potenzialmente duplicato: \'%s.\' \n \nDesideri aggiungere comunque questo elemento, sostituire quello esistente o annullare l\'azione? + Sembra che nella tua libreria esista già un elemento potenzialmente duplicato: \'%s.\' +\n +\nDesideri aggiungere comunque questo elemento, sostituire quello esistente o annullare l\'azione? Inserisci PIN PIN Inserisci PIN corrente @@ -590,7 +609,8 @@ Dopo alcuni tentativi falliti, il prompt si chiuderà. Riavvia semplicemente l\'app per riprovare. È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. Non preferito - %s \nresiduo + %s +\nresiduo Preferito Nome e URL del repository copiato! @@ -632,12 +652,20 @@ Seleziona tutto Deseleziona tutto Elimina (%1$d | %2$s) - Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s? \n \n%2$s - Eliminerai definitivamente anche tutti gli episodi delle seguenti serie: \n \n%s - Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie? \n \n%s + Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s? +\n +\n%2$s + Eliminerai definitivamente anche tutti gli episodi delle seguenti serie: +\n +\n%s + Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie? +\n +\n%s Data di rilascio (dal più vecchio) Elimina file - Sei sicuro di voler eliminare definitivamente i seguenti elementi? \n \n%s + Sei sicuro di voler eliminare definitivamente i seguenti elementi? +\n +\n%s Anteprima barra di avanzamento Abilita miniatura di anteprima sulla barra di avanzamento Nessun sottotitolo caricato @@ -670,7 +698,7 @@ Data di messa in onda (più vecchia) Valutazione %s Avvio del processo di aggiornamento plugin! - %d plugin aggiornati correttamente + %d plugin aggiornati con successo! Nessun plugin è stato aggiornato. Notifiche lettore La notifica del lettore per controllare la riproduzione in background @@ -733,7 +761,7 @@ Cancella suggerimenti Mostra pannello cast Info sui media - Nome fonte + Nome sorgente Luminosità extra Attiva il filtro di luminosità quando viene superato il 100% della luminosità dello schermo extra_brightness_enabled @@ -753,20 +781,7 @@ %d download in coda %d download in coda - Priorità fonti - Decidi come le fonti video devono essere ordinate nel lettore + Priorità sorgente + Decidi come le sorgenti video devono essere ordinate nel lettore Mostra sovrapposizione metadati lettore - Video - Anteprima - Live - Impostazioni profilo - Nascondi fonti con priorità negativa - Nascondi fonti con errori - - %d collegamento nascosto - %d collegamenti nascosti - %d collegamenti nascosti - - Mostra orologio in tempo reale - Mostra un orologio in tempo reale nella parte superiore dello schermo. Si applica alla homepage e al lettore diff --git a/app/src/main/res/values-b+iw/strings.xml b/app/src/main/res/values-b+iw/strings.xml index 3b2d0153a..0b0479679 100644 --- a/app/src/main/res/values-b+iw/strings.xml +++ b/app/src/main/res/values-b+iw/strings.xml @@ -6,7 +6,8 @@ לשנות ספק מהירות (%.2fx) דירוג: %.1f - נמצא עדכון חדש! \n%1$s -> %2$s + נמצא עדכון חדש! +\n%1$s -> %2$s סינון %d דקות קלאודסטרים @@ -145,8 +146,10 @@ המשך -30 +30 - %dדקות \nנותרו - ‬פעולה זאת תמחק לצמיתות את %s \nהאם אתם בטוחים? + %dדקות +\nנותרו + ‬פעולה זאת תמחק לצמיתות את %s +\nהאם אתם בטוחים? מתמשך משך זמן דירוג @@ -422,8 +425,10 @@ קרדיטים מיין בחר ספרייה - נראה שהספרייה שלכם ריקה :( \nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. - קובץ מצב בטוח נמצא! \nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. + נראה שהספרייה שלכם ריקה :( +\nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. + קובץ מצב בטוח נמצא! +\nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. לא ניתן להתקין את הגרסה החדשה של האפליקציה הורדת אצווה תוסף @@ -439,7 +444,11 @@ הורד: %d מוגבל: %d לא מורד: %d - לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים. \n \nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה. \n \nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט. + לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים. +\n +\nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה. +\n +\nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט. הצג מאגרים קהילתיים רשימה ציבורית לשים את הכתוביות באותיות רישיות @@ -521,7 +530,13 @@ קביעה כברירת מחדל עבר מעקף ספק אינטרנט - כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. \n \nמקור א: 3 \nאיכות ב: 7 \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! + כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. +\n +\nמקור א: 3 +\nאיכות ב: 7 +\nיגרמו לעדיפות הסרטון להיות 10. +\n +\nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! עונה %1$d פרק %2$d תשודר ב: %1$d שעות %2$d דקות %3$d שניות %1$d דקות %2$d שניות diff --git a/app/src/main/res/values-b+ja/strings.xml b/app/src/main/res/values-b+ja/strings.xml index c5e6272b2..16cfb49c6 100644 --- a/app/src/main/res/values-b+ja/strings.xml +++ b/app/src/main/res/values-b+ja/strings.xml @@ -62,7 +62,8 @@ ローディング… ブラウザで開く シーズン - 残り \n%d分 + 残り +\n%d分 再生エピソード ダウンロード済 バックアップ @@ -81,7 +82,8 @@ 次のランダム 戻り 評価: %.1f - 新しいアップデートを発見! \n%1$s -> %2$s + 新しいアップデートを発見! +\n%1$s -> %2$s %d分 %sを検索… ソース @@ -729,8 +731,4 @@ ソースの優先順位 プレイヤーでのビデオソースの並び順を設定します - プレイヤーメタデータオーバーレイを表示 - 動画 - プレビュー - ライブ diff --git a/app/src/main/res/values-b+kn/strings.xml b/app/src/main/res/values-b+kn/strings.xml index ba6da787c..22a45b906 100644 --- a/app/src/main/res/values-b+kn/strings.xml +++ b/app/src/main/res/values-b+kn/strings.xml @@ -83,7 +83,8 @@ ಶೇರ್ ಫೈಲ್ ಅಳಿಸಿ ಹೆಚ್ಚಿನ ಮಾಹಿತಿ - ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ \n%1$s-%2$s + ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ +\n%1$s-%2$s ಲೋಡಿಂಗ್… ಡೌನ್‌ಲೋಡ್ ಭಾಷೆಗಳನ್ನು ಮಾಡಿ ಲೈವ್‌ಸ್ಟ್ರೀಮ್ ಪ್ಲೇ ಮಾಡಿ diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 4feeabe33..9b07d259e 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -11,7 +11,8 @@ 배경 미리보기 속도 (%.2fx) 평점: %.1f - 새로운 업데이트! \n%1$s -> %2$s + 새로운 업데이트! +\n%1$s -> %2$s %d분 CloudStream CloudStream으로 재생 @@ -27,7 +28,7 @@ 공유 브라우저로 열기 브라우저 - 로딩 스킵 + 로딩 건너뛰기 로딩중… 시청 보류 @@ -56,8 +57,8 @@ 재생 속도 글자 색상 윤곽선 색상 - 자막 배경 색상 - 자막 창 색상 + 배경 색상 + 배경 색상 윤곽선 유형 자막 높이 폰트 @@ -123,7 +124,7 @@ 어두운 오버레이 대신 앱 플레이어의 시스템 밝기를 사용합니다 시청 진행 상황 업데이트 현재 에피소드 진행 상황을 자동으로 동기화합니다 - 데이터 복원 + 백업에서 데이터 복원 데이터 백업 파일에서 데이터를 복원하지 못했습니다 %s 저장된 데이터 @@ -160,8 +161,10 @@ 평점 +30 - %s이(가) 영구적으로 삭제됩니다 \n정말 삭제하시겠습니까? - %d분 \n남음 + %s가 영구 삭제됩니다 +\n정말 삭제하시겠습니까? + %d분 +\n남음 사이트 시간 개요 @@ -181,7 +184,7 @@ %s로 재생 자동 다운로드 다운로드 소스 목록 - 링크 초기화 + 링크 새로고침 자막 다운로드 화질 라벨 더빙 라벨 @@ -192,7 +195,7 @@ 크기 조정 소스 오프닝 스킵 - 다음에 업데이트 + 이 업데이트 건너뛰기 선호하는 화질 (WiFi) 선호하는 화질 (모바일 데이터) 플레이어 내 표시 정보 @@ -294,7 +297,11 @@ 저장소 삭제 사용하려는 사이트 목록 다운로드 다운로드됨: %d - CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. \n \nDiscord에 가입하거나 온라인으로 검색해 보세요. + CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. +\n +\nSky UK Limited의 무분별한 DMCA 게시 중단으로 인해 앱에서 저장소 사이트를 연결 할 수 없습니다. +\n +\nDiscord에 가입하거나 온라인으로 검색해 보세요. 커뮤니티 저장소 보기 공개 목록 자막 대문자화 표시 @@ -315,7 +322,7 @@ 충돌 정보 보기 언어 에피소드 %d 공개! - Picture-in-picture 모드 + PIP 모드 플레이어 크기 조정 버튼 미니플레이어를 통해 다른 앱 상단에서 계속 재생합니다 레터박스 제거 @@ -324,7 +331,7 @@ 백업 파일을 성공적으로 로드하였습니다 정보 고급 검색 - 설정 프로세스 재실행 + 설정 프로세스 다시 실행 APK 인스톨러 Github 소스 오류 @@ -431,16 +438,16 @@ 먼저 확장 프로그램을 설치하세요 앱을 찾을 수 없음 모든 언어 - %s 스킵 + 건너뛰기 %s 오프닝 엔딩 혼합 엔딩 혼합 오프닝 크레딧 - 인트로 + 소개 기록 삭제 기록 - 오프닝/엔딩 시 스킵 팝업 표시 + 오프닝/엔딩 시 건너뛰기 팝업 표시 텍스트가 너무 많습니다. 클립보드에 저장할 수 없습니다. 시청에서 삭제 정말 종료하시겠습니까? @@ -459,8 +466,10 @@ 알파벳순 (A에서 Z) 알파벳순 (Z에서 A) 다음으로 열기 - 라이브러리가 비어 있습니다 :( \n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요. - 안전 모드 파일을 찾았습니다! \n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다. + 라이브러리가 비어 있습니다 :( +\n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요. + 안전 모드 파일을 찾았습니다! +\n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다. HLS 재생목록 내부 플레이어 선호하는 동영상 플레이어 @@ -476,7 +485,7 @@ @string/home_play 플롯을 찾을 수 없음 설명을 찾을 수 없음 - 로그캣 🐈 보기 + Logcat 🐈 표시 애니메이션용 필러 에피소드 표시 통과 계속 @@ -508,11 +517,11 @@ 보안 계정 리포지토리에서 플러그인을 찾을 수 없습니다 - 복사 완료! + 복사됨! 레포지토리 이름 및 URL 본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다. CloudStream 위키 - 링크 초기화 완료 + 링크 새로고침 완료 백업 빈도 즐겨찾기 QR 이미지 @@ -563,11 +572,17 @@ 기본값 설정 구독 사용 - 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? 전부 대체 추가 즐겨찾기에서 %s 제거 - 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: \n \n%s \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: +\n +\n%s +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? 계정 선택 기본 계정 사용 회전 @@ -584,7 +599,7 @@ 재설정 플러그인 다운로드를 필터링할 모드 선택 CloudStream 데이터 백업이 완료되었습니다. 드문 경우지만, 기기에 따라 앱 접속이 안 되는 오류가 발생할 수 있습니다. 만약 앱이 열리지 않는다면, 앱 데이터를 완전히 삭제(초기화)한 후 이 백업 파일로 복구해 주시기 바랍니다. 이용에 불편을 드려 대단히 죄송합니다. - 스마트폰이나 컴퓨터에서 %s 위의 코드를 입력하세요 + 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 구독 중인 TV 쇼의 알림을 받고 다운로드를 끊김 없이 완료하려면, CloudStream의 백그라운드 실행 권한이 필요합니다. \'확인\'을 누른 후 나타나는 요청 창에서 \'허용\'을 선택해 주세요.\n\n참고로, 이 권한을 허용한다고 해서 배터리가 계속 소모되는 것은 아닙니다. 알림을 받거나 공식 확장 프로그램에서 영상을 다운로드할 때처럼 꼭 필요한 상황에서만 백그라운드 작업을 수행합니다. 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택 화면에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. \n \n참고 A: 3 \n품질 B: 7 \n총 비디오 우선 순위는 10입니다. \n \n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! 시즌 %1$d 에피소드 %2$d 공개 예정 @@ -593,7 +608,8 @@ 추천목록 보기 플레이어에 속도 옵션을 추가합니다 %s 후 공개 예정 - %s \n남음 + %s +\n남음 잠재적 중복 발견 %s의 PIN 입력 즐겨찾기에서 제거 @@ -611,10 +627,18 @@ 로컬 비디오 열기 파일 삭제 삭제 (%1$d | %2$s) - 다음 항목을 영구적으로 삭제 하시겠습니까? \n \n%s - 다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s? \n \n%2$s - 또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다: \n \n%s - 다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까? \n \n%s + 다음 항목을 영구적으로 삭제 하시겠습니까?? +\n +\n%s + 다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s? +\n +\n%2$s + 또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다: +\n +\n%s + 다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까?? +\n +\n%s 공개일 (최신순) 공개일 (오래된순) 플레이어 내 버튼명 숨기기 @@ -715,7 +739,7 @@ 입력하는 동안 검색어 제안 표시 출연진 정보 표시 - 자막 배경 테두리 곡률 + 배경 테두리 곡률 공급자 새로고침 검색어 제안 제안 삭제 @@ -730,7 +754,4 @@ 새로고침 최대 밝기 확장 활성화 플레이어에 메타데이터 오버레이 표시 - 비디오 - 프리뷰 - 라이브 diff --git a/app/src/main/res/values-b+lt/strings.xml b/app/src/main/res/values-b+lt/strings.xml index ff92ac093..cb2d816f3 100644 --- a/app/src/main/res/values-b+lt/strings.xml +++ b/app/src/main/res/values-b+lt/strings.xml @@ -25,18 +25,19 @@ Kopijuoti Duoti bananai Daugiau informacijos - Atsisiuntimai + Atsiuntimai Automatiškai pasirinkti kalbą Klaida kraunant nuorodas +30 - Atsisiuntimas baigtas + Atsiuntimas baigtas Tęsti žiūrėjimą - Rastas atnaujinimas!\n%1$s -> %2$s + Rastas atnaujinimas! +\n%1$s -> %2$s Atsisiųsti kalbas Ieškoti naudojant tiekėjus Grįžti atgal Siunčiama - Daugiau parinkčių + Daugiau pasirinkčiu Paleisti seriją Grotuvo greitis %d Bananai duoti kūrėjams @@ -45,7 +46,7 @@ Žanrai -30 Serijos plakatas - Gali reikėti VPN šiam tiekėjui, kad veiktų teisingai + Gali reikėti VPN šitam tiekėjui, kad veiktų teisingai Ieškoti %s… Github Nėra duotu bananų @@ -66,7 +67,7 @@ Atšaukti Pradėti Filmukas - Atsisiuntimas atšauktas + Atsiuntimas atšauktas Išplėstinė paieška Tuščias sąrašas. Pabandykite pasirinkti kitą sąrašą. Chromecast subtitrai @@ -87,16 +88,17 @@ Pratęsti siuntimą Azijietiškos dramos Serija - Jūsų biblioteka tuščia :( \nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos. + Jūsų biblioteka tuščia :( +\nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos. Pradėti sekančia seriją, kai dabartinė baigsis Teksto spalva Užbaigta Naudoti sistemos ryškumą programos grotuve vietoj tamsumo Nepavyko atstatyti duomenis iš failo %s - Paleisti anonsą - Paleisti gyvą transliaciją + Paleisti anonsa + Paleisti gyva transliacija Nerasta serijų - Šis tiekėjas yra iš Torrent\'ų, rekomenduojamas VPN + Šis tiekėjas yra iš Torrentų, VPN rekomenduojama naudoti Nepavyko Plakatas Paleisti failą @@ -108,21 +110,21 @@ Perdaryti nustatymo procesą %1$d-%2$d Duoti bananą kūrėjams - Sugrįšti + Sugryšti Nuoroda nukopijuota į iškarpinę Paieška Informacija - Praleisti įkėlimą + Praleisti įkėlima Informacija Serija %d bus išleista Išsaugoti Perdaug teksto. Nepavyko išsaugoti i iškarpynę. - Atsisiuntimas nepavyko + Atsiuntimas nepavyko Pasidalinti Pagrindinis Plakatas Šaltiniai Nustatymai - Paieška + Ieškoti Kraunama… Pašalinti Daugiau informacijos @@ -138,7 +140,7 @@ Uždaryti Nustatyti žiūrėjimo statusą Paleisti su CloudStream - Subtitrų lygis + Subtitrų iškėlimas Serijos Skleisti: %s Sezonas @@ -179,7 +181,11 @@ 127.0.0.1 Atsiųsta %1$d %2$s Praleisti %s - Pagal numatytuosius nustatymus „CloudStream“ neturi įdiegtų svetainių. Turite įdiegti svetaines iš saugyklų. \n \nDėl beprotiško DMCA reikalavimų, kurios atliko Sky UK Limited 🤮, negalime susieti saugyklos svetainės programoje. \n \nPrisijunkite prie mūsų Discord arba ieškokite internete. + Pagal numatytuosius nustatymus „CloudStream“ neturi įdiegtų svetainių. Turite įdiegti svetaines iš saugyklų. +\n +\nDėl beprotiško DMCA reikalavimų, kurios atliko Sky UK Limited 🤮, negalime susieti saugyklos svetainės programoje. +\n +\nPrisijunkite prie mūsų Discord arba ieškokite internete. Mobilūs duomenys šaunusPrisijungimoVardas Autoriai @@ -248,51 +254,4 @@ Ar tikrai norite išeiti? Pašalinti iš žiūrimų Garso takelis - Užpildymas - %d min - Namai - Atsisiuntimų eilė - Balso atpažinimas nepasiekiamas - Pradėkite kalbėti… - Nežiūrimas - Paleisti visą seriją - Šis įrašas yra Torrent\'e, tai reiškia, kad tavo įrašo veikla gali būti sekama. Įsitikinkite, kad suprantate Torrenting prieš tęsiant. - Atkurti ryšį… - Pasirinkite elementus, kuriuos norite pašalinti - Šiuo metu atsisiuntimų nėra. - Šiuo metu atsisiuntimų eilėje nėra. - Pasiekiama žiūrėti neprisijungus - Pasirinkti viską - Panaikinti visus pasirinkimus - Tinklo srautas - Atidaryti vietinį video - Nuorodos perkrautos - Subtitrai - Repozitorijos pavadinimas ir adresas - nukopijuota! - Naujos serijos pranešimas - Ieškoti kituose plėtiniuose - Rodyti rekomendacijas - Apvado spalva - Rėmelio tipas - Ieškoti naudojant tipus - Laikyti ilgai, kad sugražinti pradinius nustatymus - Įkelti šriftus sudedant juos į %s - Metaduomenys nesuteikiamos svetainės, vaizdo įkėlimas nepavyks, jeigu jo nebus svetainėje. - Aprašymas - Nėra siužeto aprašymo - Aprašymas nerastas - Rodyti Logcat - Vaizdas vaizde - Tęsia atkūrimą mažame grotuve virš kitų programų - Grotuvo dydžio pakeitimo mygtukas - Pašalinti juodas paraštes - Subtitrai - Chromecast subtitrų nustatymai - Atkūrimo sparta - Prideda greičio pasirinkimą grotuve - Braukite, kas ieškotumėte - Braukite į šonus, kad kontroliuotumėte padėtį vaizdo įraše - Braukite, kad pakeistumėte nustatymus - Braukite aukštyn arba žemyn ekrano kairėje arba dešinėje, jei norite pakeisti ryškumą arba garsą diff --git a/app/src/main/res/values-b+lv/strings.xml b/app/src/main/res/values-b+lv/strings.xml index 531b5420b..7d74354ea 100644 --- a/app/src/main/res/values-b+lv/strings.xml +++ b/app/src/main/res/values-b+lv/strings.xml @@ -12,7 +12,8 @@ Apskatīt background Ātrums (%.2fx) Lidzīgi: %.1f - Jauns atjauninājums atrasts! \n%1$s -> %2$s + Jauns atjauninājums atrasts! +\n%1$s -> %2$s %d galvenais CloudStream Atskaņo ar cloudstream @@ -192,8 +193,10 @@ Atsākt -30 +30 - Šis pilnibā dzesīs %s \nEsat parliecināts? - %dm \natlikušas + Šis pilnibā dzesīs %s +\nEsat parliecināts? + %dm +\natlikušas Pabeigts Statuss Gads @@ -287,12 +290,12 @@ Parasts Izskats Funkcijas - Vispārīgie + Ģenerāls Randomā poga Rādīt izlases pogu Sākums un Bibliotēka sadaļās Papildinājuma valodas Lietotnes izkārtojums - Vēlamie multimediji + Izvēlētā media Iespējot nepiedienīgu, izaicinošu saturu (NSFW) atbalstītajos papildinājumos Subtitru kodējums Devēji @@ -372,7 +375,7 @@ Kļūda Noņemt slēgtos parakstus no subtitriem Noņemt lieko no subtitriem (piemēram, reklāmu) - Atlasīt pēc vēlamās multimediju valodas + Filtrēt pēc vēlamās multivides valodas Ekstras Treileris https://piemērs.com/piemērs.mp4 @@ -382,7 +385,7 @@ Iepriekšējais Izlaist uzstādīšanu Mainiet lietotnes izskatu, lai tā atbilstu savai ierīcei - Ko jūs vēlētos skatīt + Ko tu vēlies redzēt Pabeigts Papildinājumi Pievienot repozitoriju @@ -455,7 +458,8 @@ Alfabētiskā secībā (Z līdz A) Atlasiet Bibliotēka Atvērt ar - Šķiet, ka jūsu bibliotēka ir tukša :( \nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai. + Šķiet, ka jūsu bibliotēka ir tukša :( +\nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai. Atgriest Anulēts %s abonements %d sērija izlaista! @@ -607,11 +611,4 @@ Vai tiešām vēlaties neatgriezeniski dzēst visas šī seriāla, raidījuma epizodes?\n\n%s Pašlaik nav nevienas rindā ievietotas lejupielādes. Atvērt vietējo video - Multimedija - Multimediju informācija - Iespējojiet torrentu Iestatījumi/Pakalpojumu sniedzēji/Vēlamie multimediji sadaļā - Paziņojums par jaunu epizodi - Papildu spilgtums - Papildu spilgtums iespējots - Meklēšanas ieteikumi diff --git a/app/src/main/res/values-b+mk/strings.xml b/app/src/main/res/values-b+mk/strings.xml index 4e37afdea..99ee9c2ac 100644 --- a/app/src/main/res/values-b+mk/strings.xml +++ b/app/src/main/res/values-b+mk/strings.xml @@ -126,7 +126,8 @@ Откажи Паузирај Продолжи - Ова трајно ќе го избрише %s \nДали си сигурен? + Ова трајно ќе го избрише %s +\nДали си сигурен? Во тек Изгледанo Статус @@ -244,7 +245,7 @@ TC Претплатен на %s Преводи - Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! + Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! Недостасуваат дозволи за складирање. Обиди се повторно. Зачувај Вчитај од датотека @@ -416,7 +417,8 @@ Почна да презема %1$d %2$s… Автоматски ажурирања на приклучоци -30 - %dm \nпреостанува + %dm +\nпреостанува Видео кеш на дискот https://example.com/example.mp4 Готово @@ -445,7 +447,7 @@ Грешка при правење резервна копија на %s Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат. - Прикажи информации за плеерот + Резолуција на видео плеер Големина на видео баферот Распоред Стандардно @@ -585,7 +587,8 @@ По неколку неуспешни обиди, известувањето ќе се затвори. Едноставно вклучи ја апликацијата повторно за да се обидеш повторно. Медиуми Претстои во %s - %s \nпреостанати + %s +\nпреостанати За да се обезбедат непрекинати преземања и известувања за претплатените ТВ серии, CloudStream треба дозвола за работа во позадина. Со притискање на „ОК“, ќе ви биде прикажан дијалог за барање дозвола. Ве молиме, притиснете „Дозволи“.\n\nИмајте предвид дека оваа дозвола не значи дека CS3 ќе ја троши вашата батерија. Ќе работи во позадина само кога е потребно, како на пример при примање известувања или преземање видеа од официјални екстензии. Грешка при пристапот до таблата со исечоци, обиди се повторно. Грешка при копирање, молам копирај го логот и контактирај ја поддршката на апликацијата. @@ -705,37 +708,4 @@ Горе во центар Горе на десно Пушти ја целата серија - Редица за преземање - Моментално нема преземања во редицата. - Дополнителна осветленост - Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот - овозможена_дополнителна_осветленост - Предлози за пребарување - Прикажувај предлози за пребарување додека пишуваш - Исчисти предлози - Прикажи преклоп со метаподатоци на плеерот - Прикажи панел за емитување - Инсталирај предиздавачка верзија - Предиздавачката верзија е веќе инсталирана. - Неуспешна инсталација на предиздавачката верзија. - Видео - Текст на епизода - Информации за медиумот - Преглед - Приоритет на извор - Одреди како ќе се подредуваат видео изворите во плеерот - Име на изворот - Преземи сѐ - Откажи сѐ - Дали сакате да ја преземете епизодата %s? - Дали сакате да ги откажете сите преземања во редицата? - - %d активно преземање - %d активни преземања - - - %d преземање во редицата - %d преземања во редицата - - Во живо diff --git a/app/src/main/res/values-b+ml/strings.xml b/app/src/main/res/values-b+ml/strings.xml index 6c5dd3d89..d1c9409a3 100644 --- a/app/src/main/res/values-b+ml/strings.xml +++ b/app/src/main/res/values-b+ml/strings.xml @@ -3,7 +3,8 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s + പുതിയ അപ്ഡേറ്റ്! +\n%1$s -> %2$s ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക @@ -83,9 +84,9 @@ പ്ലേയർ സബ്‌ടൈറ്റിലുകളുടെ സെറ്റിങ്‌സ് - വീഡിയോയിൽ ഇരുവശങ്ങളിലേക്കും സ്വൈപ്പ് ചെയ്താൽ സ്ഥാനത്തെ നിയന്ത്രിക്കാം + വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക - തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ നീക്കുക + തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക മുന്നോട്ട് അല്ലെങ്കിൽ പിന്നിലേക്ക് നീങ്ങാൻ വലത്തോട്ടോ ഇടത്തോട്ടോ രണ്ടുതവണ ടാപ്പുചെയ്യുക തിരയുക @@ -114,7 +115,8 @@ റദ്ദാക്കുക നിർത്തുക തുടരുക - സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക \nഉറപ്പാണോ? + സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക +\nഉറപ്പാണോ? തുടരുന്നു പൂർത്തിയായി അവസ്ഥ @@ -186,11 +188,13 @@ ലോഡിംഗ്… ബ്രൗസർ വീണ്ടും കാണുക - നെറ്റ്‌വർക്ക് സ്ട്രീം + സ്ട്രീം %s ൽ ഫോൻ്റ്‌സ് വെച്ചു കൊണ്ട് ഇംപോർട്ട് ചെയ്യുക പ്രശ്‌നമുണ്ടാക്കുന്ന ഒന്ന് കണ്ടെത്താൻ നിങ്ങളെ സഹായിക്കുന്നതിന് ഒരു ക്രാഷ് കാരണം എല്ലാ വിപുലീകരണങ്ങളും ഓഫാക്കി. പൊതു പട്ടിക - CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. + CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. +\n +\nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക റെൻഡറർ പിശക് @@ -257,7 +261,8 @@ ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. ചരിത്രം മായ്ക്കുക ലോഗ്കാറ്റ് കാണിക്കുക 🐈 - നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( \nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. വീഡിയോ റിപ്പോസിറ്ററി നാമവും URL ഉം പകർത്തി! @@ -267,50 +272,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - സീസൺ %1$d എപ്പിസോഡ് %2$d റിലീസ് ചെയ്യുന്ന സമയം - %1$dh %2$dm %3$ds - %1$dm %2$ds - %1$ds - തുടക്കം മുതൽ പ്ലേ ചെയ്യുക - ഡൗൺലോഡ് ക്യൂ - സ്പീച്ച് റെക്കഗ്നിഷൻ ലഭ്യമല്ല - സംസാരിക്കാൻ തുടങ്ങുക… - മുഴുനീള പരമ്പരയും പ്ലേ ചെയ്യുക - ഈ വീഡിയോ ഒരു ടോറന്റ് ആണ്, അതായത് നിങ്ങളുടെ വീഡിയോ ആക്റ്റിവിറ്റി ട്രാക്ക് ചെയ്യപ്പെടാൻ സാധ്യതയുണ്ട്.\nതുടരുന്നതിന് മുൻപ് ടോറന്റിംഗിനെക്കുറിച്ച് നിങ്ങൾക്ക് കൃത്യമായി അറിയാമെന്ന് ഉറപ്പാക്കുക. - ഡിലീറ്റ് ചെയ്യേണ്ടവ തിരഞ്ഞെടുക്കുക - നിലവിൽ ഡൗൺലോഡുകൾ ഒന്നും തന്നെയില്ല. - നിലവിൽ ക്യൂവിലുള്ള ഡൗൺലോഡുകൾ ഒന്നും തന്നെയില്ല. - ഓഫ്‌ലൈനായി കാണാൻ ലഭ്യമാണ് - എല്ലാം തിരഞ്ഞെടുക്കുക - തിരഞ്ഞെടുത്തവയെല്ലാം ഒഴിവാക്കു - ലോക്കൽ വീഡിയോ തുറക്കുക - സേവ് ചെയ്യുക - സബ്‌ടൈറ്റിൽ ഉയരം - ഫോണ്ട് - ഫോണ്ട് വലുപ്പം - സ്വയം തിരഞ്ഞെടുത്ത ഭാഷ - ഭാഷകൾ ഡൗൺലോഡ് ചെയ്യുക - സബ്ടൈറ്റിൽ ഭാഷ - സൈറ്റിൽ വിവരങ്ങൾ ലഭ്യമല്ല, ആയതിനാൽ വീഡിയോ ലോഡ് ആവുകയില്ല. - പ്ലേയറിന്റെ വലുപ്പം മാറ്റുന്ന ബട്ടൻ - സബ്ടൈട്ടിൽസ് - ക്രോംകാസ്റ്റ് സബ്ടൈട്ടിൽസ് - ക്രോംകാസ്റ്റ് സബ്ടൈട്ടിൽസ് ക്രമീകരണങ്ങൾ - പ്ലേ ചെയ്യുന്ന വേഗത - പ്ലേയറിൽ വേഗതയ്ക്കുള്ള ഓപ്ഷൻ ചേർക്കുന്നു - മുന്നോട്ടും പിന്നോട്ടും പോകുവാൻ സ്വൈപ്പ് ചെയ്യുക - ക്രമീകരണങ്ങൾ മാറ്റുവാൻ സ്വൈപ്പ് ചെയ്യുക - നാമം - ഉറവിട നാമം - ദൃശ്യമേന്മയും നാമവും - എല്ലാം ഡൗൺലോഡ് ചെയ്യുക - അടുത്ത ഭാഗം സ്വയം പ്ലേ ചെയ്യുക - ഇപ്പോളത്തെ ഭാഗം കഴിഞ്ഞയുടനെ അടുത്തത് ആരംഭിക്കുക - തൽസമയം - @string/home_play - രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ സ്ഥാനം നിയന്ത്രിക്കാം - രണ്ടുവട്ടം ടാപ്പ് ചെയ്താൽ താൽകാലികമായി നിർത്തിവെക്കാം - പ്ലേയറിലെ സ്ഥാനനിയന്ത്രണ അളവ് (സെക്കണ്ടസ്) - നടുവിൽ രണ്ട് തവണ ടാപ്പ് ചെയ്താൽ താൽകാലികമായി നിർത്തിവെക്കാം - സിസ്റ്റത്തിന്റെ വെളിച്ചക്രമീകരണം ഉപയോഗിക്കുക diff --git a/app/src/main/res/values-b+ms/strings.xml b/app/src/main/res/values-b+ms/strings.xml index a15759939..dc1cd3ee6 100644 --- a/app/src/main/res/values-b+ms/strings.xml +++ b/app/src/main/res/values-b+ms/strings.xml @@ -25,8 +25,8 @@ Episod %d akan disiarkan dalam Pelakon:%s Mod Selamat Hidup - %1$dh %2$dj %3$dm - %1$dj %2$dm + %1$dd %2$dh %3$dm + %1$dh %2$dm %dm Poster Episod Poster Utama @@ -56,7 +56,8 @@ Tutup Pratonton Resensi:%.1f - Kemas kini baru dijumpai! \n%1$s -> %2$s + Kemas kini baru dijumpai! +\n%1$s -> %2$s %d min Main dari mula Musim %1$d Episod %2$d akan dikeluarkan di @@ -485,7 +486,7 @@ Kemaskini dan sandaran Ketik dua kali untuk mencari Gunakan kecerahan sistem dalam pemain apl dan bukannya tindanan gelap - %1$dj %2$dm %3$ds + %1$dh %2$dm %3$ds %1$dm %2$ds %1$ds Pengecaman pertuturan tidak tersedia diff --git a/app/src/main/res/values-b+mt/strings.xml b/app/src/main/res/values-b+mt/strings.xml index 8f8bf6cd7..ea859ee29 100644 --- a/app/src/main/res/values-b+mt/strings.xml +++ b/app/src/main/res/values-b+mt/strings.xml @@ -18,7 +18,8 @@ Ibdel Il-fornitur veloċità (%.2fx) Klassifikazzjoni: %.1f - Aġġornament ġdid misjub! \n%1$s -> %2$s + Aġġornament ġdid misjub! +\n%1$s -> %2$s %d min CloudStream Ara bil-CloudStream diff --git a/app/src/main/res/values-b+my/strings.xml b/app/src/main/res/values-b+my/strings.xml index d360d095d..0938e4f98 100644 --- a/app/src/main/res/values-b+my/strings.xml +++ b/app/src/main/res/values-b+my/strings.xml @@ -11,7 +11,8 @@ နောက်သို့ နောက်ခံပုံရိပ်ကို အကြိုကြည့်ရန် အဆင့်: %.1f - အပ်ဒိတ်အသစ်! \n%1$s -> %2$s + အပ်ဒိတ်အသစ်! +\n%1$s -> %2$s စစ်ထုတ်မှု %d မိနစ် CloudStream @@ -110,8 +111,10 @@ အပိုင်း အပိုင်းများ %1$d-%2$d - ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s \nသင်သေချာပါသလား။ - %dမိနစ် \nကျန်ရိှသည် + ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s +\nသင်သေချာပါသလား။ + %dမိနစ် +\nကျန်ရိှသည် ထုတ်လွှင့်နေဆဲ ထုတ်လွှင့်မှုပြီးဆုံး အခြေအနေ @@ -339,7 +342,11 @@ သမားရိုးကျ ထည့်သွင်းသူ ထွက်ချိန်တွင် အက်ပ်ကို အပ်ဒိတ်လုပ်ပါမည် - CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ \n \nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ \n \nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ + CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ +\n +\nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ +\n +\nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ အခြားသူများ၏ရီပိုစစ်ထရီများကိုရှာဖွေမည် အသံများ အသံဖိုင်များ @@ -415,7 +422,8 @@ အက်ပ်အပ်ဒိတ်အားဒေါင်းလုဒ်လုပ်နေသည်… အက်ပ်အပ်ဒိတ်အားသွင်းနေသည်… အပ်ဒိတ်ဖြစ်မှု (အသစ် မှ အဟောင်း) - သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( \nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ + သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( +\nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ သုံးရန် တည်းဖြတ်ရန် အရည်အသွေးများ @@ -508,7 +516,8 @@ စာရင်းသွင်းပြီး %s စာရင်းသွင်းမှုပယ်ဖျက်ပြီး %s ဤစာရင်းသည် ဗလာဖြစ်နေသည်။ အခြားတစ်ခုသို့ ပြောင်းကြည့်ပါ။ - Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ \nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ + Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ +\nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ အပိုင်းသစ် %d ထွက်ပြီ ပရိုဖိုင် %d ဝိုင်ဖိုင် @@ -516,7 +525,13 @@ ပုံသေထားရန် ပရိုဖိုင်များ အကူအညီ - ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ \n \nအရင်းအမြစ် A: 3 \nအရည်အသွေး B: 7 \nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ \n \nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် + ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ +\n +\nအရင်းအမြစ် A: 3 +\nအရည်အသွေး B: 7 +\nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ +\n +\nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် ပရိုဖိုင်နောက်ခံ UI ကို မှန်ကန်စွာ ဖန်တီး၍မရပါ၊ ၎င်းသည် အဓိက ချို့ယွင်းချက်တစ်ခုဖြစ်ပြီး ချက်ချင်းသတင်းပို့သင့်သည်။ %s သင်နဂိုတည်းကသတ်မှတ်ပြီး diff --git a/app/src/main/res/values-b+ne/strings.xml b/app/src/main/res/values-b+ne/strings.xml index 49e5a9350..8a432a505 100644 --- a/app/src/main/res/values-b+ne/strings.xml +++ b/app/src/main/res/values-b+ne/strings.xml @@ -15,7 +15,8 @@ मुख्य पोस्टर %1$s Ep %2$d अभिनेता:%s - नयाँ अपडेट भेटियो! \n%1$s -> %2$s + नयाँ अपडेट भेटियो! +\n%1$s -> %2$s फिलर %d मिनेट क्लाउडस्ट्रीम diff --git a/app/src/main/res/values-b+nl/strings.xml b/app/src/main/res/values-b+nl/strings.xml index feec84263..e3bbee121 100644 --- a/app/src/main/res/values-b+nl/strings.xml +++ b/app/src/main/res/values-b+nl/strings.xml @@ -19,7 +19,8 @@ Snelheid (%.2fx) Beoordeeld: %.1fAls - Nieuwe update gevonden! \n%1$s -> %2$s + Nieuwe update gevonden! +\n%1$s -> %2$s Filler %d min CloudStream @@ -181,8 +182,10 @@ Hervatten -30 +30 - Dit wordt zeker permanent verwijderd %s \nWeet u het zeker? - %dm \nremaining + Dit wordt zeker permanent verwijderd %s +\nWeet u het zeker? + %dm +\nremaining Voortdurende Voltooid Status @@ -481,7 +484,8 @@ Verwijderen uit bekeken App wordt bijgewerkt bij afsluiten Gesorteerd - Je bibliotheek is leeg :( \nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek. + Je bibliotheek is leeg :( +\nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek. Uitgeschakeld: %d Stop Niet gedownload: %d @@ -503,7 +507,8 @@ %s ( Uitgeschakeld) Herstart de app om veranderingen te zien. Gedownload: %d - Veilige mode bestand gevonden! \nGeen extensies laden bij het opstarten totdat het bestand is verwijderd. + Veilige mode bestand gevonden! +\nGeen extensies laden bij het opstarten totdat het bestand is verwijderd. Nee Beoordeling ( Hoog naar Laag) Veilige mode aan @@ -515,7 +520,11 @@ Wis repository Uitgeschreven bij %s Terugkeren - CloudStream heeft standaard geen sites geïnstalleerd. U moet de sites uit repositories installeren. \n \nVanwege een hersenloze DMCA verwijdering door Sky UK Limited 🤮 kunnen we de repository site niet linken in de app. \n \nWord lid van onze Discord of zoek online. + CloudStream heeft standaard geen sites geïnstalleerd. U moet de sites uit repositories installeren. +\n +\nVanwege een hersenloze DMCA verwijdering door Sky UK Limited 🤮 kunnen we de repository site niet linken in de app. +\n +\nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op Wifi @@ -527,7 +536,13 @@ Kwaliteiten Profiel achtergrond Gebruik - Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. \n \nBron A: 3 \nKwaliteit B: 7 \nHeeft een totale prioriteit van de video van 10. \n \nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! + Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. +\n +\nBron A: 3 +\nKwaliteit B: 7 +\nHeeft een totale prioriteit van de video van 10. +\n +\nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! Profiel %d Repository niet gevonden, controleer de URL en probeer een VPN Geen plug-ins gevonden in de repository @@ -539,7 +554,11 @@ Favorieten %s toegevoegd aan favorieten Aangemeld als %s - Er zijn mogelijk dubbele items gevonden in uw bibliotheek: \n \n%s \n \nWilt u dit item toch toevoegen, de bestaande vervangen of de actie annuleren? + Er zijn mogelijk dubbele items gevonden in uw bibliotheek: +\n +\n%s +\n +\nWilt u dit item toch toevoegen, de bestaande vervangen of de actie annuleren? Voer PIN in voor %s Backupfrequentie Mogelijk Duplicaat Gevonden @@ -558,7 +577,9 @@ Abonneer Verwijder uit favorieten Selecteer een Account - Het lijkt erop dat er al een mogelijk duplicaat bestaat in uw bibliotheek: \'%s.\' \n \nWilt u dit item toch toevoegen, het bestaande item vervangen of de actie annuleren? + Het lijkt erop dat er al een mogelijk duplicaat bestaat in uw bibliotheek: \'%s.\' +\n +\nWilt u dit item toch toevoegen, het bestaande item vervangen of de actie annuleren? PIN invoeren PIN Huidige PIN invoeren @@ -661,38 +682,4 @@ Fout bij toegang tot het Klembord, Probeer het opnieuw. Fout bij het kopiëren. Kopieer alsjeblieft de logcat en neem contact op met de app-ondersteuning. Afwijzen - Video - Voorbeeld - Bron Prioriteit - Bepaal hoe de videobronnen worden gesorteerd in de speler - Ontgrendel CloudStream - Versleutel met Biometrie - Reset - verschijningsdatum (Nieuw naar Oud) - verschijningsdatum (Oud naar Nieuw) - Verberg de namen van de besturingselementen van de speler - Ondertiteling nog niet geladen - Back-up folder locatie - Aangepast - Bevestig voor afsluiten - Toon dialoogvenster voordat de app wordt afgesloten - Randgrote - Laat Speler Metadata Overlay zien - Kon de apparaat PIN niet vinden, probeer locale authenticatie - Zet torrent aan in Instellingen/Providers/Media voorkeur - Herstart de app en accepteer de Stream Torrent pop-up om verder te gaan. - Handmatige Update Knop - Open de app met Vingerafdruk, Face ID, PIN, Pattern en Wachtwoord. - Seekbar preview - Zet preview thumbnail aan op seekbar - Software decodering - Software decodering staat de mogelijkheid toe om de speler videobestanden af te laten spelen op jouw apparaat, maar het kan een haperige of onstabiele kijkervaring zorgen op hoge resolutie. - Volume heeft de 100% overschreden - Update Plugins - Geen plugins zijn geupdate. - Speler meldingen - De speler melding om de kijkervaring van de achtergrond de besturen - Online - Hoeveel verschillende items kunnen in parallel gedownload worden - Parallel downloads diff --git a/app/src/main/res/values-b+nn/strings.xml b/app/src/main/res/values-b+nn/strings.xml index b29e142b0..245bf6618 100644 --- a/app/src/main/res/values-b+nn/strings.xml +++ b/app/src/main/res/values-b+nn/strings.xml @@ -15,7 +15,8 @@ Fyllstoff Forhandsvis bakgrunnsbilete Vurdert: %.1f - Ny oppdatering tilgjengeleg! \n%1$s -> %2$s + Ny oppdatering tilgjengeleg! +\n%1$s -> %2$s %d minutt Miniatyrbilete Episode %d vil bli sleppt om @@ -114,8 +115,10 @@ Gjenoppta -30 +30 - Dette vil slette %s permanent. \nEr du sikker på dette? - %dm \ngjenstår + Dette vil slette %s permanent. +\nEr du sikker på dette? + %dm +\ngjenstår Pågåande Fullført År @@ -188,14 +191,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - Sesong %1$d Episode %2$d blir sleppt om - Spel av frå start - Nedlastingskø - Semmegjenkjenning er ikkje tilgjengeleg - Snakk no… - Nettlesar - Fjerna - Strøm Torrent - Spel heile serien - Denne filmen er ein Torrent, som betyr at bruken din kan bli spora\nSett deg inn i bruk av Torrent-resursar før du fortsetter. diff --git a/app/src/main/res/values-b+no/strings.xml b/app/src/main/res/values-b+no/strings.xml index 7d309a2f8..55b5303eb 100644 --- a/app/src/main/res/values-b+no/strings.xml +++ b/app/src/main/res/values-b+no/strings.xml @@ -12,7 +12,8 @@ Avspillingshastighet (%.2fx) Vurdert: %.1f - Ny oppdatering funnet! \n%1$s -> %2$s + Ny oppdatering funnet! +\n%1$s -> %2$s CloudStream Hjem Søk @@ -134,7 +135,8 @@ Avbryt Stopp Gjenoppta - Dette vil slette %s \nEr du sikker? + Dette vil slette %s +\nEr du sikker? Pågående Fullført Posisjon @@ -351,7 +353,8 @@ %1$d-%2$d Offentlig liste programtillegg - %dm \nigjen + %dm +\nigjen Videooppløsning Synkroniser undertekster Undertekstforsinkelse @@ -370,7 +373,11 @@ Ugyldig nettadresse Knippe-nedlasting Slett pakkebrønn - CloudStream har ingen sider installert som forvalg. Du må installere sidene fra pakkebrønner. \n \nSom følge av en hjernedød DMCA-forespørsel fra Sky UK Limited 🤮 kan vi ikke lenke til pakkebrønnssiden i programmet. \n \nTa del i vår Discord, eller søk på nett. + CloudStream har ingen sider installert som forvalg. Du må installere sidene fra pakkebrønner. +\n +\nSom følge av en hjernedød DMCA-forespørsel fra Sky UK Limited 🤮 kan vi ikke lenke til pakkebrønnssiden i programmet. +\n +\nTa del i vår Discord, eller søk på nett. Bruk dette hvis undertekster vises %d ms for sent Programtillegg innlastet Lydspor @@ -505,8 +512,10 @@ ISP-omgåelser Denne listen er tom. Prøv å bytte til en annen. Sorter - Fant fil for trygt modus. \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. - Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. + Fant fil for trygt modus. +\nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. + Biblioteket ditt er tomt :( +\nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. Rediger Profiler Favoritter diff --git a/app/src/main/res/values-b+or/strings.xml b/app/src/main/res/values-b+or/strings.xml index ce7f74290..40a2915fd 100644 --- a/app/src/main/res/values-b+or/strings.xml +++ b/app/src/main/res/values-b+or/strings.xml @@ -84,7 +84,8 @@ ବ୍ୟାକଅପ୍ ଆଣ୍ଡ୍ରଏଡ୍ ଟିଵି ଅଙ୍ଗଭଙ୍ଗୀ - ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! \n%1$s -> %2$s + ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! +\n%1$s -> %2$s ଅଵଧି ଆପ୍ ବ୍ୟାକଅପ୍ ଫାଇଲ୍ ଧାରଣ ହେଲା diff --git a/app/src/main/res/values-b+pl/strings.xml b/app/src/main/res/values-b+pl/strings.xml index 418a4a2ca..d1b1cf1c5 100644 --- a/app/src/main/res/values-b+pl/strings.xml +++ b/app/src/main/res/values-b+pl/strings.xml @@ -2,7 +2,8 @@ Prędkość (%.2fx) Ocena: %.1f - Znaleziono nową aktualizację! \n%1$s -> %2$s + Znaleziono nową aktualizację! +\n%1$s -> %2$s Filler %d min %1$s Odc. %2$d @@ -176,8 +177,10 @@ Odtwórz -30 +30 - Spowoduje to trwałe usunięcie %s \nCzy jesteś pewien? - %dm \npozostało + Spowoduje to trwałe usunięcie %s +\nCzy jesteś pewien? + %dm +\npozostało Bieżący Zakończone Status @@ -243,7 +246,7 @@ Aktualizacja Domyślna jakość (WiFi) Maksymalna liczba znaków w tytule odtwarzacza - Pokaż informacje o odtwarzaczu + Pokaż informacje o odtwarzaczu Rozmiar bufora wideo Długość bufora wideo Pamięć podręczna wideo na dysku @@ -382,7 +385,9 @@ Wyłączono: %d Nie pobrano: %d Zaaktualizowano %d rozszerzeń - CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. \n \nDołącz do naszego Discorda lub poszukaj online. + CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. +\n +\nDołącz do naszego Discorda lub poszukaj online. Zobacz repozytoria społeczności Publiczna lista Wszystkie napisy wielką literą @@ -482,9 +487,11 @@ Alfabetycznie (od Z do A) Wybierz bibliotekę Biblioteka - Twoja biblioteka jest pusta :( \nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki. + Twoja biblioteka jest pusta :( +\nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki. Ta lista jest pusta. Spróbuj przełączyć się na inną. - Znaleziono plik trybu bezpiecznego. \nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. + Znaleziono plik trybu bezpiecznego. +\nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. Używana ilość przewijania, gdy widoczny jest odtwarzacz Ukryty odtwarzacz - ilość przewijania Android TV @@ -508,7 +515,13 @@ Obchodzi blokadę surowych adresów URL GitHuba za pomocą jsDelivr. Może powodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHubem. Włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) - W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. \n \nŹródło A: 3 \nJakość B: 7 \nŁączny priorytet wideo będzie wynosił 10. \n \nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! + W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. +\n +\nŹródło A: 3 +\nJakość B: 7 +\nŁączny priorytet wideo będzie wynosił 10. +\n +\nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! Profil %d Wi-Fi Dane mobilne @@ -528,7 +541,11 @@ Usunięto %s z ulubionych Ulubione Dodano %s do ulubionych - W swojej bibliotece znaleziono potencjalne duplikaty: \n \n%s \n \nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? + W swojej bibliotece znaleziono potencjalne duplikaty: +\n +\n%s +\n +\nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? Wprowadź PIN dla %s Częstotliwość tworzenia kopii zapasowych Znaleziono potencjalny duplikat @@ -545,7 +562,9 @@ Zasubskrybuj Usuń z ulubionych Wybierz konto - Wygląda się, że potencjalny duplikat już znajduje się w bibliotece: \'%s\'. \' \n \nCzy chciałbyś dodać ten element, zastąpić istniejący, czy anulować akcję? + Wygląda się, że potencjalny duplikat już znajduje się w bibliotece: \'%s\'. \' +\n +\nCzy chciałbyś dodać ten element, zastąpić istniejący, czy anulować akcję? Wprowadź PIN PIN Linki załadowane ponownie @@ -571,7 +590,8 @@ Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. Kopia zapasowa Twoich danych CloudStream została teraz utworzona. Chociaż prawdopodobieństwo tego jest bardzo niskie, wszystkie urządzenia mogą zachowywać się inaczej. W rzadkich przypadkach, gdy dostęp do aplikacji zostanie zablokowany, należy całkowicie wyczyścić dane aplikacji i przywrócić je z kopii zapasowej. Bardzo nam przykro z powodu wszelkich niedogodności z tym związanych. Usuń z ulubionych - %s \npozostało + %s +\npozostało Dodaj do ulubionych Nazwa repozytorium i adres URL Błąd dostępu do schowka. Spróbuj ponownie. @@ -613,11 +633,19 @@ Zaznacz wszystkie Zaznacz elementy do usunięcia Odznacz wszystkie - Czy na pewno chcesz na stałe usunąć następujące elementy? \n \n%s - Usuniesz na stale wszystkie odcinki następującego serialu: \n \n%s - Czy na pewno chcesz na stałe usunąć wszystkie odcinki następującego serialu? \n \n%s + Czy na pewno chcesz na stałe usunąć następujące elementy? +\n +\n%s + Usuniesz na stale wszystkie odcinki następującego serialu: +\n +\n%s + Czy na pewno chcesz na stałe usunąć wszystkie odcinki następującego serialu? +\n +\n%s Usuń pliki - Czy na pewno chcesz na stałe usunąć następujące odcinki %1$s? \n \n%2$s + Czy na pewno chcesz na stałe usunąć następujące odcinki %1$s? +\n +\n%2$s Usuń (%1$d | %2$s) Podgląd paska przewijania Włącz podgląd miniatury na pasku wyszukiwania @@ -649,7 +677,7 @@ Odcinek (malejąco) Ocena (najwyższa) Zaktualizuj wtyczki - Pomyślnie zaktualizowano wtyczki: %d + Pomyślnie zaktualizowano wtyczki: %d! Nie zaktualizowano żadnych wtyczek. Zaktualizuj wtyczki ręcznie Rozpoczęcie procesu aktualizacji wtyczek! @@ -693,7 +721,7 @@ Przeładuj dostawcę Odtwarzaj inne źródło" Nazwa - Rozdzielczość i nazwa + Rozdzielczość i nazwa Dolne lewe Wyrównanie napisów Dolne środkowe @@ -714,16 +742,16 @@ Wyczyść sugestie Pokaż panel obsady Nazwa źródła - Informacje o multimediach + Informacje o multimediach Dodatkowa jasność Włącz filtr jasności, gdy jasność wyświetlacza przekroczy 100% Włączono dodatkową jasność Kolejka pobierania - Obecnie nie ma żadnych plików do pobrania w kolejce. + Obecnie nie ma żadnych plików do pobrania w kolejce. Pobierz wszystkie Anuluj wszystkie Czy chcesz pobrać odcinek %s? - Czy chcesz anulować wszystkie pliki do pobrania z kolejki? + Czy chcesz anulować wszystkie pliki do pobrania z kolejki? %d aktywne pobieranie %d aktywne pobierania @@ -731,26 +759,12 @@ %d aktywnych pobierań - %d pobieranie w kolejce - %d pobierania w kolejce - %d pobierań w kolejce - %d pobierań w kolejce + %d pobieranie w kolejce + %d pobierania w kolejce + %d pobierań w kolejce + %d pobierań w kolejce Priorytet źródła - Zdecyduj, jak mają być sortowane źródła wideo w odtwarzaczu + Zdecyduj, jak mają być sortowane źródła wideo w odtwarzaczu Pokaż nakładkę metadanych odtwarzacza - Wideo - Zapowiedź - Na żywo - Ustawienia profilu - Ukryj źródła z negatywnym priorytetem - Ukryj źródła z błędami - - %d ukryty odnośnik - %d ukryte odnośniki - %d ukrytych odnośników - %d ukrytych odnośników - - Pokaż zegar czasu rzeczywistego - Wyświetl zegar czasu rzeczywistego u góry ekranu. Dotyczy strony głównej i odtwarzacza diff --git a/app/src/main/res/values-b+pt+BR/strings.xml b/app/src/main/res/values-b+pt+BR/strings.xml index 455478b5a..b856246e8 100644 --- a/app/src/main/res/values-b+pt+BR/strings.xml +++ b/app/src/main/res/values-b+pt+BR/strings.xml @@ -180,7 +180,8 @@ Continuar -30 +30 - Isso apagará %s permanentemente \nVocê tem certeza? + Isso apagará %s permanentemente +\nVocê tem certeza? %dm\nrestantes Em andamento Concluído @@ -395,7 +396,9 @@ Transferido: %d Desativado: %d Não transferido: %d - CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. \n \nEntre no nosso Discord ou pesquise online. + CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. +\n +\nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -438,7 +441,8 @@ Abrir com Selecionar Biblioteca Passou nos testes - Sua biblioteca está vazia :0 \nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. + Sua biblioteca está vazia :0 +\nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. Qualidade preferida de reprodução (Dados Móveis) Legado Biblioteca @@ -456,8 +460,15 @@ Alfabética(Z => A) Qualidade Perfil de plano de fundo - Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. \n \nFonte A: 3 \nQualidade B: 7 \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! - Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. + Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! + Arquivo de modo de segurança encontrado! +\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. Inscrito em %s Episódio %d lançado! Selecionar padrão @@ -542,7 +553,11 @@ Duplicata em potencial encontrada Adicionar Substituir - Possíveis itens duplicados foram encontrados em sua biblioteca: \n \n %s \n \nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? + Possíveis itens duplicados foram encontrados em sua biblioteca: +\n +\n %s +\n +\nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? Insira o PIN Insira o PIN para %s Insira o PIN atual @@ -561,7 +576,9 @@ Links recarregados Frequência de backup Substitua tudo - Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' \n \nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? Inscrever-se Cancelar inscrição Usar conta padrão @@ -582,7 +599,8 @@ A autenticação biométrica não é compatível com este dispositivo Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. Após algumas tentativas fracassadas, o prompt será fechado. Basta reiniciar o aplicativo para tentar novamente. - %s \nrestante(s) + %s +\nrestante(s) Favorito Não favorito copiado! @@ -616,7 +634,7 @@ Reproduzir do começo Reprovou alguns testes Excluir plugin - Atualmente não há downloads disponíveis. + Você não baixou nada :/ Ocultar os nomes dos controles do player Abrir arquivo de vídeo Data de lançamento (do novo ao antigo) @@ -736,7 +754,7 @@ Nome da fonte Baixar tudo Cancelar tudo - Você deseja baixar o episódio%s? + Você deseja baixar o episódio%s Você gostaria de cancelar todos os downloads da fila? %ddownload ativo @@ -748,8 +766,4 @@ %d downloads na sequência %d downloads na sequência - Mostrar sobreposição de metadados do reprodutor - Vídeo - Visualização - Ao vivo diff --git a/app/src/main/res/values-b+pt/strings.xml b/app/src/main/res/values-b+pt/strings.xml index 960a91d0d..f9dd3083b 100644 --- a/app/src/main/res/values-b+pt/strings.xml +++ b/app/src/main/res/values-b+pt/strings.xml @@ -18,7 +18,8 @@ Visualizar plano de fundo Velocidade (%.2fx) Classificado: %.1f - Nova atualização encontrada! \n%1$s -> %2$s + Nova atualização encontrada! +\n%1$s -> %2$s Preenchimento CloudStream Assistir com o CloudStream @@ -61,7 +62,7 @@ Transmitir Erro a Carregar Links Armazenamento Interno - Dub + Dob Leg Eliminar Ficheiro Reproduzir Ficheiro @@ -100,7 +101,7 @@ Importar fontes colocando em %s Continuar a Assistir Remover - Mais informações + Mais info Uma VPN pode ser necessária para que este fornecedor funcione corretamente Este fornecedor é um torrent, uma VPN é recomendada Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site. @@ -142,7 +143,7 @@ Procurar Contas e segurança Atualizações e cópias de segurança - Informações + Info Procura Avançada Mostra resultados separados por fornecedor Mostrar episódios de enchimento para anime @@ -175,8 +176,10 @@ Cancelar Pôr em Pausa Retomar - Isto apagará %s permanentemente \nTem a certeza? - %dm \nem falta + Isto apagará %s permanentemente +\nTem a certeza? + %dm +\nem falta Em Curso Concluído Estado @@ -318,9 +321,9 @@ Carregar de arquivo Carregar da Internet Arquivo baixado - Principal - Suporte - Plano de fundo + Protagonista + Coadjuvante + Figurante Aleatório Em breve… Imagem de Poster @@ -361,7 +364,9 @@ Transferido: %d Desativado: %d Não transferido: %d - O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. \n \nJunte-se ao nosso Discord ou pesquise online. + O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. +\n +\nJunte-se ao nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -469,8 +474,10 @@ Atualizando shows inscritos Alfabético (A a Z) Avaliações (Crescente) - A sua biblioteca está vazia :( \nEntre numa conta da biblioteca ou adicione espectáculos à sua biblioteca local. - Arquivo de modo de segurança encontrado! \nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. + A sua biblioteca está vazia :( +\nEntre numa conta da biblioteca ou adicione espectáculos à sua biblioteca local. + Arquivo de modo de segurança encontrado! +\nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. Contorno do provedor de serviço de internet (ISP) Links Recursos do Player @@ -515,7 +522,13 @@ Ajuda Qualidades Perfil de fundo - Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. \n \nFonte A: 3 \nQualidade B: 7 \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! + Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! Selecionar o modo para filtrar a transferência de plug-ins Não foi possível criar corretamente a interface do utilizador, trata-se de um GRANDE BUG e deve ser comunicado imediatamente %s Desativar @@ -523,7 +536,7 @@ Repositório não encontrado, verifique o URL e tente a VPN Você já votou Cancelar Inscrição - Inscrever-se + Subscrever Favoritos A recarregar links Frequência de Backup @@ -547,8 +560,14 @@ Selecione uma conta Gerenciar contas Usar conta padrão - Potenciais itens duplicados foram encontrados na sua biblioteca: \n \n%s \n \nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? - Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' \n \nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? + Potenciais itens duplicados foram encontrados na sua biblioteca: +\n +\n%s +\n +\nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? Mostrar recomendações Adiciona uma opção de velocidade no leitor Testar todas as extensões @@ -565,7 +584,8 @@ Desfavorito Bloqueio com biometria copiado! - %s \nrestante + %s +\nrestante Erro ao aceder à área de transferência, tente novamente. Erro ao copiar, copie o logcat e contacte o suporte da aplicação. Desbloquear o CloudStream @@ -591,7 +611,9 @@ Pré-visualização na barra de progresso Ativar a miniatura de pré-visualização na barra de progresso Autenticação Local - Irá também eliminar permanentemente todos os episódios da seguinte série: \n \n%s + Irá também eliminar permanentemente todos os episódios da seguinte série: +\n +\n%s Eliminar (%1$d | %2$s) Visite %s no seu smartphone ou computador e introduza o código acima Recomeçar @@ -604,9 +626,15 @@ Próximo em %s Eliminar Ficheiros Aviso - Tem a certeza que pretende eliminar permanentemente os seguintes items? \n \n%s - Tem a certeza que pretende eliminar permanentemente os seguintes episódios em %1$s? \n \n%2$s - Tem a certeza que pretende eliminar permanentemente todos os episódios da seguinte série? \n \n%s + Tem a certeza que pretende eliminar permanentemente os seguintes items? +\n +\n%s + Tem a certeza que pretende eliminar permanentemente os seguintes episódios em %1$s? +\n +\n%2$s + Tem a certeza que pretende eliminar permanentemente todos os episódios da seguinte série? +\n +\n%s Segurança Contas QR Code @@ -686,7 +714,7 @@ Imagem Atualizada com Sucesso Marcar como assistido o episódio Removar marcação de assistido até esse episódio - Recarregar + Recarregado Provedor de Recarregamento Reproduzir do servidor alternativo" Nome @@ -733,8 +761,4 @@ %d downloads na fila %d downloads na fila - Mostrar sobreposição de metadados do player - Vídeo - Pré-visualização - Ao Vivo diff --git a/app/src/main/res/values-b+qt/strings.xml b/app/src/main/res/values-b+qt/strings.xml index 6bbb0ddba..bbe145842 100644 --- a/app/src/main/res/values-b+qt/strings.xml +++ b/app/src/main/res/values-b+qt/strings.xml @@ -185,7 +185,8 @@ u ooah uo ahauao huhuu hauu h a ou oh ouhuouhoaaha aaooohhouhhha hauauuu - aaaaaaa uuuuuu \n%1$s -> %2$s + aaaaaaa uuuuuu +\n%1$s -> %2$s %1$s aaou %2$d oouaaahh %s aaaaaaugh ouh %d uuoogahaaah ooua-h-ha @@ -227,7 +228,8 @@ aaaaaaaaaaahhhgh-aooohoooo aau aooooghaao aagh aaaaaaaaaaaa oooh, aaough, ooga oguuu aaaaaaaaaaa ooooooohghh a-a-aaauo - %dmmmmmm.. \naaaaooughugh + %dmmmmmm.. +\naaaaooughugh aooohuohaaaa ooooagh oooooogh-aaaaaogh guuuaaaahhhhhhhaaa @@ -276,7 +278,9 @@ aaaagg uug oooogg oooogg - oooohhhoogg uuh uh uuuhh aaaaggguh og ooooggg uug aagg ek aaaaggg oog aaahh aagg uuuugggooohh \n \nJoin uuh uuuuggg ag uuuuhh eeeeek + oooohhhoogg uuh uh uuuhh aaaaggguh og ooooggg uug aagg ek aaaaggg oog aaahh aagg uuuugggooohh +\n +\nJoin uuh uuuuggg ag uuuuhh eeeeek uuugg aaaagg oogg uugg uh aaagg @@ -309,7 +313,13 @@ ooooggg %d aahh oooogggk - eeek aag uug uuuuhh ooh aak uuuuggg ooh uuuuhhh ug g eeeek oog h uuuuhh oooogggh ag aahh oooohh aaaagg uh oog uuuugg uuuugggog uug oog uh uuh aaaagg uuuuuukg uug aah uuuuuuk uuuuuukg ak ooh uuuhh aaaagggk \n \nSource A: 3 \nQuality B: 7 \nWill uuhh k uuuuhhhk ooogg uuuuhhhh uk 10 \n \nNOTE: ah uug oog ug 10 og oogg uug aaaahh uuhh uuuugggaaaahh oogg aaaahhh oogg uuhh aahh uh loaded! + eeek aag uug uuuuhh ooh aak uuuuggg ooh uuuuhhh ug g eeeek oog h uuuuhh oooogggh ag aahh oooohh aaaagg uh oog uuuugg uuuugggog uug oog uh uuh aaaagg uuuuuukg uug aah uuuuuuk uuuuuukg ak ooh uuuhh aaaagggk +\n +\nSource A: 3 +\nQuality B: 7 +\nWill uuhh k uuuuhhhk ooogg uuuuhhhh uk 10 +\n +\nNOTE: ah uug oog ug 10 og oogg uug aaaahh uuhh uuuugggaaaahh oogg aaaahhh oogg uuhh aahh uh loaded! uuuuhhhug %s aaaaggg uuhh aaaagggug oog @@ -478,7 +488,10 @@ uuh uuhh uuuuggg uuuhh %s ooogg oh oooohhhog aaaahhh uuh - oh ooooggg oogg k uuuuuukaahh uuuugggag uuhh ooooggg oooogg ah oogg library: \'%s\' \n \n \nWould uug uugg uh aak oogg oohh anyway, oooohhh uuh oooohhhg one, oh aaaagg eek action? + oh ooooggg oogg k uuuuuukaahh uuuugggag uuhh ooooggg oooogg ah oogg library: \'%s\' +\n +\n +\nWould uug uugg uh aak oogg oohh anyway, oooohhh uuh oooohhhg one, oh aaaagg eek action? ooogg uuh uuhh g ooogg oooogg oh aah uuuugg uugg oogg ak aaagg ooh oooohhhuug aagg uug aahh uug oooohhhg ak oooohh uuuuuuk oh uuh aaaagggag @@ -510,7 +523,8 @@ uuuugg oogg oh eeeeek eeeeeek uuuugggg aagg uuugg uuuuggg +30 - %s \nremaining + %s +\nremaining aagg uuuuhhh uuuuhh aag aah oogg Fingerprint, eeek ID, PIN, aaaaggg uuh aaaagggh eeeeek uuuhh oh aaaagg @@ -530,13 +544,21 @@ aaaagg aaahh aaaahhhg uuuhh oooohhh uh %s oohh uuuuggg uuhh uuugg - uuh oohh oogg aaaahhhuugg uuuugg oog aaaagggh ak uuh uuuuhhhog series: \n \n%s + uuh oohh oogg aaaahhhuugg uuuugg oog aaaagggh ak uuh uuuuhhhog series: +\n +\n%s aak aaaagggah oooohh uuuhh oooogg (%1$d | %2$s) - uuh uug oohh uuk aagg oh uuuuggguugg aaaahh uuh aaaagggah items? \n \n%s - uug ooh aagg ooh uuuk ag aaaahhhuuhh oooohh ooh oooohhhug oooohhhh ah %1$s? \n \n%2$s - ooh aag oogg uuh aagg ug eeeeeekoohh aaaahh aah uuuugggg uk aag oooohhhek series? \n \n%s + uuh uug oohh uuk aagg oh uuuuggguugg aaaahh uuh aaaagggah items? +\n +\n%s + uug ooh aagg ooh uuuk ag aaaahhhuuhh oooohh ooh oooohhhug oooohhhh ah %1$s? +\n +\n%2$s + ooh aag oogg uuh aagg ug eeeeeekoohh aaaahh aah uuuugggg uk aag oooohhhek series? +\n +\n%s %1$s %2$s aaaahhh uuuhh eeeeek oooohhhg ah uuk uuh aahh ug aaaaggg aaak ooh oooohhh space, aahh ug oooohhh ek @@ -590,8 +612,10 @@ uuuuggg (Old og New) uuuuggguuuhh (Z ak A) uuuugg uuuuggg - uuhh eeeeeek ek uuugg :( \nLog oh ag h aaaaggg uuuuggg ah uuk ooogg ag uugg uuugg oooohhh - oohh aagg uugg found! \nNot aaaaggg aak uuuugggaag ah uuuuhhh aaaak oogg ah eeeeeek + uuhh eeeeeek ek uuugg :( +\nLog oh ag h aaaaggg uuuuggg ah uuk ooogg ag uugg uuugg oooohhh + oohh aagg uugg found! +\nNot aaaaggg aak uuuugggaag ah uuuuhhh aaaak oogg ah eeeeeek uuuuggguuugg oohh %s ooooggg %d released! uugg @@ -599,7 +623,11 @@ ooh ooooggg oooohhh uuuuhhhoog uh ooh oooogg ag ek oooohhh correctly, uuhh uh g ooogg aah uug aaaagg uh uuuugggk uuuuhhhaahh %s - uuuuggguk uuuuhhhoh uuugg aahh aaak uuugg ag oohh library: \n \n%s \n \nWould aag aagg og uuk uugg oogg anyway, aaaahhh ooh uuuugggh ones, ek uuuuhh aah action? + uuuuggguk uuuuhhhoh uuugg aahh aaak uuugg ag oohh library: +\n +\n%s +\n +\nWould aag aagg og uuk uugg oogg anyway, aaaahhh ooh uuuugggh ones, ek uuuuhh aah action? oooohhhag uug oooohh uuh eeeek aaaaggg h oooogg aaaaak uug uuuugg aaaaaakaagg uuuuhh uuuuhhhuh oooogggag ag aaaahh uuuuggguugg aaagg ek aaagg aaaagggaagg diff --git a/app/src/main/res/values-b+ro/strings.xml b/app/src/main/res/values-b+ro/strings.xml index b1852640c..5f612c640 100644 --- a/app/src/main/res/values-b+ro/strings.xml +++ b/app/src/main/res/values-b+ro/strings.xml @@ -19,7 +19,8 @@ Viteză (%.2fx) Evaluare: %.1f - Actualizare nouă găsită! \n%1$s -> %2$s + Actualizare nouă găsită! +\n%1$s -> %2$s Filler %d min @@ -176,8 +177,10 @@ Continuă -30 +30 - Sunteți pe cale să ștergeți definitiv %s \nSunteți sigur? - %dm \nrămas + Sunteți pe cale să ștergeți definitiv %s +\nSunteți sigur? + %dm +\nrămas În curs de desfășurare Finalizat Status @@ -427,7 +430,8 @@ Funcții Autori Adaugă depozit - Biblioteca ta este goală :( \nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. + Biblioteca ta este goală :( +\nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. Eliminați subtitrările închise din subtitrări Descărcați lista de site-uri pe care doriți să le utilizați Evaluare (Ridicat la Scăzut) @@ -467,10 +471,13 @@ URL invalid Toate extensiile au fost dezactivate din cauza unei defecțiuni pentru a vă ajuta să o găsiți pe cea care cauzează probleme. Se descarcă actualizarea aplicației… - CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. \n \nAlăturați-vă Discord-ului nostru sau căutați online. + CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. +\n +\nAlăturați-vă Discord-ului nostru sau căutați online. A început să descarce %1$d %2$s… Mod sigur pornit - Fișier Mod Sigur găsit! \nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. + Fișier Mod Sigur găsit! +\nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. Scoateți de la urmărit Actualizat (Vechi la Nou) Reporniți aplicația pentru a vedea schimbările. @@ -524,7 +531,13 @@ Actualizări al aplicației Subtitrări Dezactivați - Aici puteți schimba modul în care sunt ordonate sursele. Dacă un videoclip are o prioritate mai mare, acesta va apărea mai sus în selecția surselor. Suma dintre prioritatea sursei și prioritatea calității reprezintă prioritatea video. \n \nSursa A: 3 \nCalitate B: 7 \nVa avea o prioritate video combinată de 10. \n \nNOTĂ: Dacă suma este 10 sau mai mare, playerul va sări automat peste încărcare atunci când este încărcat link-ul respectiv! + Aici puteți schimba modul în care sunt ordonate sursele. Dacă un videoclip are o prioritate mai mare, acesta va apărea mai sus în selecția surselor. Suma dintre prioritatea sursei și prioritatea calității reprezintă prioritatea video. +\n +\nSursa A: 3 +\nCalitate B: 7 +\nVa avea o prioritate video combinată de 10. +\n +\nNOTĂ: Dacă suma este 10 sau mai mare, playerul va sări automat peste încărcare atunci când este încărcat link-ul respectiv! Nu s-a găsit plugin-uri în depozit Nu s-a găsit depozitul, verificați URL-ul și încercați cu un VPN Editați @@ -540,12 +553,18 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor Ați votat deja - Elemente potențial duplicate au fost găsite în biblioteca ta: \n \n%s \n \nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? + Elemente potențial duplicate au fost găsite în biblioteca ta: +\n +\n%s +\n +\nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? %s a fost adăugat la favoriți/te %s a fost eliminat din favoriți/te Adaugă la favoriți/te Elimină din favoriți/te - Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' \n \nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? + Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' +\n +\nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? Introduce PIN-ul pentru %s Introduce PIN-ul actual Introduce PIN-ul @@ -600,7 +619,8 @@ Resetați Activați comutarea automată a orientării ecranului pe baza orientării video Blocare cu biometrie - %s \nrămase + %s +\nrămase Următorul în %s CloudStream Wiki Sezonul %1$d Episod %2$d va fi lansat în diff --git a/app/src/main/res/values-b+ru/strings.xml b/app/src/main/res/values-b+ru/strings.xml index 38e576baa..5c271f871 100644 --- a/app/src/main/res/values-b+ru/strings.xml +++ b/app/src/main/res/values-b+ru/strings.xml @@ -34,7 +34,8 @@ Предпросмотр фона Скорость (%.2fx) Оценили: %.1f - Новое обновление найдено! \n%1$s -> %2$s + Новое обновление найдено! +\n%1$s -> %2$s Заполнитель CloudStream Убрать @@ -171,8 +172,10 @@ Продолжить -30 +30 - Это будет удалено безвозвратно%s \nВы уверены? - %d мин. \nосталось + Это будет удалено безвозвратно%s +\nВы уверены? + %d мин. +\nосталось Завершено Год Рейтинг @@ -408,7 +411,9 @@ Трейлер %s (отключено) Далее - В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. \n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. + В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n +\nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название Предыдущий @@ -521,7 +526,13 @@ Вы уже проголосовали Никаких дополнений не обнаружено в источнике Поставить обычный - Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. \n \nИсточник А: 3 \nКачество Б: 7 \nБудет иметь общий приоритет видео 10. \n \nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! + Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. +\n +\nИсточник А: 3 +\nКачество Б: 7 +\nБудет иметь общий приоритет видео 10. +\n +\nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! Ссылки перезагружены Выберите учётную запись %s убрано из любимых @@ -575,7 +586,8 @@ Аудиокнига Медиа Разблокируйте приложение с помощью отпечатка пальца, Face ID, ПИН-кода, шаблона и пароля. - %s \nосталось + %s +\nосталось Отключить оптимизацию батареи Аутентификация по паролю/ПИН-коду Биометрическая аутентификация на этом устройстве не поддерживается @@ -605,7 +617,9 @@ Вы уверены, что хотите навсегда удалить все серии в данном сериале? \n \n%s Выберите элементы для удаления Удалить (%1$d | %2$s) - Вы уверены, что хотите навсегда удалить данный объект? \n \n%s + Вы уверены, что хотите навсегда удалить данный объект? +\n +\n%s Невозможно получить ПИН-код устройства, попробуйте локальную аутентификацию Откройте %s на вашем смартфоне или компьютере и введите данный код CloudStream Вики @@ -735,8 +749,4 @@ Отменить всё Вы хотите загрузить эпизод %s? Вы хотите отменить всё запланированные загрузки? - Показывать наложения метаданных проигрывателя - Видео - Предпросмотр - Прямой эфир diff --git a/app/src/main/res/values-b+sk/strings.xml b/app/src/main/res/values-b+sk/strings.xml index f48b27143..8c1c4695f 100644 --- a/app/src/main/res/values-b+sk/strings.xml +++ b/app/src/main/res/values-b+sk/strings.xml @@ -1,6 +1,7 @@ - Našla sa nová aktualizácia! \n%1$s -> %2$s + Našla sa nová aktualizácia! +\n%1$s -> %2$s Výplň %1$dh %2$dm Epizóda %d bude vydaná za @@ -331,8 +332,10 @@ %1$s %2$s Vylúčenie zodpovednosti NSFW - Týmto sa natrvalo vymaže %s \nSte si istý? - %dm \nzostáva + Týmto sa natrvalo vymaže %s +\nSte si istý? + %dm +\nzostáva Prebieha Dokončené Rozloženie emulátora @@ -358,7 +361,9 @@ Zmazať repozitár URL adresa repozitára Verejný zoznam - CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. \n \nPripojte sa k nášmu Discord alebo vyhľadajte online. + CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. +\n +\nPripojte sa k nášmu Discord alebo vyhľadajte online. Nepodarilo sa nainštalovať novú verziu aplikácie Upozornenie: CloudStream 3 nenesie žiadnu zodpovednosť za používanie rozšírenia tretích strán a neposkytuje žiadnu podporu pre nich! Pridať repozitár diff --git a/app/src/main/res/values-b+so/strings.xml b/app/src/main/res/values-b+so/strings.xml index 0eb787311..9e4e9f9f1 100644 --- a/app/src/main/res/values-b+so/strings.xml +++ b/app/src/main/res/values-b+so/strings.xml @@ -27,7 +27,8 @@ %d dqq Kadinka Qiimaysan: %.1f - App cusub baa soo baxay! \n%1$s -> %2$s + App cusub baa soo baxay! +\n%1$s -> %2$s Soo dejinta Raadi Dookhyo kale @@ -182,9 +183,11 @@ Tirtir faylka Sii wado -30 - Dhamaantii waa la saari doona %s \nSow ma hubtid? + Dhamaantii waa la saari doona %s +\nSow ma hubtid? Fashil ka yimi xigashada - %ddq \nAyaa hadhsan + %ddq +\nAyaa hadhsan Dhamaystirmay Sannadka Qiimaynta @@ -424,7 +427,11 @@ Xayiran: %d Aan dejinayn: %d Waxa la cusbooneysiiyey %d sidkane - Ugu horreyn cloudstream ma laha wax websaydyo uu filimaanta kasoo xigto, waxay noqonaysaa inaad adigu rakibato reboositarradooda... \n \nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... \n \nDiscord naga soo qabo ama internetka ka baadh. + Ugu horreyn cloudstream ma laha wax websaydyo uu filimaanta kasoo xigto, waxay noqonaysaa inaad adigu rakibato reboositarradooda... +\n +\nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... +\n +\nDiscord naga soo qabo ama internetka ka baadh. Soo deji dhamaan sidkanayaasha reboositarkan? Boodhka Boodhka xalqadda diff --git a/app/src/main/res/values-b+sv/strings.xml b/app/src/main/res/values-b+sv/strings.xml index 0740e0b8d..bf352e5e2 100644 --- a/app/src/main/res/values-b+sv/strings.xml +++ b/app/src/main/res/values-b+sv/strings.xml @@ -2,7 +2,8 @@ Betygsatt: %.1f Hastighet (%.2fx) - Ny uppdatering hittad! \n%1$s -> %2$s + Ny uppdatering hittad! +\n%1$s -> %2$s CloudStream Hem Sök @@ -116,7 +117,8 @@ Ta bort nerladdad fil Ta bort Avbryt - %s kommer att raderas permanent \nÄr du helt säker? + %s kommer att raderas permanent +\nÄr du helt säker? Pågående Färdig Status @@ -229,7 +231,8 @@ %1$d %2$s %1$s %2$d%3$s -30 - %dm \nåterstår + %dm +\nåterstår NSFW OVA Torrent @@ -481,12 +484,16 @@ Visa community databaser Blandad inledning Skippa %s - CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. \n \nGå med i vår Discord eller sök online. + CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. +\n +\nGå med i vår Discord eller sök online. Välj bibliotek - Ditt bibliotek är tomt :( \nLogga in på ett bibliotekskonto eller lägg till program i ditt lokala bibliotek. + Ditt bibliotek är tomt :( +\nLogga in på ett bibliotekskonto eller lägg till program i ditt lokala bibliotek. Visa hoppa över popups för introduktion/eftertexter Ta bort från sett - Fil i säkertläge hittades! \nLaddar inte några tillägg vid start tills filen har tagits bort. + Fil i säkertläge hittades! +\nLaddar inte några tillägg vid start tills filen har tagits bort. Uppdaterar prenumererade program Prenumererad Prenumerera @@ -551,9 +558,21 @@ Använd standard konto PIN-kod Sök mängden som används när spelaren är dold - Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \n \n\'%s.\' \n \nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? - Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \'%s.\' \n \nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? - Här kan du ändra hur källorna ska sorteras, om en video har högre prioritet visas den högre upp i källvalet. Summan av källprioriteten och kvalitetsprioriteten är videoprioriteten. \n \nKälla A: 3 \nKvalitet B: 7 \nKommer att ha en kombinerad videoprioritet på 10. \n \nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! + Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: +\n +\n\'%s.\' +\n +\nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? + Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \'%s.\' +\n +\nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? + Här kan du ändra hur källorna ska sorteras, om en video har högre prioritet visas den högre upp i källvalet. Summan av källprioriteten och kvalitetsprioriteten är videoprioriteten. +\n +\nKälla A: 3 +\nKvalitet B: 7 +\nKommer att ha en kombinerad videoprioritet på 10. +\n +\nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! Avisering om nytt avsnitt Sök i andra tillägg Visa rekommendationer @@ -568,7 +587,8 @@ Efter några misslyckade försök stängs prompten. Starta bara om appen för att försöka igen. Favorit Ta bort från favoriter - %s \nkvarstår + %s +\nkvarstår kopierad! Lagringsnamn och URL För att säkerställa oavbrutna nedladdningar och aviseringar för prenumererade tv-program behöver CloudStream tillstånd att köras i bakgrunden. Genom att trycka på OK kommer du få en förfrågningsdialogruta. Tryck då på \'Tillåt\'.\n\nObservera att denna behörighet inte betyder att CS3 kommer att tömma ditt batteri. Den fungerar bara i bakgrunden när det behövs, till exempel när du tar emot aviseringar eller laddar ner videor från officiella tillägg. @@ -597,7 +617,9 @@ Autentisera lokalt QR-kodbild PIN-koden har upphört att gälla! - Är du säker på att du vill radera följande avsnitt permanent i %1$s? \n \n%2$s + Är du säker på att du vill radera följande avsnitt permanent i %1$s? +\n +\n%2$s Aktivera förhandsgranskningsminiatyr i sökfältet Förhandsgranskning av sökfältet Spela från början @@ -611,9 +633,15 @@ Avmarkera alla Radera Filer Radera (%1$d | %2$s) - Är du säker på att du vill ta bort följande objekt permanent? \n \n%s - Du kommer också permanent att radera alla avsnitt i följande serie: \n \n%s - Är du säker på att du permanent vill radera alla avsnitt i följande serie? \n \n%s + Är du säker på att du vill ta bort följande objekt permanent? +\n +\n%s + Du kommer också permanent att radera alla avsnitt i följande serie: +\n +\n%s + Är du säker på att du permanent vill radera alla avsnitt i följande serie? +\n +\n%s Ta bort insticksprogram Dölj namnen på spelarens kontroller Besök %s på din smartphone eller dator och ange koden ovan @@ -632,7 +660,7 @@ Aktivera torrent i Inställningar/Leverantörer/Föredragen media Avkodningsfel Ladda in första möjliga - Visa inte + Dölj Kantstorlek Egen Stöds ej @@ -732,16 +760,4 @@ Gör alla undertexter kursivstila Bakgrundsradie Visa spelarens metadata överlägg - Video - Förhandsvisning - Live - Visa realtidsklocka - Visa en realtidsklocka högst upp på skärmen. Gäller startsidan och spelaren - Profilinställningar - Dölj källor med negativ prioritet - Dölj källor med fel - - %d dold länk - %d dolda länkar - diff --git a/app/src/main/res/values-b+ta/strings.xml b/app/src/main/res/values-b+ta/strings.xml index 2537eba0c..fda0c36fd 100644 --- a/app/src/main/res/values-b+ta/strings.xml +++ b/app/src/main/res/values-b+ta/strings.xml @@ -358,7 +358,8 @@ முன்மாதிரி தளவமைப்பு பதிவிறக்கம் செய்யப்பட்ட கோப்பு பகுத்தல் - உங்கள் நூலகம் காலியாக உள்ளது :( \n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். + உங்கள் நூலகம் காலியாக உள்ளது :( +\n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். குழுவிலகவும் சுயவிவரங்கள் முள் 4 எழுத்துகளாக இருக்க வேண்டும் @@ -565,7 +566,8 @@ மதிப்பீடு (குறைந்த முதல் உயர் வரை) புதுப்பிக்கப்பட்டது (பழையது புதியது) இந்த பட்டியல் காலியாக உள்ளது. இன்னொரு இடத்திற்கு மாற முயற்சிக்கவும். - பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! \n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. + பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! +\n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. சந்தா காட்சிகளைப் புதுப்பித்தல் சந்தா எபிசோட் %d வெளியானது! @@ -609,7 +611,7 @@ பயன்பாட்டிலிருந்து வெளியேறுவதற்கு முன் உரையாடலைக் காட்டு உள்நாட்டில் அங்கீகரிக்கவும் தள்ளுபடி - உங்கள் ச்மார்ட்போன் அல்லது கணினியில் %s ஐப் பார்வையிட்டு மேலே உள்ள குறியீட்டை உள்ளிடவும் + உங்கள் ச்மார்ட்போன் அல்லது கணினியில் %s ஐப் பார்வையிட்டு மேலே உள்ள குறியீட்டை உள்ளிடவும் சாதன முள் குறியீட்டைப் பெற முடியாது, உள்ளக அங்கீகாரத்தை முயற்சிக்கவும் முள் குறியீடு இப்போது காலாவதியானது! விளிம்பு அளவு @@ -652,7 +654,7 @@ செருகுநிரல்களைப் புதுப்பிக்கவும் செருகுநிரல்களை கைமுறையாக புதுப்பிக்கவும் சொருகி புதுப்பிப்பு செயல்முறையைத் தொடங்குகிறது! - %d செருகுநிரல்(கள்) வெற்றிகரமாக புதுப்பிக்கப்பட்டது + வெற்றிகரமாக புதுப்பிக்கப்பட்டது %d சொருகி (கள்)! செருகுநிரல்கள் எதுவும் புதுப்பிக்கப்படவில்லை. பிளேயர் அறிவிப்புகள் பின்னணியில் இருந்து பின்னணியைக் கட்டுப்படுத்துவதற்கான பிளேயர் அறிவிப்பு @@ -731,17 +733,4 @@ %d பதிவிறக்கம் வரிசையில் உள்ளது %d பதிவிறக்கங்கள் வரிசையில் உள்ளன - பிளேயர் மேனிலை தரவு மேலடுக்கைக் காட்டு - ஒளிதோற்றம் - உண்மையான நேர கடிகாரத்தைக் காட்டு - திரையின் மேற்புறத்தில் நிகழ் நேர கடிகாரத்தைக் காட்டு. முகப்புப்பக்கம் மற்றும் பிளேயருக்குப் பொருந்தும் - முன்னோட்டம் - சுயவிவர அமைப்புகள் - எதிர்மறையான முன்னுரிமையுடன் ஆதாரங்களை மறை - பிழைகளுடன் ஆதாரங்களை மறைக்கவும் - - %d மறைக்கப்பட்ட இணைப்பு - %d மறைக்கப்பட்ட இணைப்புகள் - - வாழ்க diff --git a/app/src/main/res/values-b+tl/strings.xml b/app/src/main/res/values-b+tl/strings.xml index 4ed229ca7..4050ddbd7 100644 --- a/app/src/main/res/values-b+tl/strings.xml +++ b/app/src/main/res/values-b+tl/strings.xml @@ -15,7 +15,8 @@ Bilis (%.2fx) Rated: %.1f - Bagong update! \n%1$s -> %2$s + Bagong update! +\n%1$s -> %2$s CloudStream Home Maghanap @@ -137,7 +138,8 @@ Kanselahin I-pause I-resume - This will permanently delete %s \nAre you sure? + This will permanently delete %s +\nAre you sure? Patuloy Tapos na Katayuan diff --git a/app/src/main/res/values-b+tr/strings.xml b/app/src/main/res/values-b+tr/strings.xml index e4a2dbc37..fb0600c1a 100644 --- a/app/src/main/res/values-b+tr/strings.xml +++ b/app/src/main/res/values-b+tr/strings.xml @@ -19,7 +19,8 @@ Hız (%.2fx) Puan: %.1f - Yeni güncelleme bulundu! \n%1$s -> %2$s + Yeni güncelleme bulundu! +\n%1$s -> %2$s Dolgu %d dakika CloudStream @@ -186,7 +187,8 @@ Sürdür -30 +30 - %s tamamen silinecek \nEmin misiniz? + %s tamamen silinecek +\nEmin misiniz? %dd \nkaldı Devam ediyor Tamamlandı @@ -483,8 +485,10 @@ Çok fazla metin. Panoya kaydedilemiyor. Kütüphane Tarayıcı - Kütüphaneniz boş :( \nBir kütüphane hesabında oturum açın veya yerel kütüphanenize programlar ekleyin. - Güvenli mod dosyası bulundu! \nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. + Kütüphaneniz boş :( +\nBir kütüphane hesabında oturum açın veya yerel kütüphanenize programlar ekleyin. + Güvenli mod dosyası bulundu! +\nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. Şuna Göre Sırala Sırala Güncellenme (Yeniden Eskiye) @@ -527,7 +531,13 @@ Mobil veri Varsayılanı ayarla Düzenle - Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. \n \nKaynak A: 3 \nKalite B: 7 \nBirleştirilmiş video önceliği 10 olacaktır. \n \nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! + Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. +\n +\nKaynak A: 3 +\nKalite B: 7 +\nBirleştirilmiş video önceliği 10 olacaktır. +\n +\nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! Kaliteler Profil arkaplanı UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s @@ -536,7 +546,11 @@ Favoriler %s favorilere eklendi %s olarak giriş yapıldı - Kütüphanenizde potensiyel kopya ürünler bulundu: \n \n%s \n \nYine de ekleyerek var olanları değiştirmek mi istersiniz, yoksa iptal etmek mi? + Kütüphanenizde potensiyel kopya ürünler bulundu: +\n +\n%s +\n +\nYine de ekleyerek var olanları değiştirmek mi istersiniz, yoksa iptal etmek mi? %s için PIN girin Yedekleme sıklığı Potensiyel Kopya Bulundu @@ -559,7 +573,9 @@ Depo bulunamadı, bağlantı adresini kontrol edin veya VPN ile deneyin Zaten oyladınız Depoda eklenti bulunamadı - Görünüşe göre potansiyel bir kopya kütüphanenizde zaten bulunuyor: \'%s\' \n \nYine de ekleyerek var olanı değiştirmek mi istersiniz, yoksa iptal etmek mi? + Görünüşe göre potansiyel bir kopya kütüphanenizde zaten bulunuyor: \'%s\' +\n +\nYine de ekleyerek var olanı değiştirmek mi istersiniz, yoksa iptal etmek mi? PIN girin PIN Geçerli PIN\'i Giriniz @@ -621,12 +637,20 @@ Oynatıcı kontrolünün adlarını gizle Baştan Oynat Tümünü Seç - Aşağıdaki öğeleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%s - %1$s içindeki aşağıdaki bölümleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%2$s + Aşağıdaki öğeleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%s + %1$s içindeki aşağıdaki bölümleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%2$s Dosyaları Silin Sil (%1$d | %2$s) - Aşağıdaki dizideki tüm bölümleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%s - Ayrıca aşağıdaki dizideki tüm bölümleri kalıcı olarak sileceksiniz: \n \n%s + Aşağıdaki dizideki tüm bölümleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%s + Ayrıca aşağıdaki dizideki tüm bölümleri kalıcı olarak sileceksiniz: +\n +\n%s Silinecek Öğeleri Seçin Tüm Seçimi Kaldır Çevrimdışı izlemek için kullanılabilir @@ -661,7 +685,7 @@ Tarih %s Eklentileri Güncelle Eklenti güncellemesi başlıyor! - %d eklenti başarıyla güncellendi + %d eklenti başarıyla güncellendi! Hiçbir eklenti güncellenmedi. Eklentileri el ile güncelle Oynatıcı bildirimi arka planda oynatmasını kontrol etmek içindir @@ -746,16 +770,4 @@ Öncelikli kaynak Oynatıcıda video kaynaklarının nasıl sıralanacağını belirleyin Meta Verileri Katmanını Göster - Canlı - Ön Gösterim - Video - Gerçek zamanlı saati göster - Ekranın üst kısmında gerçek zamanlı bir saat göster. Ana sayfa ve oynatıcı için uygulanır - Profil ayarları - Eksi önceliğe sahip kaynakları gizle - Hatalı kaynakları gizle - - %d gizli bağlantı - %d gizli bağlantı - diff --git a/app/src/main/res/values-b+uk/strings.xml b/app/src/main/res/values-b+uk/strings.xml index 56e5a916e..2a9352183 100644 --- a/app/src/main/res/values-b+uk/strings.xml +++ b/app/src/main/res/values-b+uk/strings.xml @@ -1,21 +1,21 @@ Плакат - Постер епізоду - Завантаження скасовано - Змінити постачальника + Постер Епізоду + Завантаження Скасовано + Змінити Постачальника Назад Рейтинг: %.1f У ролях: %s - %d епізод вийде через + Епізод %d вийде через Плакат %1$s Еп %2$d - %1$d дн %2$d год %3$d хв - %1$d год %2$d хв - %d хв - Головний постер - Наступне випадкове - Попередній перегляд тла + %1$dд %2$dгод %3$dхв + %1$dгод %2$dхв + %dхв + Головний Постер + Наступний Випадковий + Попередній Перегляд Заднього Фону Швидкість (%.2fx) Знайдено нове оновлення!\n%1$s –> %2$s Пошук @@ -24,29 +24,29 @@ Налаштування Пошук… Пошук на %s… - Немає даних - Більше налаштувань + Немає Даних + Більше Опцій Наступний епізод Жанри - Відкрити в браузері - Пропустити завантаження + Відкрити в Браузері + Пропустити Завантаження Завантаження… Завершено - Заплановано - Покинуто - Відтворити фільм - Відтворити трейлер - Транслювати торрент + Планую Дивитися + Скинуто + Відтворити Фільм + Відтворити Трейлер + Транслювати Торрент Повторити з’єднання… Назад - Відтворити епізод + Відтворити Епізод Завантажено Завантаження - Завантаження завершено + Завантаження Завершено Дуб. Суб. - Видалити файл - Відновити завантаження + Видалити Файл + Відновити Завантаження Приховати Переглянути Подробиці @@ -57,26 +57,26 @@ Скопіювати Закрити Зберегти - Швидкість програвача - Колір вікна - Тип межі + Швидкість Плеєра + Колір Вікна + Тип Межі Шрифт - Розмір шрифту + Розмір Шрифту Пошук за постачальниками Пошук за типами - Жодного банана не надано - Автовибір мови - Завантажити мови - Мова субтитрів - Утримуйте, щоби скинути до типових + Жодного Benenes не надано + Авто-Вибір Мови + Завантажити Мови + Мова Субтитрів + Утримуйте, щоби скинути до типових налаштувань Імпортуйте шрифти, помістивши їх до %s - Продовжити перегляд + Продовжити Перегляд Вилучити Докладніше Цей постачальник є торентом, рекомендується використовувати VPN Опис - Сюжет не знайдено - Опис не знайдено + Сюжет Не Знайдено + Опис Не Знайдено Показати Logcat 🐈 Продовжувати відтворення в мініатюрному програвачі поверх інших застосунків Прибрати чорні смуги @@ -92,33 +92,33 @@ Філлер Відтворити в CloudStream Мережева трансляція - Переглядаю + Переглядання Поділитися Відкладено - Передивляюся + Переглядаю Повторно Завантажити - Відтворити трансляцію + Відтворити Трансляцію Джерела Субтитри - Внутрішнє сховище - Завантаження призупинено - Завантаження розпочато - Не вдалося завантажити - Оновлення розпочато - Помилка завантаження посилань - Призупинити завантаження - Переглянути файл + Внутрішнє Сховище + Завантаження Призупинено + Завантаження Розпочато + Завантаження не Вдалося + Оновлення Розпочато + Помилка Завантаження Посилань + Призупинити Завантаження + Переглянути Файл Докладніше - Фільтрувати закладки + Фільтрувати Закладки Очистити - Налаштування субтитрів - Колір тла - Висота субтитрів - Колір тексту - Колір обведення + Налаштування Субтитрів + Колір Тла + Висота Субтитрів + Колір Тексту + Колір Обведення Автовідтворення наступного епізоду Проведіть збоку в бік, щоби керувати часом відтворення у відео - Дано %d бананів розробникам + %d Benenes надано розробникам Кнопка зміни розміру програвача @string/home_play Для коректної роботи цього постачальника може знадобитися VPN @@ -138,12 +138,12 @@ Дані збережено Помилка резервного копіювання %s Пошук - Облікові записи та безпека - Оновлення та резервне копіювання + Облікові Записи та Безпека + Оновлення та Резервне Копіювання Подробиці - Розширений пошук + Розширений Пошук Показувати результати пошуку, розділені за постачальниками - Показувати філлери для аніме + Показувати наповнювачі для аніме Показувати трейлери Приховати вибрану якість відео у результатах пошуку Автозавантаження розширень @@ -153,25 +153,25 @@ Github Застосунок для ранобе від тих самих розробників Застосунок для аніме від тих самих розробників - Дати бананів розробникам - Мова застосунку + Дати benene розробникам + Мова Застосунку Цей постачальник не має підтримування Chromecast - Посилань не знайдено - Переглянути епізод + Посилань Не Знайдено + Переглянути Епізод Скинути до типових значень - Немає сезону + Немає Сезону Епізоди %1$d %2$s С Е - Видалити файл + Видалити Файл Видалити Скасувати Відновити -30 Це назавжди видалить %s\nВи впевнені? - %d хв\nзалишилося - Виходить + %dхв\nзалишилося + Триває Завершено Рейтинг Тривалість @@ -184,16 +184,16 @@ Телесеріали Мультфільми Аніме - ОВА - Азіатські драми - Прямі трансляції + OVA + Азіатські Драми + Прямі Трансляції Інші Серіал Мультфільм Аніме - Документалка - Азіатська драма - Пряма трансляція + Документальний Фільм + Азіатська Драма + Пряма Трансляція Відео Помилка джерела Віддалена помилка @@ -203,15 +203,15 @@ Переглянути в %s Автозавантаження Завантажити дзеркало - Перевірити наявність оновлень + Перевірити Наявність Оновлень Забл./Розбл. Пропустити ОП Не показувати знову Оновити - Бажана якість перегляду (Wi-Fi) + Бажана якість перегляду (WiFi) Заголовок Перемкнути елементи інтерфейсу на плакаті - Оновлення не знайдено + Оновлення Не Знайдено Натисніть двічі праворуч або ліворуч, щоби перемотати вперед або назад Використовувати системну яскравість у програвачі замість темного накладання Завантажено файл резервної копії @@ -222,10 +222,10 @@ Автооновлення розширень Автоматично встановлювати всі розширення, які ще не встановлено, з доданих сховищ. Автоматично перевіряти нові оновлення після запуску застосунку. - Посилання скопійовано до буфера обміну - Деякі пристрої не підтримують новий встановлювач пакетів. Спробуйте старий варіант, якщо оновлення не встановлюються. + Покликання скопійовано до буфера обміну + Деякі пристрої не підтримують новий інсталятор пакетів. Спробуйте старий варіант, якщо оновлення не встановлюються. Приєднуйтеся до Discord - Дано банани + Дано benene Рік +30 %1$s %2$d%3$s @@ -240,21 +240,21 @@ Стислий зміст Фільми Перезавантажити посилання - Документалки + Документальні Фільми NSFW Фільм - ОВА + OVA Торент Мітка якості NSFW Несподівана помилка програвача Помилка завантаження, перевірте дозвіл на зберігання - Епізод через Chromecast + Chromecast епізод Мітка субтитрів Джерело Завантажити субтитри Мітка дубляжу - Пропустити це оновлення + Пропустити це Оновлення Усе На весь екран Розтягнути @@ -275,9 +275,9 @@ Макет застосунку Бажані медіа Автоматично - Макет телевізора - Макет телефона - Макет емулятора + Телевізійна Обгортка + Телефона обгортка + Емуляторна обгортка Основний колір Тема застосунку Розташування назви плаката @@ -296,7 +296,7 @@ %d / 10 /%d %s автентифіковано - Не вдалося ввійти в %s + Не вдалося увійти в %s Нічого Звичайний Мін. @@ -319,9 +319,9 @@ HDR SDR Web - Зображення плаката + Зображення Плаката Програвач - Роздільність та заголовок + Роздільна здатність та заголовок Недійсний ID Недійсна URL-адреса Резервне копіювання @@ -341,15 +341,15 @@ DNS через HTTPS Шлях завантаження Додайте двійника наявного сайту, з іншою URL-адресою - Показувати аніме з дубляжем / із субтитрами + Показувати Дубльоване/З Субтитрами Аніме Застереження Розширення Дії 127.0.0.1 Макет Кодування субтитрів - Увімкнути NSFW у підтримуваних розширеннях - Макет + Увімкнути NSFW вміст на підтримуваних Розширеннях + Обгортка Постачальники https://example.com %1$s %2$s @@ -375,7 +375,7 @@ Фільтрувати за бажаною мовою медіа 4K Заголовок - Роздільність + Роздільна здатність Помилка Трейлер Додатково @@ -388,7 +388,7 @@ TS TC Вилучати роздуття субтитрів - Джерело переходу (необов’язково) + Referer (необов’язково) Далі Переглядайте відео на цих мовах Пропустити налаштування @@ -396,7 +396,7 @@ Готово Розширення Додати репозиторій - Назва репозиторію (необов’язково) + Назва репозиторію (Опціонально) URL-адреса репозиторію або короткий код Розширення завантажено Розширення завантажено @@ -432,7 +432,7 @@ Список відтворення HLS Вбудований програвач Ендінґ - Підсумок + Коротке повторення Пропустити %s Змішаний ендінґ Подяки @@ -449,10 +449,10 @@ Установлення оновлення застосунку… Не вдалося встановити нову версію застосунку Застарілий - Встановлювач пакунків + Встановлювач Пакунків Застосунок буде оновлено після виходу Це також призведе до видалення всіх розширень сховища - Усі мови + Усі Мови Назад Змініть вигляд застосунку відповідно до вашого пристрою Розширення видалено @@ -467,26 +467,26 @@ Застосунок не знайдено Змішаний опенінґ Вилучити з переглянутого - Оновленням (від старого до нового) - Оновленням (від нового до старого) + Оновленням (від Старого до Нового) + Оновленням (від Нового до Старого) Бібліотека Сортувати - Рейтингом (від високого до низького) + Рейтингом (від Високого до Низького) Сортувати за Алфавітом (від А до Я) - Рейтингом (від низького до високого) + Рейтингом (від Низького до Високого) Ваша бібліотека порожня :(\nУвійдіть в обліковий запис бібліотеки або додайте щось до вашої локальної бібліотеки. Алфавітом (від Я до А) - Оберіть бібліотеку + Оберіть Бібліотеку Відкрити з Браузер Цей список порожній. Спробуйте перейти до іншого. Файл безпечного режиму знайдено!\nРозширення не завантажуватимуться під час запуску, доки файл не буде видалено. Android TV - Прогрвач приховано: крок перемотування - Програвач показано: крок перемотування + Прогрвач Приховано – Крок Перемотування + Програвач Показано – Крок Перемотування Крок перемотування, який використовується, коли програвач видимий - Крок перемотування, який використовується, коли програвач прихований + Крок перемотування, який використовується, коли плеєр прихований Провалено Пройдено Перезапустити @@ -500,10 +500,10 @@ Ви відписалися від %s Епізод %d випущено! Повернути - Проксі GitHub + GitHub Проксі Не вдалось отримати доступ до GitHub. Увімкнення проксі-сервера jsDelivr… Обходи ISP - Обхід блокування прямих посилань GitHub за допомогою jsDelivr. Може спричинити затримку оновлень на кілька днів. + Обхід блокування чистих gitHub URLs за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (мобільні дані) Встановити типові Профілі @@ -527,7 +527,7 @@ Вподобані %s додано до вподобаних У вашій бібліотеці виявлено можливі дублікати:\n\n%s\n\nУсе одно хочете додати цей елемент, замінити наявні чи скасувати дію? - Знайдено можливий дублікат + Знайдено Можливий Дублікат Заблокувати профіль Додати до обраного Замінити все @@ -538,32 +538,32 @@ Додати Підписатися Вилучити з обраного - Оберіть обліковий запис + Оберіть Обліковий Запис Схоже, що у вашій бібліотеці вже є можливий дублікат: \'%s.\'\n\nУсе одно хочете додати цей елемент, замінити наявний чи скасувати дію? Уведіть PIN-код PIN-код Уведіть поточний PIN-код Увійшли як %s Уведіть PIN-код для %s - Використовувати типовий обліковий запис + Використовувати Типовий Обліковий Запис Пропускати вибір облікового запису під час запуску - Керувати обліковими записами + Керувати Обліковими Записами Редагувати обліковий запис Показувати кнопку перемикання орієнтації екрана Обернути - Посилання перезавантажені + Посилання Перезавантажені Автообертання Увімкнути автоматичну зміну орієнтації екрана відповідно до відео Додати налаштування швидкості до програвача - Перевірити всі розширення + Перевірити всі Розширення Пошук в інших розширеннях Показати рекомендації - Ця перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. + Ця Перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. Сповіщення про новий епізод - Автентифікація за паролем / PIN-кодом + Автентифікація за Паролем/PIN-кодом Розблокуйте CloudStream - Біометричне блокування - Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, графічного ключа або пароля. + Біометричне Блокування + Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, Графічного Ключа або Пароля. Щойно було виконано резервне копіювання даних CloudStream. Хоча ймовірність цього вкрай мала, усі пристрої можуть поводитися по-різному. У рідкісних випадках, якщо ви втратите доступ до застосунку, повністю очистіть дані застосунку та відновіть їх із резервної копії. Просимо вибачення за будь-які незручності, що можуть виникнути. Біометрична автентифікація не підтримується на цьому пристрої Після кількох невдалих спроб вікно запиту зникне. Перезапустіть застосунок, щоби спробувати ще раз. @@ -572,11 +572,11 @@ Додати до вподобаного скопійовано! Назва репозиторію та URL - Помилка копіювання, скопіюйте logcat та зверніться до служби підтримки застосунку. - Помилка доступу до буфера обміну, спробуйте ще раз. - Гаразд + Помилка копіювання, Будь-ласка скопіюйте logcat та зверніться до служби підтримки застосунку. + Помилка доступу до буфера обміну, Будь-ласка спробуйте ще раз. + OK Вимкнути оптимізацію батареї - Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши «Гаразд», ви побачите діалогове вікно запиту. Натисніть «Дозволити».\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. + Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши OK, ви побачите діалогове вікно запиту. Натисніть \'Дозволити\'.\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. Споживання батареї застосунком уже змінено на необмежене Не вдається відкрити подробиці про застосунок CloudStream. Аудіокнига @@ -584,7 +584,7 @@ Медіа Скинути Наступний через %s - %1$d сезон %2$d епізод вийде через + Сезон %1$d Епізод %2$d вийде через Оберіть пристрій для трансляції Трансляція через дзеркало CloudStream Wiki @@ -592,71 +592,71 @@ Облікові записи Зображення QR-коду Відкрити сховище - Відвідайте %s на своєму смартфоні або комп’ютері та введіть вищевказаний код - Не вдається отримати PIN-код пристрою, спробуйте локальну автентифікацію - Термін дії PIN-коду закінчився! - Термін дії коду закінчується через %1$d хв %2$d с - Локальна автентифікація + Відвідайте %s на своєму смартфоні або комп\'ютері та введіть вищевказаний код + Не вдається отримати PIN-код пристрою, спробуйте локальну аутентифікацію + PIN-код зараз закінчився ! + Термін дії коду закінчується через %1$dхв %2$dс + Локальна Аутентифікація Відхилити - Відтворити з початку + Відтворити з Початку Попередження Видалити розширення Наразі завантажень немає. Приховати назви елементів керування в програвачі Відкрити локальне відео - Датою випуску (від нових до старих) - Датою випуску (від старих до нових) - Оберіть елементи для видалення - Обрати все - Зняти виділення + Датою випуску (від Нових до Старих) + Датою випуску (від Старих до Нових) + Оберіть Елементи для Видалення + Обрати Все + Зняти Вибір Всіх Видалити (%1$d | %2$s) Ви впевнені, що хочете назавжди видалити такі епізоди в %1$s?\n\n%2$s Ви також назавжди видалите всі епізоди в такому серіалі:\n\n%s - Доступно для перегляду поза мережею - Видалити файли + Доступно для перегляду в оффлайн режимі + Видалити Файли Ви впевнені, що хочете назавжди видалити такі елементи?\n\n%s Ви впевнені, що хочете назавжди видалити всі епізоди в такому серіалі?\n\n%s Попередній перегляд на шкалі перегляду Увімкнути мініатюру попереднього перегляду на шкалі перегляду Субтитри ще не завантажено Підтвердіть перед виходом - Показати + Відобразити Показувати діалог перед виходом із застосунку - Не показувати + Не відображати Розташування теки для резервних копій Власний - Це відео – торрент, це означає, що ваша відеоактивність може відстежуватися.\nПереконайтеся, що розумієте, що таке торрент, перед тим як продовжити. + Це відео – Торрент, це означає, що ваша відео діяльність може відстежуватися.\nПереконайтеся, що розумієте, що таке Торрент, перед тим як продовжити. Розмір обведення Аудіо Подкаст Непідтримувана помилка Помилка кодування Завантажити перший доступний - Увімкніть торент в Налаштування - > Постачальники - > Бажані медіа - Перезапустіть застосунок та прийміть спливне вікно «Транслювати торрент», щоби продовжити. + Увімкніть торент в Налаштування/Постачальники/Бажані медіа + Перезапустіть застосунок та прийміть спливне вікно Stream Torrent, щоби продовжити. Програмне декодування Програмне декодування дозволяє плеєру відтворювати відеофайли, які не підтримуються вашим пристроєм, але може спричинити затримки або нестабільне відтворення у високій роздільній здатності. - Датою виходу (найновіша) - Епізодом (за зростанням) - Рейтингом (найнижчий) + Датою виходу (Найновіша) + Епізодом (За Зростанням) + Рейтингом (Найнижчий) Рейтинг %s - Епізодом (за спаданням) - Рейтингом (найвищий) - Датою виходу (найстаріша) + Епізодом (за Спаданням) + Рейтингом (Найвищий) + Датою виходу (Найстаріша) Еп. %s Дата %s - Оновити розширення - Успішно оновлено %d розширення(-ь) + Оновити Розширення + Успішно оновлено %d розширення(-ь)! Оновити розширення вручну Починається оновлення розширень! Не оновлено жодного розширення. Сповіщення програвача для керування відтворенням у фоновому режимі Сповіщення програвача Розпізнавання мовлення недоступне - Почніть говорити… + Почніть Говорити… Вбудовані Мережеві - Радіус тла + Радіус Тла Зробити всі субтитри потовщеним Зробити всі субтитри курсивом Гучність перевищила 100% @@ -664,8 +664,8 @@ Одночасних з’єднань Змінює межі екрана Обрізання зображення - Перейти до завантажень - Немає з’єднання з мережею. \n\nПерепід’єднайтеся до мережі та спробуйте ще раз або перегляньте завантажені відео локально. + Перейти до Завантажень + Немає підключення до Інтернету.\n\nБудь ласка, підключіться до Інтернету та спробуйте ще раз або перегляньте завантажені відео офлайн. Скільки різних елементів можна завантажити паралельно Паралельних завантажень Скільки одночасних з’єднань може використовувати кожне завантаження @@ -674,46 +674,46 @@ Завжди запитувати Змінювати швидкість при утриманні Утримуйте, щоб отримати 2-кратну швидкість - %1$d год %2$d хв %3$d с - %1$d хв %2$d с - %1$d с + %1$dгод %2$dхв %3$dс + %1$dхв %2$dс + %1$dс Мітка рейтингу Немає облікового запису - Редагувати зображення профілю - Введіть URL-адресу зображення профілю - URL-адресу не знайдено - Недійсна URL-адреса або зображення - Зображення успішно оновлено + Редагувати Зображення Профілю + Введіть URL-адресу Зображення Профілю + URL-адресу Не Знайдено + Недійсна URL-адреса або Зображення + Зображення Успішно Оновлено Позначити як переглянуте до цього епізоду Вилучити переглянуті до цього епізоду Перезавантажено - Перезавантажити постачальника + Перезавантажити Постачальника Послуг Переглянути в дзеркалі" Назва - Роздільність та назва - Вирівнювання субтитрів + Роздільна здатність та назва + Вирівнювання Субтитрів Внизу ліворуч Внизу по центру Внизу праворуч - Посередині зліва - Посередині в центрі - Посередині справа + Середній лівий + Середній центр + Середній правий Угорі ліворуч - Угорі в центрі + Верхній центр Угорі праворуч - Відтворити весь серіал - Встановити передрелізну версію - Передрелізна версія вже встановлена. - Не вдалося встановити передрелізну версію. - Текст епізоду - Пропозиції пошуку + Відтворити Повні Серії + Встановити перед-релізну версію + Перед-релізна версія вже встановлена. + Не вдалося встановити перед-релізну версію. + Текст Епізоду + Пропозиції Пошуку Показувати підказки пошуку під час введення тексту - Очистити пропозиції + Очистити Пропозиції Додаткова яскравість Увімкнути фільтр яскравості при перевищенні 100% яскравості дисплея extra_brightness_enabled Показати панель трансляції - Інформація про медіа + Інформація Про Медіа Назва джерела Черга завантаження Наразі немає завантажень у черзі. @@ -735,19 +735,5 @@ Пріоритетне джерело Виберіть спосіб сортування джерел відео у програвачі - Показувати накладання метаданих програвача - Відео - Передперегляд - Наживо - Показувати поточний час - Показувати поточний час у верхній частині екрана (на головній та у програвачі) - Налаштування профілю - Приховати джерела з від’ємним пріоритетом - Приховати джерела з помилками - - %d приховане посилання - %d приховані посилання - %d прихованих посилань - %d прихованих посилань - + Показувати Накладання Метаданих Програвача diff --git a/app/src/main/res/values-b+ur/strings.xml b/app/src/main/res/values-b+ur/strings.xml index 94728e2f8..9a72e586f 100644 --- a/app/src/main/res/values-b+ur/strings.xml +++ b/app/src/main/res/values-b+ur/strings.xml @@ -10,7 +10,8 @@ ذریعہ تبدیل کریں پس منظر کا دیکھنا درجہ بندی: %.1f - نیا update آگیا ہے! \n%1$s -> %2$s + نیا update آگیا ہے! +\n%1$s -> %2$s بھرنے والا %d منٹ %1$d دن %2$d گھنٹے %3$d منٹ @@ -188,8 +189,10 @@ از سر نو شروع کریں -30 +30 - یہ مستقل طور پر حذف ہوجائے گا %s \nتمھيں يقين ہے? - %dm \nباقی + یہ مستقل طور پر حذف ہوجائے گا %s +\nتمھيں يقين ہے? + %dm +\nباقی احوال مکمل حالت @@ -438,7 +441,8 @@ پلیئردکھایا گیا - Seek Amount پلیئر کے نظر آنے پر استعمال کی جانے والی Seek Amount پلیئر کے چھپنے پر استعمال ہونے والی seek amount - سیف موڈ فائل مل گئی! \nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ + سیف موڈ فائل مل گئی! +\nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ شروع کریں ناکام کامیاب ہو گیا @@ -450,7 +454,8 @@ آئی ایس پی بائی پاسز %s کو سبسکرائب کیا Bypass blocking of raw github URLs using jsDelivr. اپ ڈیٹس میں کچھ دنوں کی تاخیر ہو سکتی ہے - آپ کی لائبریری خالی ہے:( \nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ + آپ کی لائبریری خالی ہے:( +\nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ غلط URL براؤزر ویب @@ -499,7 +504,9 @@ سب ٹائٹلز سے بند کیپشنز کو ہٹا دیں اپنے آلے کے مطابق ایپ کی شکل تبدیل کریں اگلے - CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ \n \nہمارے Discord میں شامل ہوں یا آن لائن تلاش کریں۔ + CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ +\n +\nہمارے Discord میں شامل ہوں یا آن لائن تلاش کریں۔ تمام ایکسٹینشنز کریش کی وجہ سے آف کر دی گئیں تاکہ آپ کو پریشانی کا باعث تلاش کرنے میں مدد مل سکے۔ پہلے ایکسٹینشن انسٹال کریں بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ @@ -521,7 +528,13 @@ آپ نے پہلے ہی ووٹ دیا ہے مخزن میں کوئی پلگ انز نہیں ملا ترجیحی تعین کریں - یہاں آپ تبدیلی کرسکتے ہیں کہ سورسز کو کس طرح کی ترتیب دی جائے۔ اگر ایک ویڈیو کی زیادہ پرائیورٹی ہوتی ہے تو یہ سورس کی انتخاب میں زیادہ اوپر آئے گی۔ سورس کی پرائیورٹی اور کوالٹی کی پرائیورٹی کا مجموعہ ویڈیو کی پرائیورٹی ہوتی ہے۔ \n \nسورس A: 3 \nکوالٹی B: 7 \nاس کا مجموعی ویڈیو پرائیورٹی 10 ہوتی ہے۔ \n \nنوٹ: اگر مجموعہ 10 یا اس سے زیادہ ہو تو پلیر وہ لنک لوڈ کرنے کو خود بخود چھوڑ دے گا! + یہاں آپ تبدیلی کرسکتے ہیں کہ سورسز کو کس طرح کی ترتیب دی جائے۔ اگر ایک ویڈیو کی زیادہ پرائیورٹی ہوتی ہے تو یہ سورس کی انتخاب میں زیادہ اوپر آئے گی۔ سورس کی پرائیورٹی اور کوالٹی کی پرائیورٹی کا مجموعہ ویڈیو کی پرائیورٹی ہوتی ہے۔ +\n +\nسورس A: 3 +\nکوالٹی B: 7 +\nاس کا مجموعی ویڈیو پرائیورٹی 10 ہوتی ہے۔ +\n +\nنوٹ: اگر مجموعہ 10 یا اس سے زیادہ ہو تو پلیر وہ لنک لوڈ کرنے کو خود بخود چھوڑ دے گا! پسندیدہ %s کو پسندیدہ میں شامل کیا گیا %s کو پسندیدہ سے ختم کیا گیا @@ -548,8 +561,14 @@ جمع کریں سب کو بدل دیں بدل دیں - آپ کی لائبریری میں ممکنہ ڈپلیکیٹ آئٹمز مل گئے ہیں: \n \n%s \n \nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ - ایسا معلوم ہوتا ہے کہ آپ کی لائبریری میں ممکنہ طور پر ڈپلیکیٹ آئٹم پہلے سے موجود ہے: \'%s۔\' \n \nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ + آپ کی لائبریری میں ممکنہ ڈپلیکیٹ آئٹمز مل گئے ہیں: +\n +\n%s +\n +\nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ + ایسا معلوم ہوتا ہے کہ آپ کی لائبریری میں ممکنہ طور پر ڈپلیکیٹ آئٹم پہلے سے موجود ہے: \'%s۔\' +\n +\nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ پروفائل لاک کریں آغاز پر اکاؤنٹ کا انتخاب چھوڑ دیں ویڈیو واقفیت کی بنیاد پر اسکرین کی سمت بندی کی خودکار سوئچنگ کو فعال کریں @@ -557,7 +576,8 @@ کاپی کر لیا! ذخیرہ کا نام اور URL %s میں آنے والا ہے - %s \nباقی + %s +\nباقی کلپ بورڈ تک رسائی میں خرابی، براہ کرم دوبارہ کوشش کریں۔ کاپی کرنے میں خرابی، براہ کرم logcat کاپی کریں اور ایپ سپورٹ سے رابطہ کریں۔ سبسکرائب شدہ ٹی وی شوز کے لیے بلاتعطل ڈاؤن لوڈز اور اطلاعات کو یقینی بنانے کے لیے، CloudStream کو پس منظر میں چلنے کی اجازت درکار ہے۔ OK دبانے سے، آپ کو App info پر بھیج دیا جائے گا۔ وہاں اسکرول کریں 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 اور battery usage کو 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 کردیں، اس اجازت کا مطلب یہ نہیں ہے کہ CS3 آپ کی بیٹری ختم کردے گا۔ یہ صرف ضرورت پڑنے پر پس منظر میں کام کرے گا، جیسے کہ جب اطلاعات موصول ہوں یا آفیشل ایکسٹینشنز سے ویڈیوز ڈاؤن لوڈ کریں۔ اگر آپ منسوخ کرنے کا انتخاب کرتے ہیں، تو آپ اس ترتیب کو بعد میں 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 میں ایڈجسٹ کر سکتے ہیں۔ diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 3e2a93fb4..c0f4744fd 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -14,21 +14,22 @@ Poster tập phim Poster chính Tập tiếp theo ngẫu nhiên - Thoát + Quay lại Thay đổi Nguồn phim Xem trước hình nền Tốc độ (%.2fx) Đánh giá: %.1f - Đã tìm thấy bản cập nhật mới! \n%1$s -> %2$s + Đã tìm thấy bản cập nhật mới! +\n%1$s -> %2$s Bộ lọc %d phút CloudStream Phát bằng CloudStream - Trang chủ - Tìm kiếm + Trang Chủ + Tìm Kiếm Tải xuống - Cài đặt + Cài Đặt Tìm kiếm… Tìm kiếm %s… Không có dữ liệu @@ -36,7 +37,7 @@ Tập tiếp theo Thể loại Chia sẻ - Phát bằng Trình duyệt + Mở bằng trình duyệt Bỏ tải Đang tải… Đang xem @@ -46,9 +47,9 @@ Xem sau Xem lại Phát - Phát Trực tiếp + Phát Livestream Phát Torrent - Nguồn + Nguồn phim Phụ đề Thử kết nối lại… Quay lại @@ -57,7 +58,7 @@ Tải xuống Đã tải xuống Đang tải xuống - Tải xuống đã tạm dừng + Đã tạm dừng tải xuống Tải xuống đã bắt đầu Tải xuống thất bại Tải xuống đã hủy @@ -87,7 +88,7 @@ Tốc độ phát Cài đặt phụ đề Màu chữ - Màu viền + Màu viền chữ Màu nền Màu cửa sổ Kiểu viền @@ -101,7 +102,7 @@ Tự động chọn ngôn ngữ Ngôn ngữ tải xuống Ngôn ngữ phụ đề - Nhấn giữ để đặt lại mặc định + Nhấn giữ để đặt lại về mặc định Thêm phông chữ tại %s Tiếp tục xem Xóa @@ -128,10 +129,10 @@ Vuốt lên hoặc xuống cạnh trái hoặc phải để điều chỉnh độ sáng hoặc âm lượng Tự động phát tập tiếp theo Phát tập tiếp theo sau khi hết tập hiện tại - Nhấn hai lần để tua - Nhấn hai lần để tạm dừng + Nhấn 2 lần để tua + Nhấn 2 lần để tạm dừng Thời lượng tua (Giây) - Nhấn hai lần vào cạnh trái hoặc phải để tua về trước hoặc sau + Nhấn 2 lần vào cạnh trái hoặc phải để tua về trước hoặc sau Nhấn vào giữa hai lần để tạm dừng Sử dụng độ sáng hệ thống Dùng độ sáng hệ thống thay cho lớp phủ tối trong trình phát ứng dụng @@ -166,7 +167,7 @@ Ngôn ngữ ứng dụng Nguồn phim này chưa hỗ trợ Chromecast Không tìm thấy liên kết - Đã sao chép liên kết vào bảng nhớ tạm + Đã sao chép liên kết vào bộ nhớ tạm Phát Tập phim Đặt lại giá trị mặc định Mùa @@ -185,7 +186,8 @@ Tiếp tục -30 +30 - %s sẽ bị xoá vĩnh viễn \nBạn có chắc chắn không? + %s sẽ bị xoá vĩnh viễn +\nBạn có chắc chắn muốn xóa? %d phút\ncòn lại Đang chiếu Hoàn thành @@ -228,14 +230,14 @@ Lỗi nguồn phim Lỗi nguồn từ xa Lỗi kết xuất - Lỗi trình phát bất ngờ + Đã có lỗi xảy ra. Vui lòng thử lại sau Lỗi tải xuống. Hãy kiểm tra quyền truy cập bộ nhớ - Truyền tập phim - Truyền nguồn thay thế + Phát tập phim bằng Chromecast + Phản chiếu màn hình bằng Chromecast Phát bằng ứng dụng Phát bằng %s Tự động tải xuống - Tải xuống nguồn thay thế + Nguồn tải dự phòng Tải lại các liên kết Tải xuống phụ đề Nhãn chất lượng phim @@ -248,20 +250,20 @@ Khóa Thu phóng Nguồn - Bỏ qua Giới thiệu + Bỏ qua giới thiệu Không hiện lại Bỏ qua bản cập nhật này Cập nhật Chất lượng xem ưu tiên (WiFi) Số ký tự tối đa tiêu đề trình phát - Hiển thị thông tin trình phát - Kích thước bộ đệm video - Thời lượng bộ đệm video + Hiện thông tin trình phát + Kích thước bộ nhớ đệm video + Thời lượng bộ nhớ đệm Bộ nhớ đệm video trên thiết bị Xoá bộ nhớ đệm hình ảnh và video Sẽ gây lỗi nếu đặt quá cao trên thiết bị có bộ nhớ thấp như Android TV. - Sẽ gây lỗi nếu đặt quá cao trên thiết bị có dung lượng lưu trữ thấp như Android TV. - DNS qua HTTPS + Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV. + DNS over HTTPS Rất hữu ích để bỏ chặn ISP Bản sao trang web Xoá trang web @@ -293,7 +295,7 @@ Đặt tên phim dưới poster Mật khẩu - Tên người dùng + Tài khoản Email 127.0.0.1 Tên trang mới @@ -313,10 +315,10 @@ %d / 10 /?? /%d - %s đã xác thực - Không thể đăng nhập tại %s + Đã xác thực %s + Không thể xác thực %s - Không có + Mặc định Bình thường Tất cả Tối đa @@ -337,7 +339,7 @@ https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300 https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog --> - Phụ đề của bạn sẽ trông tương tự như thế này + Bạch kim rất quý nên sẽ dùng để lắp vô xương Được đề xuất Đã tải %s Chọn từ tệp @@ -345,7 +347,7 @@ Tệp đã tải xuống Vai chính Vai phụ - Diễn viên quần chúng + Lý lịch Nguồn Ngẫu nhiên Sắp có… @@ -389,9 +391,9 @@ Bạn muốn xem gì Hoàn tất Tiện ích mở rộng - Thêm kho nguồn phim - Tên kho nguồn phim (Tùy chọn) - URL kho nguồn phim hoặc Mã ngắn + Thêm kho tiện ích + Tên kho tiện ích (Tùy chọn) + URL kho tiện ích hoặc Mã ngắn Tiện ích mở rộng đã tải Tiện ích mở rộng đã xoá Không tải được %s @@ -402,22 +404,24 @@ Tải xuống hàng loạt tiện ích mở rộng tiện ích mở rộng - Việc này sẽ xóa tất cả tiện ích mở rộng trong kho nguồn phim - Xoá kho nguồn phim + Việc này sẽ xóa tất cả tiện ích mở rộng trong kho tiện ích + Xoá kho tiện ích Tải xuống danh sách các trang web bạn muốn sử dụng Đã tải xuống: %d Đã vô hiệu: %d Chưa tải xuống: %d - CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. \n \nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. - Xem các kho nguồn phim của cộng đồng + CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. +\n +\nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. + Xem kho lưu trữ của cộng đồng Danh sách công khai In hoa toàn bộ phụ đề Cảnh báo: CloudStream không chịu trách nhiệm về các tiện ích mở rộng bên thứ ba và không cung cấp bất kỳ sự hỗ trợ nào! - %s (Đã vô hiệu) - Âm thanh & video + %s (Đã vô hiệu hoá) + Âm thanh & Độ phân giải Âm thanh - Video - Khởi động lại ứng dụng để thấy các thay đổi. + Độ phân giải video + Khởi động lại ứng dụng để thấy câc thay đổi. Chế độ an toàn được bật Tất cả tiện ích mở rộng đã được tắt do ứng dụng bị ngừng bất thường để giúp bạn tìm ra vấn đề gây lỗi. Xem thông tin sự cố @@ -426,11 +430,11 @@ Tự động tải xuống tiện ích mở rộng Làm lại tiến trình thiết lập Trình cài đặt APK - Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử chọn chế độ tương thích cũ nếu các bản cập nhật không thể cài đặt. + Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử chọn chế độ tương thích cũ nếu các bản cập nhật không cài đặt. %1$s %2$d%3$s Phát Trailer - Tự động cài đặt tất cả tiện ích mở rộng chưa được cài đặt từ những kho nguồn phim đã thêm. - Cập nhật đã bắt đầu + Tự động cài đặt tất cả tiện ích mở rộng chưa được cài đặt từ những kho tiện ích đã thêm. + Bắt đầu cập nhật Liên kết Danh sách HLS Trình phát ưu tiên @@ -469,8 +473,8 @@ Danh đề Giới thiệu Xoá lịch sử - Hiển thị cửa sổ bật lên của bỏ qua giới thiệu cho mở đầu/kết thúc - Văn bản quá dài. Không thể lưu vào bảng nhớ tạm. + Hiện các popup bỏ qua cho mở đầu/kết thúc + Văn bản quá dài. Không thể lưu vào bộ nhớ tạm. Xoá khỏi đã xem Bạn có chắc muốn thoát? @@ -484,7 +488,8 @@ Chế độ tương thích cũ Đã cập nhật (Mới đến Cũ) Đã cập nhật (Cũ đến Mới) - Thư viện của bạn đang trống :( \nĐăng nhập vào tài khoản thư viện hoặc thêm phim vào thư viện cục bộ. + Thư viện của bạn đang trống :( +\nĐăng nhập vào tài khoản thư viện hoặc thêm phim vào thư viện cục bộ. Mở bằng Siêu dữ liệu không được cung cấp bởi trang web, video sẽ không tải được nếu nó không tồn tại trên trang web. Trình cài đặt gói @@ -512,7 +517,8 @@ Dừng Bỏ chặn nhà mạng Đã bỏ đăng ký %s - Tìm thấy tệp Safe mode! \nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. + Tìm thấy tệp Safe mode! +\nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. Hoàn tác Đang cập nhật các phim đã đăng kí Bỏ chặn các URL gốc của GitHub bằng jsDelivr. Có thể làm cập nhật bị trễ vài ngày. @@ -529,18 +535,28 @@ Hồ sơ Trợ giúp Nền hồ sơ - Tại đây bạn có thể thay đổi cách sắp xếp các nguồn. Nếu video có mức độ ưu tiên cao hơn thì video đó sẽ xuất hiện cao hơn trong lựa chọn nguồn. Tổng ưu tiên nguồn và ưu tiên chất lượng là ưu tiên video. \n \nNguồn A: 3 \nChất lượng B: 7 \nSẽ có mức độ ưu tiên video kết hợp là 10. \n \nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! + Tại đây bạn có thể thay đổi cách sắp xếp các nguồn. Nếu video có mức độ ưu tiên cao hơn thì video đó sẽ xuất hiện cao hơn trong lựa chọn nguồn. Tổng ưu tiên nguồn và ưu tiên chất lượng là ưu tiên video. +\n +\nNguồn A: 3 +\nChất lượng B: 7 +\nSẽ có mức độ ưu tiên video kết hợp là 10. +\n +\nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! Chất lượng Bạn đã bình chọn - Tắt - Không tìm thấy kho nguồn phim, hãy kiểm tra URL và thử lại với VPN + Vô hiệu hoá + Không tìm thấy kho tiện ích, hãy kiểm tra URL và thử lại với VPN Không tìm thấy tiện ích mở rộng Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s Chọn chế độ lọc tiện ích mở rộng tải xuống %s đã xóa khỏi mục yêu thích Yêu thích %s đã thêm vào mục yêu thích - Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: \n \n%s \n \nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động? + Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: +\n +\n%s +\n +\nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động? Tần suất sao lưu Đã tìm thấy bản sao tiềm năng Khóa hồ sơ @@ -553,8 +569,10 @@ Thêm vào Đăng ký Xóa khỏi mục yêu thích - Chọn hồ sơ của bạn - Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%s.\' \n \nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động? + Ai đang xem + Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%s.\' +\n +\nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động? Nhập mã PIN PIN Nhập mã PIN hiện tại @@ -565,7 +583,7 @@ Quản lý hồ sơ Chỉnh sửa hồ sơ Tải lại liên kết - Tìm kiếm trong tiện ích mở rộng khác + Tìm kiếm tiện ích mở rộng khác Hiển thị đề xuất Kiểm tra tất cả Tiện ích mở rộng Xoay @@ -575,7 +593,7 @@ Bật tự động xoay màn hình theo hướng của video Tự động xoay đã sao chép! - Lỗi truy cập Bảng nhớ tạm, Vui lòng thử lại. + Lỗi truy cập Bộ nhớ tạm, Vui lòng thử lại. Lỗi sao chép, Vui lòng sao chép logcat và liên hệ hỗ trợ ứng dụng. Yêu thích OK @@ -595,17 +613,17 @@ Bài kiểm tra này chỉ dành cho các nhà phát triển và không xác nhận hay phủ nhận việc hoạt động của bất kỳ tiện ích mở rộng nào. Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn Phương tiện - Tên và URL kho nguồn phim + Tên và URL kho tiện ích Đặt lại Để đảm bảo quá trình tải xuống và thông báo cho các chương trình truyền hình đã đăng ký không bị gián đoạn, CloudStream cần có quyền chạy ở chế độ nền. Bằng cách nhấn OK, một hộp thoại yêu cầu sẽ hiển thị. Vui lòng nhấn \"Cho phép\".\n\nXin lưu ý, quyền này không có nghĩa là CS3 sẽ làm hao pin của bạn. Nó sẽ chỉ hoạt động ở chế độ nền khi cần thiết, chẳng hạn như khi nhận được thông báo hoặc tải xuống video từ các tiện ích mở rộng chính thức. Mùa %1$d Tập %2$d sẽ được phát hành vào - Sắp ra mắt sau %s + Sắp tới sau %s Chọn thiết bị truyền Bảo mật Tài khoản Mã QR Bỏ qua - Mở kho nguồn phim + Mở kho tiện ích CloudStream Wiki Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên Mã PIN đã hết hạn! @@ -613,7 +631,7 @@ Không lấy được mã PIN, vui lòng thử xác thực cục bộ Không có tải xuống nào. Xác thực cục bộ - Truyền nguồn thay thế + Phản chiếu màn hình Phát từ đầu Mở video có sẵn Cảnh báo @@ -624,10 +642,16 @@ Bỏ chọn tất cả Xoá các tệp Xoá (%1$d | %2$s) - Bạn có chắc chắn muốn xóa vĩnh viễn các mục sau không? \n \n%s - Bạn có chắc chắn muốn xóa vĩnh viễn các tập trong %1$s? \n \n%2$s + Bạn có chắc chắn muốn xóa vĩnh viễn các mục sau không? +\n +\n%s + Bạn có chắc chắn muốn xóa vĩnh viễn các tập trong %1$s? +\n +\n%2$s Bạn cũng sẽ xóa vĩnh viễn tất cả các tập trong loạt phim: \n \n%s - Bạn có chắc chắn muốn xóa vĩnh viễn tất cả các tập trong loạt phim này không? \n \n%s + Bạn có chắc chắn muốn xóa vĩnh viễn tất cả các tập trong loạt phim này không? +\n +\n%s Xóa tiện ích mở rộng Ngày phát hành (Cũ đến mới) Ẩn tên các nút điều khiển trình phát @@ -635,7 +659,7 @@ Xem trước trên thanh tua Chưa tải phụ đề nào Xác nhận trước khi thoát - Hiển thị hộp thoại xác nhận trước khi thoát ứng dụng + Hiện hộp thoại xác nhận trước khi thoát ứng dụng Không hiển thị Hiển thị Vị trí thư mục sao lưu @@ -644,8 +668,8 @@ Video này là Torrent, điều này có nghĩa là hoạt động video của bạn có thể được theo dõi.\nHãy đảm bảo rằng bạn hiểu về Torrent trước khi tiếp tục. Lỗi mã hóa Giải mã phần mềm cho phép phát các tệp video không được thiết bị của bạn hỗ trợ, nhưng có thể gây ra phản hồi chậm hoặc phát lại không ổn định ở độ phân giải cao. - Giải mã phần mềm - 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. + Bộ giải mã ứng dụng + Khởi động lại ứng dụng và chấp nhận cửa sổ Stream Torrent để tiếp tục. Kích hoạt torrent trong Cài đặt/Nguồn phim/Thể loại ưu tiên Tải phụ đề đầu tiên có sẵn Âm thanh @@ -685,7 +709,7 @@ Đến mục Tải xuống Không có kết nối Internet. \n\nVui lòng kết nối Internet rồi thử lại, hoặc xem các nội dung đã tải xuống khi đang ngoại tuyến. Thay đổi khung hiển thị màn hình - Quét chồng lấn + Vượt khung Thay đổi kích thước poster Kích thước poster Tăng tốc độ phát khi nhấn giữ @@ -700,12 +724,12 @@ URL hoặc ảnh không hợp lệ Đã cập nhật ảnh thành công Đánh dấu là đã xem đến tập này - Xóa đã xem đến tập này + Xóa những tập đã xem đến tập này Đã tải lại Tải lại nguồn phim Tên Độ phân giải và tên - Phát nguồn thay thế" + Phản chiếu màn hình" Căn chỉnh phụ đề Dưới trái Dưới giữa @@ -725,13 +749,13 @@ Cài đặt bản phát hành trước thất bại. Tập Bật bộ lọc độ sáng khi độ sáng màn hình vượt quá 100% - Hiển thị bảng dàn diễn viên + Hiển thị bảng diễn viên Thông tin video Độ sáng bổ sung Tên nguồn Hàng đợi tải xuống Không có tải xuống đang chờ nào. - Quyết định cách sắp xếp các nguồn video trong trình phát + Quyết định cách sắp xếp các nguồn video trong trình phát. Ưu tiên nguồn Tải xuống tất cả Hủy tất cả @@ -745,15 +769,4 @@ Đã bật độ sáng bổ sung Hiển thị lớp phủ siêu dữ liệu trình phát - Trực tiếp - Video - Xem trước - Hiển thị đồng hồ thời gian thực - Hiển thị đồng hồ thời gian thực ở phía trên màn hình. Áp dụng cho trang chủ và trình phát - Cài đặt hồ sơ - Ẩn nguồn có độ ưu tiên âm - Ẩn nguồn có lỗi - - %d liên kết bị ẩn - diff --git a/app/src/main/res/values-b+zh+TW/strings.xml b/app/src/main/res/values-b+zh+TW/strings.xml index cfd8adf05..e5879ae37 100644 --- a/app/src/main/res/values-b+zh+TW/strings.xml +++ b/app/src/main/res/values-b+zh+TW/strings.xml @@ -19,7 +19,8 @@ 速度(%.2fx) 評分:%.1f - 發現新版本! \n%1$s -> %2$s + 發現新版本! +\n%1$s -> %2$s 填充 %d 分鐘 CloudStream @@ -186,8 +187,10 @@ 繼續 -30 +30 - 這將永遠刪除 %s \n你確定嗎? - 剩下 \n%d 分鐘 + 這將永遠刪除 %s +\n你確定嗎? + 剩下 +\n%d 分鐘 連載中 已完結 狀態 @@ -410,7 +413,9 @@ 已停用:%d 未下載:%d 已更新 %d 外掛程式 - CloudStream 預設沒有安裝網站。你需要從儲存庫安裝網站。 \n \n加入我們的 Discord 或在網路上搜尋。 + CloudStream 預設沒有安裝網站。你需要從儲存庫安裝網站。 +\n +\n加入我們的 Discord 或在網路上搜尋。 查看 公開清單 字幕全大寫 @@ -498,7 +503,8 @@ 按字母順序(A 到 Z) 按字母順序(Z 到 A) 選擇媒體庫 - 找到安全模式檔案! \n在刪除此檔案之前,將不會在啟動時載入任何擴充功能。 + 找到安全模式檔案! +\n在刪除此檔案之前,將不會在啟動時載入任何擴充功能。 日誌 失敗 通過 @@ -516,7 +522,8 @@ 還原 無法存取 GitHub。 正在開啟 jsDelivr proxy… 使用 jsDelivr 繞過直接使用 GitHub 網址時的存取封鎖。 可能導致更新延遲數天。 - 您的媒體庫是空的 :( \n登入媒體庫帳號或將節目新增到您本機的媒體庫。 + 您的媒體庫是空的 :( +\n登入媒體庫帳號或將節目新增到您本機的媒體庫。 您的媒體庫是空的。可嘗試以不同的帳號登入。 正在更新訂閱節目 備份頻率 @@ -524,7 +531,11 @@ 我的最愛 %s 已加入我的最愛 以 %s 的身分登入 - 您的媒體庫中似乎有多個重覆的項目: \n \n%s \n \n您要強制加入、取代已有項目、還是取消操作? + 您的媒體庫中似乎有多個重覆的項目: +\n +\n%s +\n +\n您要強制加入、取代已有項目、還是取消操作? 輸入 %s 的 PIN 碼 行動數據 找到可能重覆的項目 @@ -556,11 +567,19 @@ 找不到資源庫,請檢查網址與 VPN 設定 您已完成投票 在資源庫中找不到外掛程式 - 您的媒體庫中似乎有重覆的項目:「%s」。 \n \n您要強制加入、取代已有項目、還是取消操作? + 您的媒體庫中似乎有重覆的項目:「%s」。 +\n +\n您要強制加入、取代已有項目、還是取消操作? 設回預設 輸入 PIN 碼 PIN 碼 - 您可在此調整來源的排序方式。具有愈小的優先值的影片,在來源選擇中顯示得愈前面。來源優先值與品質優先值的加總就是影片優先值。 \n例如: \n來源 A:3 \n品質 B: 7 \n則該來源的影片優先值為 10。 \n \n注意:如果加總達到 10 或更高,則載入該連結時播放器將自動跳過載入! + 您可在此調整來源的排序方式。具有愈小的優先值的影片,在來源選擇中顯示得愈前面。來源優先值與品質優先值的加總就是影片優先值。 +\n例如: +\n來源 A:3 +\n品質 B: 7 +\n則該來源的影片優先值為 10。 +\n +\n注意:如果加總達到 10 或更高,則載入該連結時播放器將自動跳過載入! 輸入目前的 PIN 碼 顯示切換畫面方向的按鈕 選擇篩選外掛程式下載的模式 @@ -580,7 +599,8 @@ 顯示推薦 在播放器中新增速度選項 即將在 %s 推出 - %s \n剩餘 + %s +\n剩餘 測試所有擴充功能 停用電池優化 有聲書 @@ -744,7 +764,4 @@ 軟體解碼使程式可以播放裝置不支援的影片,但可能導致播放高解析的影片時的延遲或不穩定。 音量已超過 100% 顯示播放器元資料遮罩層 - 影片 - 預覽 - 播放中 diff --git a/app/src/main/res/values-b+zh/strings.xml b/app/src/main/res/values-b+zh/strings.xml index dc7eab451..de0cb834a 100644 --- a/app/src/main/res/values-b+zh/strings.xml +++ b/app/src/main/res/values-b+zh/strings.xml @@ -19,7 +19,8 @@ 速度(%.2fx) 评分:%.1f - 发现新版本! \n%1$s -> %2$s + 发现新版本! +\n%1$s -> %2$s 填充 %d 分钟 CloudStream @@ -187,8 +188,10 @@ 继续 -30 +30 - 这将永久删除 %s \n您确定吗? - %d 分钟 \n剩余 + 这将永久删除 %s +\n您确定吗? + %d 分钟 +\n剩余 连载中 已完结 状态 @@ -411,7 +414,9 @@ 已禁用:%d 未下载:%d 已更新 %d 插件 - CloudStream 默认不安装片源。您需要从仓库中安装片源。 \n \n加入我们的 Discord 或在网上搜索。 + CloudStream 默认不安装片源。您需要从仓库中安装片源。 +\n +\n加入我们的 Discord 或在网上搜索。 查看社区仓库 公开列表 字幕全大写 @@ -481,7 +486,8 @@ 应用退出后将会更新 插件已下载 从已观看中移除 - 发现安全模式文件! \n启动时不加载任何扩展,直到文件被删除。 + 发现安全模式文件! +\n启动时不加载任何扩展,直到文件被删除。 浏览器 排序方式 @@ -494,7 +500,8 @@ 字母排序(从 Z 到 A) 选择库 打开方式 - 您的库是空的 :( \n登录库账户或添加节目到您的本地库。 + 您的库是空的 :( +\n登录库账户或添加节目到您的本地库。 此列表是空的,请尝试切换到另一个。 播放器显示 - 快进快退秒数 播放器可见时使用的快进快退秒数 @@ -527,7 +534,13 @@ 配置文件 帮助 移动流量 - 在这里,您可以更改源的排序方式。如果视频具有更高的优先级,它将在源选择中显示得更高。源优先级和质量优先级的总和就是视频优先级。 \n \n来源 A:3 \n质量 B: 7 \n组合视频优先级为 10。 \n \n注意:如果总和为 10 或更多,则加载该链接时播放器将自动跳过加载! + 在这里,您可以更改源的排序方式。如果视频具有更高的优先级,它将在源选择中显示得更高。源优先级和质量优先级的总和就是视频优先级。 +\n +\n来源 A:3 +\n质量 B: 7 +\n组合视频优先级为 10。 +\n +\n注意:如果总和为 10 或更多,则加载该链接时播放器将自动跳过加载! 质量 个人资料背景 PIN @@ -557,8 +570,14 @@ 您已投票 %s已添加到收藏夹 %s已从收藏夹中删除 - 您的资料库中似乎已经存在一个可能相同的项目:\'%s.\' \n \n您想添加该项目、替换现有项目还是取消操作? - 在您的资料库中发现了潜在的重复项目: \n \n%s \n \n您想添加此项目、替换现有项目还是取消操作? + 您的资料库中似乎已经存在一个可能相同的项目:\'%s.\' +\n +\n您想添加该项目、替换现有项目还是取消操作? + 在您的资料库中发现了潜在的重复项目: +\n +\n%s +\n +\n您想添加此项目、替换现有项目还是取消操作? 确认PIN 输入来自 %s 的 PIN 码 锁定个人资料 @@ -578,7 +597,8 @@ 解锁 CloudStream 使用生物识别技术锁定 密码或 PIN 验证 - %s \n剩余 + %s +\n剩余 测试所有扩展 已复制! 访问剪贴板出错,请重试。 @@ -624,10 +644,18 @@ 全不选 删除文件 删除 (%1$d | %2$s) - 您确定要永久删除以下项目吗? \n \n%s - 您确定要永久删除 %1$s中的下述剧集吗? \n \n%2$s - 您还将永久删除下述系列中的所有剧集: \n \n%s - 您确定要永久删除下述系列的所有剧集吗? \n \n%s + 您确定要永久删除以下项目吗? +\n +\n%s + 您确定要永久删除 %1$s中的下述剧集吗? +\n +\n%2$s + 您还将永久删除下述系列中的所有剧集: +\n +\n%s + 您确定要永久删除下述系列的所有剧集吗? +\n +\n%s 发布日期(从新至旧) 无法获取设备 PIN 码,尝试本地身份验证 PIN 码现已过期! @@ -655,7 +683,7 @@ 得分 %s 日期 %s 更新插件 - 成功更新了 %d 个插件 + 成功更新了 %d 个插件! 没有插件被更新。 手动更新插件 集数(升序) @@ -744,15 +772,4 @@ 确定在播放器中如何排列视频源的顺序 已启用额外亮度 显示播放器元数据遮罩层 - 视频 - 预览 - 播放中 - 显示实时时钟 - 在屏幕顶部显示实时时钟。应用于主页和播放器 - 配置文件设置 - 隐藏负优先级的来源 - 隐藏有错误的来源 - - %d 条隐藏链接 - diff --git a/app/src/main/res/values-b+be/strings.xml b/app/src/main/res/values-be/strings.xml similarity index 97% rename from app/src/main/res/values-b+be/strings.xml rename to app/src/main/res/values-be/strings.xml index 9fbf8a8d5..1a47a6a63 100644 --- a/app/src/main/res/values-b+be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -34,7 +34,7 @@ Шукаць па %s… Распазнаванне маўлення недаступна Пачніце размаўляць… - Няма дадзеных + Няма даных Больш параметраў Наступная серыя Жанры @@ -125,10 +125,10 @@ Працягнуць прагляд Выдаліць Больш інфармацыі - @string/home_play + \@string/home_play Для карэктнай працы гэтага пастаўшчыка можа спатрэбіцца VPN Гэты пастаўшчык — Torrent, рэкамендуецца VPN - Вэб-сайт не пастаўляе метададзеных, загрузіць відэа не ўдасца, калі на сайце яго няма. + Вэб-сайт не пастаўляе метаданых, загрузіць відэа не ўдасца, калі на сайце яго няма. Апісанне Сюжэту не знойдзена Апісання не знойдзена @@ -159,12 +159,12 @@ Выкарыстоўваць сістэмную яркасць у прайгравальніку замест цёмнага накладання Абнаўляць працэс прагляду Аўтаматычна сінхранізаваць прагрэс бягучай серыі - Аднавіць дадзеныя з рэзервовай копіі - Рэзервовае капіраванне дадзеных + Аднавіць даныя з рэзервовай копіі + Рэзервовае капіраванне даных Частата рэзервовага капіравання Загружаны файл рэзервовай копіі - Не ўдалося аднавіць дадзеныя з файла %s - Дадзеныя захаваны + Не ўдалося аднавіць даныя з файла %s + Даныя захаваны Няма дазволу да сховішча. Паспрабуйце ізноў. Памылка пры рэзервовым капіраванні %s Пошук @@ -247,7 +247,7 @@ Сціслы агляд у чарзе Субцітраў няма - Прадвызначаны + Прадвызначанае Свабодна Ужыта Праграма @@ -315,7 +315,7 @@ Прапусціць гэта абнаўленне Абнавіць Прыярытэтная якасць прагляду (WiFi) - Прыярытэтная якасць прагляду (Мабільная перадача дадзеных) + Прыярытэтная якасць прагляду (Мабільная перадача даных) Максімальная колькасць сімвалаў у загалоўку праглядальніка Паказваць інфармацыю ў прайгравальніку Памер буфера відэа @@ -458,7 +458,7 @@ Раздзяляльнасць Звесткі пра медыя Памылковы ID - Памылковыя дадзеныя + Памылковыя даныя Памылковы URL-адрэс Памылка Прыбраць схаваныя цітры з субцітраў @@ -480,7 +480,7 @@ Назва рэпазіторыя (неабавязкова) URL-адрас рэпазіторыя або кароткі код Убудова загружана - Убудова спампавана + Убудава спампавана Убудова выдалена Не ўдалося загрузіць %s 18+ @@ -499,7 +499,7 @@ Спампавана: %d Выключана: %d Не спампавана: %d - Абноўлена %d убудоў + Абноўлена %d плагіна(ў) Прадвызначана на CloudStream няма ўсталяваных вэб-сайтаў. Вам трэба ўсталяваць вэб-сайты з рэпазіторыяў. \n \nДалучыцеся да нашага сервера Discord або пашукайце ў сетцы. Праглядзець рэпазіторыі ад супольнасці Публічны спіс @@ -595,7 +595,7 @@ Адпісацца Профіль %d Wi-Fi - Мабільная перадача дадзеных + Мабільная перадача даных Выбраць як прадвызначаны Выкарыстоўваць Рэдагаваць @@ -643,7 +643,7 @@ Праверка сапраўднасці біяметрыяй не падтрымліваецца на гэтай прыладзе Разблакіруйце праграму адбіткам пальца, Face ID, PIN-кодам, узорам разблакіроўкі або паролем. Праз некалькі няўдалых спроб акно з запытам закрыецца. Проста перазапусціце праграму, каб паўтарыць спробу. - Вашы дадзеныя CloudStream былібылі толькі што зарэзерваваныя. Нягледзячы на тое, што магчымасць вельмі маленькая, усе прылады могуць паводзіць сябе па-рознаму. У рэдкасным выпадку, калі вы страціце доступ да праграмы, поўнасцю ачысціце даныя і аднавіце іх праз рэзервовую копію. Выбачайце за любую нязручнасць, якая можа з гэтага атрымацца. + Вашы даныя CloudStream былі зарэзерваваныя. Нягледзячы на тое, што магчымасць вельмі маленькая, усе прылады могуць паводзіць сябе па-рознаму. У рэдкасным выпадку, калі вы страціце доступ да праграмы, поўнасцю ачысціце даныя і аднавіце іх праз рэзервовую копію. Выбачайце за любую нязручнасць, якая можа з гэтага атрымацца. Скінуць CloudStream-Вікі Наведайце %s на вашым смартфоне або камп\'ютары і ўвядзіце код вышэй @@ -735,8 +735,5 @@ %d спампоўванняў у чарзе %d спампоўванняў у чарзе - Паказваць накладанне з метададзенымі ў прайгравальніку - Відэа - Перадпрагляд - Ужывую + Паказваць накладанне з метаданымі ў прайгравальніку diff --git a/app/src/main/res/values-b+ca/strings.xml b/app/src/main/res/values-ca/strings.xml similarity index 99% rename from app/src/main/res/values-b+ca/strings.xml rename to app/src/main/res/values-ca/strings.xml index 621243c78..495eb5e3c 100644 --- a/app/src/main/res/values-b+ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -123,7 +123,7 @@ Continua mirant Eliminar Més info - @string/home_play + \@string/home_play Potser necessiteu una VPN per que aquest proveïdor funcioni correctament Aquest proveïdor és un Torrent, es recomana fer servir una VPN El lloc no proveeix metadades, la càrrega del video fallarà si no existeix al lloc. diff --git a/app/src/main/res/values-b+sq/strings.xml b/app/src/main/res/values-sq/strings.xml similarity index 62% rename from app/src/main/res/values-b+sq/strings.xml rename to app/src/main/res/values-sq/strings.xml index 828066ead..51d2379de 100644 --- a/app/src/main/res/values-b+sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -75,7 +75,7 @@ E disponueshme për shikim offline Zgjidh të gjitha Çzgjidh të gjitha - Përditësimi filloi + Update-i filloi Transmetim në rrjet Hap video lokale Gabim gjatë ngarkimit të lidhjeve @@ -83,7 +83,7 @@ Memorie e brendshme Dublim Titra - Fshi skedarin + Fshi Skedarin Luaj skedarin Rifillo shkarkimin Ndalo shkarkimin @@ -117,7 +117,7 @@ Madhësia e shkrimit Kërko duke përdorur ofruesit Kërko duke përdorur tipet - %d Banane të dhëna për developer-at + %d Banane të dhëna per developer-at Asnjë Banane e dhënë Zgjedhje automatike e gjuhës Shkarko gjuhët @@ -127,7 +127,7 @@ Vazhdo shikimin Hiq Më shumë informacion - @string/home_play + \@string/home_play Një VPN mund të nevojitet që ky ofrues të funksionojë në rregull Ky ofrues është Torrent, rekomandohet një VPN Metadata nuk ofrohet nga kjo faqe, ngarkimi i videos do të dështojë nëse nuk ekziston në këtë faqe. @@ -216,7 +216,7 @@ %1$s %2$d%3$s Asnje sezon Episodi - Episode + Episodet %1$d-%2$d %1$d %2$s Vazhdon në %s @@ -224,8 +224,8 @@ E Nuk u gjet asnjë episod Fshij - Fshi skedarin - Fshi skedarët + Fshij skedarin + Fshij skedarët Fshij (%1$d | %2$s) Anulo Ndalo @@ -309,7 +309,7 @@ Teksti i episodit Zgjidh elementët e ndërfaqes mbi poster Nuk u gjet asnjë përditësim - Kontrollo për përditësime + Kontrollo për përditësim Çelës Ndrysho madhësinë Burimi @@ -494,254 +494,4 @@ Të gjitha %s janë shkarkuar tashmë Asnjë shtesë nuk u gjet në repository Repository nuk u gjet, verifiko URL-në ose provoje me VPN - Të gjitha shtesat u ndaluan për shkak të një gabimi për të ndihmuar në gjetjen e shkaktarit. - Lista publike - CloudStream nuk vjen me faqe të instaluara. Duhet ti shtosh faqet nga repository-t.\n\nNa u bashko në Discord ose kërko në internet. - Autorët - shtesë - Titrat vetëm me shkronja të mëdha - Luajtësi i preferuar i videove - Përshkrimi - Shiko repository-t e komunitetit - Modaliteti i sigurt është aktive - Të pa shkarkuara: %d - Instalo shtesën para - Rifillo aplikacionin për të parë ndryshimet. - Çaktivizuar: %d - Pauzë - Gjurmë - Shkarkimi në grup përfundoi - Kjo do të heqë të gjitha shtesat - Gjuha - Rifillo - Fshi shtesën - Madhësia - Shiko të dhënat e gabimit - Të suportuara - Shkarkuar: %d - Gjurmë video-je - U përditësuan %d shtesa - Lista e HLS - shtesa - Vlerësimi: %s - Paralajmërim: CloudStream nuk mban asnjë përgjegjësi për përdorimin e shtesave të palëve të treta dhe nuk ofron asnjë mbështetje për to! - Statusi - Fshi repository-n - Versioni - %s (Çaktivizuar) - Shkarko listën e faqeve që dëshiron të përdorësh - Gjurmë audio-je - Video - Luajtësi i integruar - Pyet gjithmonë - Zgjidh pajisjen për transmetim - Aplikacioni nuk u gjet - Të gjitha gjuhët - Kalo %s - Hapjen - Mbylljen - Përmbledhjen - Mbylljen e përzier - Hapjen e përzier - Kreditet - Parashikimin - Intron - Pastro historikun - Historia - Shfaq butonat për kapërcimin e hyrjes/mbylljes - Teksti është shumë i gjatë. Nuk mund të ruhet në Clipboard. - Gabim gjatë aksesimit të Clipboard-it. Ju lutem provoni përsëri. - Kopjimi dështoi. Kopjo logcat-in dhe kontakto support-in e aplikacionit. - Shënoje si e përfunduar - Hiqe nga të shikuarat - Jeni i sigurt që dëshironi të dilni? - Po - Jo - Në rregull - Largo - Hap repository-n - Çaktivizo kursimin e baterisë - Për të siguruar shkarkime të pandërprera dhe njoftime për serialet e abonuar, CloudStream ka nevojë për leje për të funksionuar në sfond. Duke shtypur OK, do t’ju shfaqet një dialog kërkese. Ju lutem shtypni “Lejo”.\n\nJu lutemi vini re: kjo leje nuk do të thotë që CS3 do të harxhojë baterinë tuaj. Do të funksionojë në sfond vetëm kur është e nevojshme, si p.sh. kur merr njoftime ose shkarkon video nga shtesat zyrtare. - Përdorimi i baterisë është i vendosur si i pakufizuar - Informacioni i aplikacionit CloudStream nuk mund të hapet. - Po shkarkohet përditësimi i aplikacionit… - Po instalohet përditësimi i aplikacionit… - Instalimi i versionit të ri dështoi - Version i vjetër - Instaluesi i paketës - Aplikacioni do të përditësohet sapo të mbyllet - Rendit nga - Rendit - Vlerësimi (nga më i larti te më i ulëti) - Vlerësimi (nga më i ulëti te më i larti) - Përditësuar (nga më i riu te më i vjetri) - Përditësuar (nga më i vjetri te më i riu) - Alfabetike (A–Z) - Alfabetike (Z–A) - Episodet (rritëse) - Episodet (zbritëse) - Vlerësimi (më i larti) - Vlerësimi (më i ulëti) - Data e publikimit (më e fundit) - Data e publikimit (më e hershmja) - Ep %s - Vlerësimi %s - Data %s - Hape me - Kjo listë është bosh. Provo një tjetër. - U gjet skedari i modalitetit të sigurt!\nNuk do të ngarkohen asnjë shtesë gjatë nisjes derisa skedari të hiqet. - Riktheje në gjendjen e mëparshme - Duke përditësuar serialet që ndiqni - I abonuar - I abonuar në %s - U çabonove nga %s - Episodi %d u publikua! - Abonohu - Çabonohu - Profili %d - Wi-Fi - Mobile data - Vendose si parazgjedhje - Përdor - Ndrysho - Prioriteti i burimit - Vendos si duhet të renditen burimet e videos në luajtës - Profilet - Ndihmë - Këtu mund të ndryshoni si renditen burimet. Nëse një video ka prioritet më të lartë, do të shfaqet më lart në përzgjedhjen e burimeve. Shuma e prioritetit të burimit dhe prioritetit të cilësisë është prioriteti i videos.\n\nBurimi A: 3\nCilësia B: 7\nDo të ketë një prioritet të kombinuar video-je prej 10.\n\nSHËNIM: Nëse shuma është 10 ose më shumë, luajtësi do të kapërcejë automatikisht ngarkimin kur të hapet ai link! - Cilësitë - Sfondi i profilit - UI nuk mundi të krijohej siç duhet, ky është një PROBLEM SERIOZ dhe duhet raportuar menjëherë %s - Ju keni votuar - Të preferuarat - %s u shtua te të preferuarat - %s u hoq nga të preferuarat - Shto te të preferuarat - Hiqe nga të preferuarat - U gjet dublikatë e mundshme - Shto - Zëvendëso - Zëvendëso të gjitha - Duket se një artikull i mundshëm i dyfishuar tashmë ekziston në bibliotekën tuaj: ‘%s’.\n\nDëshironi ta shtoni gjithsesi, ta zëvendësoni atë ekzistuesin, apo ta anuloni veprimin? - Janë gjetur artikuj të mundshëm të dyfishtë në bibliotekën tuaj:\n\n%s\n\nDëshironi ta shtoni gjithsesi, të zëvendësoni ato ekzistuesit, apo të anuloni veprimin? - Fut PIN-in - Fut PIN-in për %s - Fut PIN-in aktual - Kyç profilin - PIN - PIN-i i gabuar. Ju lutem provoni përsëri. - PIN-i duhet të ketë 4 karaktere - Zgjidh një Llogari - Asnjë llogari - Menaxho Llogaritë - Ndrysho llogarinë - I identifikuar si %s - Kapërce përzgjedhjen e llogarisë gjatë nisjes - Përdor llogarinë e paracaktuar - Rrotullo - Shfaq një buton për ndryshimin e orientimit të ekranit - Aktivizo rrotullimin automatik të ekranit sipas orientimit të videos - Rrotullim automatik - I preferuar - Hiq nga të preferuarat - Hap CloudStream-in - Kyç me biometrikë - Verifikim me fjalëkalim/PIN - Biometrikat nuk janë të disponueshme në këtë pajisje - Hap aplikacionin me gjurmë gishti, Face ID, PIN, Pattern ose fjalëkalim. - Pas disa përpjekjesh të dështuara, rifillo aplikacionin për ta provuar përsëri. - Të dhënat tuaja të CloudStream janë kopjuar tani. Edhe pse mundësia është shumë e ulët, disa pajisje mund të sillen ndryshe. Në raste të rralla, nëse bllokoheni nga hyrja në aplikacion, fshini plotësisht të dhënat e aplikacionit dhe rikthejini ato nga kopja rezervë. Na vjen shumë keq për çdo shqetësim që mund të shkaktohet nga kjo. - Rivendos - CloudStream Wiki - Vizitoni %s në telefonin ose kompjuterin tuaj dhe futni kodin e mësipërm - Nuk u mor kodi PIN i pajisjes, provo verifikim lokal - PIN-i ka skaduar! - Kodi skadon pas %1$dm %2$ds - Data e publikimit (nga më e reja te më e vjetra) - Data e publikimit (nga më e vjetra te më e reja) - Fshih emrat e kontrolleve të luajtësit - Shiriti i kërkimit - Aktivizo miniaturën e pamjes paraprake në shiritin kërkues - Nuk ka titra të ngarkuara ende - Vendndodhja e dosjes së kopjes rezervë - I personalizuar - Konfirmo para daljes - Shfaq dialogun para daljes nga aplikacioni - Shfaqe - Mos e shfaq - Madhësia e konturit - Aktivizo torrent-in në Cilësimet/Ofruesit/Media e preferuar - Rifillo aplikacionin dhe prano dritaren “Stream Torrent” për të vazhduar. - Dekodim me softuer - Dekodimi me softuer i lejon player-it të luajë video që nuk mbështeten nga pajisja juaj, por mund të shkaktojë vonesa ose paqëndrueshmëri në rezolucione të larta. - Volumi është mbi 100% - Rrëshqit lart përsëri për të kaluar mbi 100% - Përditëso shtesat - Përditëso shtesat manualisht - Filloi procesi i përditësimit të shtesave! - %d Shtes(a) u përditësuan me sukses - Nuk u përditësua asnjë shtesë. - Njoftimet e luajtësit - Njoftimi për kontrollin e luajtjes nga sfondi - I integruar - Online - Bëji të gjitha titrat me shkronja të trasha - Bëji të gjitha titrat me shkronja të pjerrëta - Rrezja e sfondit - Sa elementë të ndryshëm mund të shkarkohen njëkohësisht - Shkarkimet paralele - Lidhje paralele - Numri i lidhjeve paralele për çdo shkarkim - Shko tek Shkarkimet - Nuk ka lidhje interneti.\n\nJu lutemi lidhuni me internetin dhe provojeni përsëri, ose shikoni shkarkimet tuaja offline. - Ndryshon kufijtë e ekranit - Tejskenim - Ndryshon madhësinë e posterave - Madhësia e posterit - Ndërrimi i shpejtësisë me shtypje të gjatë - Mbaj shtypur për shpejtësi 2x - Ndrysho foton e profilit - Fut URL-n e fotos së profilit - Nuk u gjet asnjë URL - URL ose imazh i pavlefshëm - Imazhi u përditësua me sukses - Shëno si të parë deri në këtë episod - Hiq të parët deri në këtë episod - U ringarkuan - Ringarko ofruesin - Emri - Emri i burimit - Rezolucioni dhe emri - Shkarko të gjitha - Anulo të gjitha - Dëshiron të shkarkosh episodin %s? - Dëshiron të anulosh të gjitha shkarkimet në radhë? - Pozicionimi i titrave - Poshtë majtas - Poshtë në mes - Poshtë djathtas - Në mes majtas - Në mes në qendër - Në mes djathtas - Lart majtas - Lart në mes - Lart djathtas - - %d shkarkim aktiv - %d shkarkime aktive - - - %d shkarkim në radhë - %d shkarkime në radhë - - Live - Cilësimet e profil-it - Fshih burimet me prioritet negativ - Fshih burimet me error-e - - %d link i fshehur - %d linqe të fshehura - - Shfaq orën në kohë reale - Shfaq një orë në krye të ekranit. Aplikohet në faqen kryesore dhe në luajtës diff --git a/app/src/main/res/values/donottranslate-strings.xml b/app/src/main/res/values/donottranslate-strings.xml index bb222918c..6a4c82713 100644 --- a/app/src/main/res/values/donottranslate-strings.xml +++ b/app/src/main/res/values/donottranslate-strings.xml @@ -101,7 +101,6 @@ opensubtitles_key subdl_key animeskip_key - tv_layout_clock_key pref_category_security_key pref_category_gestures_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d530b59f..31cf951cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,8 +374,6 @@ Looks Features General - Show real time clock - Show a real time clock at the top of the screen. Applies to the homepage and player Random Button Show random button on Homepage and Library Extension languages @@ -624,13 +622,6 @@ Subscribe Unsubscribe Profile %d - Profile settings - Hide sources with a negative priority - Hide sources with errors - - %d hidden link - %d hidden links - Wi-Fi Mobile data Set default @@ -733,7 +724,7 @@ Update plugins manually Starting plugin update process! - Successfully updated %d plugin(s) + Successfully updated %d plugin(s)! No plugins were updated. Player notifications The player notification for controlling the playback from the background @@ -792,4 +783,5 @@ %d downloads queued Live + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a63b47264..f386bb62e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -472,9 +472,6 @@