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/pull_request.yml b/.github/workflows/pull_request.yml index 433816e0f..8f5c62866 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,11 +26,6 @@ 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 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 75a9698c8..66a55ae88 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -268,7 +268,6 @@ dependencies { // Extensions & Other Libs implementation(libs.jsoup) // HTML Parser - implementation(libs.ksoup) // HTML Parser implementation(libs.rhino) // Run JavaScript implementation(libs.safefile) // To Prevent the URI File Fu*kery coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor 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 index 84ef1fee0..80c7b49b0 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt @@ -2,7 +2,6 @@ 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 @@ -30,19 +29,16 @@ class SerializationClassTester { 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 instance = Instancio.create(kClass.java) - val jacksonJson = jacksonMapper.writeValueAsString(instance) - val kotlinxJson = serializeWithKotlinx(kClass, instance) + val jacksonJson = jacksonMapper.writeValueAsString(instance) + val kotlinxJson = serializeWithKotlinx(kClass, instance) - assertEquals( - jacksonJson, - kotlinxJson, - """ + assertEquals( + jacksonJson, + kotlinxJson, + """ Serialization mismatch for: ${kClass.qualifiedName} @@ -53,15 +49,8 @@ class SerializationClassTester { $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")}") + ) + println("Identical serialization for: ${kClass.jvmName}") } } @@ -71,33 +60,31 @@ class SerializationClassTester { 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) + val instance = Instancio.create(kClass.java) + // Convert to JSON to get example JSON object + // We prefer jackson here because the app may have many jackson JSON strings in local storage + val originalJson = jacksonMapper.writeValueAsString(instance) - // Create an object from the JSON using kotlinx - val serializer = - kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass) - assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!") - val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson) + // Create an object from the JSON using 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) + // 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, - """ + // 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} @@ -108,15 +95,8 @@ class SerializationClassTester { $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")}") + ) + println("Identical deserialization for: ${kClass.jvmName}") } } @@ -136,9 +116,9 @@ class SerializationClassTester { 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 + kClass.java.annotations.any { + it is Serializable + } } } diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt new file mode 100644 index 000000000..15ad532f8 --- /dev/null +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt @@ -0,0 +1,157 @@ +package com.lagradost.cloudstream3.utils.serializers + +import android.net.Uri +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KeepGeneratedSerializer +import kotlinx.serialization.Serializable +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +@OptIn(ExperimentalSerializationApi::class) +@KeepGeneratedSerializer +@Serializable(with = NonEmptyData.Serializer::class) +data class NonEmptyData( + val title: String = "", + val tags: List = emptyList(), + val meta: Map = emptyMap(), + val name: String = "hello", +) { + object Serializer : NonEmptySerializer(NonEmptyData.generatedSerializer()) +} + +@OptIn(ExperimentalSerializationApi::class) +@KeepGeneratedSerializer +@Serializable(with = WriteOnlyData.Serializer::class) +data class WriteOnlyData( + val fieldA: String = "", + val fieldB: String = "", +) { + object Serializer : WriteOnlySerializer( + WriteOnlyData.generatedSerializer(), + setOf("fieldB"), + ) +} + +@OptIn(ExperimentalSerializationApi::class) +@KeepGeneratedSerializer +@Serializable(with = MultiWriteOnly.Serializer::class) +data class MultiWriteOnly( + val fieldA: String = "", + val fieldB: String = "", + val fieldC: String = "", +) { + object Serializer : WriteOnlySerializer( + MultiWriteOnly.generatedSerializer(), + setOf("fieldB", "fieldC"), + ) +} + +@Serializable +data class UriData( + @Serializable(with = UriSerializer::class) + val uri: Uri = Uri.EMPTY, +) + +class SerializerTest { + + @Test + fun nonEmptySerializerOmitsEmptyStrings() { + val data = NonEmptyData(title = "", name = "hello") + val result = data.toJson() + assertFalse(result.contains("title")) + assertTrue(result.contains("name")) + } + + @Test + fun nonEmptySerializerOmitsEmptyLists() { + val data = NonEmptyData(tags = emptyList(), name = "hello") + val result = data.toJson() + assertFalse(result.contains("tags")) + } + + @Test + fun nonEmptySerializerOmitsEmptyMaps() { + val data = NonEmptyData(meta = emptyMap(), name = "hello") + val result = data.toJson() + assertFalse(result.contains("meta")) + } + + @Test + fun nonEmptySerializerKeepsNonEmptyFields() { + val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v")) + val result = data.toJson() + assertTrue(result.contains("title")) + assertTrue(result.contains("tags")) + assertTrue(result.contains("meta")) + } + + @Test + fun nonEmptySerializerDoesNotAffectDeserialization() { + val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}""" + val result = parseJson(input) + assertEquals("hello", result.title) + assertEquals(listOf("a"), result.tags) + assertEquals(mapOf("k" to "v"), result.meta) + assertEquals("world", result.name) + } + + @Test + fun writeOnlySerializerOmitsFieldOnSerialize() { + val data = WriteOnlyData(fieldA = "hello", fieldB = "secret") + val result = data.toJson() + assertTrue(result.contains("fieldA")) + assertFalse(result.contains("fieldB")) + } + + @Test + fun writeOnlySerializerDeserializesNormally() { + val input = """{"fieldA":"hello","fieldB":"secret"}""" + val result = parseJson(input) + assertEquals("hello", result.fieldA) + assertEquals("secret", result.fieldB) + } + + @Test + fun writeOnlySerializerDeserializesMissingAsDefault() { + val input = """{"fieldA":"hello"}""" + val result = parseJson(input) + assertEquals("hello", result.fieldA) + assertEquals("", result.fieldB) + } + + @Test + fun writeOnlySerializerHandlesMultipleKeys() { + val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2") + val result = data.toJson() + assertTrue(result.contains("fieldA")) + assertFalse(result.contains("fieldB")) + assertFalse(result.contains("fieldC")) + } + + @Test + fun uriSerializerSerializesUriToString() { + val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) + val result = data.toJson() + assertTrue(result.contains("https://example.com/path?query=1")) + } + + @Test + fun uriSerializerDeserializesStringToUri() { + val input = """{"uri":"https://example.com/path?query=1"}""" + val result = parseJson(input) + assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri) + } + + @Test + fun uriSerializerRoundtripsCorrectly() { + val data = UriData(uri = Uri.parse("https://example.com/path?query=1")) + val encoded = data.toJson() + val decoded = parseJson(encoded) + assertEquals(data.uri, decoded.uri) + } +} diff --git a/app/src/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/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index b2ef28348..bbe7d97de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -7,7 +7,7 @@ package com.lagradost.cloudstream3 @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) class AcraApplication { companion object { @@ -15,14 +15,14 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.context"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) val context get() = CloudStreamApp.context @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.removeKeys(folder)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) fun removeKeys(folder: String): Int? = CloudStreamApp.removeKeys(folder) @@ -30,7 +30,7 @@ class AcraApplication { @Deprecated( message = "AcraApplication is deprecated, use CloudStreamApp instead", replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(path, value)"), - level = DeprecationLevel.ERROR + level = DeprecationLevel.WARNING ) fun 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/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5d39f6554..90583011d 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 { @@ -786,6 +784,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true) .setEnterAnim(R.anim.enter_anim) .setExitAnim(R.anim.exit_anim) @@ -816,24 +815,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa 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 +1212,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 +1426,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) } } } @@ -2018,7 +2014,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } try { - if (getKey(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..a864b5fb7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt @@ -42,6 +42,7 @@ 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( @@ -160,7 +161,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/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/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 6054bbfaf..debd3f0eb 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 } } @@ -838,16 +832,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 +849,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 +858,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/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 60e41e8b3..b6997d494 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.syncproviders -import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTime @@ -8,10 +7,6 @@ import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.base64Encode import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames import java.net.URI import java.security.SecureRandom @@ -21,32 +16,32 @@ 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) = @@ -56,25 +51,20 @@ data class AuthToken( refreshTokenLifetime != null && unixTime + 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 ) /** @@ -84,11 +74,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( @@ -111,12 +102,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 */ @@ -229,7 +223,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..0b8c3e5ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt @@ -2,6 +2,7 @@ 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 @@ -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( @@ -66,7 +66,7 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { var found: List? = null for (item in searchCache) { // 120 min save - if (item.idPrefix == idPrefix && item.query == query && (unixTime - item.unixTime) < 60 * 120) { + if (item.query == query && (unixTime - item.unixTime) < 60 * 120) { found = item.response break } @@ -79,7 +79,7 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { // only cache valid return values if (returnValue.isNotEmpty()) { - val add = SavedSearchResponse(unixTime, returnValue, query, idPrefix) + val add = SavedSearchResponse(unixTime, returnValue, query) searchCache.withLock { if (searchCache.size > CACHE_SIZE) { searchCache[searchCacheIndex] = add // rolling cache 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..441eb1bf2 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 @@ -27,10 +27,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 @@ -55,7 +54,7 @@ class AniListApi : SyncAPI() { val token = AuthToken( accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"), - // refreshToken = sanitizer["refresh_token"], + //refreshToken = sanitizer["refresh_token"], accessTokenLifetime = APIHolder.unixTime + sanitizer["expires_in"]!!.toLong(), ) return token @@ -82,6 +81,7 @@ class AniListApi : SyncAPI() { override fun urlToId(url: String): String? = url.removePrefix("$mainUrl/anime/").removeSuffix("/") + private fun getUrlFromId(id: Int): String { return "$mainUrl/anime/$id" } @@ -103,6 +103,7 @@ class AniListApi : SyncAPI() { 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 { @@ -156,13 +157,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? { 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 +260,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 +300,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,7 +458,7 @@ class AniListApi : SyncAPI() { cacheTime = 0, ).text - return tryParseJson(data) ?: throw ErrorLoadingException("Error parsing $data") + return tryParseJson(data) ?: throw ErrorLoadingException("Error parsing $data") } } @@ -504,6 +506,7 @@ class AniListApi : SyncAPI() { type = AniListStatusType.None, ) } + } private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? { @@ -519,84 +522,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 +613,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? { @@ -685,6 +672,7 @@ class AniListApi : SyncAPI() { 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,44 +711,33 @@ class AniListApi : SyncAPI() { } } } - } + } """ val text = postApi(auth.token, query) - return tryParseJson(text) + return text?.toKotlinObject() } 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, @@ -770,6 +747,7 @@ class AniListApi : SyncAPI() { progress: Int? ): Boolean { val userID = auth.user.id + val q = // Delete item if status type is None if (type == AniListStatusType.None) { @@ -813,22 +791,22 @@ class AniListApi : SyncAPI() { 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 +839,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..9eb49b4bd 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,5 +1,6 @@ package com.lagradost.cloudstream3.syncproviders.providers + import androidx.annotation.StringRes import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.APIHolder @@ -22,8 +23,6 @@ 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 @@ -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) + } } @@ -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 = @@ -201,7 +205,7 @@ class KitsuApi: SyncAPI() { publicScore = Score.from(anime.ratingTwenty, 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? { @@ -251,13 +256,15 @@ class KitsuApi: SyncAPI() { 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,6 +453,7 @@ 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 limit = 500 @@ -441,44 +462,49 @@ class KitsuApi: SyncAPI() { 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 @@ -520,100 +546,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: Int?, + @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( string ?: return null )?.time?.div(1000) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -622,13 +641,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 +662,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 +689,7 @@ object Kitsu { "https://kitsu.io/api/graphql", headers = headers, data = mapOf("query" to query) - ).parsed() + ).parsed() } private val cache: MutableMap, Map> = @@ -752,52 +771,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..0809ccc43 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 @@ -21,9 +21,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 @@ -52,17 +51,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 +76,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(), refreshToken = token.refreshToken, - accessToken = token.accessToken, + accessToken = token.accessToken ) } @@ -98,7 +96,7 @@ class MALApi : SyncAPI() { return AuthUser( id = user.id, name = user.name, - profilePicture = user.picture, + profilePicture = user.picture ) } @@ -117,7 +115,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 ) } } @@ -139,89 +137,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,7 +223,7 @@ class MALApi : SyncAPI() { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.mainPicture?.large, + posterUrl = node.mainPicture?.large ) } @@ -258,7 +249,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, @@ -345,7 +336,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 +346,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 +361,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 = APIHolder.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,34 +454,29 @@ 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? { @@ -536,7 +522,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 +531,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 +585,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 +597,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..15ef6bfab 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 @@ -20,8 +20,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" @@ -101,7 +99,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 +179,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 +220,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..075c08bb8 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,11 @@ 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 @@ -32,12 +32,7 @@ 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 +45,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 +69,63 @@ 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 = APIHolder.unixTime, + ) { + /** Returns true if cache is newer than cacheDays */ + fun isFresh(): Boolean { + return validUntil > APIHolder.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 = APIHolder.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, APIHolder.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? { val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson(it) + tryParseJson>(it) } - 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 +169,7 @@ class SimklApi : SyncAPI() { ) ) ) - } catch (_: Exception) { + } catch (e: Exception) { null } } @@ -222,7 +185,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 +204,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 +300,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 +341,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 +369,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 +383,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 +396,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,12 +407,12 @@ 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)) } @@ -484,17 +424,19 @@ class SimklApi : SyncAPI() { 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 +446,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 +490,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 +534,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 +548,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(APIHolder.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(APIHolder.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 +607,10 @@ class SimklApi : SyncAPI() { return } } - this.add(newItem) } + // fun merge( first: List?, second: List? @@ -730,17 +644,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 +672,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 +705,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 +743,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 +771,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 +786,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 +799,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 +829,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 +852,7 @@ class SimklApi : SyncAPI() { episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, - oldScore = null, + oldScore = null ) } } @@ -935,11 +860,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 = APIHolder.unixTime val simklStatus = newStatus as? SimklSyncStatus + val builder = SimklScoreBuilder.Builder() .apiUrl(this.mainUrl) .score(newStatus.score?.toInt(10), simklStatus?.oldScore) @@ -953,6 +879,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,19 +892,22 @@ 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? { @@ -988,10 +918,12 @@ class SimklApi : SyncAPI() { 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 +938,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 +957,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 +980,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 +1037,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 +1053,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 +1069,7 @@ class SimklApi : SyncAPI() { val tokenResponse = app.post( "$mainUrl/oauth/token", json = TokenRequest(code) ).parsedSafe() ?: return null + return AuthToken( accessToken = tokenResponse.accessToken, ) @@ -1135,7 +1080,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/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/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index e73a8c75a..c5f8fa3d9 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( @@ -572,4 +568,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..d7e10c814 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 @@ -719,7 +719,7 @@ class CS3IPlayer : IPlayer { **/ var preferredAudioTrackLanguage: String? = null get() { - return field ?: getKey( + return field ?: getKey( "$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field )?.also { 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 20c37dcf4..9d8ac09bf 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,71 +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, - ).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 @@ -1013,7 +1026,6 @@ open class FullScreenPlayer : AbstractPlayerFragment( } toggleEpisodesOverlay(true) } - else -> return null // Avoid capturing all input } return true @@ -1202,10 +1214,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 91e3ff970..17bef3ec0 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 @@ -117,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 @@ -509,8 +507,7 @@ class GeneratorPlayer : FullScreenPlayer() { showDownloadProgress(DownloadEvent(0, 0, 0, null)) - // uiReset() // Removed due to UX - + uiReset() currentSelectedLink = link // setEpisodes(viewModel.getAllMeta() ?: emptyList()) setPlayerDimen(null) @@ -793,58 +790,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 { @@ -1539,7 +1525,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"}, " + @@ -1598,7 +1584,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( @@ -1618,7 +1604,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (!description.isNullOrBlank()) { descView.isVisible = true - descView.text = description.html() + descView.text = description } else { descView.isVisible = false @@ -1712,10 +1698,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) } } 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 15d25148c..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 @@ -55,7 +55,7 @@ object QualityDataHelper { fun getSourcePriority(profile: Int, name: String?): Int { if (name == null) return DEFAULT_SOURCE_PRIORITY - return getKey( + return getKey( "$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, DEFAULT_SOURCE_PRIORITY @@ -94,7 +94,7 @@ object QualityDataHelper { } fun getQualityPriority(profile: Int, quality: Qualities): Int { - return getKey( + return getKey( "$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile", quality.value.toString(), quality.defaultPriority @@ -223,4 +223,4 @@ object QualityDataHelper { if (target == null) return Qualities.Unknown return Qualities.entries.minBy { abs(it.value - target) } } -} +} \ 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/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/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 354424978..57f5aa870 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. @@ -139,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() { @@ -150,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 { @@ -248,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 @@ -359,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/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..0cbef9cf2 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,8 +17,8 @@ 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 @@ -26,6 +26,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Levenshtein 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 { + -Levenshtein.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/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 544dd9335..8c2e8e344 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 { 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 cf892424f..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 @@ -261,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), @@ -293,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 c0be85a3f..7278fcdd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -147,7 +147,6 @@ object AppContextUtils { text.toSpanned() } } - /** Get channel ID by name */ @SuppressLint("RestrictedApi") private fun buildWatchNextProgramUri( @@ -365,22 +364,15 @@ 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 hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) } - .map { it.name }) + hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name }) return hashSet } @@ -471,8 +463,7 @@ object AppContextUtils { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = - apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } return if (currentPrefMedia.isEmpty()) { allApis } else { @@ -526,12 +517,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), @@ -539,12 +531,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() @@ -554,7 +547,9 @@ object AppContextUtils { navigate( R.id.global_to_navigation_settings_plugins, PluginsFragment.newInstance( - repositoryData, + repositoryName, + repositoryURL, + false, ) ) } @@ -562,7 +557,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() @@ -689,7 +684,7 @@ object AppContextUtils { "$seasonNameShort${rSeason}:$episodeNameShort${rEpisode}" } else if (rEpisode != null) { "$episodeNameShort$rEpisode" - } else null + }else null } fun Activity?.loadCache() { @@ -712,7 +707,7 @@ object AppContextUtils { fun loadResult( url: String, apiName: String, - name: String, + name : String, startAction: Int = 0, startValue: Int = 0 ) { @@ -722,7 +717,7 @@ object AppContextUtils { fun FragmentActivity.loadResult( url: String, apiName: String, - name: String, + name : String, startAction: Int = 0, startValue: Int = 0 ) { @@ -848,8 +843,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) 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..62426197e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -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,20 +118,18 @@ 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") @@ -147,7 +143,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 +152,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 +166,7 @@ object BackupUtils { context: Context?, backupFile: BackupFile, restoreSettings: Boolean, - restoreDataStore: Boolean, + restoreDataStore: Boolean ) { if (context == null) return if (restoreSettings) { @@ -199,9 +195,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) @@ -217,13 +213,17 @@ object BackupUtils { fileStream = stream.openNew() printStream = PrintWriter(fileStream) printStream.print(backupFile.toJson()) - showToast(R.string.backup_success, Toast.LENGTH_LONG) + + 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 +242,7 @@ object BackupUtils { name, folder = null, extension = ext, - tryResume = false, + tryResume = false ) } @@ -264,7 +264,7 @@ object BackupUtils { activity, restoredValue, restoreSettings = true, - restoreDataStore = true, + restoreDataStore = true ) activity.runOnUiThread { activity.recreate() } } catch (e: Exception) { @@ -305,7 +305,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 +317,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/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/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index f4d6b4271..b01f6e07e 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 { 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..6e74fa00a 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 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,115 @@ 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 = tryParseJson(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") } } - 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? ) } 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..8c50afee7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -200,7 +200,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 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 4c0717b3f..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) } @@ -161,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..7cb190667 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 @@ -1640,11 +1640,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/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/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/repository_item.xml b/app/src/main/res/layout/repository_item.xml index 0550b5943..801ec75e5 100644 --- a/app/src/main/res/layout/repository_item.xml +++ b/app/src/main/res/layout/repository_item.xml @@ -80,8 +80,8 @@ android:layout_gravity="center_vertical" android:layout_marginEnd="5dp" android:textColor="?attr/grayTextColor" - android:visibility="gone" tools:text="Votes: 10K" + android:visibility="gone" tools:visibility="visible" /> - - - diff --git a/app/src/main/res/menu/repository_search.xml b/app/src/main/res/menu/repository_search.xml deleted file mode 100644 index 57c416aef..000000000 --- a/app/src/main/res/menu/repository_search.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-b+ja/strings.xml b/app/src/main/res/values-b+ja/strings.xml index 2b611ef69..3de9a971a 100644 --- a/app/src/main/res/values-b+ja/strings.xml +++ b/app/src/main/res/values-b+ja/strings.xml @@ -729,5 +729,4 @@ ソースの優先順位 プレイヤーでのビデオソースの並び順を設定します - プレイヤーメタデータオーバーレイを表示 diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 4feeabe33..14d327372 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -56,8 +56,8 @@ 재생 속도 글자 색상 윤곽선 색상 - 자막 배경 색상 - 자막 창 색상 + 배경 색상 + 배경 색상 윤곽선 유형 자막 높이 폰트 @@ -715,7 +715,7 @@ 입력하는 동안 검색어 제안 표시 출연진 정보 표시 - 자막 배경 테두리 곡률 + 배경 테두리 곡률 공급자 새로고침 검색어 제안 제안 삭제 diff --git a/app/src/main/res/values-b+ml/strings.xml b/app/src/main/res/values-b+ml/strings.xml index 6c5dd3d89..c2b25c5ee 100644 --- a/app/src/main/res/values-b+ml/strings.xml +++ b/app/src/main/res/values-b+ml/strings.xml @@ -83,9 +83,9 @@ പ്ലേയർ സബ്‌ടൈറ്റിലുകളുടെ സെറ്റിങ്‌സ് - വീഡിയോയിൽ ഇരുവശങ്ങളിലേക്കും സ്വൈപ്പ് ചെയ്താൽ സ്ഥാനത്തെ നിയന്ത്രിക്കാം + വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക - തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ നീക്കുക + തെളിച്ചം അല്ലെങ്കിൽ വോളിയം മാറ്റാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക മുന്നോട്ട് അല്ലെങ്കിൽ പിന്നിലേക്ക് നീങ്ങാൻ വലത്തോട്ടോ ഇടത്തോട്ടോ രണ്ടുതവണ ടാപ്പുചെയ്യുക തിരയുക @@ -186,7 +186,7 @@ ലോഡിംഗ്… ബ്രൗസർ വീണ്ടും കാണുക - നെറ്റ്‌വർക്ക് സ്ട്രീം + സ്ട്രീം %s ൽ ഫോൻ്റ്‌സ് വെച്ചു കൊണ്ട് ഇംപോർട്ട് ചെയ്യുക പ്രശ്‌നമുണ്ടാക്കുന്ന ഒന്ന് കണ്ടെത്താൻ നിങ്ങളെ സഹായിക്കുന്നതിന് ഒരു ക്രാഷ് കാരണം എല്ലാ വിപുലീകരണങ്ങളും ഓഫാക്കി. പൊതു പട്ടിക @@ -267,50 +267,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+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 3b2b3876f..952bafee7 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -553,7 +553,7 @@ Thêm vào Đăng ký Xóa khỏi mục yêu thích - Chọn hồ sơ của bạn + Chọn một Hồ sơ 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 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 @@