diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 30bedcc1b..b5960d5d9 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -71,7 +71,6 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - uses: actions/checkout@v6 diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml new file mode 100644 index 000000000..e354d657d --- /dev/null +++ b/.github/workflows/issue_action.yml @@ -0,0 +1,98 @@ +name: Issue automatic actions + +on: + issues: + types: [opened] + +permissions: + contents: read + issues: write + +jobs: + issue-moderator: + runs-on: ubuntu-latest + steps: + - name: Generate access token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.GH_APP_ID }} + private_key: ${{ secrets.GH_APP_KEY }} + + - name: Similarity analysis + id: similarity + uses: actions-cool/issues-similarity-analysis@v1 + with: + token: ${{ steps.generate_token.outputs.token }} + filter-threshold: 0.60 + title-excludes: '' + comment-title: | + ### Your issue looks similar to these issues: + Please close if duplicate. + comment-body: '${index}. ${similarity} #${number}' + + - name: Label if possible duplicate + if: steps.similarity.outputs.similar-issues-found =='true' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible duplicate"] + }) + + - uses: actions/checkout@v6 + + - name: Automatically close issues that dont follow the issue template + uses: lucasbento/auto-close-issues@v1.0.2 + with: + github-token: ${{ steps.generate_token.outputs.token }} + issue-close-message: | + @${issue.user.login}: hello! :wave: + This issue is being automatically closed because it does not follow the issue template." + closed-issues-label: "invalid" + + - name: Check if issue mentions a provider + id: provider_check + env: + GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}" + run: | + wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" + pip3 install httpx + RES="$(python3 ./check_issue.py)" + echo "name=${RES}" >> $GITHUB_OUTPUT + + - name: Comment if issue mentions a provider + if: steps.provider_check.outputs.name != 'none' + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ steps.generate_token.outputs.token }} + body: | + Hello ${{ github.event.issue.user.login }}. + Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). + + Found provider name: `${{ steps.provider_check.outputs.name }}` + + - name: Label if mentions provider + if: steps.provider_check.outputs.name != 'none' + uses: actions/github-script@v9 + with: + github-token: ${{ steps.generate_token.outputs.token }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["possible provider issue"] + }) + + - name: Add eyes reaction to all issues + uses: actions-cool/emoji-helper@v1.0.0 + with: + type: 'issue' + token: ${{ steps.generate_token.outputs.token }} + emoji: 'eyes' diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b5b17ba6a..d9a20a04b 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -62,7 +62,6 @@ jobs: SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Create pre-release diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8f5c62866..675ce3b2f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,7 +27,7 @@ jobs: cache-read-only: false - name: Run Gradle - run: ./gradlew assemblePrereleaseDebug lint check + run: ./gradlew assemblePrereleaseDebug lint - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02c1f99e8..ae5301929 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { alias(libs.plugins.android.application) alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.serialization) } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) @@ -104,8 +103,8 @@ android { applicationId = "com.lagradost.cloudstream3" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = libs.versions.versionCode.get().toInt() - versionName = libs.versions.versionName.get() + versionCode = 68 + versionName = "4.7.0" manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get() @@ -207,11 +206,9 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.json) androidTestImplementation(libs.core) - androidTestImplementation(libs.espresso.core) + implementation(libs.junit.ktx) androidTestImplementation(libs.ext.junit) - androidTestImplementation(libs.instancio.core) - androidTestImplementation(libs.junit.ktx) - androidTestImplementation(libs.kotlin.test) + androidTestImplementation(libs.espresso.core) // Android Core & Lifecycle implementation(libs.core.ktx) @@ -222,7 +219,6 @@ dependencies { implementation(libs.bundles.lifecycle) implementation(libs.bundles.navigation) implementation(libs.kotlinx.collections.immutable) - implementation(libs.kotlinx.serialization.json) // JSON Parser // Design & UI implementation(libs.preference.ktx) @@ -259,15 +255,13 @@ dependencies { // Extensions & Other Libs implementation(libs.jsoup) // HTML Parser implementation(libs.rhino) // Run JavaScript + implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance implementation(libs.safefile) // To Prevent the URI File Fu*kery coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9 implementation(libs.jackson.module.kotlin) // JSON Parser implementation(libs.zipline) - // Deprecated; will be removed once extensions have time to migrate from using it - implementation("me.xdrop:fuzzywuzzy:1.4.0") - // Torrent Support implementation(libs.torrentserver) @@ -316,7 +310,6 @@ tasks.withType { optIn.addAll( "com.lagradost.cloudstream3.InternalAPI", "com.lagradost.cloudstream3.Prerelease", - "kotlin.uuid.ExperimentalUuidApi", ) } } diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt deleted file mode 100644 index 80c7b49b0..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.lagradost.cloudstream3 - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import dalvik.system.DexFile -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer -import kotlinx.serialization.serializerOrNull -import org.instancio.Instancio -import org.junit.Test -import org.junit.runner.RunWith -import kotlin.reflect.KClass -import kotlin.reflect.jvm.jvmName -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -@RunWith(AndroidJUnit4::class) -class SerializationClassTester { - // Same as app, or using app reference - val jacksonMapper = mapper - val kotlinxMapper = json - - @Test - fun isIdenticalSerialization() { - val serializableClasses = findSerializableClasses("com.lagradost") - println("Number of serializable classes: ${serializableClasses.size}") - - serializableClasses.forEach { kClass -> - val instance = Instancio.create(kClass.java) - - val jacksonJson = jacksonMapper.writeValueAsString(instance) - val kotlinxJson = serializeWithKotlinx(kClass, instance) - - assertEquals( - jacksonJson, - kotlinxJson, - """ - Serialization mismatch for: - ${kClass.qualifiedName} - - Jackson: - $jacksonJson - - Kotlinx: - $kotlinxJson - - """.trimIndent() - ) - println("Identical serialization for: ${kClass.jvmName}") - } - } - - @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) - @Test - fun isIdenticalDeserialization() { - val serializableClasses = findSerializableClasses("com.lagradost") - println("Number of serializable classes: ${serializableClasses.size}") - - serializableClasses.forEach { kClass -> - val instance = Instancio.create(kClass.java) - // Convert to JSON to get example JSON object - // We prefer jackson here because the app may have many jackson JSON strings in local storage - val originalJson = jacksonMapper.writeValueAsString(instance) - - // Create an object from the JSON using kotlinx - val serializer = - kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass) - assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!") - val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson) - - // Create an object from the JSON using jackson - val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java) - - - // Deep inspect both object using the mapper toJson function. - // This deep equality check can be performed using other methods, but this just works. - val jacksonJson = mapperDecoded.toJson() - val kotlinxJson = kotlinxDecoded.toJson() - - assertEquals( - jacksonJson, - kotlinxJson, - """ - Serialization mismatch for: - ${kClass.qualifiedName} - - Jackson: - $jacksonJson - - Kotlinx: - $kotlinxJson - - """.trimIndent() - ) - println("Identical deserialization for: ${kClass.jvmName}") - } - } - - // DEX files are the best solution to read all our classes dynamically. - // classgraph could be used instead, but it only gives results on the JVM, not Android. - @Suppress("DEPRECATION") - private fun findSerializableClasses(packageName: String): List> { - val context = InstrumentationRegistry - .getInstrumentation() - .targetContext - - val dexFile = DexFile(context.packageCodePath) - return dexFile.entries() - .toList() - .filter { it.startsWith(packageName) } - .mapNotNull { - runCatching { Class.forName(it).kotlin }.getOrNull() - }.filter { kClass -> - // Not possible to use .hasAnnotation() on newer Android versions. - kClass.java.annotations.any { - it is Serializable - } - } - } - - @OptIn(InternalSerializationApi::class) - @Suppress("UNCHECKED_CAST") - private fun serializeWithKotlinx( - kClass: KClass<*>, - value: Any - ): String { - val serializer = kClass.serializer() as KSerializer - return kotlinxMapper.encodeToString(serializer, value) - } -} 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 deleted file mode 100644 index 15ad532f8..000000000 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import android.net.Uri -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KeepGeneratedSerializer -import kotlinx.serialization.Serializable -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test - -@OptIn(ExperimentalSerializationApi::class) -@KeepGeneratedSerializer -@Serializable(with = NonEmptyData.Serializer::class) -data class NonEmptyData( - val title: String = "", - val tags: List = 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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ee4c978f2..b2c7091b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,47 +22,6 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - >(USER_PROVIDER_API)?.let { list -> @@ -1653,7 +1657,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ioSafe { initAll() // No duplicates (which can happen by registerMainAPI) - apis = allProviders.distinctBy { it } + apis = synchronized(allProviders) { + allProviders.distinctBy { it } + } } // val navView: BottomNavigationView = findViewById(R.id.nav_view) @@ -1961,7 +1967,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (BuildConfig.DEBUG) { var providersAndroidManifestString = "Current androidmanifest should be:\n" - allProviders.withLock { + synchronized(allProviders) { for (api in allProviders) { providersAndroidManifestString += "() )*/ - - if (video.tvType.isEpisodeBased()) { - video.season?.let { intent.putExtra("introdb_season", it) } - video.episode.let { intent.putExtra("introdb_episode", it) } - } - - val position = getViewPos(video.id)?.position - if (position != null) - putExtra("position", position.toInt()) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - val position = intent?.getIntExtra("position", -1) ?: -1 - val duration = intent?.getIntExtra("duration", -1) ?: -1 - Log.d("MPV", "Position: $position, Duration: $duration") - updateDurationAndPosition(position.toLong(), duration.toLong()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt deleted file mode 100644 index 348be440a..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.lagradost.cloudstream3.actions.temp - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.net.toUri -import com.lagradost.cloudstream3.actions.OpenInAppAction -import com.lagradost.cloudstream3.ui.result.LinkLoadingResult -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.utils.txt - -/** https://github.com/Kindness-Kismet/only_player/tree/main - * https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */ -class OnlyPlayer : OpenInAppAction( - txt("Only Player"), - "one.only.player", - intentClass = "one.only.player.feature.player.PlayerActivity" -) { - override val oneSource = true - override suspend fun putExtra( - context: Context, - intent: Intent, - video: ResultEpisode, - result: LinkLoadingResult, - index: Int? - ) { - /** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */ - intent.apply { - val link = result.links[index!!] - setData(link.url.toUri()) - - putExtra("headers", Bundle().apply { - for ((key, value) in link.headers) { - putExtra(key, value) - } - }) - } - } - - override fun onResult(activity: Activity, intent: Intent?) { - /* onResult does not get called */ - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index e1496db06..efa028d14 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.actions.VideoClickAction import com.lagradost.cloudstream3.actions.VideoClickActionHolder import kotlin.Throws + abstract class Plugin : BasePlugin() { /** * Called when your Plugin is loaded @@ -25,7 +26,9 @@ abstract class Plugin : BasePlugin() { fun registerVideoClickAction(element: VideoClickAction) { Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction") element.sourcePlugin = this.filename - VideoClickActionHolder.allVideoClickActions.add(element) + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.add(element) + } } /** @@ -37,4 +40,4 @@ abstract class Plugin : BasePlugin() { * This will add a button in the settings allowing you to add custom settings */ var openSettings: ((context: Context) -> Unit)? = null -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index debd3f0eb..eae14a6c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -610,7 +610,7 @@ object PluginManager { return false } InputStreamReader(stream).use { reader -> - manifest = parseJson(reader.readText()) + manifest = parseJson(reader, BasePlugin.Manifest::class.java) } } @@ -651,15 +651,9 @@ object PluginManager { context.resources.configuration ) } - synchronized(plugins) { - plugins[filePath] = pluginInstance - } - synchronized(classLoaders) { - classLoaders[loader] = pluginInstance - } - synchronized(urlPlugins) { - urlPlugins[data.url ?: filePath] = pluginInstance - } + plugins[filePath] = pluginInstance + classLoaders[loader] = pluginInstance + urlPlugins[data.url ?: filePath] = pluginInstance if (pluginInstance is Plugin) { pluginInstance.load(context) } else { @@ -695,20 +689,21 @@ object PluginManager { } // remove all registered apis - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { - removePluginMapping(it) + synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { + removePluginMapping(it) + } + } + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - APIHolder.allProviders.withLock { - APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename } + synchronized(extractorApis) { + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } } - extractorApis.withLock { - extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename } - } - - VideoClickActionHolder.allVideoClickActions.withLock { - VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename } + synchronized(VideoClickActionHolder.allVideoClickActions) { + VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename } } synchronized(classLoaders) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt index e07747a86..028356e76 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet @@ -187,16 +186,6 @@ class DownloadQueueService : Service() { debugAssert({ timeTaken == null }, { "Downloader startup should not time out" }) totalDownloadFlow - .debounce { (instances, queue) -> - // Filter away incorrect transient queue states. - // For example when we pop the queue and add a download instance there exists a transient state where - // there is no queue and no download instances (leading to an early exit) - if (instances.isEmpty() && queue.isEmpty()) { - 500.milliseconds - } else { - 0.milliseconds - } - } .takeWhile { (instances, queue) -> // Stop if destroyed isRunning diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index 184a9fbcc..83a7a0984 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -36,9 +36,11 @@ import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.txt +import me.xdrop.fuzzywuzzy.FuzzySearch import java.net.URL import java.security.SecureRandom import java.util.Date 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 0b8c3e5ae..7a93f96f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt @@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf /** Stateless safe abstraction of SubtitleAPI */ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @@ -24,30 +24,26 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { ) // maybe make this a generic struct? right now there is a lot of boilerplate - private val searchCache = atomicListOf() + private val searchCache = threadSafeListOf() private var searchCacheIndex: Int = 0 - private val resourceCache = atomicListOf() + private val resourceCache = threadSafeListOf() private var resourceCacheIndex: Int = 0 const val CACHE_SIZE = 20 } @WorkerThread suspend fun resource(data: SubtitleEntity): Result = runCatching { - val cached = resourceCache.withLock { - var found: SubtitleResource? = null + synchronized(resourceCache) { for (item in resourceCache) { // 20 min save if (item.query == data && (unixTime - item.unixTime) < 60 * 20) { - found = item.response - break + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached val returnValue = api.resource(freshAuth(), data) - resourceCache.withLock { + synchronized(resourceCache) { val add = SavedResourceResponse(unixTime, returnValue, data) if (resourceCache.size > CACHE_SIZE) { resourceCache[resourceCacheIndex] = add // rolling cache @@ -62,25 +58,22 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @WorkerThread suspend fun search(query: SubtitleSearch): Result> { return runCatching { - val cached = searchCache.withLock { - var found: List? = null + synchronized(searchCache) { for (item in searchCache) { // 120 min save if (item.query == query && (unixTime - item.unixTime) < 60 * 120) { - found = item.response - break + return@runCatching item.response } } - found } - if (cached != null) return@runCatching cached - val returnValue = api.search(freshAuth(), query) ?: emptyList() + val returnValue = + api.search(freshAuth(), query) ?: emptyList() // only cache valid return values if (returnValue.isNotEmpty()) { val add = SavedSearchResponse(unixTime, returnValue, query) - searchCache.withLock { + synchronized(searchCache) { if (searchCache.size > CACHE_SIZE) { searchCache[searchCacheIndex] = add // rolling cache searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE @@ -93,3 +86,4 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { } } } + diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index f30a64748..e5f9aca84 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -10,8 +10,8 @@ import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting -import com.lagradost.cloudstream3.utils.Levenshtein import com.lagradost.cloudstream3.utils.UiText +import me.xdrop.fuzzywuzzy.FuzzySearch import java.util.Date /** @@ -138,7 +138,7 @@ abstract class SyncAPI : AuthAPI() { ListSorting.Query -> if (query != null) { items.sortedBy { - -Levenshtein.partialRatio( + -FuzzySearch.partialRatio( query.lowercase(), it.name.lowercase() ) } @@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() { override var score: Score? = null, val tags: List? = null ) : SearchResponse -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 177018e19..7a46b4113 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -50,8 +50,7 @@ class AniListApi : SyncAPI() { override suspend fun login(redirectUrl: String, payload: String?): AuthToken? { val sanitizer = splitRedirectUrl(redirectUrl) val token = AuthToken( - accessToken = sanitizer["access_token"] - ?: throw ErrorLoadingException("No access token"), + accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"), //refreshToken = sanitizer["refresh_token"], accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(), ) @@ -84,8 +83,8 @@ class AniListApi : SyncAPI() { return "$mainUrl/anime/$id" } - override suspend fun search(auth: AuthData?, query: String): List? { - val data = searchShows(query) ?: return null + override suspend fun search(auth : AuthData?, query: String): List? { + val data = searchShows(name) ?: return null return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, @@ -97,7 +96,7 @@ class AniListApi : SyncAPI() { } } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") val season = getSeason(internalId).data.media @@ -159,7 +158,7 @@ class AniListApi : SyncAPI() { ) } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val internalId = id.toIntOrNull() ?: return null val data = getDataAboutId(auth ?: return null, internalId) ?: return null @@ -460,7 +459,7 @@ class AniListApi : SyncAPI() { } } - private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? { + private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? { val q = """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) @@ -507,7 +506,7 @@ class AniListApi : SyncAPI() { } - private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? { + private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? { return app.post( "https://graphql.anilist.co/", headers = mapOf( @@ -639,7 +638,7 @@ class AniListApi : SyncAPI() { } } - override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? { + override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? { val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy { convertAniListStringToStatus(it.status ?: "").stringRes }?.mapValues { group -> @@ -667,7 +666,7 @@ class AniListApi : SyncAPI() { ) } - private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? { + private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? { val userID = auth.user.id val mediaType = "ANIME" @@ -715,7 +714,7 @@ class AniListApi : SyncAPI() { return text?.toKotlinObject() } - suspend fun toggleLike(auth: AuthData, id: Int): Boolean { + suspend fun toggleLike(auth : AuthData, id: Int): Boolean { val q = """mutation (${'$'}animeId: Int = $id) { ToggleFavourite (animeId: ${'$'}animeId) { anime { @@ -738,7 +737,7 @@ class AniListApi : SyncAPI() { data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( - auth: AuthData, + auth : AuthData, id: Int, type: AniListStatusType, score: Score?, @@ -787,7 +786,7 @@ class AniListApi : SyncAPI() { return data != "" } - private suspend fun getUser(token: AuthToken): AniListUser? { + private suspend fun getUser(token : AuthToken): AniListUser? { val q = """ { Viewer { 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 e15a77c64..29c3c0c17 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt @@ -27,8 +27,9 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.text.SimpleDateFormat +import java.time.Instant import java.time.LocalDate -import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale @@ -201,7 +202,7 @@ class KitsuApi: SyncAPI() { id = id, totalEpisodes = anime.episodeCount, title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(), - publicScore = Score.from(anime.ratingTwenty, 20), + publicScore = Score.from(anime.ratingTwenty.toString(), 20), duration = anime.episodeLength, synopsis = anime.synopsis, airStatus = when(anime.status) { @@ -249,7 +250,7 @@ class KitsuApi: SyncAPI() { } return SyncStatus( - score = Score.from(anime.ratingTwenty, 20), + score = Score.from(anime.ratingTwenty.toString(), 20), status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)), isFavorite = null, watchedEpisodes = anime.progress, @@ -453,8 +454,8 @@ class KitsuApi: SyncAPI() { private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array { - val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount") - val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","updatedAt", "status") + val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount") + val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status") val limit = 500 var url = "$apiUrl/library-entries?filter[userId]=$userId&filter[kind]=anime&include=anime&page[limit]=$limit&page[offset]=0&fields[anime]=${animeSelectedFields.joinToString(",")}&fields[libraryEntries]=${libraryEntriesSelectedFields.joinToString(",")}" @@ -525,7 +526,7 @@ class KitsuApi: SyncAPI() { this.id, this.attributes.progress, numEpisodes, - Score.from(this.attributes.ratingTwenty, 20), + Score.from(this.attributes.ratingTwenty.toString(), 20), parseDateLong(this.attributes.updatedAt), "Kitsu", TvType.Anime, @@ -534,9 +535,12 @@ class KitsuApi: SyncAPI() { null, plot = synopsis, releaseDate = if (startDate == null) null else try { - Date.from(LocalDate.parse(startDate).atStartOfDay() - .atZone(ZoneId.systemDefault()) - .toInstant()) + Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(startDate) + ) + ) } catch (_: RuntimeException) { null } @@ -579,7 +583,7 @@ class KitsuApi: SyncAPI() { @JsonProperty("avatar") val avatar: KitsuUserAvatar?, /* User list anime attributes */ @JsonProperty("progress") val progress: Int?, - @JsonProperty("ratingTwenty") val ratingTwenty: Int?, + @JsonProperty("ratingTwenty") val ratingTwenty: Float?, @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("status") val status: String?, ) @@ -628,7 +632,7 @@ class KitsuApi: SyncAPI() { const val KITSU_CACHED_LIST: String = "kitsu_cached_list" private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) } catch (e: Exception) { 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 c0a80b3c9..ba0195be6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -98,9 +98,9 @@ class MALApi : SyncAPI() { ) } - override suspend fun search(auth: AuthData?, query: String): List? { + override suspend fun search(auth : AuthData?, query: String): List? { val auth = auth?.token?.accessToken ?: return null - val url = "$apiUrl/v2/anime?q=$query&limit=$MAL_MAX_SEARCH_LIMIT" + val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" val res = app.get( url, headers = mapOf( "Authorization" to "Bearer $auth", @@ -122,7 +122,7 @@ class MALApi : SyncAPI() { Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() override suspend fun updateStatus( - auth: AuthData?, + auth : AuthData?, id: String, newStatus: SyncAPI.AbstractSyncStatus ): Boolean { @@ -225,7 +225,7 @@ class MALApi : SyncAPI() { ) } - override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? { + override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? { val auth = auth?.token?.accessToken ?: return null val internalId = id.toIntOrNull() ?: return null val url = @@ -271,7 +271,7 @@ class MALApi : SyncAPI() { } } - override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? { + override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? { val auth = auth?.token?.accessToken ?: return null // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get @@ -477,7 +477,7 @@ class MALApi : SyncAPI() { @JsonProperty("start_time") val startTime: String? ) - override suspend fun library(auth: AuthData?): LibraryMetadata? { + override suspend fun library(auth : AuthData?): LibraryMetadata? { val list = getMalAnimeListSmart(auth ?: return null)?.groupBy { convertToStatus(it.listStatus?.status ?: "").stringRes }?.mapValues { group -> @@ -505,7 +505,7 @@ class MALApi : SyncAPI() { ) } - private suspend fun getMalAnimeListSmart(auth: AuthData): Array? { + private suspend fun getMalAnimeListSmart(auth : AuthData): Array? { return if (requireLibraryRefresh) { val list = getMalAnimeList(auth.token) setKey(MAL_CACHED_LIST, auth.user.id.toString(), list) 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 3110b23ac..c4095e2d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.Score import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING @@ -29,7 +30,6 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import com.lagradost.cloudstream3.utils.txt import java.math.BigInteger @@ -117,8 +117,13 @@ class SimklApi : SyncAPI() { * Gets cached object, if object is not fresh returns null and removes it from cache */ inline fun getKey(path: String): T? { + // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject" + val type = mapper.typeFactory.constructParametricType( + SimklCacheWrapper::class.java, + T::class.java + ) val cache = getKey(SIMKL_CACHE_KEY, path)?.let { - tryParseJson>(it) + mapper.readValue>(it, type) } return if (cache?.isFresh() == true) { @@ -911,7 +916,7 @@ class SimklApi : SyncAPI() { override suspend fun search(auth: AuthData?, query: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 8ec082520..93a79689e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.newSearchResponseList -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) { val hash: Pair ) - private val cache = atomicListOf() + private val cache = threadSafeListOf() private var cacheIndex: Int = 0 const val CACHE_SIZE = 20 @@ -66,7 +66,9 @@ class APIRepository(val api: MainAPI) { private fun afterPluginsLoaded(forceReload: Boolean) { if (forceReload) { - cache.clear() + synchronized(cache) { + cache.clear() + } } } @@ -89,25 +91,21 @@ class APIRepository(val api: MainAPI) { val fixedUrl = api.fixUrl(url) val lookingForHash = Pair(api.name, fixedUrl) - val cached = cache.withLock { - var found: LoadResponse? = null + synchronized(cache) { for (item in cache) { // 10 min save if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { - found = item.response - break + return@withTimeout item.response } } - found } - if (cached != null) return@withTimeout cached api.load(fixedUrl)?.also { response -> // Remove all blank tags as early as possible response.tags = response.tags?.filter { it.isNotBlank() } val add = SavedLoadResponse(unixTime, response, lookingForHash) - cache.withLock { + synchronized(cache) { if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache cacheIndex = (cacheIndex + 1) % CACHE_SIZE @@ -217,4 +215,4 @@ class APIRepository(val api: MainAPI) { return false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 2aadfb13c..f91d40f28 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -12,6 +12,9 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListView import androidx.appcompat.app.AlertDialog +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.google.android.gms.cast.MediaLoadOptions import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaSeekOptions @@ -102,6 +105,9 @@ data class MetadataHolder( class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { + private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() + init { view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setOnClickListener { @@ -443,4 +449,4 @@ class ControllerActivity : ExpandedControllerActivity() { SkipNextEpisodeController(skipOpButton) ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 8d48f5a68..e0609c0e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(apis.withLock { apis.first { it.hasMainPage } }) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index c5f8fa3d9..6e28c128d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -210,13 +210,14 @@ class LibraryFragment : BaseFragment( syncId: SyncIdName, apiName: String? = null, ) { - val availableProviders = allProviders.filter { - it.supportedSyncNames.contains(syncId) - }.map { it.name } + - // Add the api if it exists - (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } - ?: emptyList()) - + val availableProviders = synchronized(allProviders) { + allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } + ?: emptyList()) + } val baseOptions = listOf( LibraryOpenerType.Default, LibraryOpenerType.None, 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 d7e10c814..aa44b9235 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -96,7 +96,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus -import com.lagradost.cloudstream3.utils.CLEARKEY_DRM_UUID +import com.lagradost.cloudstream3.utils.CLEARKEY_UUID import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount @@ -104,9 +104,9 @@ import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.PLAYREADY_DRM_UUID +import com.lagradost.cloudstream3.utils.PLAYREADY_UUID import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName -import com.lagradost.cloudstream3.utils.WIDEVINE_DRM_UUID +import com.lagradost.cloudstream3.utils.WIDEVINE_UUID import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp import kotlinx.coroutines.delay import okhttp3.Interceptor @@ -118,7 +118,6 @@ import java.util.concurrent.Executors import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession -import kotlin.uuid.toJavaUuid const val TAG = "CS3ExoPlayer" const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language" @@ -1279,7 +1278,7 @@ class CS3IPlayer : IPlayer { item.drm?.let { drm -> when (drm.uuid) { - CLEARKEY_DRM_UUID.toJavaUuid() -> { + CLEARKEY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1300,8 +1299,8 @@ class CS3IPlayer : IPlayer { .createMediaSource(item.mediaItem) } - WIDEVINE_DRM_UUID.toJavaUuid(), - PLAYREADY_DRM_UUID.toJavaUuid() -> { + WIDEVINE_UUID, + PLAYREADY_UUID -> { // Use headers from DrmMetadata for media requests val client = dataSourceFactory ?: throw IllegalArgumentException("Must supply onlineSource") @@ -1915,7 +1914,7 @@ class CS3IPlayer : IPlayer { drm = DrmMetadata( kid = link.kid, key = link.key, - uuid = link.uuid.toJavaUuid(), + uuid = link.uuid, kty = link.kty, licenseUrl = link.licenseUrl, keyRequestParameters = link.keyRequestParameters, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index a086cc16f..7a42cea93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -58,23 +58,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { enableEdgeToEdgeCompat() setContentView(R.layout.empty_layout) Log.i(TAG, "onCreate") - handleIntent(intent) - /** - * Use moveTaskToBack instead of finish() so there is always exactly one task - * entry in recents, always reflecting the current file. - * - * finish() destroys the Activity but may leave the task in recents. Each new file - * open can create a new task entry, so recents accumulates stale entries for old - * files. The user then taps a stale entry and gets the wrong file. - * - * moveTaskToBack keeps the Activity alive in the background. There is only ever - * one task entry in recents. New files opened from the file manager arrive via - * onNewIntent on the live instance, updating the player immediately. The single - * recents entry always reflects the current state, ensuring we load the - * correct file. - */ - attachBackPressedCallback("DownloadedPlayerActivity") { moveTaskToBack(true) } + handleIntent(intent) + attachBackPressedCallback("DownloadedPlayerActivity") { finish() } } private fun handleIntent(intent: Intent) { @@ -97,11 +83,11 @@ class DownloadedPlayerActivity : AppCompatActivity() { url != null -> playLink(this, url) data != null -> playUri(this, data) extraText != null -> playLink(this, extraText) - else -> finishAndRemoveTask() + else -> { finish(); return } } } else if (data?.scheme == "content") { playUri(this, data) - } else finishAndRemoveTask() + } else finish() } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 4ba933e13..26706699b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -945,18 +945,12 @@ open class FullScreenPlayer : AbstractPlayerFragment( player.handleEvent(CSPlayerEvent.SkipCurrentChapter) } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER -> { // space is not captured due to navigation + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation player.handleEvent(CSPlayerEvent.PlayPauseToggle) } - // KEYCODE_DPAD_CENTER and KEYCODE_ENTER both act as a "select/confirm" button. - // Some remotes (e.g. LG Magic Remote) send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER. - // When the player UI or a dialog is visible, we let the event pass through (return null) - // so the focused button/item can handle the click normally, rather than always toggling - // play/pause. Only when the UI is hidden do we treat it as a play/pause toggle. - KeyEvent.KEYCODE_DPAD_CENTER, - KeyEvent.KEYCODE_ENTER -> { - if (isShowing || isDialogOpen()) { + KeyEvent.KEYCODE_DPAD_CENTER -> { + if (isShowing) { return null } // If UI is not shown make click instantly skip to next chapter even if locked 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 17bef3ec0..9ee85a941 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -143,11 +143,7 @@ class GeneratorPlayer : FullScreenPlayer() { const val STOP_ACTION = "stopcs3" private val generators = ConcurrentHashMap>() - fun newInstance( - generator: VideoGenerator<*>, - index: Int, - syncData: HashMap? = null - ): Bundle { + fun newInstance(generator: VideoGenerator<*>, index : Int, syncData: HashMap? = null): Bundle { Log.i(TAG, "newInstance = $syncData") val uuid = UUID.randomUUID().toString() generators[uuid] = generator @@ -182,14 +178,12 @@ class GeneratorPlayer : FullScreenPlayer() { private var isNextEpisode: Boolean = false // this is used to reset the watch time private var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none - private val allMeta: List? - get() = viewModel.state.generatorState?.allMeta?.filterIsInstance() - ?.map { episode -> - // Refresh all the episodes watch duration - getViewPos(episode.id)?.let { data -> - episode.copy(position = data.position, duration = data.duration) - } ?: episode - } + private val allMeta: List? get() = viewModel.state.generatorState?.allMeta?.filterIsInstance()?.map { episode -> + // Refresh all the episodes watch duration + getViewPos(episode.id)?.let { data -> + episode.copy(position = data.position, duration = data.duration) + } ?: episode + } private fun setSubtitles(subtitle: SubtitleData?, userInitiated: Boolean): Boolean { // If subtitle is changed and user initiated -> Save the language @@ -1547,7 +1541,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun startPlayer() { // We don't want double load when you skip loading - if (isPlayerActive.get()) { + if(isPlayerActive.get()) { return } @@ -1732,11 +1726,11 @@ class GeneratorPlayer : FullScreenPlayer() { ): SubtitleData? { val langCode = preferredAutoSelectSubtitles ?: return null if (downloads) { - sortSubs(subtitles).firstOrNull { + return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode( langCode ) - }?.let { return it } + } } if (!settings) return null @@ -2146,7 +2140,6 @@ class GeneratorPlayer : FullScreenPlayer() { fun releasePlayer() { player.release() currentSelectedSubtitles = null - currentSelectedLink = null isPlayerActive.set(false) binding?.overlayLoadingSkipButton?.isVisible = false binding?.playerLoadingOverlay?.isVisible = true @@ -2159,31 +2152,19 @@ class GeneratorPlayer : FullScreenPlayer() { activity?.popCurrentPage() } - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt("index", viewModel.episodeIndex) - super.onSaveInstanceState(outState) - } - override fun onBindingCreated(binding: FragmentPlayerBinding, savedInstanceState: Bundle?) { viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] val uuid = savedInstanceState?.getString("uuid") ?: arguments?.getString("uuid") val index = savedInstanceState?.getInt("index") ?: arguments?.getInt("index") - val generator = generators[uuid] + + viewModel.attachGenerator(generators[uuid], index) unwrapBundle(savedInstanceState) unwrapBundle(arguments) super.onBindingCreated(binding, savedInstanceState) - - // Avoid showing no links found - if (generator == null || index == null) { - exitPlayer() - return - } - viewModel.attachGenerator(generator, index) - context?.let { ctx -> val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) showName = settingsManager.getBoolean(ctx.getString(R.string.show_name_key), true) @@ -2212,18 +2193,14 @@ class GeneratorPlayer : FullScreenPlayer() { preferredAutoSelectSubtitles = getAutoSelectLanguageTagIETF() - val selectedLink = currentSelectedLink - if (selectedLink == null) { + if (currentSelectedLink == null) { viewModel.loadLinks() - } else { - // Recreated view, so we need to recreate the - loadLink(selectedLink, true) } binding.overlayLoadingSkipButton.setOnClickListener { // Mark as "success" early viewModel.modifyState { - copy(loading = Resource.Success(Unit)) + copy(loading = Resource.Success(true)) } } @@ -2241,13 +2218,11 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentStamps) { (stamps, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe + observe(viewModel.currentStamps) { stamps -> player.addTimeStamps(stamps) } - observe(viewModel.currentSubtitles) { (subtitles, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe + observe(viewModel.currentSubtitles) { subtitles -> player.setActiveSubtitles(subtitles) // If the file is downloaded then do not select auto select the subtitles @@ -2258,9 +2233,7 @@ class GeneratorPlayer : FullScreenPlayer() { autoSelectSubtitles() } } - observe(viewModel.loadingLinks) { (loading, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe - + observe(viewModel.loadingLinks) { loading -> when (loading) { is Resource.Loading -> { releasePlayer() @@ -2281,9 +2254,7 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentLinks) { (links, instance) -> - if (instance != viewModel.state.instance) return@observe // Outdated observe - + observe(viewModel.currentLinks) { links -> val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true val wasGone = binding.overlayLoadingSkipButton.isGone @@ -2298,7 +2269,7 @@ class GeneratorPlayer : FullScreenPlayer() { } safe { - if (!isPlayerActive.get() && viewModel.state.links.any { link -> + if (viewModel.state.links.any { link -> getLinkPriority(currentQualityProfile, link.first) >= QualityDataHelper.AUTO_SKIP_PRIORITY } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index dcf976612..ac25347b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.net.Uri import androidx.core.content.ContextCompat.getString -import androidx.navigation.NavOptions import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.actions.temp.CloudStreamPackage import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson @@ -13,15 +12,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile object OfflinePlaybackHelper { - /** - * Pop any existing player off the nav back stack before pushing the new one, - * keeping the stack flat (at most one player at a time). This prevents an - * OOM when many files are opened in sequence via DownloadedPlayerActivity. - */ - private val replacePlayerNavOptions = NavOptions.Builder() - .setPopUpTo(R.id.navigation_player, inclusive = true, saveState = false) - .build() - fun playLink(activity: Activity, url: String) { activity.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( @@ -30,8 +20,7 @@ object OfflinePlaybackHelper { BasicLink(url) ), id = url.hashCode() ), 0 - ), - replacePlayerNavOptions + ) ) } @@ -63,8 +52,7 @@ object OfflinePlaybackHelper { subs, if (id != -1) id else null, ), 0 - ), - replacePlayerNavOptions + ) ) return true } @@ -88,8 +76,7 @@ object OfflinePlaybackHelper { ) ) ), 0 - ), - replacePlayerNavOptions + ) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index e3c390d50..049ed06d6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -47,9 +47,8 @@ data class VideoState( val subtitles: PersistentSet = persistentSetOf(), val links: PersistentSet = persistentSetOf(), val stamps: PersistentList = persistentListOf(), - val loading: Resource = Resource.Loading(), + val loading: Resource = Resource.Loading(), val generatorState: GeneratorState? = null, - val instance: Int, ) { /** * This acts as a local cache for sorted links that are not copied over by the copy constructor. @@ -115,11 +114,6 @@ data class VideoState( fun set(items: Collection): VideoState = copy(stamps = items.toPersistentList()) } -data class VideoLive( - val value: T, - val instance: Int, -) - class PlayerGeneratorViewModel : ViewModel() { companion object { const val TAG = "PlayViewGen" @@ -129,7 +123,7 @@ class PlayerGeneratorViewModel : ViewModel() { var generator: VideoGenerator<*>? = null @Volatile - var episodeIndex: Int = 0 + private var episodeIndex: Int = 0 /** * The state of the video player, only modify it by modifyState to make sure observe is called, @@ -138,21 +132,20 @@ class PlayerGeneratorViewModel : ViewModel() { * This value can be used without Synchronized or locking when reading, as all fields are immutable. * */ @Volatile - var state = VideoState(instance = 0) + var state = VideoState() private set - private val _currentLinks = - MutableLiveData>>>(null) - val currentLinks: LiveData>>> = _currentLinks + private val _currentLinks = MutableLiveData>>(setOf()) + val currentLinks: LiveData>> = _currentLinks - private val _currentSubtitles = MutableLiveData>>(null) - val currentSubtitles: LiveData>> = _currentSubtitles + private val _currentSubtitles = MutableLiveData>(setOf()) + val currentSubtitles: LiveData> = _currentSubtitles - private val _loadingLinks = MutableLiveData>>() - val loadingLinks: LiveData>> = _loadingLinks + private val _loadingLinks = MutableLiveData>() + val loadingLinks: LiveData> = _loadingLinks - private val _currentStamps = MutableLiveData>>(null) - val currentStamps: LiveData>> = _currentStamps + private val _currentStamps = MutableLiveData>(emptyList()) + val currentStamps: LiveData> = _currentStamps /** * Modifies the `state` variable safely, and with the correct observe behavior. @@ -165,15 +158,6 @@ class PlayerGeneratorViewModel : ViewModel() { val oldState = state state = op.invoke(oldState) - /** New instance, always push state */ - if (state.instance != oldState.instance) { - _currentSubtitles.postValue(VideoLive(state.subtitles, state.instance)) - _currentStamps.postValue(VideoLive(state.stamps, state.instance)) - _currentLinks.postValue(VideoLive(state.links, state.instance)) - _loadingLinks.postValue(VideoLive(state.loading, state.instance)) - return - } - /** * Only post the changed values, this makes sure we do not invoke the "observe" * @@ -181,15 +165,15 @@ class PlayerGeneratorViewModel : ViewModel() { * to avoid comparing the entire set or list as "Persistent" classes will hold the same reference if they are unchanged. * */ if (state.links !== oldState.links) - _currentLinks.postValue(VideoLive(state.links, state.instance)) + _currentLinks.postValue(state.links) if (state.stamps !== oldState.stamps) - _currentStamps.postValue(VideoLive(state.stamps, state.instance)) + _currentStamps.postValue(state.stamps) if (state.subtitles !== oldState.subtitles) - _currentSubtitles.postValue(VideoLive(state.subtitles, state.instance)) + _currentSubtitles.postValue(state.subtitles) /** Normal equality here as it is not a collection */ if (state.loading != oldState.loading) - _loadingLinks.postValue(VideoLive(state.loading, state.instance)) + _loadingLinks.postValue(state.loading) } private val _currentSubtitleYear = MutableLiveData(null) @@ -268,10 +252,13 @@ class PlayerGeneratorViewModel : ViewModel() { loadLinks() } - fun attachGenerator(newGenerator: VideoGenerator<*>, index: Int) { - Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index") - generator = newGenerator - episodeIndex = index + fun attachGenerator(newGenerator: VideoGenerator<*>?, index: Int?) { + if (generator == null) { + generator = newGenerator + if (index != null) { + episodeIndex = index + } + } } /** @@ -334,49 +321,45 @@ class PlayerGeneratorViewModel : ViewModel() { } fun loadLinks(sourceTypes: Set = LOADTYPE_INAPP) { - Log.i(TAG, "loadLinks with generator=$generator and index=$episodeIndex") + Log.i(TAG, "loadLinks") currentJob?.cancel() val index = episodeIndex - // Clear old data and reset the state - modifyState { - VideoState( - loading = Resource.Loading(), - generatorState = generator?.let { gen -> - GeneratorState( - meta = gen.videos.getOrNull(index), - nextMeta = gen.videos.getOrNull(index + 1), - id = gen.getId(index), - response = (gen as? RepoLinkGenerator)?.page, - index = index, - allMeta = gen.videos - ) - }, - instance = instance + 1 - ) - } - currentJob = viewModelScope.launchSafe { + // Clear old data and reset the state + modifyState { + VideoState( + generatorState = generator?.let { gen -> + GeneratorState( + meta = gen.videos.getOrNull(index), + nextMeta = gen.videos.getOrNull(index + 1), + id = gen.getId(index), + response = (gen as? RepoLinkGenerator)?.page, + index = index, + allMeta = gen.videos + ) + } + ) + } + // Load more data val loadingState = safeApiCall { generator?.generateLinks( sourceTypes = sourceTypes, clearCache = forceClearCache, callback = { link -> - if (isActive) - modifyState { - add(link) - } + modifyState { + add(link) + } }, isCasting = false, offset = index, subtitleCallback = { link -> - if (isActive && isValidSubtitle(link)) + if (isValidSubtitle(link)) modifyState { add(link) } }) - Unit } if (!isActive) { @@ -385,13 +368,9 @@ class PlayerGeneratorViewModel : ViewModel() { /** Only mark as success if we have not skipped loading */ modifyState { - if (!isActive) { - this - } else { - when (loading) { - is Resource.Loading -> copy(loading = loadingState) - else -> this - } + when (loading) { + is Resource.Loading -> copy(loading = loadingState) + else -> this } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index c519e0de2..7dfe3cf59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -83,7 +83,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -1325,7 +1324,7 @@ class ResultViewModel2 : ViewModel() { episodeIds: Array, watchState: VideoWatchState ) { - val watchStateString = watchState.toJson() + val watchStateString = DataStore.mapper.writeValueAsString(watchState) episodeIds.forEach { if (getVideoWatchState(it.toInt()) != watchState) { editor.setKeyRaw( @@ -1686,13 +1685,14 @@ class ResultViewModel2 : ViewModel() { } val realRecommendations = ArrayList() - val apiNames = apis.filter { - it.name.contains("gogoanime", true) || - it.name.contains("9anime", true) - }.map { - it.name + val apiNames = synchronized(apis) { + apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name + } } - meta.recommendations?.forEach { rec -> apiNames.forEach { name -> realRecommendations.add(rec.copy(apiName = name)) @@ -2706,4 +2706,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index f60588e35..27db8d1ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() { private var suggestionJob: Job? = null - private var repos = apis.withLock { apis.map { APIRepository(it) } } + private var repos = synchronized(apis) { apis.map { APIRepository(it) } } fun clearSearch() { _searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false))) @@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun reloadRepos() { - repos = apis.withLock { apis.map { APIRepository(it) } } + repos = synchronized(apis) { apis.map { APIRepository(it) } } } fun searchAndCancel( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 57f5aa870..dbf2ff1dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -219,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { } fun showAdd() { - val providers = allProviders.distinctBy { it::class }.sortedBy { it.name } + val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( providers.map { "${it.name} (${it.mainUrl})" }, -1, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index c8478a840..076f17a0a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() { getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { currentLangTags -> - val languagesTagName = APIHolder.apis.withLock { - listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) + - APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } - .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } + val languagesTagName = synchronized(APIHolder.apis) { + listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) + + APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } + .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji } val currentIndexList = currentLangTags.map { langTag -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 0cbef9cf2..dfc61eba5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -23,7 +23,7 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread -import com.lagradost.cloudstream3.utils.Levenshtein +import me.xdrop.fuzzywuzzy.FuzzySearch import java.io.File // String => repository url @@ -246,7 +246,7 @@ class PluginsViewModel : ViewModel() { this.sortedBy { it.plugin.second.name } } else { this.sortedBy { - -Levenshtein.partialRatio( + -FuzzySearch.partialRatio( it.plugin.second.name.lowercase(), query.lowercase() ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 22500d931..818f1fd79 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -40,7 +40,7 @@ class TestViewModel : ViewModel() { get() = scope != null private var filter = ProviderFilter.All - private val providers = atomicListOf>() + private val providers = threadSafeListOf>() private var passed = 0 private var failed = 0 private var total = 0 @@ -51,9 +51,9 @@ class TestViewModel : ViewModel() { } private fun postProviders() { - providers.withLock { + synchronized(providers) { val filtered = when (filter) { - ProviderFilter.All -> providers.toList() + ProviderFilter.All -> providers ProviderFilter.Passed -> providers.filter { it.second.success } ProviderFilter.Failed -> providers.filter { !it.second.success } } @@ -68,7 +68,7 @@ class TestViewModel : ViewModel() { } private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) { - providers.withLock { + synchronized(providers) { val index = providers.indexOfFirst { it.first == api } if (index == -1) { providers.add(api to results) @@ -81,14 +81,14 @@ class TestViewModel : ViewModel() { } fun init() { - total = APIHolder.allProviders.withLock { APIHolder.allProviders.size } + total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size } updateProgress() } fun startTest() { scope = CoroutineScope(Dispatchers.Default) - val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() } + val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() } total = apis.size failed = 0 passed = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 8c2e8e344..501ee0eef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment( if (isSetup) if ( // If any available languages - apis.distinctBy { it.lang }.size > 1 + synchronized(apis) { apis.distinctBy { it.lang }.size > 1 } ) { findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index c18be8a2f..3c4a09ade 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 7278fcdd7..1377ccd08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -369,10 +369,28 @@ object AppContextUtils { } fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name }) + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } + .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list return hashSet } @@ -463,7 +481,9 @@ object AppContextUtils { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } return if (currentPrefMedia.isEmpty()) { allApis } else { 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 62426197e..88cb7481c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -10,6 +10,7 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R @@ -20,12 +21,11 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi.Companion.KITSU_CACHED_LIST -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs +import com.lagradost.cloudstream3.utils.DataStore.mapper import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream @@ -133,7 +133,9 @@ object BackupUtils { ) @Suppress("UNCHECKED_CAST") - private fun getBackup(context: Context): BackupFile { + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } @@ -212,7 +214,7 @@ object BackupUtils { fileStream = stream.openNew() printStream = PrintWriter(fileStream) - printStream.print(backupFile.toJson()) + printStream.print(mapper.writeValueAsString(backupFile)) showToast( R.string.backup_success, @@ -257,8 +259,8 @@ object BackupUtils { val input = activity.contentResolver.openInputStream(uri) ?: return@ioSafe - val text = input.bufferedReader().readText() - val restoredValue = parseJson(text) + val restoredValue = + mapper.readValue(input) restore( activity, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 02ee69791..0a1db85fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -2,16 +2,17 @@ package com.lagradost.cloudstream3.utils import android.content.Context import android.content.SharedPreferences -import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral import kotlin.reflect.KClass import kotlin.reflect.KProperty +import androidx.core.content.edit /** Used to display metadata about downloads and resume watching */ const val DOWNLOAD_HEADER_CACHE = "download_header_cache" @@ -87,18 +88,8 @@ data class Editor( } object DataStore { - // Extensions shouldn't have really been using this version of it, but it seems - // some have. Since there has always been a very easy alternative, we won't - // need to deprecate it that long, and should be able to fully remove it - // once extensions at least use the other version. - @Deprecated( - "Please do not use the mapper version from DataStore. Preferably use methods from AppUtils " + - "to parse JSON. However, you can use the stable-API version of the mapper at " + - "com.lagradost.cloudstream3.mapper to access the mapper directly if necessary.", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("com.lagradost.cloudstream3.mapper"), - ) - val mapper = com.lagradost.cloudstream3.mapper + val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() private fun getPreferences(context: Context): SharedPreferences { return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) @@ -108,6 +99,7 @@ object DataStore { return getPreferences(this) } + fun getFolderName(folder: String, path: String): String { return "${folder}/${path}" } @@ -173,17 +165,17 @@ object DataStore { fun Context.setKey(path: String, value: T) { try { getSharedPrefs().edit { - putString(path, value?.toJsonLiteral()) + putString(path, mapper.writeValueAsString(value)) } } catch (e: Exception) { logError(e) } } - fun Context.getKey(path: String, valueType: Class): T? { + fun Context.getKey(path: String, valueType: Class): T? { try { val json: String = getSharedPrefs().getString(path, null) ?: return null - return parseJson(json, valueType.kotlin) + return json.toKotlinObject(valueType) } catch (e: Exception) { return null } @@ -194,11 +186,11 @@ object DataStore { } inline fun String.toKotlinObject(): T { - return parseJson(this) + return mapper.readValue(this, T::class.java) } - fun String.toKotlinObject(valueType: Class): T { - return parseJson(this, valueType.kotlin) + fun String.toKotlinObject(valueType: Class): T { + return mapper.readValue(this, valueType) } // GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR @@ -222,4 +214,4 @@ object DataStore { inline fun Context.getKey(folder: String, path: String, defVal: T?): T? { return getKey(getFolderName(folder, path), defVal) ?: defVal } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt index 96193fe45..9d5c75289 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri -import android.os.Build import android.os.Build.VERSION.SDK_INT import android.util.Log import android.widget.ImageView @@ -12,7 +11,6 @@ import coil3.EventListener import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader -import coil3.decode.BitmapFactoryDecoder import coil3.disk.DiskCache import coil3.dispose import coil3.load @@ -24,86 +22,82 @@ import coil3.request.CachePolicy import coil3.request.ErrorResult import coil3.request.ImageRequest import coil3.request.allowHardware -import coil3.request.bitmapConfig import coil3.request.crossfade import coil3.util.DebugLogger import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.network.buildDefaultClient +import okhttp3.HttpUrl import okio.Path.Companion.toOkioPath import java.io.File import java.nio.ByteBuffer object ImageLoader { + private const val TAG = "CoilImgLoader" - internal fun buildImageLoader(context: PlatformContext): ImageLoader { - val isBrokenHardware = hasPotentialBrokenHardware() - return ImageLoader.Builder(context) + + internal fun buildImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context) .crossfade(200) - .allowHardware(SDK_INT >= 28 && !isBrokenHardware) + .allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder .diskCachePolicy(CachePolicy.ENABLED) .networkCachePolicy(CachePolicy.ENABLED) .memoryCache { - MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache - .strongReferencesEnabled(false) + MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching .build() } .diskCache { DiskCache.Builder() .directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath()) .maxSizeBytes(512L * 1024 * 1024) // 512 MB - .maxSizePercent(0.04) // max 4% of storage for disk caching + .maxSizePercent(0.04) // Use 4 % of the device's storage space for disk caching .build() } /** Pass interceptors with care, unnecessary passing tokens to servers or image hosting services causes unauthorized exceptions **/ - .components { - add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) - if (isBrokenHardware) { - add(BitmapFactoryDecoder.Factory()) - } // sw decoder - } - .apply { - if (isBrokenHardware) { // coil will auto choose optimal config on modern device - bitmapConfig(Bitmap.Config.ARGB_8888) - } - setupCoilLogger() + .components { add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) } + .also { + it.setupCoilLogger() + Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.") } .build() - } - /** DebugLogger on debug builds which won't slow down release builds & use EventListener for + /** Use DebugLogger on debug builds which won't slow down release builds & use EventListener for Errors on release builds. **/ internal fun ImageLoader.Builder.setupCoilLogger() { if (BuildConfig.DEBUG) { logger(DebugLogger()) + Log.d(TAG, "setupCoilLogger: Activated DEBUG_LOGGER FOR COIL") } else { eventListener(object : EventListener() { override fun onError(request: ImageRequest, result: ErrorResult) { super.onError(request, result) - Log.e(TAG, "Image load error: ${result.throwable.message ?: result.throwable}") - Log.e(TAG, " URL: ${request.data}") - Log.e(TAG, " allowHardware: ${request.allowHardware}") - Log.e(TAG, " hardware: ${Build.HARDWARE}, board: ${Build.BOARD}") + Log.e(TAG, "Error loading image: ${result.throwable}") } }) + Log.d(TAG, "setupCoilLogger: Activated EVENT_LISTENER FOR COIL") } } - /** coil's built in loader attached w/ global synchronized instance **/ + /** we use coil's built in loader with our global synchronized instance, this way we achieve + latest and complete functionality as well as stability **/ private fun ImageView.loadImageInternal( imageData: Any?, headers: Map? = null, builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations ) { - // clear image to avoid loading & flickering issue at fast scrolling (~recycler view/lazy column) + // clear image to avoid loading & flickering issue at fast scrolling (e.g, an image recycler) this.dispose() - if (imageData == null) return + + if(imageData == null) return // Just in case + // setImageResource is better than coil3 on resources due to attr - if (imageData is Int) { - this.setImageResource(imageData); return + if(imageData is Int) { + this.setImageResource(imageData) + return } - // headers can be overridden by extensions. + + // Use Coil's built-in load method but with our custom module & a decent USER-AGENT always + // which can be overridden by extensions. this.load(imageData, SingletonImageLoader.get(context)) { this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder -> headerBuilder["User-Agent"] = USER_AGENT @@ -111,22 +105,11 @@ object ImageLoader { headerBuilder[key] = value } }.build()) + builder() // if passed } } - private fun hasPotentialBrokenHardware(): Boolean { - val hardware = Build.HARDWARE?.lowercase() ?: "" - val board = Build.BOARD?.lowercase() ?: "" - val model = Build.MODEL?.lowercase() ?: "" - val manufacturer = Build.MANUFACTURER?.lowercase() ?: "" - val allwinnerPatterns = listOf("sun50iw9", "h713", "allwinner", "sunxi") - val problematicModels = - listOf("hy320", "hy300", "a10plus", "magcubic", "sinoy", "android tv box") - return allwinnerPatterns.any { it in hardware || it in board || it in manufacturer } || - problematicModels.any { it in model } - } - /** TYPE_SAFE_LOADERS **/ fun ImageView.loadImage( imageData: UiImage?, @@ -155,6 +138,12 @@ object ImageLoader { builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( + imageData: HttpUrl?, + headers: Map? = null, + builder: ImageRequest.Builder.() -> Unit = {} + ) = loadImageInternal(imageData = imageData, headers = headers, builder = builder) + fun ImageView.loadImage( imageData: File?, builder: ImageRequest.Builder.() -> Unit = {} @@ -184,4 +173,4 @@ object ImageLoader { imageData: ByteBuffer?, builder: ImageRequest.Builder.() -> Unit = {} ) = loadImageInternal(imageData = imageData, builder = builder) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index b01f6e07e..8bcd1b88e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -93,9 +93,9 @@ object InAppUpdater { private suspend fun Activity.getReleaseUpdate(): Update { val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(url, headers = headers).text - ).toList() + ) val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""") val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""") @@ -103,7 +103,9 @@ object InAppUpdater { !rel.prerelease }.sortedWith(compareBy { release -> release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> - versionRegex.find(it1)?.groupValues?.let { + versionRegex.find( + it1 + )?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } } @@ -148,9 +150,9 @@ object InAppUpdater { "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/git/ref/tags/pre-release" val releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases" val headers = mapOf("Accept" to "application/vnd.github.v3+json") - val response = parseJson>( + val response = parseJson>( app.get(releaseUrl, headers = headers).text - ).toList() + ) val found = response.lastOrNull { rel -> rel.prerelease || rel.tagName == "pre-release" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 6e74fa00a..351e77c8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.APIHolder.apis //import com.lagradost.cloudstream3.animeproviders.AniflixProvider import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import java.util.concurrent.TimeUnit object SyncUtil { @@ -71,7 +71,7 @@ object SyncUtil { val url = "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json" val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text - val mapped = tryParseJson(response) + val mapped = parseJson(response) val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id @@ -96,8 +96,10 @@ object SyncUtil { .mapNotNull { it.url }.toMutableList() if (type == "anilist") { // TODO MAKE BETTER - apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { - current.add("${it.mainUrl}/anime/$id") + synchronized(apis) { + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } } } return current @@ -167,4 +169,4 @@ object SyncUtil { @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("deletedAt") val deletedAt: String? ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 8c50afee7..91c8a2fc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* +import org.junit.Assert import kotlin.random.Random object TestingUtils { - open class TestResult(val success: Boolean) { companion object { val Pass = TestResult(true) @@ -48,10 +48,6 @@ object TestingUtils { messageLog.add(Message(LogLevel.Error, message)) } } - - private fun fail(message: String): Nothing = throw AssertionError(message) - private fun assertTrue(message: String, condition: Boolean) { if (!condition) fail(message) } - private fun assertNotNull(message: String, value: Any?) { if (value == null) fail(message) } class TestResultList(val results: List) : TestResult(true) class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) @@ -91,7 +87,7 @@ object TestingUtils { } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider marked as hasMainPage, while in reality is has not been implemented") + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") } is CancellationException -> { @@ -119,7 +115,7 @@ object TestingUtils { api.search(query, 1)?.items?.takeIf { it.isNotEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented search()") + Assert.fail("Provider has not implemented search()") } else if (e is CancellationException) { throw e } @@ -129,7 +125,7 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - fail("Api ${api.name} did not return any search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { TestResultList(searchResults) @@ -220,7 +216,7 @@ object TestingUtils { // return TestResult(validResults) } catch (e: Throwable) { if (e is NotImplementedError) { - fail("Provider has not implemented load()") + Assert.fail("Provider has not implemented load()") } throw e } @@ -232,14 +228,14 @@ object TestingUtils { url: String?, logger: Logger ): TestResult { - assertNotNull("Api ${api.name} has invalid url on episode", url) + Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> logger.log("Video loaded: ${link.name}") - assertTrue( + Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 ) @@ -249,12 +245,12 @@ object TestingUtils { logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { - fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") + Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") } } catch (e: Throwable) { when (e) { is NotImplementedError -> { - fail("Provider has not implemented loadLinks()") + Assert.fail("Provider has not implemented loadLinks()") } else -> { @@ -280,7 +276,7 @@ object TestingUtils { // Test Homepage val homepage = testHomepage(api, logger) - assertTrue("Homepage failed to load", homepage.success) + Assert.assertTrue("Homepage failed to load", homepage.success) val homePageList = (homepage as? TestResultList)?.results ?: emptyList() // Test Search Results @@ -291,7 +287,7 @@ object TestingUtils { listOf("over", "iron", "guy")).take(3) val searchResults = testSearch(api, searchQueries, logger) - assertTrue("Failed to get search results", searchResults.success) + Assert.assertTrue("Failed to get search results", searchResults.success) searchResults as TestResultList // Test Load and LoadLinks @@ -325,4 +321,4 @@ object TestingUtils { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt index 7cb190667..12fcc0c33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt @@ -804,7 +804,6 @@ object VideoDownloadManager { private suspend fun resolve( startByte: Long, endByte: Long?, - buffer: ByteArray, callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) ): Long = withContext(Dispatchers.IO) { var currentByte: Long = startByte @@ -823,6 +822,7 @@ object VideoDownloadManager { ) val requestStream = request.body.byteStream() + val buffer = ByteArray(bufferSize) var read: Int try { @@ -853,7 +853,6 @@ object VideoDownloadManager { suspend fun resolveSafe( index: Int, retries: Int = 3, - buffer: ByteArray, callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) ): Boolean { var start = chuckStartByte.getOrNull(index) ?: return false @@ -862,7 +861,7 @@ object VideoDownloadManager { for (i in 0 until retries) { try { // in case - start = resolve(start, end, buffer, callback) + start = resolve(start, end, callback) // no end defined, so we don't care exactly where it ended if (end == null) return true // we have download more or exactly what we needed @@ -1159,29 +1158,13 @@ object VideoDownloadManager { } } - // Reuse a download buffer to decrease unnecessary alloc - val buffer = ByteArray(items.bufferSize) - - // This will take up the first available job and resolve + // this will take up the first available job and resolve while (true) { if (!isActive) return@launch - - var isTooFarAhead = false fileMutex.withLock { if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed ) return@launch - - // Limit RAM usage by throttling if too much data is downloaded but not yet written to disk - // 50MB limit - if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) { - isTooFarAhead = true - } - } - - if (isTooFarAhead) { - delay(500) - continue } // mutex just in case, we never want this to fail due to multithreading @@ -1192,7 +1175,7 @@ object VideoDownloadManager { // in case something has gone wrong set to failed if the fail is not caused by // user cancellation - if (!items.resolveSafe(index, buffer = buffer, callback = callback)) { + if (!items.resolveSafe(index, callback = callback)) { fileMutex.withLock { if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed @@ -1350,23 +1333,10 @@ object VideoDownloadManager { launch(Dispatchers.IO) { while (true) { if (!isActive) return@launch - - var isTooFarAhead = false fileMutex.withLock { if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed ) return@launch - - // Limit RAM usage by throttling if too much data is downloaded but not yet written to disk - // 50MB limit - if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) { - isTooFarAhead = true - } - } - - if (isTooFarAhead) { - delay(500) - continue } // mutex just in case, we never want this to fail due to multithreading diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt deleted file mode 100644 index 7c73a6889..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/serializers/UriSerializer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import android.net.Uri -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -/** - * Custom KSerializer for Android's [Uri] type. - * - * Uri is an Android platform type and cannot be annotated with @Serializable directly. - * Registering it in a SerializersModule globally would require a custom module passed to - * every Json instance, which adds hidden coupling. This serializer is also used sparingly - * across the codebase, so the overhead of a global registration isn't justified. - * Instead, we keep it explicit so that each usage site opts in intentionally and the - * serialization behavior remains visible. - * - * Usage: - * - * @Serializable - * data class MyData( - * @Serializable(with = UriSerializer::class) - * val uri: Uri, - * ) - */ -object UriSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Uri) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Uri { - return Uri.parse(decoder.decodeString()) - } -} diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 04a2a1f1f..407de4a3f 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -11,7 +11,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="640dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 077929d87..3a3076943 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -12,7 +12,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="680dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 88a318874..76231a2d3 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -11,7 +11,6 @@ android:id="@+id/player_metadata_scrim" android:layout_width="640dp" android:layout_height="match_parent" - android:layout_marginTop="-10dp" android:background="@drawable/bg_player_metadata_scrim_netflix" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/values-arz/strings.xml b/app/src/main/res/values-arz/strings.xml index 4a4cb755f..55344e519 100644 --- a/app/src/main/res/values-arz/strings.xml +++ b/app/src/main/res/values-arz/strings.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-b+af/strings.xml b/app/src/main/res/values-b+af/strings.xml index b81848db6..81d7a96ae 100644 --- a/app/src/main/res/values-b+af/strings.xml +++ b/app/src/main/res/values-b+af/strings.xml @@ -24,7 +24,8 @@ Rand tipe Klaar Afgelaai Kyk verder - Nuwe opdatering gevind! \n%1$s -> %2$s + Nuwe opdatering gevind! +\n%1$s -> %2$s Laai Tale af Soek deur verskaffers te gebruik Gaan terug diff --git a/app/src/main/res/values-b+am/strings.xml b/app/src/main/res/values-b+am/strings.xml index 56b71f5a0..7fd3274b9 100644 --- a/app/src/main/res/values-b+am/strings.xml +++ b/app/src/main/res/values-b+am/strings.xml @@ -8,7 +8,8 @@ %1$dሰዓት %2$dደቂቃ ፖስተር የወረዱ - አዲስ ማሻሻያ ተገኝቷል! \n%1$s -> %2$s + አዲስ ማሻሻያ ተገኝቷል! +\n%1$s -> %2$s ተመለስ ተጨማሪ አማራጮች በማየት ላይ diff --git a/app/src/main/res/values-b+apc/strings.xml b/app/src/main/res/values-b+apc/strings.xml index 10ba0a88c..3bf88ce13 100644 --- a/app/src/main/res/values-b+apc/strings.xml +++ b/app/src/main/res/values-b+apc/strings.xml @@ -29,7 +29,8 @@ فرجي الـLogcat 🐈 +30 كفي حضر - في أپدايت جديدة! \n%1$s ← %2$s + في أپدايت جديدة! +\n%1$s ← %2$s نزل الترجمات مع الڤيديو عوزو المصادر لَ تنبّشو رجاع @@ -95,7 +96,8 @@ لون الكتيبة مخلص عوز قوة ضوّ الشاشة تبع السيستام بدل من تغميئ الڤيديو - فشل ترجيع النسخة الإحتياطية من ملف \n%s + فشل ترجيع النسخة الإحتياطية من ملف +\n%s مشّي المقطع الدعائي مشّي البث المباشر م لقينا ولا حلقة @@ -164,7 +166,8 @@ طفي الترجمة القصة مستعمل - %dد \nباقي + %dد +\nباقي عم ينعرض حاليًا بلايحة النَطر حالة @@ -191,7 +194,7 @@ في مشكلة بجهاز العرض (Renderer error) العِنوان پروكسي \"گِت هَب\" - فرجي معلومات مشغل الڤيديو + جودة مشغل الڤيديو ملصق الترجمة أوڤا نَزِل من مصادر وجودات مختلفة @@ -358,7 +361,11 @@ العشوائي يللي بعده خيال عم نجدِد المثلثلات يللي مشتركينلها - مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن: \n \n%s \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟ + مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن: +\n +\n%s +\n +\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟ نزلت %1$d %2$s معرف مش صالح أفّي %s @@ -366,7 +373,9 @@ حطو الأرقام السرية لـ\"%s\" الطريقة القديمة معلى - \"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. \n \nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. + \"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. +\n +\nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. زبد تتبع 3G/4G… نفَتح %s @@ -379,7 +388,8 @@ دايمًا كتوب ب أحرف كاپيتال، A بدل a مشغل الڤيديو المفضل 4K - بَلَش تنزيل %1$d %2$s \n… + بَلَش تنزيل %1$d %2$s +\n… الوصف شوف الريپويات تبع مجتمع \"كلاود ستريم\" إنت هلّق بال وضع الآمن @@ -409,7 +419,9 @@ أفّى الإعداد فتت ع أكونت \"%s\" تبعك حدود خطية - في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا \n(UI was unable to be created correctly) \n%s + في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا +\n(UI was unable to be created correctly) +\n%s عَدِل تجَدَد (من الجديد للقديم) TC @@ -465,7 +477,8 @@ SD الإضافات شيل الإعلانات من الترجمة - رفّكن فاضي ☹ \nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. + رفّكن فاضي ☹ +\nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. اسم الريپو (مش ضروري) الجودات بيانات مش صالحة @@ -492,7 +505,8 @@ رايتينگ (من الواطي للعالي) فتاح من ملف طفي - لقينا ملف الوضع الآمن! \nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف. + لقينا ملف الوضع الآمن! +\nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف. مش مغير وقت الترجمة مشكلة مصدر @@ -517,7 +531,7 @@ إضافات م قدرنا نفتح %s رايتينگ: %s - تحزير: \"كلاود ستريم\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا! + تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا! الحالة محي الريپو مشغل الڤيديو @@ -526,7 +540,10 @@ إنتو أصلًا مصوتين كاميرا م لقينا ولا إضافة بال ريپو - مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: \n\"%s\" \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟ + مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: +\n\"%s\" +\n +\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟ رايط مش صالح 1000 مللي ثانية إصدار @@ -546,7 +563,13 @@ @string/home_play شيلو من لايحة المحتوى الحاضرينو الإعتمادات - فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n \nمتلًا: \nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). \nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). \n \nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! + فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. +\n +\nمتلًا: +\nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). +\nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). +\n +\nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! حطو الأرقام السرية الحالية صوت حط كبسة لبرم إتجاه الشاشة @@ -566,14 +589,16 @@ رمز/كلمة مرور للمصادقة فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، أو الپاسورد. بعد كذا محاولة فاشلة، هيدا الشباك رح يسكر. بكل بساطة، سكر الآپ ورجاع فتحه حتى تجرب بعد مرة. - %s \nباقي + %s +\nباقي المصادقة البيومترية مش مدعومة ع هالجهاز شيله من المفضل اسم وعنوان الريپو نتسخ! فيه ارور بال وصول ل الكليپ-بورد. پليز جرب مرة أخرى. فيه ارور بال نسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. - هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. + هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. +\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. أوكي وقف اپتميزايشن بطارية جهازك بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" @@ -609,13 +634,21 @@ نقي الإشيا اللي بدك تمحيها موجود لينحضر بلا إنترنت محي الفايلات - متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟ \n \n%s - رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟ \n \n%s + متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟ +\n +\n%s + رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟ +\n +\n%s نقي كل شي شيل التنقاية محي (%1$d | %2$s) - متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟ \n \n%2$s - متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟ \n \n%s + متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟ +\n +\n%2$s + متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟ +\n +\n%s صورة زغيرة مع التقريب وال تبعيد بت حط صورة زغير من الڤيديو إنت و عم بت قرب أو ترجع بال ڤيديو بعد مش معمول لود لولا ترجمة @@ -706,33 +739,4 @@ فوق، عال شمال فوق، بال نُص فوق، عال يمين - ليستة التنزيلات - مافي شي عم يتنزّل هلّق. - قوة ضو إضافية - بت حط فلتر للبرايتنس لمّا تعلي قوة الضو ل أكتر من 100% - extra_brightness_enabled - اقتراحات التنبيش - بت فرجي اقتراحات إنتا و عم بت نَبّش - مساح الاقتراحات - فرجي ميتا-ديتا فوق الڤيديو - فرجي ليستة الممثلين - ڤيديو - معلومات الڤيديو - أولوية المصدر - حدد ترتيب المصادر بال مشغل - اسم المصدر - نزلن كلن - لغين كلن - بدك تنزل الحلقة %s؟ - بدك تلغي كل شي عم يتنَزَّل؟ - - م شي عم يتنزل - شي واحد عم يتنزل - - - مافي شي بعد بده يبلش يتنزل - فيه شي واحد بعد بده يبلش يتنزل - - لايڤ - پريڤيو diff --git a/app/src/main/res/values-b+ar/strings.xml b/app/src/main/res/values-b+ar/strings.xml index f543136dc..f7186c0ef 100644 --- a/app/src/main/res/values-b+ar/strings.xml +++ b/app/src/main/res/values-b+ar/strings.xml @@ -12,7 +12,8 @@ سرعة (%.2fx) تقييم: %.1f - يوجد تحديث جديد! \n%1$s -> %2$s + يوجد تحديث جديد! +\n%1$s -> %2$s %d دقيقة CloudStream تشغيل بواسطة CloudStream @@ -175,8 +176,10 @@ إستئناف -٣٠ +٣٠ - سوف يتم الحذف نهائيا %s \nهل أنت متأكد? - %dm \nمتبقية + سوف يتم الحذف نهائيا %s +\nهل أنت متأكد? + %dm +\nمتبقية جاري التنفيذ اكتمل الحالة @@ -398,7 +401,9 @@ تم تحميل: %d مُعطل %d غير مُحمل: %d - لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات. \n \nانضم إلى ديسكورد أو ابحث عبر الإنترنت. + لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات. +\n +\nانضم إلى ديسكورد أو ابحث عبر الإنترنت. عرض مستودعات المجتمع قائمة عامة جميع الترجمات حروف كبيرة @@ -488,13 +493,15 @@ التقييم (من الأعلى إلى الأدنى) التقييم (من الأدنى إلى الأعلى) الترتيب الأبجدي (من ي إلى أ) - مكتبتك فارغة :( \nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية. + مكتبتك فارغة :( +\nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية. محدث (من القديم إلى الجديد) فرز حسب افرز فتح بواسطة المكتبة - تم العثور على ملف الوضع الآمن! \nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. + تم العثور على ملف الوضع الآمن! +\nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف. مدة التقديم عنما يكون المشغل مخفيا مدة التقديم - المشغل مخفي تلفزيون أندرويد @@ -526,7 +533,13 @@ تعديل الملفات التعريفية مساعدة - ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. \n \nالمصدر أ: 3 \nالجودة ب: 7 \nسيكون لها أولوية فيديو مجمعة تبلغ 10. \n \nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! + ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nسيكون لها أولوية فيديو مجمعة تبلغ 10. +\n +\nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! النوعيات خلفية الملف الشخصي تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s @@ -539,7 +552,11 @@ تمت إزالة %s من المفضلة المفضلة تمت إضافة %s إلى المفضلة - احتمال وجود تكرارات في مكتبتك. \n \n%s \n \nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟ + احتمال وجود تكرارات في مكتبتك. +\n +\n%s +\n +\nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟ احتمال أن يكون موجود بالفعل قفل الحساب اضافة الى المفضلة @@ -552,7 +569,9 @@ إشترك إزالة من المفضلة اختار حساب - يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'. \n \nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟ + يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'. +\n +\nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟ ادخال ال PIN PIN أدخل ال PIN الحالي @@ -580,7 +599,8 @@ مصادقة كلمة المرور/رقم التعريف الشخصي بعد عدة محاولات فاشلة، سيتم إغلاق المطالبة. ما عليك سوى إعادة تشغيل التطبيق للمحاولة مرة أخرى. لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا. - %s \nمتبقي + %s +\nمتبقي المفضلة إزالة من المفضلة اسم و عنوان المخزن @@ -622,13 +642,21 @@ الرجاء تحديد العناصر للحذف تحديد الكل حذف الملفات - هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟ \n \n%2$s - ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا: \n \n%s + هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟ +\n +\n%2$s + ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا: +\n +\n%s حذف (%1$d | %2$s) متاح للمشاهدة في وضع عدم الاتصال إلغاء تحديد الكل - هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟ \n \n%s - هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟ \n \n%s + هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟ +\n +\n%s + هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟ +\n +\n%s معاينة شريط البحث تمكين معاينة الصورة المصغرة على شريط البحث لم يتم تحميل أي ترجمات بعد @@ -721,7 +749,7 @@ اقتراحات البحث عرض اقتراحات البحث أثناء الكتابة مسح الاقتراحات - عرض لوحة فريق التمثيل + عرض لوحة البث تثبيت الإصدار التجريبي تم تثبيت الإصدار التجريبي بالفعل. فشل تثبيت الإصدار التجريبي. @@ -753,7 +781,4 @@ %d تنزيل قيد الانتظار عرض واجهة منبثقة للبيانات الوصفية للمشغِّل - مقطع - استعراض - البث قائم diff --git a/app/src/main/res/values-b+ars/strings.xml b/app/src/main/res/values-b+ars/strings.xml index cd3830a01..3104e6a9a 100644 --- a/app/src/main/res/values-b+ars/strings.xml +++ b/app/src/main/res/values-b+ars/strings.xml @@ -35,7 +35,8 @@ توقف التنزيل خطط للمشاهدة إعادة المشاهدة - !تم العثور على تحديث جديد \n%1$s -> %2$s + !تم العثور على تحديث جديد +\n%1$s -> %2$s %.1f:قدر %dاقل كلاودستريم @@ -156,15 +157,23 @@ تم التحديث (من الجديد إلى القديم) تم التحديث (القديم إلى الجديد) أبجديًا (من الألف إلى الياء) - مكتبتك فارغة :( \nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. - !تم العثور على ملف الوضع الآمن \n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف + مكتبتك فارغة :( +\nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. + !تم العثور على ملف الوضع الآمن +\n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف ارجع تحديث العروض المشتركة الوضع العادي حرر ملفات تعريفية مساعدة - .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو \n \nالمصدر أ: 3 \nالجودة ب: 7 \nستكون أولوية الفيديو المدمجة .10 \n \n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط + .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nستكون أولوية الفيديو المدمجة .10 +\n +\n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط لقد صوت بالفعل أبجديًا (ياء إلى ألف) ترتيب حسب @@ -218,7 +227,8 @@ قدم وصف يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى - نهائيا %sسيؤدي هذا الى حذف \nهل أنت متأكد؟ + نهائيا %sسيؤدي هذا الى حذف +\nهل أنت متأكد؟ الخط حجم الخط زيل @@ -245,7 +255,8 @@ 🐈عرض لوجكات سجل صور في صور - %d \nباقي + %d +\nباقي مصدر اللاعب مخفي - ابحث عن المبلغ تكرار النسخ الاحتياطي diff --git a/app/src/main/res/values-b+as/strings.xml b/app/src/main/res/values-b+as/strings.xml index b7efb3341..16be7e313 100644 --- a/app/src/main/res/values-b+as/strings.xml +++ b/app/src/main/res/values-b+as/strings.xml @@ -252,9 +252,12 @@ পুনৰ আৰম্ভ কৰক -৩০ +৩০ - এইটো স্থায়ীভাৱে %s ডিলিট কৰিব। \nআপুনি নিশ্চিত নেকি? - %dm \nবাকী - %s \nবাকী + এইটো স্থায়ীভাৱে %s ডিলিট কৰিব। +\nআপুনি নিশ্চিত নেকি? + %dm +\nবাকী + %s +\nবাকী চলমান সম্পূৰ্ণ স্থিতি @@ -453,7 +456,9 @@ %d প্লাগইন আপডেট কৰা হ\'ল নিষ্ক্ৰিয় কৰা: %d ডাউনলোড কৰা নহয়: %d - CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব। \n \nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক। + CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব। +\n +\nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক। সম্প্ৰদায়ৰ ৰিপ\'জিট\'ৰিসমূহ চাওক সকলো চাবটাইটল মুকলি আখৰত সতর্কতা: CloudStream 3 কোৱা নাই যে তৃতীয় পক্ষৰ বৃদ্ধিসমূহ ব্যৱহাৰ কৰিবলৈ আপুনি সম্পূৰ্ণ দায়িত্ব ল\'ব আৰু কোনো সহায় নাপাব! @@ -518,9 +523,11 @@ বৰ্ণানুক্ৰমিক (A ৰ পৰা Z) পুথিভঁৰালী বাছক ইয়াৰ সহায়ত খুলক - আপোনাৰ পুথিভঁৰালী খালি আছে :( \nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক। + আপোনাৰ পুথিভঁৰালী খালি আছে :( +\nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক। এই তালিকা খালি। অন্য এটি তালিকালৈ সলনি কৰি চাওক। - নিরাপদ ম\'ড ফাইল পোৱা গৈছে! \nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে। + নিরাপদ ম\'ড ফাইল পোৱা গৈছে! +\nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে। ঘূৰাই দিয়া সদস্যতা গ্ৰহণ কৰা শ্ব\'সমূহ আপডেট কৰিছে %s-ত সদস্যতা গ্ৰহণ কৰা হৈছে @@ -532,7 +539,13 @@ সম্পাদনা কৰক প্ৰ\'ফাইলসমূহ সহায় - ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ। \n \nউৎস A: 3 \nগুণ B: 7 \nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10। \n \nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব! + ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ। +\n +\nউৎস A: 3 +\nগুণ B: 7 +\nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10। +\n +\nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব! প্ৰ\'ফাইলৰ পটভূমি UI সঠিকভাৱে সৃষ্টি কৰিব পৰা নগ\'ল, ই এটা গুৰুত্বপূৰ্ণ সমস্যা আৰু তাক অবিলম্বে জনোৱা উচিত %s আপুনি ইতিমধ্যে ভোট দিছে @@ -543,8 +556,14 @@ প্ৰিয় তালিকাৰ পৰা আঁতৰ কৰক সম্ভাৱ্য নকল বস্ত্ত পোৱা গৈছে সকলো প্ৰতিস্থাপন কৰক - আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\' \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? - আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে: \n \n%s \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? + আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\' +\n +\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? + আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে: +\n +\n%s +\n +\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে? %s ৰ বাবে পিন সন্নিবিষ্ট কৰক বৰ্তমান পিন সন্নিবিষ্ট কৰক প্ৰফাইল লক কৰক @@ -569,7 +588,8 @@ ডাউনলোড এপ্‌ আৰম্ভণিৰ পিছত নতুন আপডেটৰ সন্ধান কৰক। একেই ডেভেলপাৰৰ দ্বাৰা এনিম এপ্‌ - নতুন আপডেট পোৱা গ’ল! \n%1$s -> %2$s + নতুন আপডেট পোৱা গ’ল! +\n%1$s -> %2$s ফিলাৰ CloudStreamৰে প্লে কৰক সন্ধান @@ -589,10 +609,18 @@ প্ৰয়োগ কৰক ফাইলসমূহ ডিলিট কৰক ডিলিট (%1$d | %2$s) - আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s - %1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি? \n \n%2$s - আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব: \n \n%s - আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s + আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি? +\n +\n%s + %1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি? +\n +\n%2$s + আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব: +\n +\n%s + আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি? +\n +\n%s মুক্তিৰ তাৰিখ (পুৰণাৰ পৰা নতুন) সতৰ্কবাৰ্তা স্থানীয়ভাৱে প্ৰমাণীকৰণ কৰক diff --git a/app/src/main/res/values-b+bg/strings.xml b/app/src/main/res/values-b+bg/strings.xml index 4fb7757fd..e59f19462 100644 --- a/app/src/main/res/values-b+bg/strings.xml +++ b/app/src/main/res/values-b+bg/strings.xml @@ -16,7 +16,8 @@ Визуализация на фона Скорост (%.2fx) Оценка: %.1f - Намерена е нова актуализация! \n%1$s -> %2$s + Намерена е нова актуализация! +\n%1$s -> %2$s Шаблон %d мин CloudStream @@ -182,8 +183,10 @@ Продължи -30 30 - Това ще изтрие за постоянно %s \nСигурни ли сте? - %dm \nостава + Това ще изтрие за постоянно %s +\nСигурни ли сте? + %dm +\nостава Продължава Завършен Статус @@ -402,7 +405,9 @@ Деактивирано: %d Не е изтеглено: %d Актуализирани %d плъгини - CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища. \n \nПрисъединете се към нашия Дискорд или потърсете онлайн. + CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища. +\n +\nПрисъединете се към нашия Дискорд или потърсете онлайн. Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви @@ -514,10 +519,12 @@ Профил %d По азбучен ред (A до Z) Отваряне с - Вашата библиотека е празна :( \nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека. + Вашата библиотека е празна :( +\nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека. Използване Епизод %d е публикуван! - Намерен е файл за безопасен режим! \nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат. + Намерен е файл за безопасен режим! +\nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат. Вече сте гласували Задаване по подразбиране ПИН трябва да е 4 символа @@ -544,11 +551,23 @@ Скрит играч - сума за търсене Сумата за търсене, използвана, когато играчът е скрит Актуализирано (от ново към старо) - Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото. \n \nИзточник A: 3 \nКачество B: 7 \nЩе има комбиниран видео приоритет от 10. \n \nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди! + Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото. +\n +\nИзточник A: 3 +\nКачество B: 7 +\nЩе има комбиниран видео приоритет от 10. +\n +\nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди! Замени Замени Всички - Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“. \n \nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието? - Във вашата библиотека са намерени потенциални дублиращи се елементи: \n \n%s \n \nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието? + Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“. +\n +\nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието? + Във вашата библиотека са намерени потенциални дублиращи се елементи: +\n +\n%s +\n +\nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието? Заключи Профил Вкарай Сегашен ПИН Управлявай Профили diff --git a/app/src/main/res/values-b+bn/strings.xml b/app/src/main/res/values-b+bn/strings.xml index adc1b3f19..87aa8e7eb 100644 --- a/app/src/main/res/values-b+bn/strings.xml +++ b/app/src/main/res/values-b+bn/strings.xml @@ -15,7 +15,8 @@ ব্যাকগ্রাউন্ড দেখান গতি (%.2f গুণ) মূল্যায়নঃ %.1f - নতুন আপডেট এসেছে! \n%1$s -> %2$s + নতুন আপডেট এসেছে! +\n%1$s -> %2$s ফিলার %d মিনিট ক্লাউডস্ট্রিম @@ -158,7 +159,8 @@ সিনেমা ডিসকর্ডে যোগ দিন টরেন্টস - এটি স্থায়ীভাবে মুছে ফেলা হবে %s \nআপনি কি নিশ্চিত? + এটি স্থায়ীভাবে মুছে ফেলা হবে %s +\nআপনি কি নিশ্চিত? থামুন -৩০ গিটহাব @@ -182,7 +184,8 @@ ব্যবহৃত লাইব্রেরী আমাদের তৈরি ছোট উপন্যাস পড়ার অ্যাপ্লিকেশন - %d মি \nবাকি + %d মি +\nবাকি অন্যান্য চলমান এশিয়ান নাটক @@ -305,7 +308,8 @@ password123 আসছে %s সময়ের মধ্যে বাতিল করুন - %s \nঅবশিষ্ট + %s +\nঅবশিষ্ট লাইভ স্ট্রিম সোর্স সমস্যা রিমোট সমস্যা diff --git a/app/src/main/res/values-b+cs/strings.xml b/app/src/main/res/values-b+cs/strings.xml index 983a03db7..2ee23c1c5 100644 --- a/app/src/main/res/values-b+cs/strings.xml +++ b/app/src/main/res/values-b+cs/strings.xml @@ -16,7 +16,8 @@ Rychlost (%.2fx) Hodnocení: %.1f - Nalezena nová aktualizace! \n%1$s -> %2$s + Nalezena nová aktualizace! +\n%1$s -> %2$s Výplň %d min CloudStream @@ -171,8 +172,10 @@ Pokračovat -30 +30 - Toto nevratně smaže %s \nJste si jisti? - %dm \nzbývá + Toto nevratně smaže %s +\nJste si jisti? + %dm +\nzbývá Probíhající Dokončena Stav @@ -413,7 +416,9 @@ Doplněk stažen 18+ Spuštěno stahování %1$d %2$s… - CloudStream nemá ve výchozím nastavení nainstalované žádné zdroje. Je třeba je nainstalovat z repozitářů. \n \nPřipojte se na náš Discord nebo hledejte na internetu. + CloudStream nemá ve výchozím nastavení nainstalované žádné zdroje. Je třeba je nainstalovat z repozitářů. +\n +\nPřipojte se na náš Discord nebo hledejte na internetu. Zakázáno: %d Aktualizováno %d doplňků Zobrazit informace o pádu @@ -441,7 +446,8 @@ Nepodařilo se nainstalovat novou verzi aplikace Původní Aplikace bude po ukončení aktualizována - Vaše knihovna je prázdná :( \nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny. + Vaše knihovna je prázdná :( +\nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny. Vybrat knihovnu Hodnocení (od nejvyššího) Hodnocení (od nejnižšího) @@ -449,7 +455,8 @@ Seřadit podle Řazení Tento seznam je prázdný. Zkuste přepnout na jiný. - Nalezen soubor bezpečného režimu! \nDo odebrání souboru nebudeme načítat žádná rozšíření. + Nalezen soubor bezpečného režimu! +\nDo odebrání souboru nebudeme načítat žádná rozšíření. Aktualizováno (od nejnovějšího) Aktualizováno (od nejstaršího) Abecedně (od A do Z) @@ -530,7 +537,13 @@ Nápověda Kvality Pozadí profilu - Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. \n \nZdroj A: 3 \nKvalita B: 7 \nBudou mít celkovou prioritu videa 10. \n \nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! + Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. +\n +\nZdroj A: 3 +\nKvalita B: 7 +\nBudou mít celkovou prioritu videa 10. +\n +\nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! Nepodařilo se správně vytvořit rozhraní. Toto je VÁŽNÁ CHYBA, kterou je potřeba ihned nahlásit %s Vypnout Výběr režimu pro filtrování stahování doplňků @@ -540,7 +553,11 @@ %s odebráno z oblíbených Oblíbené %s přidáno do oblíbených - Ve vaší knihovně byl nalezen potenciální duplikát: \n \n%s \n \nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? + Ve vaší knihovně byl nalezen potenciální duplikát: +\n +\n%s +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? Frekvence záloh Nalezena potenciální duplicita Zamknout profil @@ -554,7 +571,9 @@ Odebírat Odebrat z oblíbených Vyberte účet - Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%s“. \n \nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? + Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%s“. +\n +\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci? Zadejte PIN PIN Zadejte současný PIN @@ -583,7 +602,8 @@ Po několika nezdařilých pokusech se okno zavře. Pro opětovný pokus restartujte aplikaci. Vaše data z aplikace CloudStream byla nyní zálohována. Ačkoli je tato možnost velmi malá, různá zařízení se mohou chovat různě. Ve výjimečném případě, že se vám přístup k aplikaci zablokuje, data aplikace zcela vymažte a obnovte je ze zálohy. Velmi se omlouváme za případné nepříjemnosti z toho plynoucí. Odebrat z oblíbených - %s \nzbývá + %s +\nzbývá Přidat do oblíbených Název a adresa repozitáře Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. @@ -624,12 +644,20 @@ Zvolte položky k odstranění Dostupné pro sledování offline Vybrat vše - Opravdu chcete trvale odstranit následující položky? \n \n%s - Opravdu chcete trvale odstranit následující epizody v %1$s? \n \n%2$s - Opravdu chcete trvale odstranit všechny epizody v následujících sériích? \n \n%s + Opravdu chcete trvale odstranit následující položky? +\n +\n%s + Opravdu chcete trvale odstranit následující epizody v %1$s? +\n +\n%2$s + Opravdu chcete trvale odstranit všechny epizody v následujících sériích? +\n +\n%s Zrušit výběr všeho Odstranit soubory - Také trvale odstraníte všechny epizody v následujících sériích: \n \n%s + Také trvale odstraníte všechny epizody v následujících sériích: +\n +\n%s Odstranit (%1$d | %2$s) Náhled v liště přehrávače Povolit náhled miniatur na liště přehrávače @@ -751,7 +779,4 @@ Priorita zdrojů Rozhodněte, jak mají být řazeny zdroje videí v přehrávači Zobrazit překrytí metadat v přehrávači - Video - Náhled - Živě diff --git a/app/src/main/res/values-b+de/strings.xml b/app/src/main/res/values-b+de/strings.xml index e674fafd6..eb939ec5e 100644 --- a/app/src/main/res/values-b+de/strings.xml +++ b/app/src/main/res/values-b+de/strings.xml @@ -27,7 +27,8 @@ Hintergrundbildvorschau Geschwindigkeit (%.2fx) Bewertung: %.1f - Neues Update gefunden! \n%1$s -> %2$s + Neues Update gefunden! +\n%1$s -> %2$s Füller %d Min CloudStream @@ -187,8 +188,10 @@ Fortsetzen -30 +30 - Dadurch wird %s permanent gelöscht \nBist du dir sicher? - %dm \nverbleibend + Dadurch wird %s permanent gelöscht +\nBist du dir sicher? + %dm +\nverbleibend Laufend Abgeschlossen Status @@ -252,7 +255,7 @@ Update Bevorzugte Videoqualität (WLAN) Videoplayertitel max. Zeichen - Zeige Playerinformationen + Playerinformationen anzeigen Videopuffergröße Videopufferlänge Video-Cache in Speicher @@ -398,7 +401,9 @@ Heruntergeladen: %d Deaktiviert: %d Nicht heruntergeladen: %d - CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden. \n \nTrete unserem Discord Server bei oder suche online. + CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden. +\n +\nTrete unserem Discord Server bei oder suche online. Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben @@ -479,9 +484,11 @@ Alphabetisch (Z zu A) Bibliothek auswählen Öffnen mit - Deine Bibliothek ist leer :( \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. + Deine Bibliothek ist leer :( +\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln. - Datei für den abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. + Datei für den abgesicherten Modus gefunden! +\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. Player ausgeblendet - Betrag zum vor- und zurückspulen Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist Der Betrag, welcher verwendet wird, wenn der Player ausgeblendet ist @@ -515,7 +522,13 @@ Hilfe Qualitäten Profil-Hintergrund - Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. \n \nQuelle A: 3 \nQualität B: 7 \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! + Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. +\n +\nQuelle A: 3 +\nQualität B: 7 +\nWerden eine kombinierte Videopriorität von 10 haben. +\n +\nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! Filtermodus für Plugin-Downloads auswählen Es wurde bereits abgestimmt Keine Plugins im Repository gefunden @@ -547,8 +560,14 @@ Kontoauswahl beim Starten überspringen Konten verwalten Konto bearbeiten - Es wurden potentielle Duplikate in deiner Bibliothek gefunden: \n \n%s \n \nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? - Deine Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\' \n \nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? + Es wurden potentielle Duplikate in deiner Bibliothek gefunden: +\n +\n%s +\n +\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? + Deine Bibliothek enthält möglicherweise schon ein Duplikat dieses Elements: \'%s\' +\n +\nMöchtest du dieses Element dennoch hinzufügen, das existierende ersetzen oder diese Aktion abbrechen? Links wurden neu geladen Drehen Zeige einen Umschalter für Bildschirmorientierung an @@ -568,7 +587,8 @@ kein Favorit Dieser Bildschirm wurde nach einigen Fehlversuchen geschlossen. Starte die App neu. Ihre CloudStream-Daten wurden gesichert. Obwohl die Wahrscheinlichkeit dieses seltenen Falles sehr gering ist, verhalten sich alle Geräte unterschiedlich. Falls Sie im schlimmsten Fall den Zugriff zur App verlieren, löschen Sie die App-Daten vollständig und stellen Sie die Sicherung wieder her. Jegliche Unannehmlichkeiten, die Ihnen dadurch entstehen, bedauern wir sehr. - %s \nausstehend + %s +\nausstehend Favorit Kopiert! Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. @@ -587,7 +607,7 @@ Sicherheit Konten Repository öffnen - Besuche %s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein + Besuche%s auf dem Smartphone oder Computer und gebe den obenstehenden Code ein PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung Zur Zeit sind keine Downloads verfügbar. Lokales Video öffnen @@ -610,10 +630,18 @@ Vom Beginn an spielen Elemente zum Löschen auswählen Zum Offline-Ansehen verfügbar - Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst? \n \n%s - Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst? \n \n%2$s - Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen: \n \n%s - Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst? \n \n%s + Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst? +\n +\n%s + Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst? +\n +\n%2$s + Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen: +\n +\n%s + Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst? +\n +\n%s Veröffentlichungsdatum (von neu nach alt) Veröffentlichungsdatum (von alt nach neu) Suchleisten Vorschau @@ -712,8 +740,8 @@ Zusätzliche Helligkeit Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist Erhöhte Helligkeit aktiviert - Zeige Cast-Panel - Mediainfo + Cast-Panel zeigen + Medieninfo Quellname Alle herunterladen Möchtest du Episode %s herunter laden? @@ -731,8 +759,4 @@ Es befinden sich keine Downloads in der Warteschlange. Quellpriorität Entscheide, wie Videoquellen im Player sortiert werden sollen - Zeige Player-Metadaten - Video - Vorschau - Live diff --git a/app/src/main/res/values-b+el/strings.xml b/app/src/main/res/values-b+el/strings.xml index a38ee2682..4a53c4e58 100644 --- a/app/src/main/res/values-b+el/strings.xml +++ b/app/src/main/res/values-b+el/strings.xml @@ -110,7 +110,8 @@ Μπανάνα δόθηκε Ταχύτητα (%.2fx) Βαθμολογία: %.1f - Νέα διαθέσιμη ενημέρωση! \n%1$s -> %2$s + Νέα διαθέσιμη ενημέρωση! +\n%1$s -> %2$s Πατήστε δύο φορές στη μέση για παύση Χρήση φωτεινότητας συστήματος Χρήση φωτεινότητας συστήματος στο ενσωματωμένο πρόγραμμα αναπαραγωγής, αντί εφαρμογής προεπιλεγμένου σκούρου επικαλύμματος @@ -148,8 +149,10 @@ Ακύρωση Παύση Συνέχιση - Αυτό θα διαγράψει μόνιμα το %s \nΕίστε σίγουροι πως θέλετε να προχωρήσετε; - %dm \nαπομένουν + Αυτό θα διαγράψει μόνιμα το %s +\nΕίστε σίγουροι πως θέλετε να προχωρήσετε; + %dm +\nαπομένουν Σε εξέλιξη Κατάσταση Έτος @@ -320,7 +323,9 @@ Απενεργοποιήθηκε: %d Δεν κατέβηκε: %d Ενημερώθηκαν %d πρόσθετα - Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. \n \nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. + Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. +\n +\nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. Προβολή αποθετηρίων κοινότητας Δημόσια λίστα Κεφαλοποίηση υποτίτλων @@ -482,15 +487,23 @@ Αφαίρεση από παρακολουθημένα Περιηγητής Άνοιγμα με - Η βιβλιοθήκη σας είναι άδεια :( \nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας. - Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! \nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. + Η βιβλιοθήκη σας είναι άδεια :( +\nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας. + Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! +\nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. Αρχείο Καταγραφής Απέτυχε Πέτυχε Εκκίνηση Δε βρέθηκαν επεκτάσεις στο αποθετήριο Δε βρέθηκε αποθετήριο, ελέγξτε την URL και δοκιμάστε VPN - Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. \n \nΠηγή Α: 3 \nΠοιότητα Β: 7 \nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. \n \nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! + Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. +\n +\nΠηγή Α: 3 +\nΠοιότητα Β: 7 +\nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. +\n +\nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! Δοκιμή παρόχου Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου) Διακομιστής μεσολάβησης GitHub @@ -536,7 +549,9 @@ Εντάξει Απενεργοποιήση της εξοικονόμησης της μπαταρίας Έχετε ήδη ψηφίσει - Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' \n \nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; + Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' +\n +\nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; Εισαγωγή Τρέχον Κωδικού Κλείδωμα Προφίλ Ξεκλείδωμα Cloudstream @@ -563,7 +578,11 @@ Πιθανό αντίγραφο βρέθηκε Προσθήκη Αντικατάσταση - Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: \n \n%s \n \nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? + Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: +\n +\n%s +\n +\nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? Εισαγωγή Κωδικού για %s Κωδικός Εσφαλμένος Κωδικός. Προσπαθήστε ξανά. @@ -576,7 +595,8 @@ Παράκαμψη απαγόρευσης από raw github URLs χρησιμοποιώντας jsDelivr. Μπορεί να καθυστερήσει τις ενημερώσεις για μερικές μέρες. Εμφάνιση κουμπιού για περιστροφή οθόνης Αγαπημένο - %s \nαπομένουν + %s +\nαπομένουν Βιομετρική αυθεντικοποίηση δεν υποστηρίζεται από τη συσκευή Καστ ταινίας Για να εξασφαλιστούν αδιάκοπες λήψεις και ειδοποιήσεις για αναγραφόμενες τηλεοπτικές εκπομπές, το CloudStream χρειάζεται άδεια για να τρέξει στο παρασκήνιο. Πατώντας OK, θα εμφανιστεί ένας διάλογος αιτήματος. Παρακαλώ πατήστε \\\"Επιτρέπω\\\".\n\nΠαρακαλώ σημειώστε, αυτή η άδεια δεν σημαίνει ότι το CS3 θα αποστραγγίσει την μπαταρία σας. Θα λειτουργεί στο παρασκήνιο μόνο όταν είναι απαραίτητο, όπως κατά τη λήψη ειδοποιήσεων ή τη λήψη βίντεο από επίσημες επεκτάσεις. diff --git a/app/src/main/res/values-b+eo/strings.xml b/app/src/main/res/values-b+eo/strings.xml index 5dea7b24d..ccd18eae3 100644 --- a/app/src/main/res/values-b+eo/strings.xml +++ b/app/src/main/res/values-b+eo/strings.xml @@ -83,7 +83,8 @@ %1$dt %2$dh %3$dm %1$dh %2$dm %dm - Nova ĝisdatigo trovita! \n%1$s -> %2$s + Nova ĝisdatigo trovita! +\n%1$s -> %2$s Speciala epizodo CloudStream Elŝuto Komencite diff --git a/app/src/main/res/values-b+es/strings.xml b/app/src/main/res/values-b+es/strings.xml index 167de546d..1801d39a2 100644 --- a/app/src/main/res/values-b+es/strings.xml +++ b/app/src/main/res/values-b+es/strings.xml @@ -7,8 +7,8 @@ Descargado %1$d %2$s Borrar repositorio El episodio %d se lanzará en - %1$d h %2$d m - %d m + %1$d h %2$d m + %d m Póster Extensiones Archivo descargado @@ -80,7 +80,8 @@ Recargar enlaces /?? /%d - Esto eliminará %s permanentemente \nEstá seguro? + Esto eliminará %s permanentemente +\nEstá seguro? ¿Seguro que quieres salir? Continuar Descarga Código de idioma (es_ES) @@ -103,7 +104,7 @@ Velocidad (%.2f×) Omitir carga %1$s Ep. %2$d - %1$d d %2$d h %3$d m + %1$d d %2$d h %3$d m Reparto: %s Relleno %d min @@ -248,7 +249,8 @@ Continuar -30 +30 - %dm \nfaltante + %dm +\nfaltante En curso Completado Estado @@ -480,8 +482,10 @@ Alfabéticamente (Z a A) Seleccionar biblioteca Abrir con - Tu biblioteca está vacía :( \nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. - ¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. + Tu biblioteca está vacía :( +\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. + ¡Se encontró un archivo en modo seguro! +\nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. Reproductor visible - buscar cantidad Reproductor oculto - buscar cantidad Android TV @@ -506,7 +510,13 @@ ISP Bypasses Calidad de visualización preferida (Datos móviles) Ayuda - Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. \n \nFuente A: 3 \nCalidad B: 7 \nTendrá una prioridad en el vídeo combinada de 10. \n \nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! + Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. +\n +\nFuente A: 3 +\nCalidad B: 7 +\nTendrá una prioridad en el vídeo combinada de 10. +\n +\nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! Perfil %d Wifi Editar @@ -526,7 +536,11 @@ %s eliminado de favoritos Favoritos %s añadido a favoritos - Se han encontrado posibles elementos duplicados en su biblioteca: \n \n%s \n \n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción? + Se han encontrado posibles elementos duplicados en su biblioteca: +\n +\n%s +\n +\n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción? Posible duplicado encontrado Bloquear perfil Añadido a favoritos @@ -569,7 +583,8 @@ Ahora se ha realizado una copia de seguridad de sus datos de CloudStream. Aunque la posibilidad de que esto ocurra es muy baja, todos los dispositivos pueden comportarse de forma diferente. En el raro caso de que no puedas acceder a la aplicación, borra completamente los datos de la aplicación y restaura desde una copia de seguridad. Sentimos mucho las molestias que esto pueda ocasionarte. Favorito Eliminar de favoritos - %s \nrestante + %s +\nrestante Nombre y URL del repositorio ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. @@ -594,7 +609,7 @@ Imagen del código QR Descartar Abrir repositorio - Visita %s en tu smartphone o equipo e introduce el código anterior + Visita %s en tu smartphone o ordenador e introduce el código anterior ¡El código PIN ya ha caducado! El código caduca en %1$d mín y %2$d s No puedo obtener el código PIN del dispositivo; intente con la autenticación local @@ -606,16 +621,24 @@ Ocultar los nombres de los controles del reproductor Fecha de lanzamiento (antigua a nueva) Fecha de lanzamiento (de nueva a antigua) - ¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie? \n \n%s + ¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie? +\n +\n%s Seleccionar elementos para eliminar Disponible para visualizar sin conexión Seleccionar todo Deseleccionar todo Borrar archivos Borrar (%1$d | %2$s) - ¿Seguro que quieres borrar de forma permanente los siguientes elementos? \n \n%s - ¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s? \n \n%2$s - También borrará permanentemente todos los episodios de las siguientes series: \n \n%s + ¿Seguro que quieres borrar de forma permanente los siguientes elementos? +\n +\n%s + ¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s? +\n +\n%2$s + También borrará permanentemente todos los episodios de las siguientes series: +\n +\n%s Activar la previsualización para las miniaturas en la barra de búsqueda Previsualización de Seekbar Aún no hay subtítulos cargados @@ -672,9 +695,9 @@ Sobreexploración Cambios en el tamaño de los pósteres Tamaño del póster - %1$d h %2$d m %3$d s - %1$d m %2$d s - %1$d s + %1$d h %2$d m %3$d s + %1$d m %2$d s + %1$d s Etiqueta de valoración Mantenga presionado para duplicar la velocidad Sin cuenta @@ -733,8 +756,4 @@ %d descargas encoladas %d descargas encoladas - Mostrar superposición de metadatos del jugador - Vídeo - Vista previa - En Vivo diff --git a/app/src/main/res/values-b+fa/strings.xml b/app/src/main/res/values-b+fa/strings.xml index 1018e9a82..5fa557f08 100644 --- a/app/src/main/res/values-b+fa/strings.xml +++ b/app/src/main/res/values-b+fa/strings.xml @@ -113,7 +113,8 @@ در حال تماشا بارگیری‌ها سرعت (%.2f برابر) - بروزرسانی جدید پیدا شد! \n%1$s -> %2$s + بروزرسانی جدید پیدا شد! +\n%1$s -> %2$s پخش فیلم مرورگر پخش قسمت @@ -129,7 +130,8 @@ برای بازنشانی به پیشفرض نگه‌دارید کتابخانه در ادامه - این فرآیند بطور کامل %s را حذف می‌کند \nآیا از این کار اطمینان دارید؟ + این فرآیند بطور کامل %s را حذف می‌کند +\nآیا از این کار اطمینان دارید؟ نام مخزن و نشانی کپی شد! درباره @@ -178,11 +180,13 @@ حذف پرونده نمایش تریلر ها قسمت‌ها - %dد \nباقی‌مانده + %dد +\nباقی‌مانده گیتهاب پنهان کردن ویدیو مشخص شده از نتایج جستجو لغو - %s \nباقی‌مانده + %s +\nباقی‌مانده پیش‌فرض کارتون تورنت diff --git a/app/src/main/res/values-b+fr/strings.xml b/app/src/main/res/values-b+fr/strings.xml index bf401b20a..34bf1393d 100644 --- a/app/src/main/res/values-b+fr/strings.xml +++ b/app/src/main/res/values-b+fr/strings.xml @@ -79,7 +79,8 @@ Annuler Pause Reprendre - Cela va supprimer définitivement %s \nÊtes-vous sûr ? + Cela va supprimer définitivement %s +\nÊtes-vous sûr ? En cours Terminé Statut @@ -121,7 +122,8 @@ Mettre à jour Utile pour contourner les bloquages des FAI Emplacement de téléchargement - Nouvelle mise à jour trouvée ! \n%1$s -> %2$s + Nouvelle mise à jour trouvée ! +\n%1$s -> %2$s Épisode spécial Qualité de visionnage préférée (WiFi) Taille de la mémoire cache @@ -135,7 +137,7 @@ Afficher les animés en Anglais (Dub) / sous-titrés Disposition en mode téléphone %1$s Ep %2$d - Note : %.1f + Note : %.1f Zoom Adapter à l\'écran Disposition de l\'application @@ -143,7 +145,7 @@ Langues des extensions Médias préférées Auto - Distribution : %s + Distribution : %s %d min Rechercher sur %s… À re-regarder @@ -289,8 +291,8 @@ Application Light Novel par les mêmes devs Anime app by the same devs Rejoignez le Discord - %1$d h %2$d min - %d min + %1$d h %2$d min + %d min Lire avec CloudStream Lire en direct Fin @@ -304,9 +306,9 @@ Intro Effacer l\'historique Oui - %1$d j %2$d h %3$d min + %1$d j %2$d h %3$d min Stream - Êtes-vous sûr·e de vouloir quitter ? + Êtes-vous sûr·e de vouloir quitter ? Non Téléchargement de la mise à jour… L\'épisode %d sera publié dans @@ -319,7 +321,8 @@ Nouveau Nom du site ID invalide Installer automatiquement les plugins qui sont dans les repository mais qui n\'ont pas encore été installés. - %dm \nrestant + %dm +\nrestant En direct Autres En direct @@ -360,7 +363,7 @@ NSFW 127.0.0.1 %d / 10 - / ?? + /?? /%d SD UHD @@ -406,14 +409,14 @@ Téléchargé %1$d %2$s Tous les %s déjà téléchargés Télécharger la liste de sites que vous voulez utiliser - Téléchargé : %d + Téléchargé : %d Pistes vidéo Redémarrez l\'application pour voir les changements. Toutes les extensions ont été désactivé à cause d\'un crash pour vous aider à trouver l\'extension causant le problème. Mode sans échec activé Taille Version - Note : %s + Note : %s Description Status Installer l\'extension d\'abord @@ -429,10 +432,10 @@ Nom de dépôt (optionnel) plugin Supprimer le repository - Désactivé : %d - Non téléchargé : %d + Désactivé : %d + Non téléchargé : %d %d plugins mis-à-jour - Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci ! + Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci ! %s (Désactivé) Pistes Pistes audio @@ -445,7 +448,9 @@ Installateur de paquet plugins Cela supprimera également tous les plugins du repository - CloudStream n\'a aucun site installé par défaut. Vous devez installer les sites à partir de dépôts. \n \nRejoignez notre Discord ou cherchez en ligne. + CloudStream n\'a aucun site installé par défaut. Vous devez installer les sites à partir de dépôts. +\n +\nRejoignez notre Discord ou cherchez en ligne. Langage Afficher les popups skip pour les intro / fins Ancienne méthode d\'installation @@ -474,7 +479,8 @@ Note (basse à haute) Note (haut à bas) Alphabétique (A à Z) - Votre bibliothèque est vide :( \nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale. + Votre bibliothèque est vide :( +\nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale. Cette liste est vide. Essayez d\'en changer. Android TV Trier par @@ -483,7 +489,8 @@ Ouvrir avec Mis à jour (Nouveau vers ancien) Mis à jour (ancien vers nouveau) - Fichier du mode sans échec trouvé ! \nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. + Fichier du mode sans échec trouvé ! +\nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé. Arrêter Annuler Enregistrer @@ -508,7 +515,13 @@ Impossible d\'atteindre GitHub. Activation du proxy jsDelivr… Vous avez déjà voté Désactivé - Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. \n \nSource A : 3 \nQualité B : 7 \nLa priorité vidéo combinée sera de 10. \n \nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! + Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. +\n +\nSource A : 3 +\nQualité B : 7 +\nLa priorité vidéo combinée sera de 10. +\n +\nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! Aucun plugin trouvé dans ce dossier Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN Données mobiles @@ -541,14 +554,20 @@ PIN Favoris Connecté en tant que %s - Des doublons potentiels ont été trouvés dans votre bibliothèque : \n \n%s \n \nSouhaitez-vous ajouter cet élément, remplacer les éléments existants ou annuler l\'action ? + Des doublons potentiels ont été trouvés dans votre bibliothèque : +\n +\n%s +\n +\nSouhaitez-vous ajouter cet élément, remplacer les éléments existants ou annuler l\'action ? Saisir le code PIN pour %s Doublon potentiel trouvé Verrouiller le profil Ignorer la sélection de compte au démarrage Se désabonner S\'abonner - Il semble qu\'un élément potentiellement dupliqué existe déjà dans votre bibliothèque : \'%s. \n \nSouhaitez-vous ajouter cet élément, remplacer l\'élément existant ou annuler l\'action ? + Il semble qu\'un élément potentiellement dupliqué existe déjà dans votre bibliothèque : \'%s. +\n +\nSouhaitez-vous ajouter cet élément, remplacer l\'élément existant ou annuler l\'action ? Saisir le code PIN actuel Pivoter Les liens ont été rechargés @@ -561,7 +580,7 @@ Testez toutes les extensions Afficher les recommandations Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. - Copié ! + Copié ! Nom du dépôt et adresse internet Favori Vos données CloudStream viennent d\'être sauvegardées. Bien que cette éventualité soit très faible, tous les appareils peuvent se comporter différemment. Dans le rare cas où l\'accès à l\'application est bloqué, effacez complètement les données de l\'application et restaurez à partir d\'une sauvegarde. Nous sommes sincèrement désolés pour les désagréments occasionnés par cette situation. @@ -600,10 +619,10 @@ Ouvrir le dépôt Code expire dans %1$dm %2$ds Wiki de CloudStream - Le code PIN est maintenant expiré ! + Le code PIN est maintenant expiré ! Image du code QR Supprimer l\'extension - Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s + Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s Authentification locale Date de sortie (du plus ancien au plus récent) Date de sortie (du plus récent au plus ancien) @@ -619,9 +638,9 @@ Comptes Cette vidéo est un torrent, ce qui signifie que votre activité vidéo peut être suivie.\nAssurez-vous de comprendre le fonctionnement des torrents avant de continuer. Ignorer - Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s - Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s - Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s + Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s + Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s + Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s Recopier l’écran Aucun sous-titre n’a encore été chargé Emplacement du dossier de sauvegarde @@ -641,7 +660,7 @@ Note %s Date %s Mettre à jour les plugins - %d plugin(s) mis à jour avec succès ! + %d plugin(s) mis à jour avec succès ! Aucun plugin n\'a été mis à jour. Note (Plus Haute) Mettre à jour les plugins manuellement @@ -652,7 +671,7 @@ La notification du lecteur pour contrôler la lecture en arrière-plan Date (Plus Récent) Note (Plus Basse) - Démarrage du processus de mise à jour du plugin ! + Démarrage du processus de mise à jour du plugin ! Intégré En ligne La reconnaissance vocale n\'est pas disponible @@ -722,8 +741,8 @@ Déterminez comment les sources vidéo seront triées dans le lecteur Télécharger tout Tout annuler - Voulez-vous télécharger l\'épisode %s ? - Vous voulez annuler tous les téléchargements en file d\'attente ? + Voulez-vous télécharger l\'épisode %s ? + Vous voulez annuler tous les téléchargements en file d\'attente ? %d téléchargement actif %d téléchargements actifs @@ -735,7 +754,4 @@ %d téléchargements en attente Afficher les métadata de l\'overlay du lecteur vidéo - Vidéo - Prévisualisation - Direct diff --git a/app/src/main/res/values-b+gl/strings.xml b/app/src/main/res/values-b+gl/strings.xml index e3775b091..ede5fd0dc 100644 --- a/app/src/main/res/values-b+gl/strings.xml +++ b/app/src/main/res/values-b+gl/strings.xml @@ -13,7 +13,8 @@ Póster do Episodio Regresar Cambiar provedor - Nova actualización atopada! \n%1$s -> %2$s + Nova actualización atopada! +\n%1$s -> %2$s Recheo %d min Configuración diff --git a/app/src/main/res/values-b+hi/strings.xml b/app/src/main/res/values-b+hi/strings.xml index 4e013ab36..c9030a00a 100644 --- a/app/src/main/res/values-b+hi/strings.xml +++ b/app/src/main/res/values-b+hi/strings.xml @@ -2,7 +2,8 @@ स्पीड (%.2fx) - नया अपडेट आया है! \n%1$s -> %2$s + नया अपडेट आया है! +\n%1$s -> %2$s होम खोजें डाउनलोडस @@ -86,7 +87,8 @@ रद्द करें रोकें फिर से चलाएं - इससे %s स्थायी रूप से हट जाएगा \nक्या आपका निर्णय निश्चित है ? + इससे %s स्थायी रूप से हट जाएगा +\nक्या आपका निर्णय निश्चित है ? अभी चालू है मुकम्मल हुया स्थिति @@ -151,7 +153,11 @@ %d मिनट क्लाउडस्ट्रीम क्लाउडस्ट्रीम के साथ चलाएं - आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं: \n \n%s \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? + आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं: +\n +\n%s +\n +\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? %s के लिए पिन दर्ज करें संभावित डुप्लिकेट मिला अपडेट शुरू हुआ @@ -171,7 +177,9 @@ अकाउंट चुनिये लोडिंग स्किप करे लोडिंग… - ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\' \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? + ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\' +\n +\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे? पिन दर्ज करें पिन लिंक पुन्ह खुली diff --git a/app/src/main/res/values-b+hr/strings.xml b/app/src/main/res/values-b+hr/strings.xml index 7927a15c4..c2c0379f5 100644 --- a/app/src/main/res/values-b+hr/strings.xml +++ b/app/src/main/res/values-b+hr/strings.xml @@ -19,7 +19,8 @@ Brzina (%.2f×) Ocjena: %.1f - Pronađeno je novo ažuriranje! \n%1$s -> %2$s + Pronađeno je novo ažuriranje! +\n%1$s -> %2$s Umetak %d min CloudStream @@ -185,8 +186,10 @@ Nastavi −30 +30 - Ovo će trajno izbrisati %s \nJeste li sigurni? - %dmin \npreostalo + Ovo će trajno izbrisati %s +\nJeste li sigurni? + %dmin +\npreostalo U tijeku Završeno Status @@ -408,7 +411,9 @@ Preuzeto: %d Onemogućeno: %d Nepreuzeto: %d - CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. \n \nPridružite se našem Discordu ili tražite online. + CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. +\n +\nPridružite se našem Discordu ili tražite online. Prikaži repozitorije zajednice Javni popis Koristi velika slova za sve titlove @@ -493,9 +498,11 @@ Abecedno (Ž do A) Odaberite biblioteku Otvori sa - Vaša je biblioteka prazna :( \nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. + Vaša je biblioteka prazna :( +\nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu. - Pronađena je datoteka sigurnog načina rada! \nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. + Pronađena je datoteka sigurnog načina rada! +\nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. Prikazan player – Količina pomicanja Količina pomicanja koja se koristi kada je player vidljiv Player skriven – Količina pomicanja @@ -534,13 +541,23 @@ Onemogući U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen. Provjeri URL i pokušaj VPN - Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. \n \nIzvor A: 3 \nKvaliteta B: 7 \nImat će kombinirani prioritet videa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. +\n +\nIzvor A: 3 +\nKvaliteta B: 7 +\nImat će kombinirani prioritet videa od 10. +\n +\nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! Već si glasao/la Učestalost spremanja sigurnosne kopije %s uklonjeno iz favorita Favoriti %s dodano u favorite - Potencijalni duplikati pronađeni su u vašoj biblioteci: \n \n%s \n \nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeće ili poništiti radnju? + Potencijalni duplikati pronađeni su u vašoj biblioteci: +\n +\n%s +\n +\nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeće ili poništiti radnju? Pronađen potencijalni duplikat Zaključaj profil Dodaj u favorite @@ -553,7 +570,9 @@ Pretplata Ukloni iz favorita Odaberite račun - Čini se da potencijalno duplicirana stavka već postoji u vašoj biblioteci: \'%s.\' \n \nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeću ili poništiti radnju? + Čini se da potencijalno duplicirana stavka već postoji u vašoj biblioteci: \'%s.\' +\n +\nŽelite li ipak dodati ovu stavku koja zamjenjuje postojeću ili poništiti radnju? Unesite PIN PIN Unesite trenutni PIN @@ -577,7 +596,8 @@ Ime repozitorija i URL kopirano! Zaključaj s biometrijskim podatcima - %s \npreostalo + %s +\npreostalo Pogreška pri pristupanju međuspremnika. Pokušaj ponovo. Otključaj CloudStream Lozinka/PIN autentifikacija @@ -596,7 +616,7 @@ Otključaj aplikaciju pomoću otiska prsta, ID-a lica, PIN-a, uzorka i lozinke. Sljedeća u %s Pogreška pri kopiranju. Kopirajte zapisnik i kontaktirajte podršku aplikacije. - Da biste osigurali neprekinuta preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom na \"U redu\" prikazat će vam se dijaloški okvir s zahtjevom. Molimo vas da pritisnete \"Dopusti\". \n\nNapominjemo da ovo dopuštenje ne znači da će CS3 trošiti vašu bateriju. Aplikacija će raditi u pozadini samo kada je to potrebno, primjerice prilikom primanja obavijesti ili preuzimanja videozapisa iz službenih proširenja. + Da biste osigurali neprekinuta preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom na \"U redu\" prikazat će vam se dijaloški okvir s zahtjevom. Molimo vas da pritisnete \"Dopusti\". \n\nNapominjemo da ovo dopuštenje ne znači da će CS3 trošiti vašu bateriju. Aplikacija će raditi u pozadini samo kada je to potrebno, primjerice prilikom primanja obavijesti ili preuzimanja videozapisa iz službenih proširenja. Vaši CloudStream podaci su sada spremljeni u sigurnosnu kopiju. Iako je vjerojatnost mala, neki se uređaji mogu ponašati drugačije. Ako izgubite pristup aplikaciji, potpuno izbrišite podatke aplikacije i obnovite ih pomoću sigurnosne kopije. Ispričavamo se zbog mogućih neugodnosti. Sezona %1$d epizoda %2$d izlazi za Cast duplikat @@ -622,14 +642,22 @@ Izbriši dodatak Dostupno za gledanje offline Označi sve - Stvarno želite trajno izbrisati sljedeće stavke? \n \n%s - Stvarno želite trajno izbrisati sljedeće epizode u %1$s? \n \n%2$s - Trajno ćete izbrisati i sve epizode u sljedećim serijama: \n \n%s + Stvarno želite trajno izbrisati sljedeće stavke? +\n +\n%s + Stvarno želite trajno izbrisati sljedeće epizode u %1$s? +\n +\n%2$s + Trajno ćete izbrisati i sve epizode u sljedećim serijama: +\n +\n%s Odaberi stavke za brisanje Odznači sve Izbriši (%1$d | %2$s) Izbriši datoteke - Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji? \n \n%s + Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji? +\n +\n%s Još nije učitan nijedan titl Pretpregled trake za traženje Omogući minijaturu pregleda na traci za pretraživanje @@ -721,7 +749,7 @@ Gore u sredini Gore desno Dodatna svjetlina - Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana + Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana dodatna_svjetlina_uključena Prijedlozi za pretraživanje Prikaži prijedloge za pretraživanje tijekom tipkanja @@ -747,8 +775,4 @@ Želiš li preuzeti epizodu %s? Želiš li otkazati sva preuzimanja u redu čekanja? Prikaži ploču glumačke postave - Video - Pregled - Uživo - Prikaži sloj metapodataka playera diff --git a/app/src/main/res/values-b+hu/strings.xml b/app/src/main/res/values-b+hu/strings.xml index b753eadb2..c50b18133 100644 --- a/app/src/main/res/values-b+hu/strings.xml +++ b/app/src/main/res/values-b+hu/strings.xml @@ -10,7 +10,8 @@ Szolgáltató Váltás Sebesség (%.2fx) Értékelés: %.1f - Új frissítés található! \n%1$s -> %2$s + Új frissítés található! +\n%1$s -> %2$s %d perc %1$sEp%2$d CloudStream @@ -148,7 +149,8 @@ Ep Nem található epizód Fájl törlése - %dp \nhátra + %dp +\nhátra Időtartam Elérhető Használatban @@ -204,7 +206,8 @@ %1$s %2$d%3$s Nincs évad +30 - Ezzel véglegesen törli a %s \nBiztosan törli? + Ezzel véglegesen törli a %s +\nBiztosan törli? Folyamatban levő Év Webhely @@ -320,7 +323,8 @@ Támogatott Alkalmazásfrissítés letöltése… Frissítve (újabbtól a régebbihez) - Úgy tűnik, a könyvtárad üres :( \nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. + Úgy tűnik, a könyvtárad üres :( +\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani. Max 4K @@ -412,7 +416,9 @@ Zárt feliratok eltávolítása a feliratokból 18+ Ez az összes tároló bővítményt is törli - A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. \n \nCsatlakozz a Discord-unkhoz vagy keress online. + A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. +\n +\nCsatlakozz a Discord-unkhoz vagy keress online. Verzió Megjelölés megtekintettként Eltávolítás a megnézettek közül @@ -466,7 +472,8 @@ Közreműködők Betűrendben (Z-től az A-ig) Könyvtár kiválasztása - Biztonságos módú fájlba ütköztünk! \nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. + Biztonságos módú fájlba ütköztünk! +\nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. Normál %s betöltve Beállítás kihagyása @@ -529,8 +536,18 @@ Profilok Eltávolítás kedvencekből Adja meg a jelenlegi PIN-t - Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. \n \nForrás A: 3 \nMinőség B: 7 \nEzek összértéke egy 10-es videó prioritást eredményez. \n \nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! - Potenciálisan dupla elemek a könyvtárjában: \n \n%s \n \nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? + Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. +\n +\nForrás A: 3 +\nMinőség B: 7 +\nEzek összértéke egy 10-es videó prioritást eredményez. +\n +\nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! + Potenciálisan dupla elemek a könyvtárjában: +\n +\n%s +\n +\nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? Fiók választás kihagyása belépéskor Használjon alapértelmezett fiókot Elforgatás @@ -539,7 +556,9 @@ %s hozzáadva a kedvencekhez %s eltávolítva a kedvencekből Hozzáadás a kedvencekhez - Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' \n \nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? + Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' +\n +\nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? Adja meg a PIN-t Profil Zárolása Válasszon egy fiókot diff --git a/app/src/main/res/values-b+in/strings.xml b/app/src/main/res/values-b+in/strings.xml index 7d6475f31..9c4c7fc11 100644 --- a/app/src/main/res/values-b+in/strings.xml +++ b/app/src/main/res/values-b+in/strings.xml @@ -169,8 +169,10 @@ Lanjutkan -30 +30 - Ini akan secara permanen menghapus %s \nApakah anda yakin? - %dm \ntersisa + Ini akan secara permanen menghapus %s +\nApakah anda yakin? + %dm +\ntersisa Masih Berlanjut Tamat Status @@ -388,7 +390,9 @@ %d plugin diperbarui Lihat repositori komunitas Daftar publik - CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. \n \nBergabunglah dengan Discord kami atau cari secara online. + CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. +\n +\nBergabunglah dengan Discord kami atau cari secara online. URL Repositori atau Kode Pendek Buat Akun Error @@ -483,7 +487,8 @@ Hapus dari tontonan Peramban Pilih pustaka - Yahh daftar pustaka kamu kosong :( \nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu. + Yahh daftar pustaka kamu kosong :( +\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu. Pustaka Urutkan berdasarkan Urutkan @@ -495,7 +500,8 @@ Abjad (Z ke A) Buka dengan Yahh daftar ini kosong. Coba ganti ke yang lain. - Mode aman file ditemukan! \nTidak memuat ekstensi pada startup sampai berkas dihapus. + Mode aman file ditemukan! +\nTidak memuat ekstensi pada startup sampai berkas dihapus. Sembunyikan Pemutaran - Geser Pemutar terlihat - Geser Geser untuk menghilangkan @@ -521,7 +527,13 @@ Kualitas nonton yang diinginkan (Data Seluler) Data seluler Bantuan - Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. \n \nSumber A: 3 \nKualitas B: 7 \nAkan memiliki prioritas video yang digabung 10. \n \nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! + Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. +\n +\nSumber A: 3 +\nKualitas B: 7 +\nAkan memiliki prioritas video yang digabung 10. +\n +\nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! Profil %d Wifi Pengaturan default @@ -581,7 +593,8 @@ Setelah beberapa kali gagal, perintah akan ditutup. Cukup mulai ulang aplikasi untuk mencoba lagi. Batalkan favorit Buka kunci CloudStream - %s \ntersisa + %s +\ntersisa Favorit Kunci dengan Biometrik Nama dan URL repositori @@ -625,10 +638,18 @@ Pilih Semua Batal Pilih Semua Hapus File - Apakah Anda yakin ingin menghapus item berikut secara permanen? \n \n%s - Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen? \n \n%2$s - Anda juga akan menghapus semua episode dalam seri berikut secara permanen: \n \n%s - Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen? \n \n%s + Apakah Anda yakin ingin menghapus item berikut secara permanen? +\n +\n%s + Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen? +\n +\n%2$s + Anda juga akan menghapus semua episode dalam seri berikut secara permanen: +\n +\n%s + Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen? +\n +\n%s Hapus (%1$d | %2$s) Hapus plugin Tidak bisa mendapatkan kode PIN perangkat, coba autentikasi lokal @@ -744,7 +765,4 @@ Apakah kamu ingin mengunduh episode %s? Apakah kamu ingin membatalkan semua unduhan dalam antrean? Tampilkan overlay metadata pemutar - Live - Video - Pratinjau diff --git a/app/src/main/res/values-b+it/strings.xml b/app/src/main/res/values-b+it/strings.xml index c47a84842..55da49687 100644 --- a/app/src/main/res/values-b+it/strings.xml +++ b/app/src/main/res/values-b+it/strings.xml @@ -6,7 +6,7 @@ L\'episodio %d uscirà in %1$dd %2$dh %3$dm %1$dh %2$dm - %d min + %d min Poster Poster @@ -19,7 +19,8 @@ Velocità (%.2fx) Valutato: %.1f - Nuovo aggiornamento trovato! \n%1$s -> %2$s + Nuovo aggiornamento trovato! +\n%1$s -> %2$s Filler %d min @@ -185,8 +186,10 @@ Riprendi -30 +30 - Stai per eliminare permanentemente %s \nSei sicuro? - %dm \nrimanenti + Stai per eliminare permanentemente %s +\nSei sicuro? + %dm +\nrimanenti In corso Completato Stato @@ -409,7 +412,9 @@ Disabilitato: %d Non scaricato: %d Aggiornati %d plugin - CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. \n \nUnisciti al nostro Discord o cerca online. + CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. +\n +\nUnisciti al nostro Discord o cerca online. Visualizza i repository della comunità Lista pubblica Tutti i sottotitoli in maiuscolo @@ -497,13 +502,15 @@ Aggiornato (Da vecchio a nuovo) Alfabetico (A - Z) Alfabetico (Z - A) - La tua libreria è vuota :( \nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale. + La tua libreria è vuota :( +\nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale. Seleziona libreria Apri con Libreria Ordina Questo elenco è vuoto. Prova a passare a un altro. - File \"safe mode\" trovato! \nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. + File \"safe mode\" trovato! +\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso. Intervallo di ricerca utilizzato quando il lettore è nascosto TV Android Intervallo di ricerca utilizzato quando il lettore è visibile @@ -527,7 +534,13 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. \n \nFonte A: 3 \nQualità B: 7 \nAvranno una priorità video combinata di 10. \n \nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! + Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. +\n +\nFonte A: 3 +\nQualità B: 7 +\nAvranno una priorità video combinata di 10. +\n +\nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! Profilo %d Wi-Fi Imposta predefinito @@ -547,7 +560,11 @@ %s rimosso dai preferiti Preferiti %s aggiunto ai preferiti - Dei possibili duplicati sono stati trovati nella tua libreria: \n \n%s \n \nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione? + Dei possibili duplicati sono stati trovati nella tua libreria: +\n +\n%s +\n +\nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione? Frequenza di backup Trovato Possibile Duplicato Aggiungi ai preferiti @@ -560,7 +577,9 @@ Iscriviti Rimuovi dai preferiti Seleziona un account - Sembra che nella tua libreria esista già un elemento potenzialmente duplicato: \'%s.\' \n \nDesideri aggiungere comunque questo elemento, sostituire quello esistente o annullare l\'azione? + Sembra che nella tua libreria esista già un elemento potenzialmente duplicato: \'%s.\' +\n +\nDesideri aggiungere comunque questo elemento, sostituire quello esistente o annullare l\'azione? Inserisci PIN PIN Inserisci PIN corrente @@ -590,7 +609,8 @@ Dopo alcuni tentativi falliti, il prompt si chiuderà. Riavvia semplicemente l\'app per riprovare. È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. Non preferito - %s \nresiduo + %s +\nresiduo Preferito Nome e URL del repository copiato! @@ -632,12 +652,20 @@ Seleziona tutto Deseleziona tutto Elimina (%1$d | %2$s) - Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s? \n \n%2$s - Eliminerai definitivamente anche tutti gli episodi delle seguenti serie: \n \n%s - Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie? \n \n%s + Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s? +\n +\n%2$s + Eliminerai definitivamente anche tutti gli episodi delle seguenti serie: +\n +\n%s + Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie? +\n +\n%s Data di rilascio (dal più vecchio) Elimina file - Sei sicuro di voler eliminare definitivamente i seguenti elementi? \n \n%s + Sei sicuro di voler eliminare definitivamente i seguenti elementi? +\n +\n%s Anteprima barra di avanzamento Abilita miniatura di anteprima sulla barra di avanzamento Nessun sottotitolo caricato @@ -756,7 +784,4 @@ Priorità sorgente Decidi come le sorgenti video devono essere ordinate nel lettore Mostra sovrapposizione metadati lettore - Video - Anteprima - Live diff --git a/app/src/main/res/values-b+iw/strings.xml b/app/src/main/res/values-b+iw/strings.xml index 3b2d0153a..0b0479679 100644 --- a/app/src/main/res/values-b+iw/strings.xml +++ b/app/src/main/res/values-b+iw/strings.xml @@ -6,7 +6,8 @@ לשנות ספק מהירות (%.2fx) דירוג: %.1f - נמצא עדכון חדש! \n%1$s -> %2$s + נמצא עדכון חדש! +\n%1$s -> %2$s סינון %d דקות קלאודסטרים @@ -145,8 +146,10 @@ המשך -30 +30 - %dדקות \nנותרו - ‬פעולה זאת תמחק לצמיתות את %s \nהאם אתם בטוחים? + %dדקות +\nנותרו + ‬פעולה זאת תמחק לצמיתות את %s +\nהאם אתם בטוחים? מתמשך משך זמן דירוג @@ -422,8 +425,10 @@ קרדיטים מיין בחר ספרייה - נראה שהספרייה שלכם ריקה :( \nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. - קובץ מצב בטוח נמצא! \nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. + נראה שהספרייה שלכם ריקה :( +\nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם. + קובץ מצב בטוח נמצא! +\nלא טוען שום תוספות בהפעלה עד להסרת הקובץ. לא ניתן להתקין את הגרסה החדשה של האפליקציה הורדת אצווה תוסף @@ -439,7 +444,11 @@ הורד: %d מוגבל: %d לא מורד: %d - לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים. \n \nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה. \n \nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט. + לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים. +\n +\nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה. +\n +\nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט. הצג מאגרים קהילתיים רשימה ציבורית לשים את הכתוביות באותיות רישיות @@ -521,7 +530,13 @@ קביעה כברירת מחדל עבר מעקף ספק אינטרנט - כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. \n \nמקור א: 3 \nאיכות ב: 7 \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! + כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. +\n +\nמקור א: 3 +\nאיכות ב: 7 +\nיגרמו לעדיפות הסרטון להיות 10. +\n +\nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! עונה %1$d פרק %2$d תשודר ב: %1$d שעות %2$d דקות %3$d שניות %1$d דקות %2$d שניות diff --git a/app/src/main/res/values-b+ja/strings.xml b/app/src/main/res/values-b+ja/strings.xml index 3de9a971a..16cfb49c6 100644 --- a/app/src/main/res/values-b+ja/strings.xml +++ b/app/src/main/res/values-b+ja/strings.xml @@ -62,7 +62,8 @@ ローディング… ブラウザで開く シーズン - 残り \n%d分 + 残り +\n%d分 再生エピソード ダウンロード済 バックアップ @@ -81,7 +82,8 @@ 次のランダム 戻り 評価: %.1f - 新しいアップデートを発見! \n%1$s -> %2$s + 新しいアップデートを発見! +\n%1$s -> %2$s %d分 %sを検索… ソース diff --git a/app/src/main/res/values-b+kn/strings.xml b/app/src/main/res/values-b+kn/strings.xml index ba6da787c..22a45b906 100644 --- a/app/src/main/res/values-b+kn/strings.xml +++ b/app/src/main/res/values-b+kn/strings.xml @@ -83,7 +83,8 @@ ಶೇರ್ ಫೈಲ್ ಅಳಿಸಿ ಹೆಚ್ಚಿನ ಮಾಹಿತಿ - ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ \n%1$s-%2$s + ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ +\n%1$s-%2$s ಲೋಡಿಂಗ್… ಡೌನ್‌ಲೋಡ್ ಭಾಷೆಗಳನ್ನು ಮಾಡಿ ಲೈವ್‌ಸ್ಟ್ರೀಮ್ ಪ್ಲೇ ಮಾಡಿ diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 14d327372..9b07d259e 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -11,7 +11,8 @@ 배경 미리보기 속도 (%.2fx) 평점: %.1f - 새로운 업데이트! \n%1$s -> %2$s + 새로운 업데이트! +\n%1$s -> %2$s %d분 CloudStream CloudStream으로 재생 @@ -27,7 +28,7 @@ 공유 브라우저로 열기 브라우저 - 로딩 스킵 + 로딩 건너뛰기 로딩중… 시청 보류 @@ -123,7 +124,7 @@ 어두운 오버레이 대신 앱 플레이어의 시스템 밝기를 사용합니다 시청 진행 상황 업데이트 현재 에피소드 진행 상황을 자동으로 동기화합니다 - 데이터 복원 + 백업에서 데이터 복원 데이터 백업 파일에서 데이터를 복원하지 못했습니다 %s 저장된 데이터 @@ -160,8 +161,10 @@ 평점 +30 - %s이(가) 영구적으로 삭제됩니다 \n정말 삭제하시겠습니까? - %d분 \n남음 + %s가 영구 삭제됩니다 +\n정말 삭제하시겠습니까? + %d분 +\n남음 사이트 시간 개요 @@ -181,7 +184,7 @@ %s로 재생 자동 다운로드 다운로드 소스 목록 - 링크 초기화 + 링크 새로고침 자막 다운로드 화질 라벨 더빙 라벨 @@ -192,7 +195,7 @@ 크기 조정 소스 오프닝 스킵 - 다음에 업데이트 + 이 업데이트 건너뛰기 선호하는 화질 (WiFi) 선호하는 화질 (모바일 데이터) 플레이어 내 표시 정보 @@ -294,7 +297,11 @@ 저장소 삭제 사용하려는 사이트 목록 다운로드 다운로드됨: %d - CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. \n \nDiscord에 가입하거나 온라인으로 검색해 보세요. + CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. +\n +\nSky UK Limited의 무분별한 DMCA 게시 중단으로 인해 앱에서 저장소 사이트를 연결 할 수 없습니다. +\n +\nDiscord에 가입하거나 온라인으로 검색해 보세요. 커뮤니티 저장소 보기 공개 목록 자막 대문자화 표시 @@ -315,7 +322,7 @@ 충돌 정보 보기 언어 에피소드 %d 공개! - Picture-in-picture 모드 + PIP 모드 플레이어 크기 조정 버튼 미니플레이어를 통해 다른 앱 상단에서 계속 재생합니다 레터박스 제거 @@ -324,7 +331,7 @@ 백업 파일을 성공적으로 로드하였습니다 정보 고급 검색 - 설정 프로세스 재실행 + 설정 프로세스 다시 실행 APK 인스톨러 Github 소스 오류 @@ -431,16 +438,16 @@ 먼저 확장 프로그램을 설치하세요 앱을 찾을 수 없음 모든 언어 - %s 스킵 + 건너뛰기 %s 오프닝 엔딩 혼합 엔딩 혼합 오프닝 크레딧 - 인트로 + 소개 기록 삭제 기록 - 오프닝/엔딩 시 스킵 팝업 표시 + 오프닝/엔딩 시 건너뛰기 팝업 표시 텍스트가 너무 많습니다. 클립보드에 저장할 수 없습니다. 시청에서 삭제 정말 종료하시겠습니까? @@ -459,8 +466,10 @@ 알파벳순 (A에서 Z) 알파벳순 (Z에서 A) 다음으로 열기 - 라이브러리가 비어 있습니다 :( \n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요. - 안전 모드 파일을 찾았습니다! \n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다. + 라이브러리가 비어 있습니다 :( +\n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요. + 안전 모드 파일을 찾았습니다! +\n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다. HLS 재생목록 내부 플레이어 선호하는 동영상 플레이어 @@ -476,7 +485,7 @@ @string/home_play 플롯을 찾을 수 없음 설명을 찾을 수 없음 - 로그캣 🐈 보기 + Logcat 🐈 표시 애니메이션용 필러 에피소드 표시 통과 계속 @@ -508,11 +517,11 @@ 보안 계정 리포지토리에서 플러그인을 찾을 수 없습니다 - 복사 완료! + 복사됨! 레포지토리 이름 및 URL 본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다. CloudStream 위키 - 링크 초기화 완료 + 링크 새로고침 완료 백업 빈도 즐겨찾기 QR 이미지 @@ -563,11 +572,17 @@ 기본값 설정 구독 사용 - 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? 전부 대체 추가 즐겨찾기에서 %s 제거 - 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: \n \n%s \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: +\n +\n%s +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? 계정 선택 기본 계정 사용 회전 @@ -584,7 +599,7 @@ 재설정 플러그인 다운로드를 필터링할 모드 선택 CloudStream 데이터 백업이 완료되었습니다. 드문 경우지만, 기기에 따라 앱 접속이 안 되는 오류가 발생할 수 있습니다. 만약 앱이 열리지 않는다면, 앱 데이터를 완전히 삭제(초기화)한 후 이 백업 파일로 복구해 주시기 바랍니다. 이용에 불편을 드려 대단히 죄송합니다. - 스마트폰이나 컴퓨터에서 %s 위의 코드를 입력하세요 + 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 구독 중인 TV 쇼의 알림을 받고 다운로드를 끊김 없이 완료하려면, CloudStream의 백그라운드 실행 권한이 필요합니다. \'확인\'을 누른 후 나타나는 요청 창에서 \'허용\'을 선택해 주세요.\n\n참고로, 이 권한을 허용한다고 해서 배터리가 계속 소모되는 것은 아닙니다. 알림을 받거나 공식 확장 프로그램에서 영상을 다운로드할 때처럼 꼭 필요한 상황에서만 백그라운드 작업을 수행합니다. 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택 화면에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. \n \n참고 A: 3 \n품질 B: 7 \n총 비디오 우선 순위는 10입니다. \n \n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! 시즌 %1$d 에피소드 %2$d 공개 예정 @@ -593,7 +608,8 @@ 추천목록 보기 플레이어에 속도 옵션을 추가합니다 %s 후 공개 예정 - %s \n남음 + %s +\n남음 잠재적 중복 발견 %s의 PIN 입력 즐겨찾기에서 제거 @@ -611,10 +627,18 @@ 로컬 비디오 열기 파일 삭제 삭제 (%1$d | %2$s) - 다음 항목을 영구적으로 삭제 하시겠습니까? \n \n%s - 다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s? \n \n%2$s - 또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다: \n \n%s - 다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까? \n \n%s + 다음 항목을 영구적으로 삭제 하시겠습니까?? +\n +\n%s + 다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s? +\n +\n%2$s + 또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다: +\n +\n%s + 다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까?? +\n +\n%s 공개일 (최신순) 공개일 (오래된순) 플레이어 내 버튼명 숨기기 @@ -730,7 +754,4 @@ 새로고침 최대 밝기 확장 활성화 플레이어에 메타데이터 오버레이 표시 - 비디오 - 프리뷰 - 라이브 diff --git a/app/src/main/res/values-b+lt/strings.xml b/app/src/main/res/values-b+lt/strings.xml index db7e994f0..cb2d816f3 100644 --- a/app/src/main/res/values-b+lt/strings.xml +++ b/app/src/main/res/values-b+lt/strings.xml @@ -31,7 +31,8 @@ +30 Atsiuntimas baigtas Tęsti žiūrėjimą - Rastas atnaujinimas! \n%1$s -> %2$s + Rastas atnaujinimas! +\n%1$s -> %2$s Atsisiųsti kalbas Ieškoti naudojant tiekėjus Grįžti atgal @@ -87,7 +88,8 @@ Pratęsti siuntimą Azijietiškos dramos Serija - Jūsų biblioteka tuščia :( \nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos. + Jūsų biblioteka tuščia :( +\nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos. Pradėti sekančia seriją, kai dabartinė baigsis Teksto spalva Užbaigta @@ -179,7 +181,11 @@ 127.0.0.1 Atsiųsta %1$d %2$s Praleisti %s - Pagal numatytuosius nustatymus „CloudStream“ neturi įdiegtų svetainių. Turite įdiegti svetaines iš saugyklų. \n \nDėl beprotiško DMCA reikalavimų, kurios atliko Sky UK Limited 🤮, negalime susieti saugyklos svetainės programoje. \n \nPrisijunkite prie mūsų Discord arba ieškokite internete. + Pagal numatytuosius nustatymus „CloudStream“ neturi įdiegtų svetainių. Turite įdiegti svetaines iš saugyklų. +\n +\nDėl beprotiško DMCA reikalavimų, kurios atliko Sky UK Limited 🤮, negalime susieti saugyklos svetainės programoje. +\n +\nPrisijunkite prie mūsų Discord arba ieškokite internete. Mobilūs duomenys šaunusPrisijungimoVardas Autoriai diff --git a/app/src/main/res/values-b+lv/strings.xml b/app/src/main/res/values-b+lv/strings.xml index 9ab29cd43..7d74354ea 100644 --- a/app/src/main/res/values-b+lv/strings.xml +++ b/app/src/main/res/values-b+lv/strings.xml @@ -12,7 +12,8 @@ Apskatīt background Ātrums (%.2fx) Lidzīgi: %.1f - Jauns atjauninājums atrasts! \n%1$s -> %2$s + Jauns atjauninājums atrasts! +\n%1$s -> %2$s %d galvenais CloudStream Atskaņo ar cloudstream @@ -192,8 +193,10 @@ Atsākt -30 +30 - Šis pilnibā dzesīs %s \nEsat parliecināts? - %dm \natlikušas + Šis pilnibā dzesīs %s +\nEsat parliecināts? + %dm +\natlikušas Pabeigts Statuss Gads @@ -455,7 +458,8 @@ Alfabētiskā secībā (Z līdz A) Atlasiet Bibliotēka Atvērt ar - Šķiet, ka jūsu bibliotēka ir tukša :( \nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai. + Šķiet, ka jūsu bibliotēka ir tukša :( +\nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai. Atgriest Anulēts %s abonements %d sērija izlaista! diff --git a/app/src/main/res/values-b+mk/strings.xml b/app/src/main/res/values-b+mk/strings.xml index 4e37afdea..99ee9c2ac 100644 --- a/app/src/main/res/values-b+mk/strings.xml +++ b/app/src/main/res/values-b+mk/strings.xml @@ -126,7 +126,8 @@ Откажи Паузирај Продолжи - Ова трајно ќе го избрише %s \nДали си сигурен? + Ова трајно ќе го избрише %s +\nДали си сигурен? Во тек Изгледанo Статус @@ -244,7 +245,7 @@ TC Претплатен на %s Преводи - Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! + Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив! Недостасуваат дозволи за складирање. Обиди се повторно. Зачувај Вчитај од датотека @@ -416,7 +417,8 @@ Почна да презема %1$d %2$s… Автоматски ажурирања на приклучоци -30 - %dm \nпреостанува + %dm +\nпреостанува Видео кеш на дискот https://example.com/example.mp4 Готово @@ -445,7 +447,7 @@ Грешка при правење резервна копија на %s Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат. - Прикажи информации за плеерот + Резолуција на видео плеер Големина на видео баферот Распоред Стандардно @@ -585,7 +587,8 @@ По неколку неуспешни обиди, известувањето ќе се затвори. Едноставно вклучи ја апликацијата повторно за да се обидеш повторно. Медиуми Претстои во %s - %s \nпреостанати + %s +\nпреостанати За да се обезбедат непрекинати преземања и известувања за претплатените ТВ серии, CloudStream треба дозвола за работа во позадина. Со притискање на „ОК“, ќе ви биде прикажан дијалог за барање дозвола. Ве молиме, притиснете „Дозволи“.\n\nИмајте предвид дека оваа дозвола не значи дека CS3 ќе ја троши вашата батерија. Ќе работи во позадина само кога е потребно, како на пример при примање известувања или преземање видеа од официјални екстензии. Грешка при пристапот до таблата со исечоци, обиди се повторно. Грешка при копирање, молам копирај го логот и контактирај ја поддршката на апликацијата. @@ -705,37 +708,4 @@ Горе во центар Горе на десно Пушти ја целата серија - Редица за преземање - Моментално нема преземања во редицата. - Дополнителна осветленост - Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот - овозможена_дополнителна_осветленост - Предлози за пребарување - Прикажувај предлози за пребарување додека пишуваш - Исчисти предлози - Прикажи преклоп со метаподатоци на плеерот - Прикажи панел за емитување - Инсталирај предиздавачка верзија - Предиздавачката верзија е веќе инсталирана. - Неуспешна инсталација на предиздавачката верзија. - Видео - Текст на епизода - Информации за медиумот - Преглед - Приоритет на извор - Одреди како ќе се подредуваат видео изворите во плеерот - Име на изворот - Преземи сѐ - Откажи сѐ - Дали сакате да ја преземете епизодата %s? - Дали сакате да ги откажете сите преземања во редицата? - - %d активно преземање - %d активни преземања - - - %d преземање во редицата - %d преземања во редицата - - Во живо diff --git a/app/src/main/res/values-b+ml/strings.xml b/app/src/main/res/values-b+ml/strings.xml index c2b25c5ee..d1c9409a3 100644 --- a/app/src/main/res/values-b+ml/strings.xml +++ b/app/src/main/res/values-b+ml/strings.xml @@ -3,7 +3,8 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s + പുതിയ അപ്ഡേറ്റ്! +\n%1$s -> %2$s ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക @@ -114,7 +115,8 @@ റദ്ദാക്കുക നിർത്തുക തുടരുക - സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക \nഉറപ്പാണോ? + സ്ഥിരമായി %sനെ ഡിലീറ്റ് ചെയ്യുക +\nഉറപ്പാണോ? തുടരുന്നു പൂർത്തിയായി അവസ്ഥ @@ -190,7 +192,9 @@ %s ൽ ഫോൻ്റ്‌സ് വെച്ചു കൊണ്ട് ഇംപോർട്ട് ചെയ്യുക പ്രശ്‌നമുണ്ടാക്കുന്ന ഒന്ന് കണ്ടെത്താൻ നിങ്ങളെ സഹായിക്കുന്നതിന് ഒരു ക്രാഷ് കാരണം എല്ലാ വിപുലീകരണങ്ങളും ഓഫാക്കി. പൊതു പട്ടിക - CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. + CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. +\n +\nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക റെൻഡറർ പിശക് @@ -257,7 +261,8 @@ ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. ചരിത്രം മായ്ക്കുക ലോഗ്കാറ്റ് കാണിക്കുക 🐈 - നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( \nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. വീഡിയോ റിപ്പോസിറ്ററി നാമവും URL ഉം പകർത്തി! diff --git a/app/src/main/res/values-b+ms/strings.xml b/app/src/main/res/values-b+ms/strings.xml index a15759939..dc1cd3ee6 100644 --- a/app/src/main/res/values-b+ms/strings.xml +++ b/app/src/main/res/values-b+ms/strings.xml @@ -25,8 +25,8 @@ Episod %d akan disiarkan dalam Pelakon:%s Mod Selamat Hidup - %1$dh %2$dj %3$dm - %1$dj %2$dm + %1$dd %2$dh %3$dm + %1$dh %2$dm %dm Poster Episod Poster Utama @@ -56,7 +56,8 @@ Tutup Pratonton Resensi:%.1f - Kemas kini baru dijumpai! \n%1$s -> %2$s + Kemas kini baru dijumpai! +\n%1$s -> %2$s %d min Main dari mula Musim %1$d Episod %2$d akan dikeluarkan di @@ -485,7 +486,7 @@ Kemaskini dan sandaran Ketik dua kali untuk mencari Gunakan kecerahan sistem dalam pemain apl dan bukannya tindanan gelap - %1$dj %2$dm %3$ds + %1$dh %2$dm %3$ds %1$dm %2$ds %1$ds Pengecaman pertuturan tidak tersedia diff --git a/app/src/main/res/values-b+mt/strings.xml b/app/src/main/res/values-b+mt/strings.xml index 8f8bf6cd7..ea859ee29 100644 --- a/app/src/main/res/values-b+mt/strings.xml +++ b/app/src/main/res/values-b+mt/strings.xml @@ -18,7 +18,8 @@ Ibdel Il-fornitur veloċità (%.2fx) Klassifikazzjoni: %.1f - Aġġornament ġdid misjub! \n%1$s -> %2$s + Aġġornament ġdid misjub! +\n%1$s -> %2$s %d min CloudStream Ara bil-CloudStream diff --git a/app/src/main/res/values-b+my/strings.xml b/app/src/main/res/values-b+my/strings.xml index d360d095d..0938e4f98 100644 --- a/app/src/main/res/values-b+my/strings.xml +++ b/app/src/main/res/values-b+my/strings.xml @@ -11,7 +11,8 @@ နောက်သို့ နောက်ခံပုံရိပ်ကို အကြိုကြည့်ရန် အဆင့်: %.1f - အပ်ဒိတ်အသစ်! \n%1$s -> %2$s + အပ်ဒိတ်အသစ်! +\n%1$s -> %2$s စစ်ထုတ်မှု %d မိနစ် CloudStream @@ -110,8 +111,10 @@ အပိုင်း အပိုင်းများ %1$d-%2$d - ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s \nသင်သေချာပါသလား။ - %dမိနစ် \nကျန်ရိှသည် + ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s +\nသင်သေချာပါသလား။ + %dမိနစ် +\nကျန်ရိှသည် ထုတ်လွှင့်နေဆဲ ထုတ်လွှင့်မှုပြီးဆုံး အခြေအနေ @@ -339,7 +342,11 @@ သမားရိုးကျ ထည့်သွင်းသူ ထွက်ချိန်တွင် အက်ပ်ကို အပ်ဒိတ်လုပ်ပါမည် - CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ \n \nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ \n \nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ + CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ +\n +\nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ +\n +\nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ အခြားသူများ၏ရီပိုစစ်ထရီများကိုရှာဖွေမည် အသံများ အသံဖိုင်များ @@ -415,7 +422,8 @@ အက်ပ်အပ်ဒိတ်အားဒေါင်းလုဒ်လုပ်နေသည်… အက်ပ်အပ်ဒိတ်အားသွင်းနေသည်… အပ်ဒိတ်ဖြစ်မှု (အသစ် မှ အဟောင်း) - သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( \nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ + သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( +\nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ သုံးရန် တည်းဖြတ်ရန် အရည်အသွေးများ @@ -508,7 +516,8 @@ စာရင်းသွင်းပြီး %s စာရင်းသွင်းမှုပယ်ဖျက်ပြီး %s ဤစာရင်းသည် ဗလာဖြစ်နေသည်။ အခြားတစ်ခုသို့ ပြောင်းကြည့်ပါ။ - Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ \nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ + Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ +\nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ အပိုင်းသစ် %d ထွက်ပြီ ပရိုဖိုင် %d ဝိုင်ဖိုင် @@ -516,7 +525,13 @@ ပုံသေထားရန် ပရိုဖိုင်များ အကူအညီ - ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ \n \nအရင်းအမြစ် A: 3 \nအရည်အသွေး B: 7 \nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ \n \nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် + ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ +\n +\nအရင်းအမြစ် A: 3 +\nအရည်အသွေး B: 7 +\nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ +\n +\nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် ပရိုဖိုင်နောက်ခံ UI ကို မှန်ကန်စွာ ဖန်တီး၍မရပါ၊ ၎င်းသည် အဓိက ချို့ယွင်းချက်တစ်ခုဖြစ်ပြီး ချက်ချင်းသတင်းပို့သင့်သည်။ %s သင်နဂိုတည်းကသတ်မှတ်ပြီး diff --git a/app/src/main/res/values-b+ne/strings.xml b/app/src/main/res/values-b+ne/strings.xml index 49e5a9350..8a432a505 100644 --- a/app/src/main/res/values-b+ne/strings.xml +++ b/app/src/main/res/values-b+ne/strings.xml @@ -15,7 +15,8 @@ मुख्य पोस्टर %1$s Ep %2$d अभिनेता:%s - नयाँ अपडेट भेटियो! \n%1$s -> %2$s + नयाँ अपडेट भेटियो! +\n%1$s -> %2$s फिलर %d मिनेट क्लाउडस्ट्रीम diff --git a/app/src/main/res/values-b+nl/strings.xml b/app/src/main/res/values-b+nl/strings.xml index 3164e6b4e..e3bbee121 100644 --- a/app/src/main/res/values-b+nl/strings.xml +++ b/app/src/main/res/values-b+nl/strings.xml @@ -19,7 +19,8 @@ Snelheid (%.2fx) Beoordeeld: %.1fAls - Nieuwe update gevonden! \n%1$s -> %2$s + Nieuwe update gevonden! +\n%1$s -> %2$s Filler %d min CloudStream @@ -181,8 +182,10 @@ Hervatten -30 +30 - Dit wordt zeker permanent verwijderd %s \nWeet u het zeker? - %dm \nremaining + Dit wordt zeker permanent verwijderd %s +\nWeet u het zeker? + %dm +\nremaining Voortdurende Voltooid Status @@ -481,7 +484,8 @@ Verwijderen uit bekeken App wordt bijgewerkt bij afsluiten Gesorteerd - Je bibliotheek is leeg :( \nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek. + Je bibliotheek is leeg :( +\nLog in op een bibliotheekaccount of voeg voorstellingen toe aan uw lokale bibliotheek. Uitgeschakeld: %d Stop Niet gedownload: %d @@ -503,7 +507,8 @@ %s ( Uitgeschakeld) Herstart de app om veranderingen te zien. Gedownload: %d - Veilige mode bestand gevonden! \nGeen extensies laden bij het opstarten totdat het bestand is verwijderd. + Veilige mode bestand gevonden! +\nGeen extensies laden bij het opstarten totdat het bestand is verwijderd. Nee Beoordeling ( Hoog naar Laag) Veilige mode aan @@ -515,7 +520,11 @@ Wis repository Uitgeschreven bij %s Terugkeren - CloudStream heeft standaard geen sites geïnstalleerd. U moet de sites uit repositories installeren. \n \nVanwege een hersenloze DMCA verwijdering door Sky UK Limited 🤮 kunnen we de repository site niet linken in de app. \n \nWord lid van onze Discord of zoek online. + CloudStream heeft standaard geen sites geïnstalleerd. U moet de sites uit repositories installeren. +\n +\nVanwege een hersenloze DMCA verwijdering door Sky UK Limited 🤮 kunnen we de repository site niet linken in de app. +\n +\nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op Wifi @@ -527,7 +536,13 @@ Kwaliteiten Profiel achtergrond Gebruik - Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. \n \nBron A: 3 \nKwaliteit B: 7 \nHeeft een totale prioriteit van de video van 10. \n \nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! + Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. +\n +\nBron A: 3 +\nKwaliteit B: 7 +\nHeeft een totale prioriteit van de video van 10. +\n +\nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! Profiel %d Repository niet gevonden, controleer de URL en probeer een VPN Geen plug-ins gevonden in de repository @@ -539,7 +554,11 @@ Favorieten %s toegevoegd aan favorieten Aangemeld als %s - Er zijn mogelijk dubbele items gevonden in uw bibliotheek: \n \n%s \n \nWilt u dit item toch toevoegen, de bestaande vervangen of de actie annuleren? + Er zijn mogelijk dubbele items gevonden in uw bibliotheek: +\n +\n%s +\n +\nWilt u dit item toch toevoegen, de bestaande vervangen of de actie annuleren? Voer PIN in voor %s Backupfrequentie Mogelijk Duplicaat Gevonden @@ -558,7 +577,9 @@ Abonneer Verwijder uit favorieten Selecteer een Account - Het lijkt erop dat er al een mogelijk duplicaat bestaat in uw bibliotheek: \'%s.\' \n \nWilt u dit item toch toevoegen, het bestaande item vervangen of de actie annuleren? + Het lijkt erop dat er al een mogelijk duplicaat bestaat in uw bibliotheek: \'%s.\' +\n +\nWilt u dit item toch toevoegen, het bestaande item vervangen of de actie annuleren? PIN invoeren PIN Huidige PIN invoeren @@ -661,20 +682,4 @@ Fout bij toegang tot het Klembord, Probeer het opnieuw. Fout bij het kopiëren. Kopieer alsjeblieft de logcat en neem contact op met de app-ondersteuning. Afwijzen - Video - Voorbeeld - Bron Prioriteit - Bepaal hoe de videobronnen worden gesorteerd in de speler - Ontgrendel CloudStream - Versleutel met Biometrie - Reset - verschijningsdatum (Nieuw naar Oud) - verschijningsdatum (Oud naar Nieuw) - Verberg de namen van de besturingselementen van de speler - Ondertiteling nog niet geladen - Back-up folder locatie - Aangepast - Bevestig voor afsluiten - Toon dialoogvenster voordat de app wordt afgesloten - Randgrote diff --git a/app/src/main/res/values-b+nn/strings.xml b/app/src/main/res/values-b+nn/strings.xml index b29e142b0..245bf6618 100644 --- a/app/src/main/res/values-b+nn/strings.xml +++ b/app/src/main/res/values-b+nn/strings.xml @@ -15,7 +15,8 @@ Fyllstoff Forhandsvis bakgrunnsbilete Vurdert: %.1f - Ny oppdatering tilgjengeleg! \n%1$s -> %2$s + Ny oppdatering tilgjengeleg! +\n%1$s -> %2$s %d minutt Miniatyrbilete Episode %d vil bli sleppt om @@ -114,8 +115,10 @@ Gjenoppta -30 +30 - Dette vil slette %s permanent. \nEr du sikker på dette? - %dm \ngjenstår + Dette vil slette %s permanent. +\nEr du sikker på dette? + %dm +\ngjenstår Pågåande Fullført År @@ -188,14 +191,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - Sesong %1$d Episode %2$d blir sleppt om - Spel av frå start - Nedlastingskø - Semmegjenkjenning er ikkje tilgjengeleg - Snakk no… - Nettlesar - Fjerna - Strøm Torrent - Spel heile serien - Denne filmen er ein Torrent, som betyr at bruken din kan bli spora\nSett deg inn i bruk av Torrent-resursar før du fortsetter. diff --git a/app/src/main/res/values-b+no/strings.xml b/app/src/main/res/values-b+no/strings.xml index 7d309a2f8..55b5303eb 100644 --- a/app/src/main/res/values-b+no/strings.xml +++ b/app/src/main/res/values-b+no/strings.xml @@ -12,7 +12,8 @@ Avspillingshastighet (%.2fx) Vurdert: %.1f - Ny oppdatering funnet! \n%1$s -> %2$s + Ny oppdatering funnet! +\n%1$s -> %2$s CloudStream Hjem Søk @@ -134,7 +135,8 @@ Avbryt Stopp Gjenoppta - Dette vil slette %s \nEr du sikker? + Dette vil slette %s +\nEr du sikker? Pågående Fullført Posisjon @@ -351,7 +353,8 @@ %1$d-%2$d Offentlig liste programtillegg - %dm \nigjen + %dm +\nigjen Videooppløsning Synkroniser undertekster Undertekstforsinkelse @@ -370,7 +373,11 @@ Ugyldig nettadresse Knippe-nedlasting Slett pakkebrønn - CloudStream har ingen sider installert som forvalg. Du må installere sidene fra pakkebrønner. \n \nSom følge av en hjernedød DMCA-forespørsel fra Sky UK Limited 🤮 kan vi ikke lenke til pakkebrønnssiden i programmet. \n \nTa del i vår Discord, eller søk på nett. + CloudStream har ingen sider installert som forvalg. Du må installere sidene fra pakkebrønner. +\n +\nSom følge av en hjernedød DMCA-forespørsel fra Sky UK Limited 🤮 kan vi ikke lenke til pakkebrønnssiden i programmet. +\n +\nTa del i vår Discord, eller søk på nett. Bruk dette hvis undertekster vises %d ms for sent Programtillegg innlastet Lydspor @@ -505,8 +512,10 @@ ISP-omgåelser Denne listen er tom. Prøv å bytte til en annen. Sorter - Fant fil for trygt modus. \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. - Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. + Fant fil for trygt modus. +\nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. + Biblioteket ditt er tomt :( +\nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. Rediger Profiler Favoritter diff --git a/app/src/main/res/values-b+or/strings.xml b/app/src/main/res/values-b+or/strings.xml index ce7f74290..40a2915fd 100644 --- a/app/src/main/res/values-b+or/strings.xml +++ b/app/src/main/res/values-b+or/strings.xml @@ -84,7 +84,8 @@ ବ୍ୟାକଅପ୍ ଆଣ୍ଡ୍ରଏଡ୍ ଟିଵି ଅଙ୍ଗଭଙ୍ଗୀ - ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! \n%1$s -> %2$s + ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! +\n%1$s -> %2$s ଅଵଧି ଆପ୍ ବ୍ୟାକଅପ୍ ଫାଇଲ୍ ଧାରଣ ହେଲା diff --git a/app/src/main/res/values-b+pl/strings.xml b/app/src/main/res/values-b+pl/strings.xml index 35367a1b2..d1b1cf1c5 100644 --- a/app/src/main/res/values-b+pl/strings.xml +++ b/app/src/main/res/values-b+pl/strings.xml @@ -2,7 +2,8 @@ Prędkość (%.2fx) Ocena: %.1f - Znaleziono nową aktualizację! \n%1$s -> %2$s + Znaleziono nową aktualizację! +\n%1$s -> %2$s Filler %d min %1$s Odc. %2$d @@ -176,8 +177,10 @@ Odtwórz -30 +30 - Spowoduje to trwałe usunięcie %s \nCzy jesteś pewien? - %dm \npozostało + Spowoduje to trwałe usunięcie %s +\nCzy jesteś pewien? + %dm +\npozostało Bieżący Zakończone Status @@ -243,7 +246,7 @@ Aktualizacja Domyślna jakość (WiFi) Maksymalna liczba znaków w tytule odtwarzacza - Pokaż informacje o odtwarzaczu + Pokaż informacje o odtwarzaczu Rozmiar bufora wideo Długość bufora wideo Pamięć podręczna wideo na dysku @@ -382,7 +385,9 @@ Wyłączono: %d Nie pobrano: %d Zaaktualizowano %d rozszerzeń - CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. \n \nDołącz do naszego Discorda lub poszukaj online. + CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. +\n +\nDołącz do naszego Discorda lub poszukaj online. Zobacz repozytoria społeczności Publiczna lista Wszystkie napisy wielką literą @@ -482,9 +487,11 @@ Alfabetycznie (od Z do A) Wybierz bibliotekę Biblioteka - Twoja biblioteka jest pusta :( \nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki. + Twoja biblioteka jest pusta :( +\nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki. Ta lista jest pusta. Spróbuj przełączyć się na inną. - Znaleziono plik trybu bezpiecznego. \nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. + Znaleziono plik trybu bezpiecznego. +\nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty. Używana ilość przewijania, gdy widoczny jest odtwarzacz Ukryty odtwarzacz - ilość przewijania Android TV @@ -508,7 +515,13 @@ Obchodzi blokadę surowych adresów URL GitHuba za pomocą jsDelivr. Może powodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHubem. Włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) - W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. \n \nŹródło A: 3 \nJakość B: 7 \nŁączny priorytet wideo będzie wynosił 10. \n \nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! + W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. +\n +\nŹródło A: 3 +\nJakość B: 7 +\nŁączny priorytet wideo będzie wynosił 10. +\n +\nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! Profil %d Wi-Fi Dane mobilne @@ -528,7 +541,11 @@ Usunięto %s z ulubionych Ulubione Dodano %s do ulubionych - W swojej bibliotece znaleziono potencjalne duplikaty: \n \n%s \n \nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? + W swojej bibliotece znaleziono potencjalne duplikaty: +\n +\n%s +\n +\nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? Wprowadź PIN dla %s Częstotliwość tworzenia kopii zapasowych Znaleziono potencjalny duplikat @@ -545,7 +562,9 @@ Zasubskrybuj Usuń z ulubionych Wybierz konto - Wygląda się, że potencjalny duplikat już znajduje się w bibliotece: \'%s\'. \' \n \nCzy chciałbyś dodać ten element, zastąpić istniejący, czy anulować akcję? + Wygląda się, że potencjalny duplikat już znajduje się w bibliotece: \'%s\'. \' +\n +\nCzy chciałbyś dodać ten element, zastąpić istniejący, czy anulować akcję? Wprowadź PIN PIN Linki załadowane ponownie @@ -571,7 +590,8 @@ Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. Kopia zapasowa Twoich danych CloudStream została teraz utworzona. Chociaż prawdopodobieństwo tego jest bardzo niskie, wszystkie urządzenia mogą zachowywać się inaczej. W rzadkich przypadkach, gdy dostęp do aplikacji zostanie zablokowany, należy całkowicie wyczyścić dane aplikacji i przywrócić je z kopii zapasowej. Bardzo nam przykro z powodu wszelkich niedogodności z tym związanych. Usuń z ulubionych - %s \npozostało + %s +\npozostało Dodaj do ulubionych Nazwa repozytorium i adres URL Błąd dostępu do schowka. Spróbuj ponownie. @@ -613,11 +633,19 @@ Zaznacz wszystkie Zaznacz elementy do usunięcia Odznacz wszystkie - Czy na pewno chcesz na stałe usunąć następujące elementy? \n \n%s - Usuniesz na stale wszystkie odcinki następującego serialu: \n \n%s - Czy na pewno chcesz na stałe usunąć wszystkie odcinki następującego serialu? \n \n%s + Czy na pewno chcesz na stałe usunąć następujące elementy? +\n +\n%s + Usuniesz na stale wszystkie odcinki następującego serialu: +\n +\n%s + Czy na pewno chcesz na stałe usunąć wszystkie odcinki następującego serialu? +\n +\n%s Usuń pliki - Czy na pewno chcesz na stałe usunąć następujące odcinki %1$s? \n \n%2$s + Czy na pewno chcesz na stałe usunąć następujące odcinki %1$s? +\n +\n%2$s Usuń (%1$d | %2$s) Podgląd paska przewijania Włącz podgląd miniatury na pasku wyszukiwania @@ -693,7 +721,7 @@ Przeładuj dostawcę Odtwarzaj inne źródło" Nazwa - Rozdzielczość i nazwa + Rozdzielczość i nazwa Dolne lewe Wyrównanie napisów Dolne środkowe @@ -714,16 +742,16 @@ Wyczyść sugestie Pokaż panel obsady Nazwa źródła - Informacje o multimediach + Informacje o multimediach Dodatkowa jasność Włącz filtr jasności, gdy jasność wyświetlacza przekroczy 100% Włączono dodatkową jasność Kolejka pobierania - Obecnie nie ma żadnych plików do pobrania w kolejce. + Obecnie nie ma żadnych plików do pobrania w kolejce. Pobierz wszystkie Anuluj wszystkie Czy chcesz pobrać odcinek %s? - Czy chcesz anulować wszystkie pliki do pobrania z kolejki? + Czy chcesz anulować wszystkie pliki do pobrania z kolejki? %d aktywne pobieranie %d aktywne pobierania @@ -731,15 +759,12 @@ %d aktywnych pobierań - %d pobieranie w kolejce - %d pobierania w kolejce - %d pobierań w kolejce - %d pobierań w kolejce + %d pobieranie w kolejce + %d pobierania w kolejce + %d pobierań w kolejce + %d pobierań w kolejce Priorytet źródła - Zdecyduj, jak mają być sortowane źródła wideo w odtwarzaczu + Zdecyduj, jak mają być sortowane źródła wideo w odtwarzaczu Pokaż nakładkę metadanych odtwarzacza - Wideo - Zapowiedź - Na żywo diff --git a/app/src/main/res/values-b+pt+BR/strings.xml b/app/src/main/res/values-b+pt+BR/strings.xml index 455478b5a..b856246e8 100644 --- a/app/src/main/res/values-b+pt+BR/strings.xml +++ b/app/src/main/res/values-b+pt+BR/strings.xml @@ -180,7 +180,8 @@ Continuar -30 +30 - Isso apagará %s permanentemente \nVocê tem certeza? + Isso apagará %s permanentemente +\nVocê tem certeza? %dm\nrestantes Em andamento Concluído @@ -395,7 +396,9 @@ Transferido: %d Desativado: %d Não transferido: %d - CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. \n \nEntre no nosso Discord ou pesquise online. + CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. +\n +\nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -438,7 +441,8 @@ Abrir com Selecionar Biblioteca Passou nos testes - Sua biblioteca está vazia :0 \nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. + Sua biblioteca está vazia :0 +\nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. Qualidade preferida de reprodução (Dados Móveis) Legado Biblioteca @@ -456,8 +460,15 @@ Alfabética(Z => A) Qualidade Perfil de plano de fundo - Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. \n \nFonte A: 3 \nQualidade B: 7 \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! - Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. + Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! + Arquivo de modo de segurança encontrado! +\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. Inscrito em %s Episódio %d lançado! Selecionar padrão @@ -542,7 +553,11 @@ Duplicata em potencial encontrada Adicionar Substituir - Possíveis itens duplicados foram encontrados em sua biblioteca: \n \n %s \n \nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? + Possíveis itens duplicados foram encontrados em sua biblioteca: +\n +\n %s +\n +\nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? Insira o PIN Insira o PIN para %s Insira o PIN atual @@ -561,7 +576,9 @@ Links recarregados Frequência de backup Substitua tudo - Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' \n \nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? Inscrever-se Cancelar inscrição Usar conta padrão @@ -582,7 +599,8 @@ A autenticação biométrica não é compatível com este dispositivo Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. Após algumas tentativas fracassadas, o prompt será fechado. Basta reiniciar o aplicativo para tentar novamente. - %s \nrestante(s) + %s +\nrestante(s) Favorito Não favorito copiado! @@ -616,7 +634,7 @@ Reproduzir do começo Reprovou alguns testes Excluir plugin - Atualmente não há downloads disponíveis. + Você não baixou nada :/ Ocultar os nomes dos controles do player Abrir arquivo de vídeo Data de lançamento (do novo ao antigo) @@ -736,7 +754,7 @@ Nome da fonte Baixar tudo Cancelar tudo - Você deseja baixar o episódio%s? + Você deseja baixar o episódio%s Você gostaria de cancelar todos os downloads da fila? %ddownload ativo @@ -748,8 +766,4 @@ %d downloads na sequência %d downloads na sequência - Mostrar sobreposição de metadados do reprodutor - Vídeo - Visualização - Ao vivo diff --git a/app/src/main/res/values-b+pt/strings.xml b/app/src/main/res/values-b+pt/strings.xml index 960a91d0d..f9dd3083b 100644 --- a/app/src/main/res/values-b+pt/strings.xml +++ b/app/src/main/res/values-b+pt/strings.xml @@ -18,7 +18,8 @@ Visualizar plano de fundo Velocidade (%.2fx) Classificado: %.1f - Nova atualização encontrada! \n%1$s -> %2$s + Nova atualização encontrada! +\n%1$s -> %2$s Preenchimento CloudStream Assistir com o CloudStream @@ -61,7 +62,7 @@ Transmitir Erro a Carregar Links Armazenamento Interno - Dub + Dob Leg Eliminar Ficheiro Reproduzir Ficheiro @@ -100,7 +101,7 @@ Importar fontes colocando em %s Continuar a Assistir Remover - Mais informações + Mais info Uma VPN pode ser necessária para que este fornecedor funcione corretamente Este fornecedor é um torrent, uma VPN é recomendada Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site. @@ -142,7 +143,7 @@ Procurar Contas e segurança Atualizações e cópias de segurança - Informações + Info Procura Avançada Mostra resultados separados por fornecedor Mostrar episódios de enchimento para anime @@ -175,8 +176,10 @@ Cancelar Pôr em Pausa Retomar - Isto apagará %s permanentemente \nTem a certeza? - %dm \nem falta + Isto apagará %s permanentemente +\nTem a certeza? + %dm +\nem falta Em Curso Concluído Estado @@ -318,9 +321,9 @@ Carregar de arquivo Carregar da Internet Arquivo baixado - Principal - Suporte - Plano de fundo + Protagonista + Coadjuvante + Figurante Aleatório Em breve… Imagem de Poster @@ -361,7 +364,9 @@ Transferido: %d Desativado: %d Não transferido: %d - O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. \n \nJunte-se ao nosso Discord ou pesquise online. + O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. +\n +\nJunte-se ao nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -469,8 +474,10 @@ Atualizando shows inscritos Alfabético (A a Z) Avaliações (Crescente) - A sua biblioteca está vazia :( \nEntre numa conta da biblioteca ou adicione espectáculos à sua biblioteca local. - Arquivo de modo de segurança encontrado! \nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. + A sua biblioteca está vazia :( +\nEntre numa conta da biblioteca ou adicione espectáculos à sua biblioteca local. + Arquivo de modo de segurança encontrado! +\nNenhuma extensão será carregada na inicialização do app até que o arquivo seja removido. Contorno do provedor de serviço de internet (ISP) Links Recursos do Player @@ -515,7 +522,13 @@ Ajuda Qualidades Perfil de fundo - Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. \n \nFonte A: 3 \nQualidade B: 7 \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! + Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! Selecionar o modo para filtrar a transferência de plug-ins Não foi possível criar corretamente a interface do utilizador, trata-se de um GRANDE BUG e deve ser comunicado imediatamente %s Desativar @@ -523,7 +536,7 @@ Repositório não encontrado, verifique o URL e tente a VPN Você já votou Cancelar Inscrição - Inscrever-se + Subscrever Favoritos A recarregar links Frequência de Backup @@ -547,8 +560,14 @@ Selecione uma conta Gerenciar contas Usar conta padrão - Potenciais itens duplicados foram encontrados na sua biblioteca: \n \n%s \n \nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? - Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' \n \nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? + Potenciais itens duplicados foram encontrados na sua biblioteca: +\n +\n%s +\n +\nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? Mostrar recomendações Adiciona uma opção de velocidade no leitor Testar todas as extensões @@ -565,7 +584,8 @@ Desfavorito Bloqueio com biometria copiado! - %s \nrestante + %s +\nrestante Erro ao aceder à área de transferência, tente novamente. Erro ao copiar, copie o logcat e contacte o suporte da aplicação. Desbloquear o CloudStream @@ -591,7 +611,9 @@ Pré-visualização na barra de progresso Ativar a miniatura de pré-visualização na barra de progresso Autenticação Local - Irá também eliminar permanentemente todos os episódios da seguinte série: \n \n%s + Irá também eliminar permanentemente todos os episódios da seguinte série: +\n +\n%s Eliminar (%1$d | %2$s) Visite %s no seu smartphone ou computador e introduza o código acima Recomeçar @@ -604,9 +626,15 @@ Próximo em %s Eliminar Ficheiros Aviso - Tem a certeza que pretende eliminar permanentemente os seguintes items? \n \n%s - Tem a certeza que pretende eliminar permanentemente os seguintes episódios em %1$s? \n \n%2$s - Tem a certeza que pretende eliminar permanentemente todos os episódios da seguinte série? \n \n%s + Tem a certeza que pretende eliminar permanentemente os seguintes items? +\n +\n%s + Tem a certeza que pretende eliminar permanentemente os seguintes episódios em %1$s? +\n +\n%2$s + Tem a certeza que pretende eliminar permanentemente todos os episódios da seguinte série? +\n +\n%s Segurança Contas QR Code @@ -686,7 +714,7 @@ Imagem Atualizada com Sucesso Marcar como assistido o episódio Removar marcação de assistido até esse episódio - Recarregar + Recarregado Provedor de Recarregamento Reproduzir do servidor alternativo" Nome @@ -733,8 +761,4 @@ %d downloads na fila %d downloads na fila - Mostrar sobreposição de metadados do player - Vídeo - Pré-visualização - Ao Vivo diff --git a/app/src/main/res/values-b+qt/strings.xml b/app/src/main/res/values-b+qt/strings.xml index 6bbb0ddba..bbe145842 100644 --- a/app/src/main/res/values-b+qt/strings.xml +++ b/app/src/main/res/values-b+qt/strings.xml @@ -185,7 +185,8 @@ u ooah uo ahauao huhuu hauu h a ou oh ouhuouhoaaha aaooohhouhhha hauauuu - aaaaaaa uuuuuu \n%1$s -> %2$s + aaaaaaa uuuuuu +\n%1$s -> %2$s %1$s aaou %2$d oouaaahh %s aaaaaaugh ouh %d uuoogahaaah ooua-h-ha @@ -227,7 +228,8 @@ aaaaaaaaaaahhhgh-aooohoooo aau aooooghaao aagh aaaaaaaaaaaa oooh, aaough, ooga oguuu aaaaaaaaaaa ooooooohghh a-a-aaauo - %dmmmmmm.. \naaaaooughugh + %dmmmmmm.. +\naaaaooughugh aooohuohaaaa ooooagh oooooogh-aaaaaogh guuuaaaahhhhhhhaaa @@ -276,7 +278,9 @@ aaaagg uug oooogg oooogg - oooohhhoogg uuh uh uuuhh aaaaggguh og ooooggg uug aagg ek aaaaggg oog aaahh aagg uuuugggooohh \n \nJoin uuh uuuuggg ag uuuuhh eeeeek + oooohhhoogg uuh uh uuuhh aaaaggguh og ooooggg uug aagg ek aaaaggg oog aaahh aagg uuuugggooohh +\n +\nJoin uuh uuuuggg ag uuuuhh eeeeek uuugg aaaagg oogg uugg uh aaagg @@ -309,7 +313,13 @@ ooooggg %d aahh oooogggk - eeek aag uug uuuuhh ooh aak uuuuggg ooh uuuuhhh ug g eeeek oog h uuuuhh oooogggh ag aahh oooohh aaaagg uh oog uuuugg uuuugggog uug oog uh uuh aaaagg uuuuuukg uug aah uuuuuuk uuuuuukg ak ooh uuuhh aaaagggk \n \nSource A: 3 \nQuality B: 7 \nWill uuhh k uuuuhhhk ooogg uuuuhhhh uk 10 \n \nNOTE: ah uug oog ug 10 og oogg uug aaaahh uuhh uuuugggaaaahh oogg aaaahhh oogg uuhh aahh uh loaded! + eeek aag uug uuuuhh ooh aak uuuuggg ooh uuuuhhh ug g eeeek oog h uuuuhh oooogggh ag aahh oooohh aaaagg uh oog uuuugg uuuugggog uug oog uh uuh aaaagg uuuuuukg uug aah uuuuuuk uuuuuukg ak ooh uuuhh aaaagggk +\n +\nSource A: 3 +\nQuality B: 7 +\nWill uuhh k uuuuhhhk ooogg uuuuhhhh uk 10 +\n +\nNOTE: ah uug oog ug 10 og oogg uug aaaahh uuhh uuuugggaaaahh oogg aaaahhh oogg uuhh aahh uh loaded! uuuuhhhug %s aaaaggg uuhh aaaagggug oog @@ -478,7 +488,10 @@ uuh uuhh uuuuggg uuuhh %s ooogg oh oooohhhog aaaahhh uuh - oh ooooggg oogg k uuuuuukaahh uuuugggag uuhh ooooggg oooogg ah oogg library: \'%s\' \n \n \nWould uug uugg uh aak oogg oohh anyway, oooohhh uuh oooohhhg one, oh aaaagg eek action? + oh ooooggg oogg k uuuuuukaahh uuuugggag uuhh ooooggg oooogg ah oogg library: \'%s\' +\n +\n +\nWould uug uugg uh aak oogg oohh anyway, oooohhh uuh oooohhhg one, oh aaaagg eek action? ooogg uuh uuhh g ooogg oooogg oh aah uuuugg uugg oogg ak aaagg ooh oooohhhuug aagg uug aahh uug oooohhhg ak oooohh uuuuuuk oh uuh aaaagggag @@ -510,7 +523,8 @@ uuuugg oogg oh eeeeek eeeeeek uuuugggg aagg uuugg uuuuggg +30 - %s \nremaining + %s +\nremaining aagg uuuuhhh uuuuhh aag aah oogg Fingerprint, eeek ID, PIN, aaaaggg uuh aaaagggh eeeeek uuuhh oh aaaagg @@ -530,13 +544,21 @@ aaaagg aaahh aaaahhhg uuuhh oooohhh uh %s oohh uuuuggg uuhh uuugg - uuh oohh oogg aaaahhhuugg uuuugg oog aaaagggh ak uuh uuuuhhhog series: \n \n%s + uuh oohh oogg aaaahhhuugg uuuugg oog aaaagggh ak uuh uuuuhhhog series: +\n +\n%s aak aaaagggah oooohh uuuhh oooogg (%1$d | %2$s) - uuh uug oohh uuk aagg oh uuuuggguugg aaaahh uuh aaaagggah items? \n \n%s - uug ooh aagg ooh uuuk ag aaaahhhuuhh oooohh ooh oooohhhug oooohhhh ah %1$s? \n \n%2$s - ooh aag oogg uuh aagg ug eeeeeekoohh aaaahh aah uuuugggg uk aag oooohhhek series? \n \n%s + uuh uug oohh uuk aagg oh uuuuggguugg aaaahh uuh aaaagggah items? +\n +\n%s + uug ooh aagg ooh uuuk ag aaaahhhuuhh oooohh ooh oooohhhug oooohhhh ah %1$s? +\n +\n%2$s + ooh aag oogg uuh aagg ug eeeeeekoohh aaaahh aah uuuugggg uk aag oooohhhek series? +\n +\n%s %1$s %2$s aaaahhh uuuhh eeeeek oooohhhg ah uuk uuh aahh ug aaaaggg aaak ooh oooohhh space, aahh ug oooohhh ek @@ -590,8 +612,10 @@ uuuuggg (Old og New) uuuuggguuuhh (Z ak A) uuuugg uuuuggg - uuhh eeeeeek ek uuugg :( \nLog oh ag h aaaaggg uuuuggg ah uuk ooogg ag uugg uuugg oooohhh - oohh aagg uugg found! \nNot aaaaggg aak uuuugggaag ah uuuuhhh aaaak oogg ah eeeeeek + uuhh eeeeeek ek uuugg :( +\nLog oh ag h aaaaggg uuuuggg ah uuk ooogg ag uugg uuugg oooohhh + oohh aagg uugg found! +\nNot aaaaggg aak uuuugggaag ah uuuuhhh aaaak oogg ah eeeeeek uuuuggguuugg oohh %s ooooggg %d released! uugg @@ -599,7 +623,11 @@ ooh ooooggg oooohhh uuuuhhhoog uh ooh oooogg ag ek oooohhh correctly, uuhh uh g ooogg aah uug aaaagg uh uuuugggk uuuuhhhaahh %s - uuuuggguk uuuuhhhoh uuugg aahh aaak uuugg ag oohh library: \n \n%s \n \nWould aag aagg og uuk uugg oogg anyway, aaaahhh ooh uuuugggh ones, ek uuuuhh aah action? + uuuuggguk uuuuhhhoh uuugg aahh aaak uuugg ag oohh library: +\n +\n%s +\n +\nWould aag aagg og uuk uugg oogg anyway, aaaahhh ooh uuuugggh ones, ek uuuuhh aah action? oooohhhag uug oooohh uuh eeeek aaaaggg h oooogg aaaaak uug uuuugg aaaaaakaagg uuuuhh uuuuhhhuh oooogggag ag aaaahh uuuuggguugg aaagg ek aaagg aaaagggaagg diff --git a/app/src/main/res/values-b+ro/strings.xml b/app/src/main/res/values-b+ro/strings.xml index b1852640c..5f612c640 100644 --- a/app/src/main/res/values-b+ro/strings.xml +++ b/app/src/main/res/values-b+ro/strings.xml @@ -19,7 +19,8 @@ Viteză (%.2fx) Evaluare: %.1f - Actualizare nouă găsită! \n%1$s -> %2$s + Actualizare nouă găsită! +\n%1$s -> %2$s Filler %d min @@ -176,8 +177,10 @@ Continuă -30 +30 - Sunteți pe cale să ștergeți definitiv %s \nSunteți sigur? - %dm \nrămas + Sunteți pe cale să ștergeți definitiv %s +\nSunteți sigur? + %dm +\nrămas În curs de desfășurare Finalizat Status @@ -427,7 +430,8 @@ Funcții Autori Adaugă depozit - Biblioteca ta este goală :( \nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. + Biblioteca ta este goală :( +\nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. Eliminați subtitrările închise din subtitrări Descărcați lista de site-uri pe care doriți să le utilizați Evaluare (Ridicat la Scăzut) @@ -467,10 +471,13 @@ URL invalid Toate extensiile au fost dezactivate din cauza unei defecțiuni pentru a vă ajuta să o găsiți pe cea care cauzează probleme. Se descarcă actualizarea aplicației… - CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. \n \nAlăturați-vă Discord-ului nostru sau căutați online. + CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. +\n +\nAlăturați-vă Discord-ului nostru sau căutați online. A început să descarce %1$d %2$s… Mod sigur pornit - Fișier Mod Sigur găsit! \nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. + Fișier Mod Sigur găsit! +\nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. Scoateți de la urmărit Actualizat (Vechi la Nou) Reporniți aplicația pentru a vedea schimbările. @@ -524,7 +531,13 @@ Actualizări al aplicației Subtitrări Dezactivați - Aici puteți schimba modul în care sunt ordonate sursele. Dacă un videoclip are o prioritate mai mare, acesta va apărea mai sus în selecția surselor. Suma dintre prioritatea sursei și prioritatea calității reprezintă prioritatea video. \n \nSursa A: 3 \nCalitate B: 7 \nVa avea o prioritate video combinată de 10. \n \nNOTĂ: Dacă suma este 10 sau mai mare, playerul va sări automat peste încărcare atunci când este încărcat link-ul respectiv! + Aici puteți schimba modul în care sunt ordonate sursele. Dacă un videoclip are o prioritate mai mare, acesta va apărea mai sus în selecția surselor. Suma dintre prioritatea sursei și prioritatea calității reprezintă prioritatea video. +\n +\nSursa A: 3 +\nCalitate B: 7 +\nVa avea o prioritate video combinată de 10. +\n +\nNOTĂ: Dacă suma este 10 sau mai mare, playerul va sări automat peste încărcare atunci când este încărcat link-ul respectiv! Nu s-a găsit plugin-uri în depozit Nu s-a găsit depozitul, verificați URL-ul și încercați cu un VPN Editați @@ -540,12 +553,18 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor Ați votat deja - Elemente potențial duplicate au fost găsite în biblioteca ta: \n \n%s \n \nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? + Elemente potențial duplicate au fost găsite în biblioteca ta: +\n +\n%s +\n +\nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? %s a fost adăugat la favoriți/te %s a fost eliminat din favoriți/te Adaugă la favoriți/te Elimină din favoriți/te - Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' \n \nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? + Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' +\n +\nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? Introduce PIN-ul pentru %s Introduce PIN-ul actual Introduce PIN-ul @@ -600,7 +619,8 @@ Resetați Activați comutarea automată a orientării ecranului pe baza orientării video Blocare cu biometrie - %s \nrămase + %s +\nrămase Următorul în %s CloudStream Wiki Sezonul %1$d Episod %2$d va fi lansat în diff --git a/app/src/main/res/values-b+ru/strings.xml b/app/src/main/res/values-b+ru/strings.xml index 38e576baa..5c271f871 100644 --- a/app/src/main/res/values-b+ru/strings.xml +++ b/app/src/main/res/values-b+ru/strings.xml @@ -34,7 +34,8 @@ Предпросмотр фона Скорость (%.2fx) Оценили: %.1f - Новое обновление найдено! \n%1$s -> %2$s + Новое обновление найдено! +\n%1$s -> %2$s Заполнитель CloudStream Убрать @@ -171,8 +172,10 @@ Продолжить -30 +30 - Это будет удалено безвозвратно%s \nВы уверены? - %d мин. \nосталось + Это будет удалено безвозвратно%s +\nВы уверены? + %d мин. +\nосталось Завершено Год Рейтинг @@ -408,7 +411,9 @@ Трейлер %s (отключено) Далее - В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. \n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. + В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n +\nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название Предыдущий @@ -521,7 +526,13 @@ Вы уже проголосовали Никаких дополнений не обнаружено в источнике Поставить обычный - Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. \n \nИсточник А: 3 \nКачество Б: 7 \nБудет иметь общий приоритет видео 10. \n \nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! + Здесь вы можете изменить порядок расположения источников. Если видео имеет более высокий приоритет, оно будет отображаться выше в списке источников. Сумма приоритета источника и приоритета качества составляет приоритет видео. +\n +\nИсточник А: 3 +\nКачество Б: 7 +\nБудет иметь общий приоритет видео 10. +\n +\nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! Ссылки перезагружены Выберите учётную запись %s убрано из любимых @@ -575,7 +586,8 @@ Аудиокнига Медиа Разблокируйте приложение с помощью отпечатка пальца, Face ID, ПИН-кода, шаблона и пароля. - %s \nосталось + %s +\nосталось Отключить оптимизацию батареи Аутентификация по паролю/ПИН-коду Биометрическая аутентификация на этом устройстве не поддерживается @@ -605,7 +617,9 @@ Вы уверены, что хотите навсегда удалить все серии в данном сериале? \n \n%s Выберите элементы для удаления Удалить (%1$d | %2$s) - Вы уверены, что хотите навсегда удалить данный объект? \n \n%s + Вы уверены, что хотите навсегда удалить данный объект? +\n +\n%s Невозможно получить ПИН-код устройства, попробуйте локальную аутентификацию Откройте %s на вашем смартфоне или компьютере и введите данный код CloudStream Вики @@ -735,8 +749,4 @@ Отменить всё Вы хотите загрузить эпизод %s? Вы хотите отменить всё запланированные загрузки? - Показывать наложения метаданных проигрывателя - Видео - Предпросмотр - Прямой эфир diff --git a/app/src/main/res/values-b+sk/strings.xml b/app/src/main/res/values-b+sk/strings.xml index f48b27143..8c1c4695f 100644 --- a/app/src/main/res/values-b+sk/strings.xml +++ b/app/src/main/res/values-b+sk/strings.xml @@ -1,6 +1,7 @@ - Našla sa nová aktualizácia! \n%1$s -> %2$s + Našla sa nová aktualizácia! +\n%1$s -> %2$s Výplň %1$dh %2$dm Epizóda %d bude vydaná za @@ -331,8 +332,10 @@ %1$s %2$s Vylúčenie zodpovednosti NSFW - Týmto sa natrvalo vymaže %s \nSte si istý? - %dm \nzostáva + Týmto sa natrvalo vymaže %s +\nSte si istý? + %dm +\nzostáva Prebieha Dokončené Rozloženie emulátora @@ -358,7 +361,9 @@ Zmazať repozitár URL adresa repozitára Verejný zoznam - CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. \n \nPripojte sa k nášmu Discord alebo vyhľadajte online. + CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. +\n +\nPripojte sa k nášmu Discord alebo vyhľadajte online. Nepodarilo sa nainštalovať novú verziu aplikácie Upozornenie: CloudStream 3 nenesie žiadnu zodpovednosť za používanie rozšírenia tretích strán a neposkytuje žiadnu podporu pre nich! Pridať repozitár diff --git a/app/src/main/res/values-b+so/strings.xml b/app/src/main/res/values-b+so/strings.xml index 0eb787311..9e4e9f9f1 100644 --- a/app/src/main/res/values-b+so/strings.xml +++ b/app/src/main/res/values-b+so/strings.xml @@ -27,7 +27,8 @@ %d dqq Kadinka Qiimaysan: %.1f - App cusub baa soo baxay! \n%1$s -> %2$s + App cusub baa soo baxay! +\n%1$s -> %2$s Soo dejinta Raadi Dookhyo kale @@ -182,9 +183,11 @@ Tirtir faylka Sii wado -30 - Dhamaantii waa la saari doona %s \nSow ma hubtid? + Dhamaantii waa la saari doona %s +\nSow ma hubtid? Fashil ka yimi xigashada - %ddq \nAyaa hadhsan + %ddq +\nAyaa hadhsan Dhamaystirmay Sannadka Qiimaynta @@ -424,7 +427,11 @@ Xayiran: %d Aan dejinayn: %d Waxa la cusbooneysiiyey %d sidkane - Ugu horreyn cloudstream ma laha wax websaydyo uu filimaanta kasoo xigto, waxay noqonaysaa inaad adigu rakibato reboositarradooda... \n \nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... \n \nDiscord naga soo qabo ama internetka ka baadh. + Ugu horreyn cloudstream ma laha wax websaydyo uu filimaanta kasoo xigto, waxay noqonaysaa inaad adigu rakibato reboositarradooda... +\n +\nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... +\n +\nDiscord naga soo qabo ama internetka ka baadh. Soo deji dhamaan sidkanayaasha reboositarkan? Boodhka Boodhka xalqadda diff --git a/app/src/main/res/values-b+sv/strings.xml b/app/src/main/res/values-b+sv/strings.xml index 52d4e1d05..bf352e5e2 100644 --- a/app/src/main/res/values-b+sv/strings.xml +++ b/app/src/main/res/values-b+sv/strings.xml @@ -2,7 +2,8 @@ Betygsatt: %.1f Hastighet (%.2fx) - Ny uppdatering hittad! \n%1$s -> %2$s + Ny uppdatering hittad! +\n%1$s -> %2$s CloudStream Hem Sök @@ -116,7 +117,8 @@ Ta bort nerladdad fil Ta bort Avbryt - %s kommer att raderas permanent \nÄr du helt säker? + %s kommer att raderas permanent +\nÄr du helt säker? Pågående Färdig Status @@ -229,7 +231,8 @@ %1$d %2$s %1$s %2$d%3$s -30 - %dm \nåterstår + %dm +\nåterstår NSFW OVA Torrent @@ -481,12 +484,16 @@ Visa community databaser Blandad inledning Skippa %s - CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. \n \nGå med i vår Discord eller sök online. + CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. +\n +\nGå med i vår Discord eller sök online. Välj bibliotek - Ditt bibliotek är tomt :( \nLogga in på ett bibliotekskonto eller lägg till program i ditt lokala bibliotek. + Ditt bibliotek är tomt :( +\nLogga in på ett bibliotekskonto eller lägg till program i ditt lokala bibliotek. Visa hoppa över popups för introduktion/eftertexter Ta bort från sett - Fil i säkertläge hittades! \nLaddar inte några tillägg vid start tills filen har tagits bort. + Fil i säkertläge hittades! +\nLaddar inte några tillägg vid start tills filen har tagits bort. Uppdaterar prenumererade program Prenumererad Prenumerera @@ -551,9 +558,21 @@ Använd standard konto PIN-kod Sök mängden som används när spelaren är dold - Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \n \n\'%s.\' \n \nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? - Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \'%s.\' \n \nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? - Här kan du ändra hur källorna ska sorteras, om en video har högre prioritet visas den högre upp i källvalet. Summan av källprioriteten och kvalitetsprioriteten är videoprioriteten. \n \nKälla A: 3 \nKvalitet B: 7 \nKommer att ha en kombinerad videoprioritet på 10. \n \nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! + Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: +\n +\n\'%s.\' +\n +\nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? + Det verkar som om ett potentiellt duplicerat objekt redan finns i ditt bibliotek: \'%s.\' +\n +\nVill du lägga till det här objektet ändå, ersätta det befintliga eller avbryta åtgärden? + Här kan du ändra hur källorna ska sorteras, om en video har högre prioritet visas den högre upp i källvalet. Summan av källprioriteten och kvalitetsprioriteten är videoprioriteten. +\n +\nKälla A: 3 +\nKvalitet B: 7 +\nKommer att ha en kombinerad videoprioritet på 10. +\n +\nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! Avisering om nytt avsnitt Sök i andra tillägg Visa rekommendationer @@ -568,7 +587,8 @@ Efter några misslyckade försök stängs prompten. Starta bara om appen för att försöka igen. Favorit Ta bort från favoriter - %s \nkvarstår + %s +\nkvarstår kopierad! Lagringsnamn och URL För att säkerställa oavbrutna nedladdningar och aviseringar för prenumererade tv-program behöver CloudStream tillstånd att köras i bakgrunden. Genom att trycka på OK kommer du få en förfrågningsdialogruta. Tryck då på \'Tillåt\'.\n\nObservera att denna behörighet inte betyder att CS3 kommer att tömma ditt batteri. Den fungerar bara i bakgrunden när det behövs, till exempel när du tar emot aviseringar eller laddar ner videor från officiella tillägg. @@ -597,7 +617,9 @@ Autentisera lokalt QR-kodbild PIN-koden har upphört att gälla! - Är du säker på att du vill radera följande avsnitt permanent i %1$s? \n \n%2$s + Är du säker på att du vill radera följande avsnitt permanent i %1$s? +\n +\n%2$s Aktivera förhandsgranskningsminiatyr i sökfältet Förhandsgranskning av sökfältet Spela från början @@ -611,9 +633,15 @@ Avmarkera alla Radera Filer Radera (%1$d | %2$s) - Är du säker på att du vill ta bort följande objekt permanent? \n \n%s - Du kommer också permanent att radera alla avsnitt i följande serie: \n \n%s - Är du säker på att du permanent vill radera alla avsnitt i följande serie? \n \n%s + Är du säker på att du vill ta bort följande objekt permanent? +\n +\n%s + Du kommer också permanent att radera alla avsnitt i följande serie: +\n +\n%s + Är du säker på att du permanent vill radera alla avsnitt i följande serie? +\n +\n%s Ta bort insticksprogram Dölj namnen på spelarens kontroller Besök %s på din smartphone eller dator och ange koden ovan @@ -632,7 +660,7 @@ Aktivera torrent i Inställningar/Leverantörer/Föredragen media Avkodningsfel Ladda in första möjliga - Visa inte + Dölj Kantstorlek Egen Stöds ej @@ -732,7 +760,4 @@ Gör alla undertexter kursivstila Bakgrundsradie Visa spelarens metadata överlägg - Video - Förhandsvisning - Live diff --git a/app/src/main/res/values-b+ta/strings.xml b/app/src/main/res/values-b+ta/strings.xml index 4cde39d8f..fda0c36fd 100644 --- a/app/src/main/res/values-b+ta/strings.xml +++ b/app/src/main/res/values-b+ta/strings.xml @@ -358,7 +358,8 @@ முன்மாதிரி தளவமைப்பு பதிவிறக்கம் செய்யப்பட்ட கோப்பு பகுத்தல் - உங்கள் நூலகம் காலியாக உள்ளது :( \n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். + உங்கள் நூலகம் காலியாக உள்ளது :( +\n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். குழுவிலகவும் சுயவிவரங்கள் முள் 4 எழுத்துகளாக இருக்க வேண்டும் @@ -565,7 +566,8 @@ மதிப்பீடு (குறைந்த முதல் உயர் வரை) புதுப்பிக்கப்பட்டது (பழையது புதியது) இந்த பட்டியல் காலியாக உள்ளது. இன்னொரு இடத்திற்கு மாற முயற்சிக்கவும். - பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! \n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. + பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! +\n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. சந்தா காட்சிகளைப் புதுப்பித்தல் சந்தா எபிசோட் %d வெளியானது! diff --git a/app/src/main/res/values-b+tl/strings.xml b/app/src/main/res/values-b+tl/strings.xml index 4ed229ca7..4050ddbd7 100644 --- a/app/src/main/res/values-b+tl/strings.xml +++ b/app/src/main/res/values-b+tl/strings.xml @@ -15,7 +15,8 @@ Bilis (%.2fx) Rated: %.1f - Bagong update! \n%1$s -> %2$s + Bagong update! +\n%1$s -> %2$s CloudStream Home Maghanap @@ -137,7 +138,8 @@ Kanselahin I-pause I-resume - This will permanently delete %s \nAre you sure? + This will permanently delete %s +\nAre you sure? Patuloy Tapos na Katayuan diff --git a/app/src/main/res/values-b+tr/strings.xml b/app/src/main/res/values-b+tr/strings.xml index d711505a8..fb0600c1a 100644 --- a/app/src/main/res/values-b+tr/strings.xml +++ b/app/src/main/res/values-b+tr/strings.xml @@ -19,7 +19,8 @@ Hız (%.2fx) Puan: %.1f - Yeni güncelleme bulundu! \n%1$s -> %2$s + Yeni güncelleme bulundu! +\n%1$s -> %2$s Dolgu %d dakika CloudStream @@ -186,7 +187,8 @@ Sürdür -30 +30 - %s tamamen silinecek \nEmin misiniz? + %s tamamen silinecek +\nEmin misiniz? %dd \nkaldı Devam ediyor Tamamlandı @@ -483,8 +485,10 @@ Çok fazla metin. Panoya kaydedilemiyor. Kütüphane Tarayıcı - Kütüphaneniz boş :( \nBir kütüphane hesabında oturum açın veya yerel kütüphanenize programlar ekleyin. - Güvenli mod dosyası bulundu! \nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. + Kütüphaneniz boş :( +\nBir kütüphane hesabında oturum açın veya yerel kütüphanenize programlar ekleyin. + Güvenli mod dosyası bulundu! +\nDosya kaldırılana kadar başlangıçta herhangi bir uzantı yüklenmiyor. Şuna Göre Sırala Sırala Güncellenme (Yeniden Eskiye) @@ -527,7 +531,13 @@ Mobil veri Varsayılanı ayarla Düzenle - Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. \n \nKaynak A: 3 \nKalite B: 7 \nBirleştirilmiş video önceliği 10 olacaktır. \n \nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! + Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. +\n +\nKaynak A: 3 +\nKalite B: 7 +\nBirleştirilmiş video önceliği 10 olacaktır. +\n +\nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! Kaliteler Profil arkaplanı UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s @@ -536,7 +546,11 @@ Favoriler %s favorilere eklendi %s olarak giriş yapıldı - Kütüphanenizde potensiyel kopya ürünler bulundu: \n \n%s \n \nYine de ekleyerek var olanları değiştirmek mi istersiniz, yoksa iptal etmek mi? + Kütüphanenizde potensiyel kopya ürünler bulundu: +\n +\n%s +\n +\nYine de ekleyerek var olanları değiştirmek mi istersiniz, yoksa iptal etmek mi? %s için PIN girin Yedekleme sıklığı Potensiyel Kopya Bulundu @@ -559,7 +573,9 @@ Depo bulunamadı, bağlantı adresini kontrol edin veya VPN ile deneyin Zaten oyladınız Depoda eklenti bulunamadı - Görünüşe göre potansiyel bir kopya kütüphanenizde zaten bulunuyor: \'%s\' \n \nYine de ekleyerek var olanı değiştirmek mi istersiniz, yoksa iptal etmek mi? + Görünüşe göre potansiyel bir kopya kütüphanenizde zaten bulunuyor: \'%s\' +\n +\nYine de ekleyerek var olanı değiştirmek mi istersiniz, yoksa iptal etmek mi? PIN girin PIN Geçerli PIN\'i Giriniz @@ -621,12 +637,20 @@ Oynatıcı kontrolünün adlarını gizle Baştan Oynat Tümünü Seç - Aşağıdaki öğeleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%s - %1$s içindeki aşağıdaki bölümleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%2$s + Aşağıdaki öğeleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%s + %1$s içindeki aşağıdaki bölümleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%2$s Dosyaları Silin Sil (%1$d | %2$s) - Aşağıdaki dizideki tüm bölümleri kalıcı olarak silmek istediğinizden emin misiniz? \n \n%s - Ayrıca aşağıdaki dizideki tüm bölümleri kalıcı olarak sileceksiniz: \n \n%s + Aşağıdaki dizideki tüm bölümleri kalıcı olarak silmek istediğinizden emin misiniz? +\n +\n%s + Ayrıca aşağıdaki dizideki tüm bölümleri kalıcı olarak sileceksiniz: +\n +\n%s Silinecek Öğeleri Seçin Tüm Seçimi Kaldır Çevrimdışı izlemek için kullanılabilir @@ -746,7 +770,4 @@ Öncelikli kaynak Oynatıcıda video kaynaklarının nasıl sıralanacağını belirleyin Meta Verileri Katmanını Göster - Canlı - Ön Gösterim - Video diff --git a/app/src/main/res/values-b+uk/strings.xml b/app/src/main/res/values-b+uk/strings.xml index ba46736ac..2a9352183 100644 --- a/app/src/main/res/values-b+uk/strings.xml +++ b/app/src/main/res/values-b+uk/strings.xml @@ -1,21 +1,21 @@ Плакат - Постер епізоду - Завантаження скасовано - Змінити постачальника + Постер Епізоду + Завантаження Скасовано + Змінити Постачальника Назад Рейтинг: %.1f У ролях: %s - %d епізод вийде через + Епізод %d вийде через Плакат %1$s Еп %2$d - %1$d дн %2$d год %3$d хв - %1$d год %2$d хв - %d хв - Головний постер - Наступне випадкове - Попередній перегляд тла + %1$dд %2$dгод %3$dхв + %1$dгод %2$dхв + %dхв + Головний Постер + Наступний Випадковий + Попередній Перегляд Заднього Фону Швидкість (%.2fx) Знайдено нове оновлення!\n%1$s –> %2$s Пошук @@ -24,29 +24,29 @@ Налаштування Пошук… Пошук на %s… - Немає даних - Більше налаштувань + Немає Даних + Більше Опцій Наступний епізод Жанри - Відкрити в браузері - Пропустити завантаження + Відкрити в Браузері + Пропустити Завантаження Завантаження… Завершено - Заплановано - Покинуто - Відтворити фільм - Відтворити трейлер - Транслювати торрент + Планую Дивитися + Скинуто + Відтворити Фільм + Відтворити Трейлер + Транслювати Торрент Повторити з’єднання… Назад - Відтворити епізод + Відтворити Епізод Завантажено Завантаження - Завантаження завершено + Завантаження Завершено Дуб. Суб. - Видалити файл - Відновити завантаження + Видалити Файл + Відновити Завантаження Приховати Переглянути Подробиці @@ -57,26 +57,26 @@ Скопіювати Закрити Зберегти - Швидкість програвача - Колір вікна - Тип межі + Швидкість Плеєра + Колір Вікна + Тип Межі Шрифт - Розмір шрифту + Розмір Шрифту Пошук за постачальниками Пошук за типами - Жодного банана не надано - Автовибір мови - Завантажити мови - Мова субтитрів - Утримуйте, щоби скинути до типових + Жодного Benenes не надано + Авто-Вибір Мови + Завантажити Мови + Мова Субтитрів + Утримуйте, щоби скинути до типових налаштувань Імпортуйте шрифти, помістивши їх до %s - Продовжити перегляд + Продовжити Перегляд Вилучити Докладніше Цей постачальник є торентом, рекомендується використовувати VPN Опис - Сюжет не знайдено - Опис не знайдено + Сюжет Не Знайдено + Опис Не Знайдено Показати Logcat 🐈 Продовжувати відтворення в мініатюрному програвачі поверх інших застосунків Прибрати чорні смуги @@ -92,33 +92,33 @@ Філлер Відтворити в CloudStream Мережева трансляція - Переглядаю + Переглядання Поділитися Відкладено - Передивляюся + Переглядаю Повторно Завантажити - Відтворити трансляцію + Відтворити Трансляцію Джерела Субтитри - Внутрішнє сховище - Завантаження призупинено - Завантаження розпочато - Не вдалося завантажити - Оновлення розпочато - Помилка завантаження посилань - Призупинити завантаження - Переглянути файл + Внутрішнє Сховище + Завантаження Призупинено + Завантаження Розпочато + Завантаження не Вдалося + Оновлення Розпочато + Помилка Завантаження Посилань + Призупинити Завантаження + Переглянути Файл Докладніше - Фільтрувати закладки + Фільтрувати Закладки Очистити - Налаштування субтитрів - Колір тла - Висота субтитрів - Колір тексту - Колір обведення + Налаштування Субтитрів + Колір Тла + Висота Субтитрів + Колір Тексту + Колір Обведення Автовідтворення наступного епізоду Проведіть збоку в бік, щоби керувати часом відтворення у відео - Дано %d бананів розробникам + %d Benenes надано розробникам Кнопка зміни розміру програвача @string/home_play Для коректної роботи цього постачальника може знадобитися VPN @@ -138,12 +138,12 @@ Дані збережено Помилка резервного копіювання %s Пошук - Облікові записи та безпека - Оновлення та резервне копіювання + Облікові Записи та Безпека + Оновлення та Резервне Копіювання Подробиці - Розширений пошук + Розширений Пошук Показувати результати пошуку, розділені за постачальниками - Показувати філлери для аніме + Показувати наповнювачі для аніме Показувати трейлери Приховати вибрану якість відео у результатах пошуку Автозавантаження розширень @@ -153,25 +153,25 @@ Github Застосунок для ранобе від тих самих розробників Застосунок для аніме від тих самих розробників - Дати бананів розробникам - Мова застосунку + Дати benene розробникам + Мова Застосунку Цей постачальник не має підтримування Chromecast - Посилань не знайдено - Переглянути епізод + Посилань Не Знайдено + Переглянути Епізод Скинути до типових значень - Немає сезону + Немає Сезону Епізоди %1$d %2$s С Е - Видалити файл + Видалити Файл Видалити Скасувати Відновити -30 Це назавжди видалить %s\nВи впевнені? - %d хв\nзалишилося - Виходить + %dхв\nзалишилося + Триває Завершено Рейтинг Тривалість @@ -184,16 +184,16 @@ Телесеріали Мультфільми Аніме - ОВА - Азіатські драми - Прямі трансляції + OVA + Азіатські Драми + Прямі Трансляції Інші Серіал Мультфільм Аніме - Документалка - Азіатська драма - Пряма трансляція + Документальний Фільм + Азіатська Драма + Пряма Трансляція Відео Помилка джерела Віддалена помилка @@ -203,15 +203,15 @@ Переглянути в %s Автозавантаження Завантажити дзеркало - Перевірити наявність оновлень + Перевірити Наявність Оновлень Забл./Розбл. Пропустити ОП Не показувати знову Оновити - Бажана якість перегляду (Wi-Fi) + Бажана якість перегляду (WiFi) Заголовок Перемкнути елементи інтерфейсу на плакаті - Оновлення не знайдено + Оновлення Не Знайдено Натисніть двічі праворуч або ліворуч, щоби перемотати вперед або назад Використовувати системну яскравість у програвачі замість темного накладання Завантажено файл резервної копії @@ -222,10 +222,10 @@ Автооновлення розширень Автоматично встановлювати всі розширення, які ще не встановлено, з доданих сховищ. Автоматично перевіряти нові оновлення після запуску застосунку. - Посилання скопійовано до буфера обміну - Деякі пристрої не підтримують новий встановлювач пакетів. Спробуйте старий варіант, якщо оновлення не встановлюються. + Покликання скопійовано до буфера обміну + Деякі пристрої не підтримують новий інсталятор пакетів. Спробуйте старий варіант, якщо оновлення не встановлюються. Приєднуйтеся до Discord - Дано банани + Дано benene Рік +30 %1$s %2$d%3$s @@ -240,21 +240,21 @@ Стислий зміст Фільми Перезавантажити посилання - Документалки + Документальні Фільми NSFW Фільм - ОВА + OVA Торент Мітка якості NSFW Несподівана помилка програвача Помилка завантаження, перевірте дозвіл на зберігання - Епізод через Chromecast + Chromecast епізод Мітка субтитрів Джерело Завантажити субтитри Мітка дубляжу - Пропустити це оновлення + Пропустити це Оновлення Усе На весь екран Розтягнути @@ -275,9 +275,9 @@ Макет застосунку Бажані медіа Автоматично - Макет телевізора - Макет телефона - Макет емулятора + Телевізійна Обгортка + Телефона обгортка + Емуляторна обгортка Основний колір Тема застосунку Розташування назви плаката @@ -296,7 +296,7 @@ %d / 10 /%d %s автентифіковано - Не вдалося ввійти в %s + Не вдалося увійти в %s Нічого Звичайний Мін. @@ -319,9 +319,9 @@ HDR SDR Web - Зображення плаката + Зображення Плаката Програвач - Роздільність та заголовок + Роздільна здатність та заголовок Недійсний ID Недійсна URL-адреса Резервне копіювання @@ -341,15 +341,15 @@ DNS через HTTPS Шлях завантаження Додайте двійника наявного сайту, з іншою URL-адресою - Показувати аніме з дубляжем / із субтитрами + Показувати Дубльоване/З Субтитрами Аніме Застереження Розширення Дії 127.0.0.1 Макет Кодування субтитрів - Увімкнути NSFW у підтримуваних розширеннях - Макет + Увімкнути NSFW вміст на підтримуваних Розширеннях + Обгортка Постачальники https://example.com %1$s %2$s @@ -375,7 +375,7 @@ Фільтрувати за бажаною мовою медіа 4K Заголовок - Роздільність + Роздільна здатність Помилка Трейлер Додатково @@ -388,7 +388,7 @@ TS TC Вилучати роздуття субтитрів - Джерело переходу (необов’язково) + Referer (необов’язково) Далі Переглядайте відео на цих мовах Пропустити налаштування @@ -396,7 +396,7 @@ Готово Розширення Додати репозиторій - Назва репозиторію (необов’язково) + Назва репозиторію (Опціонально) URL-адреса репозиторію або короткий код Розширення завантажено Розширення завантажено @@ -432,7 +432,7 @@ Список відтворення HLS Вбудований програвач Ендінґ - Підсумок + Коротке повторення Пропустити %s Змішаний ендінґ Подяки @@ -449,10 +449,10 @@ Установлення оновлення застосунку… Не вдалося встановити нову версію застосунку Застарілий - Встановлювач пакунків + Встановлювач Пакунків Застосунок буде оновлено після виходу Це також призведе до видалення всіх розширень сховища - Усі мови + Усі Мови Назад Змініть вигляд застосунку відповідно до вашого пристрою Розширення видалено @@ -467,26 +467,26 @@ Застосунок не знайдено Змішаний опенінґ Вилучити з переглянутого - Оновленням (від старого до нового) - Оновленням (від нового до старого) + Оновленням (від Старого до Нового) + Оновленням (від Нового до Старого) Бібліотека Сортувати - Рейтингом (від високого до низького) + Рейтингом (від Високого до Низького) Сортувати за Алфавітом (від А до Я) - Рейтингом (від низького до високого) + Рейтингом (від Низького до Високого) Ваша бібліотека порожня :(\nУвійдіть в обліковий запис бібліотеки або додайте щось до вашої локальної бібліотеки. Алфавітом (від Я до А) - Оберіть бібліотеку + Оберіть Бібліотеку Відкрити з Браузер Цей список порожній. Спробуйте перейти до іншого. Файл безпечного режиму знайдено!\nРозширення не завантажуватимуться під час запуску, доки файл не буде видалено. Android TV - Прогрвач приховано: крок перемотування - Програвач показано: крок перемотування + Прогрвач Приховано – Крок Перемотування + Програвач Показано – Крок Перемотування Крок перемотування, який використовується, коли програвач видимий - Крок перемотування, який використовується, коли програвач прихований + Крок перемотування, який використовується, коли плеєр прихований Провалено Пройдено Перезапустити @@ -500,10 +500,10 @@ Ви відписалися від %s Епізод %d випущено! Повернути - Проксі GitHub + GitHub Проксі Не вдалось отримати доступ до GitHub. Увімкнення проксі-сервера jsDelivr… Обходи ISP - Обхід блокування прямих посилань GitHub за допомогою jsDelivr. Може спричинити затримку оновлень на кілька днів. + Обхід блокування чистих gitHub URLs за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (мобільні дані) Встановити типові Профілі @@ -527,7 +527,7 @@ Вподобані %s додано до вподобаних У вашій бібліотеці виявлено можливі дублікати:\n\n%s\n\nУсе одно хочете додати цей елемент, замінити наявні чи скасувати дію? - Знайдено можливий дублікат + Знайдено Можливий Дублікат Заблокувати профіль Додати до обраного Замінити все @@ -538,32 +538,32 @@ Додати Підписатися Вилучити з обраного - Оберіть обліковий запис + Оберіть Обліковий Запис Схоже, що у вашій бібліотеці вже є можливий дублікат: \'%s.\'\n\nУсе одно хочете додати цей елемент, замінити наявний чи скасувати дію? Уведіть PIN-код PIN-код Уведіть поточний PIN-код Увійшли як %s Уведіть PIN-код для %s - Використовувати типовий обліковий запис + Використовувати Типовий Обліковий Запис Пропускати вибір облікового запису під час запуску - Керувати обліковими записами + Керувати Обліковими Записами Редагувати обліковий запис Показувати кнопку перемикання орієнтації екрана Обернути - Посилання перезавантажені + Посилання Перезавантажені Автообертання Увімкнути автоматичну зміну орієнтації екрана відповідно до відео Додати налаштування швидкості до програвача - Перевірити всі розширення + Перевірити всі Розширення Пошук в інших розширеннях Показати рекомендації - Ця перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. + Ця Перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. Сповіщення про новий епізод - Автентифікація за паролем / PIN-кодом + Автентифікація за Паролем/PIN-кодом Розблокуйте CloudStream - Біометричне блокування - Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, графічного ключа або пароля. + Біометричне Блокування + Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, Графічного Ключа або Пароля. Щойно було виконано резервне копіювання даних CloudStream. Хоча ймовірність цього вкрай мала, усі пристрої можуть поводитися по-різному. У рідкісних випадках, якщо ви втратите доступ до застосунку, повністю очистіть дані застосунку та відновіть їх із резервної копії. Просимо вибачення за будь-які незручності, що можуть виникнути. Біометрична автентифікація не підтримується на цьому пристрої Після кількох невдалих спроб вікно запиту зникне. Перезапустіть застосунок, щоби спробувати ще раз. @@ -572,11 +572,11 @@ Додати до вподобаного скопійовано! Назва репозиторію та URL - Помилка копіювання, скопіюйте logcat та зверніться до служби підтримки застосунку. - Помилка доступу до буфера обміну, спробуйте ще раз. - Гаразд + Помилка копіювання, Будь-ласка скопіюйте logcat та зверніться до служби підтримки застосунку. + Помилка доступу до буфера обміну, Будь-ласка спробуйте ще раз. + OK Вимкнути оптимізацію батареї - Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши «Гаразд», ви побачите діалогове вікно запиту. Натисніть «Дозволити».\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. + Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши OK, ви побачите діалогове вікно запиту. Натисніть \'Дозволити\'.\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. Споживання батареї застосунком уже змінено на необмежене Не вдається відкрити подробиці про застосунок CloudStream. Аудіокнига @@ -584,7 +584,7 @@ Медіа Скинути Наступний через %s - %1$d сезон %2$d епізод вийде через + Сезон %1$d Епізод %2$d вийде через Оберіть пристрій для трансляції Трансляція через дзеркало CloudStream Wiki @@ -592,60 +592,60 @@ Облікові записи Зображення QR-коду Відкрити сховище - Відвідайте %s на своєму смартфоні або комп’ютері та введіть вищевказаний код - Не вдається отримати PIN-код пристрою, спробуйте локальну автентифікацію - Термін дії PIN-коду закінчився! - Термін дії коду закінчується через %1$d хв %2$d с - Локальна автентифікація + Відвідайте %s на своєму смартфоні або комп\'ютері та введіть вищевказаний код + Не вдається отримати PIN-код пристрою, спробуйте локальну аутентифікацію + PIN-код зараз закінчився ! + Термін дії коду закінчується через %1$dхв %2$dс + Локальна Аутентифікація Відхилити - Відтворити з початку + Відтворити з Початку Попередження Видалити розширення Наразі завантажень немає. Приховати назви елементів керування в програвачі Відкрити локальне відео - Датою випуску (від нових до старих) - Датою випуску (від старих до нових) - Оберіть елементи для видалення - Обрати все - Зняти виділення + Датою випуску (від Нових до Старих) + Датою випуску (від Старих до Нових) + Оберіть Елементи для Видалення + Обрати Все + Зняти Вибір Всіх Видалити (%1$d | %2$s) Ви впевнені, що хочете назавжди видалити такі епізоди в %1$s?\n\n%2$s Ви також назавжди видалите всі епізоди в такому серіалі:\n\n%s - Доступно для перегляду поза мережею - Видалити файли + Доступно для перегляду в оффлайн режимі + Видалити Файли Ви впевнені, що хочете назавжди видалити такі елементи?\n\n%s Ви впевнені, що хочете назавжди видалити всі епізоди в такому серіалі?\n\n%s Попередній перегляд на шкалі перегляду Увімкнути мініатюру попереднього перегляду на шкалі перегляду Субтитри ще не завантажено Підтвердіть перед виходом - Показати + Відобразити Показувати діалог перед виходом із застосунку - Не показувати + Не відображати Розташування теки для резервних копій Власний - Це відео – торрент, це означає, що ваша відеоактивність може відстежуватися.\nПереконайтеся, що розумієте, що таке торрент, перед тим як продовжити. + Це відео – Торрент, це означає, що ваша відео діяльність може відстежуватися.\nПереконайтеся, що розумієте, що таке Торрент, перед тим як продовжити. Розмір обведення Аудіо Подкаст Непідтримувана помилка Помилка кодування Завантажити перший доступний - Увімкніть торент в Налаштування - > Постачальники - > Бажані медіа - Перезапустіть застосунок та прийміть спливне вікно «Транслювати торрент», щоби продовжити. + Увімкніть торент в Налаштування/Постачальники/Бажані медіа + Перезапустіть застосунок та прийміть спливне вікно Stream Torrent, щоби продовжити. Програмне декодування Програмне декодування дозволяє плеєру відтворювати відеофайли, які не підтримуються вашим пристроєм, але може спричинити затримки або нестабільне відтворення у високій роздільній здатності. - Датою виходу (найновіша) - Епізодом (за зростанням) - Рейтингом (найнижчий) + Датою виходу (Найновіша) + Епізодом (За Зростанням) + Рейтингом (Найнижчий) Рейтинг %s - Епізодом (за спаданням) - Рейтингом (найвищий) - Датою виходу (найстаріша) + Епізодом (за Спаданням) + Рейтингом (Найвищий) + Датою виходу (Найстаріша) Еп. %s Дата %s - Оновити розширення + Оновити Розширення Успішно оновлено %d розширення(-ь)! Оновити розширення вручну Починається оновлення розширень! @@ -653,10 +653,10 @@ Сповіщення програвача для керування відтворенням у фоновому режимі Сповіщення програвача Розпізнавання мовлення недоступне - Почніть говорити… + Почніть Говорити… Вбудовані Мережеві - Радіус тла + Радіус Тла Зробити всі субтитри потовщеним Зробити всі субтитри курсивом Гучність перевищила 100% @@ -664,8 +664,8 @@ Одночасних з’єднань Змінює межі екрана Обрізання зображення - Перейти до завантажень - Немає з’єднання з мережею. \n\nПерепід’єднайтеся до мережі та спробуйте ще раз або перегляньте завантажені відео локально. + Перейти до Завантажень + Немає підключення до Інтернету.\n\nБудь ласка, підключіться до Інтернету та спробуйте ще раз або перегляньте завантажені відео офлайн. Скільки різних елементів можна завантажити паралельно Паралельних завантажень Скільки одночасних з’єднань може використовувати кожне завантаження @@ -674,46 +674,46 @@ Завжди запитувати Змінювати швидкість при утриманні Утримуйте, щоб отримати 2-кратну швидкість - %1$d год %2$d хв %3$d с - %1$d хв %2$d с - %1$d с + %1$dгод %2$dхв %3$dс + %1$dхв %2$dс + %1$dс Мітка рейтингу Немає облікового запису - Редагувати зображення профілю - Введіть URL-адресу зображення профілю - URL-адресу не знайдено - Недійсна URL-адреса або зображення - Зображення успішно оновлено + Редагувати Зображення Профілю + Введіть URL-адресу Зображення Профілю + URL-адресу Не Знайдено + Недійсна URL-адреса або Зображення + Зображення Успішно Оновлено Позначити як переглянуте до цього епізоду Вилучити переглянуті до цього епізоду Перезавантажено - Перезавантажити постачальника + Перезавантажити Постачальника Послуг Переглянути в дзеркалі" Назва - Роздільність та назва - Вирівнювання субтитрів + Роздільна здатність та назва + Вирівнювання Субтитрів Внизу ліворуч Внизу по центру Внизу праворуч - Посередині зліва - Посередині в центрі - Посередині справа + Середній лівий + Середній центр + Середній правий Угорі ліворуч - Угорі в центрі + Верхній центр Угорі праворуч - Відтворити весь серіал - Встановити передрелізну версію - Передрелізна версія вже встановлена. - Не вдалося встановити передрелізну версію. - Текст епізоду - Пропозиції пошуку + Відтворити Повні Серії + Встановити перед-релізну версію + Перед-релізна версія вже встановлена. + Не вдалося встановити перед-релізну версію. + Текст Епізоду + Пропозиції Пошуку Показувати підказки пошуку під час введення тексту - Очистити пропозиції + Очистити Пропозиції Додаткова яскравість Увімкнути фільтр яскравості при перевищенні 100% яскравості дисплея extra_brightness_enabled Показати панель трансляції - Інформація про медіа + Інформація Про Медіа Назва джерела Черга завантаження Наразі немає завантажень у черзі. @@ -735,8 +735,5 @@ Пріоритетне джерело Виберіть спосіб сортування джерел відео у програвачі - Показувати накладання метаданих програвача - Відео - Передперегляд - Наживо + Показувати Накладання Метаданих Програвача diff --git a/app/src/main/res/values-b+ur/strings.xml b/app/src/main/res/values-b+ur/strings.xml index 94728e2f8..9a72e586f 100644 --- a/app/src/main/res/values-b+ur/strings.xml +++ b/app/src/main/res/values-b+ur/strings.xml @@ -10,7 +10,8 @@ ذریعہ تبدیل کریں پس منظر کا دیکھنا درجہ بندی: %.1f - نیا update آگیا ہے! \n%1$s -> %2$s + نیا update آگیا ہے! +\n%1$s -> %2$s بھرنے والا %d منٹ %1$d دن %2$d گھنٹے %3$d منٹ @@ -188,8 +189,10 @@ از سر نو شروع کریں -30 +30 - یہ مستقل طور پر حذف ہوجائے گا %s \nتمھيں يقين ہے? - %dm \nباقی + یہ مستقل طور پر حذف ہوجائے گا %s +\nتمھيں يقين ہے? + %dm +\nباقی احوال مکمل حالت @@ -438,7 +441,8 @@ پلیئردکھایا گیا - Seek Amount پلیئر کے نظر آنے پر استعمال کی جانے والی Seek Amount پلیئر کے چھپنے پر استعمال ہونے والی seek amount - سیف موڈ فائل مل گئی! \nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ + سیف موڈ فائل مل گئی! +\nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ شروع کریں ناکام کامیاب ہو گیا @@ -450,7 +454,8 @@ آئی ایس پی بائی پاسز %s کو سبسکرائب کیا Bypass blocking of raw github URLs using jsDelivr. اپ ڈیٹس میں کچھ دنوں کی تاخیر ہو سکتی ہے - آپ کی لائبریری خالی ہے:( \nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ + آپ کی لائبریری خالی ہے:( +\nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ غلط URL براؤزر ویب @@ -499,7 +504,9 @@ سب ٹائٹلز سے بند کیپشنز کو ہٹا دیں اپنے آلے کے مطابق ایپ کی شکل تبدیل کریں اگلے - CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ \n \nہمارے Discord میں شامل ہوں یا آن لائن تلاش کریں۔ + CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ +\n +\nہمارے Discord میں شامل ہوں یا آن لائن تلاش کریں۔ تمام ایکسٹینشنز کریش کی وجہ سے آف کر دی گئیں تاکہ آپ کو پریشانی کا باعث تلاش کرنے میں مدد مل سکے۔ پہلے ایکسٹینشن انسٹال کریں بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ @@ -521,7 +528,13 @@ آپ نے پہلے ہی ووٹ دیا ہے مخزن میں کوئی پلگ انز نہیں ملا ترجیحی تعین کریں - یہاں آپ تبدیلی کرسکتے ہیں کہ سورسز کو کس طرح کی ترتیب دی جائے۔ اگر ایک ویڈیو کی زیادہ پرائیورٹی ہوتی ہے تو یہ سورس کی انتخاب میں زیادہ اوپر آئے گی۔ سورس کی پرائیورٹی اور کوالٹی کی پرائیورٹی کا مجموعہ ویڈیو کی پرائیورٹی ہوتی ہے۔ \n \nسورس A: 3 \nکوالٹی B: 7 \nاس کا مجموعی ویڈیو پرائیورٹی 10 ہوتی ہے۔ \n \nنوٹ: اگر مجموعہ 10 یا اس سے زیادہ ہو تو پلیر وہ لنک لوڈ کرنے کو خود بخود چھوڑ دے گا! + یہاں آپ تبدیلی کرسکتے ہیں کہ سورسز کو کس طرح کی ترتیب دی جائے۔ اگر ایک ویڈیو کی زیادہ پرائیورٹی ہوتی ہے تو یہ سورس کی انتخاب میں زیادہ اوپر آئے گی۔ سورس کی پرائیورٹی اور کوالٹی کی پرائیورٹی کا مجموعہ ویڈیو کی پرائیورٹی ہوتی ہے۔ +\n +\nسورس A: 3 +\nکوالٹی B: 7 +\nاس کا مجموعی ویڈیو پرائیورٹی 10 ہوتی ہے۔ +\n +\nنوٹ: اگر مجموعہ 10 یا اس سے زیادہ ہو تو پلیر وہ لنک لوڈ کرنے کو خود بخود چھوڑ دے گا! پسندیدہ %s کو پسندیدہ میں شامل کیا گیا %s کو پسندیدہ سے ختم کیا گیا @@ -548,8 +561,14 @@ جمع کریں سب کو بدل دیں بدل دیں - آپ کی لائبریری میں ممکنہ ڈپلیکیٹ آئٹمز مل گئے ہیں: \n \n%s \n \nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ - ایسا معلوم ہوتا ہے کہ آپ کی لائبریری میں ممکنہ طور پر ڈپلیکیٹ آئٹم پہلے سے موجود ہے: \'%s۔\' \n \nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ + آپ کی لائبریری میں ممکنہ ڈپلیکیٹ آئٹمز مل گئے ہیں: +\n +\n%s +\n +\nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ + ایسا معلوم ہوتا ہے کہ آپ کی لائبریری میں ممکنہ طور پر ڈپلیکیٹ آئٹم پہلے سے موجود ہے: \'%s۔\' +\n +\nکیا آپ بہرحال اس آئٹم کو شامل کرنا چاہیں گے، موجودہ کو تبدیل کرنا چاہیں گے، یا کارروائی کو منسوخ کریں گے؟ پروفائل لاک کریں آغاز پر اکاؤنٹ کا انتخاب چھوڑ دیں ویڈیو واقفیت کی بنیاد پر اسکرین کی سمت بندی کی خودکار سوئچنگ کو فعال کریں @@ -557,7 +576,8 @@ کاپی کر لیا! ذخیرہ کا نام اور URL %s میں آنے والا ہے - %s \nباقی + %s +\nباقی کلپ بورڈ تک رسائی میں خرابی، براہ کرم دوبارہ کوشش کریں۔ کاپی کرنے میں خرابی، براہ کرم logcat کاپی کریں اور ایپ سپورٹ سے رابطہ کریں۔ سبسکرائب شدہ ٹی وی شوز کے لیے بلاتعطل ڈاؤن لوڈز اور اطلاعات کو یقینی بنانے کے لیے، CloudStream کو پس منظر میں چلنے کی اجازت درکار ہے۔ OK دبانے سے، آپ کو App info پر بھیج دیا جائے گا۔ وہاں اسکرول کریں 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 اور battery usage کو 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 کردیں، اس اجازت کا مطلب یہ نہیں ہے کہ CS3 آپ کی بیٹری ختم کردے گا۔ یہ صرف ضرورت پڑنے پر پس منظر میں کام کرے گا، جیسے کہ جب اطلاعات موصول ہوں یا آفیشل ایکسٹینشنز سے ویڈیوز ڈاؤن لوڈ کریں۔ اگر آپ منسوخ کرنے کا انتخاب کرتے ہیں، تو آپ اس ترتیب کو بعد میں 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 میں ایڈجسٹ کر سکتے ہیں۔ diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 952bafee7..c0f4744fd 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -14,21 +14,22 @@ Poster tập phim Poster chính Tập tiếp theo ngẫu nhiên - Thoát + Quay lại Thay đổi Nguồn phim Xem trước hình nền Tốc độ (%.2fx) Đánh giá: %.1f - Đã tìm thấy bản cập nhật mới! \n%1$s -> %2$s + Đã tìm thấy bản cập nhật mới! +\n%1$s -> %2$s Bộ lọc %d phút CloudStream Phát bằng CloudStream - Trang chủ - Tìm kiếm + Trang Chủ + Tìm Kiếm Tải xuống - Cài đặt + Cài Đặt Tìm kiếm… Tìm kiếm %s… Không có dữ liệu @@ -46,9 +47,9 @@ Xem sau Xem lại Phát - Phát Trực tiếp + Phát Livestream Phát Torrent - Nguồn + Nguồn phim Phụ đề Thử kết nối lại… Quay lại @@ -57,7 +58,7 @@ Tải xuống Đã tải xuống Đang tải xuống - Tải xuống đã tạm dừng + Đã tạm dừng tải xuống Tải xuống đã bắt đầu Tải xuống thất bại Tải xuống đã hủy @@ -87,7 +88,7 @@ Tốc độ phát Cài đặt phụ đề Màu chữ - Màu viền + Màu viền chữ Màu nền Màu cửa sổ Kiểu viền @@ -101,7 +102,7 @@ Tự động chọn ngôn ngữ Ngôn ngữ tải xuống Ngôn ngữ phụ đề - Nhấn giữ để đặt lại mặc định + Nhấn giữ để đặt lại về mặc định Thêm phông chữ tại %s Tiếp tục xem Xóa @@ -128,10 +129,10 @@ Vuốt lên hoặc xuống cạnh trái hoặc phải để điều chỉnh độ sáng hoặc âm lượng Tự động phát tập tiếp theo Phát tập tiếp theo sau khi hết tập hiện tại - Nhấn hai lần để tua - Nhấn hai lần để tạm dừng + Nhấn 2 lần để tua + Nhấn 2 lần để tạm dừng Thời lượng tua (Giây) - Nhấn hai lần vào cạnh trái hoặc phải để tua về trước hoặc sau + Nhấn 2 lần vào cạnh trái hoặc phải để tua về trước hoặc sau Nhấn vào giữa hai lần để tạm dừng Sử dụng độ sáng hệ thống Dùng độ sáng hệ thống thay cho lớp phủ tối trong trình phát ứng dụng @@ -166,7 +167,7 @@ Ngôn ngữ ứng dụng Nguồn phim này chưa hỗ trợ Chromecast Không tìm thấy liên kết - Đã sao chép liên kết vào bảng nhớ tạm + Đã sao chép liên kết vào bộ nhớ tạm Phát Tập phim Đặt lại giá trị mặc định Mùa @@ -185,7 +186,8 @@ Tiếp tục -30 +30 - %s sẽ bị xoá vĩnh viễn \nBạn có chắc chắn không? + %s sẽ bị xoá vĩnh viễn +\nBạn có chắc chắn muốn xóa? %d phút\ncòn lại Đang chiếu Hoàn thành @@ -228,14 +230,14 @@ Lỗi nguồn phim Lỗi nguồn từ xa Lỗi kết xuất - Lỗi trình phát bất ngờ + Đã có lỗi xảy ra. Vui lòng thử lại sau Lỗi tải xuống. Hãy kiểm tra quyền truy cập bộ nhớ - Truyền tập phim - Truyền nguồn thay thế + Phát tập phim bằng Chromecast + Phản chiếu màn hình bằng Chromecast Phát bằng ứng dụng Phát bằng %s Tự động tải xuống - Tải xuống nguồn thay thế + Nguồn tải dự phòng Tải lại các liên kết Tải xuống phụ đề Nhãn chất lượng phim @@ -248,13 +250,13 @@ Khóa Thu phóng Nguồn - Bỏ qua Giới thiệu + Bỏ qua giới thiệu Không hiện lại Bỏ qua bản cập nhật này Cập nhật Chất lượng xem ưu tiên (WiFi) Số ký tự tối đa tiêu đề trình phát - Hiển thị thông tin trình phát + Hiện thông tin trình phát Kích thước bộ nhớ đệm video Thời lượng bộ nhớ đệm Bộ nhớ đệm video trên thiết bị @@ -293,7 +295,7 @@ Đặt tên phim dưới poster Mật khẩu - Tên người dùng + Tài khoản Email 127.0.0.1 Tên trang mới @@ -313,10 +315,10 @@ %d / 10 /?? /%d - %s đã xác thực - Không thể đăng nhập tại %s + Đã xác thực %s + Không thể xác thực %s - Không có + Mặc định Bình thường Tất cả Tối đa @@ -337,7 +339,7 @@ https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300 https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog --> - Phụ đề của bạn sẽ trông tương tự như thế này + Bạch kim rất quý nên sẽ dùng để lắp vô xương Được đề xuất Đã tải %s Chọn từ tệp @@ -345,7 +347,7 @@ Tệp đã tải xuống Vai chính Vai phụ - Diễn viên quần chúng + Lý lịch Nguồn Ngẫu nhiên Sắp có… @@ -389,9 +391,9 @@ Bạn muốn xem gì Hoàn tất Tiện ích mở rộng - Thêm kho nguồn phim - Tên kho nguồn phim (Tùy chọn) - URL kho nguồn phim hoặc Mã ngắn + Thêm kho tiện ích + Tên kho tiện ích (Tùy chọn) + URL kho tiện ích hoặc Mã ngắn Tiện ích mở rộng đã tải Tiện ích mở rộng đã xoá Không tải được %s @@ -402,22 +404,24 @@ Tải xuống hàng loạt tiện ích mở rộng tiện ích mở rộng - Việc này sẽ xóa tất cả tiện ích mở rộng trong kho nguồn phim - Xoá kho nguồn phim + Việc này sẽ xóa tất cả tiện ích mở rộng trong kho tiện ích + Xoá kho tiện ích Tải xuống danh sách các trang web bạn muốn sử dụng Đã tải xuống: %d Đã vô hiệu: %d Chưa tải xuống: %d - CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. \n \nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. - Xem các kho nguồn phim của cộng đồng + CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. +\n +\nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. + Xem kho lưu trữ của cộng đồng Danh sách công khai In hoa toàn bộ phụ đề Cảnh báo: CloudStream không chịu trách nhiệm về các tiện ích mở rộng bên thứ ba và không cung cấp bất kỳ sự hỗ trợ nào! - %s (Đã vô hiệu) - Âm thanh & video + %s (Đã vô hiệu hoá) + Âm thanh & Độ phân giải Âm thanh - Video - Khởi động lại ứng dụng để thấy các thay đổi. + Độ phân giải video + Khởi động lại ứng dụng để thấy câc thay đổi. Chế độ an toàn được bật Tất cả tiện ích mở rộng đã được tắt do ứng dụng bị ngừng bất thường để giúp bạn tìm ra vấn đề gây lỗi. Xem thông tin sự cố @@ -426,11 +430,11 @@ Tự động tải xuống tiện ích mở rộng Làm lại tiến trình thiết lập Trình cài đặt APK - Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử chọn chế độ tương thích cũ nếu các bản cập nhật không thể cài đặt. + Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử chọn chế độ tương thích cũ nếu các bản cập nhật không cài đặt. %1$s %2$d%3$s Phát Trailer - Tự động cài đặt tất cả tiện ích mở rộng chưa được cài đặt từ những kho nguồn phim đã thêm. - Cập nhật đã bắt đầu + Tự động cài đặt tất cả tiện ích mở rộng chưa được cài đặt từ những kho tiện ích đã thêm. + Bắt đầu cập nhật Liên kết Danh sách HLS Trình phát ưu tiên @@ -469,8 +473,8 @@ Danh đề Giới thiệu Xoá lịch sử - Hiển thị cửa sổ bật lên của bỏ qua giới thiệu cho mở đầu/kết thúc - Văn bản quá dài. Không thể lưu vào bảng nhớ tạm. + Hiện các popup bỏ qua cho mở đầu/kết thúc + Văn bản quá dài. Không thể lưu vào bộ nhớ tạm. Xoá khỏi đã xem Bạn có chắc muốn thoát? @@ -484,7 +488,8 @@ Chế độ tương thích cũ Đã cập nhật (Mới đến Cũ) Đã cập nhật (Cũ đến Mới) - Thư viện của bạn đang trống :( \nĐăng nhập vào tài khoản thư viện hoặc thêm phim vào thư viện cục bộ. + Thư viện của bạn đang trống :( +\nĐăng nhập vào tài khoản thư viện hoặc thêm phim vào thư viện cục bộ. Mở bằng Siêu dữ liệu không được cung cấp bởi trang web, video sẽ không tải được nếu nó không tồn tại trên trang web. Trình cài đặt gói @@ -512,7 +517,8 @@ Dừng Bỏ chặn nhà mạng Đã bỏ đăng ký %s - Tìm thấy tệp Safe mode! \nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. + Tìm thấy tệp Safe mode! +\nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. Hoàn tác Đang cập nhật các phim đã đăng kí Bỏ chặn các URL gốc của GitHub bằng jsDelivr. Có thể làm cập nhật bị trễ vài ngày. @@ -529,18 +535,28 @@ Hồ sơ Trợ giúp Nền hồ sơ - Tại đây bạn có thể thay đổi cách sắp xếp các nguồn. Nếu video có mức độ ưu tiên cao hơn thì video đó sẽ xuất hiện cao hơn trong lựa chọn nguồn. Tổng ưu tiên nguồn và ưu tiên chất lượng là ưu tiên video. \n \nNguồn A: 3 \nChất lượng B: 7 \nSẽ có mức độ ưu tiên video kết hợp là 10. \n \nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! + Tại đây bạn có thể thay đổi cách sắp xếp các nguồn. Nếu video có mức độ ưu tiên cao hơn thì video đó sẽ xuất hiện cao hơn trong lựa chọn nguồn. Tổng ưu tiên nguồn và ưu tiên chất lượng là ưu tiên video. +\n +\nNguồn A: 3 +\nChất lượng B: 7 +\nSẽ có mức độ ưu tiên video kết hợp là 10. +\n +\nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! Chất lượng Bạn đã bình chọn - Tắt - Không tìm thấy kho nguồn phim, hãy kiểm tra URL và thử lại với VPN + Vô hiệu hoá + Không tìm thấy kho tiện ích, hãy kiểm tra URL và thử lại với VPN Không tìm thấy tiện ích mở rộng Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s Chọn chế độ lọc tiện ích mở rộng tải xuống %s đã xóa khỏi mục yêu thích Yêu thích %s đã thêm vào mục yêu thích - Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: \n \n%s \n \nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động? + Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: +\n +\n%s +\n +\nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động? Tần suất sao lưu Đã tìm thấy bản sao tiềm năng Khóa hồ sơ @@ -553,8 +569,10 @@ Thêm vào Đăng ký Xóa khỏi mục yêu thích - Chọn 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? + Ai đang xem + Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%s.\' +\n +\nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động? Nhập mã PIN PIN Nhập mã PIN hiện tại @@ -565,7 +583,7 @@ Quản lý hồ sơ Chỉnh sửa hồ sơ Tải lại liên kết - Tìm kiếm trong tiện ích mở rộng khác + Tìm kiếm tiện ích mở rộng khác Hiển thị đề xuất Kiểm tra tất cả Tiện ích mở rộng Xoay @@ -575,7 +593,7 @@ Bật tự động xoay màn hình theo hướng của video Tự động xoay đã sao chép! - Lỗi truy cập Bảng nhớ tạm, Vui lòng thử lại. + Lỗi truy cập Bộ nhớ tạm, Vui lòng thử lại. Lỗi sao chép, Vui lòng sao chép logcat và liên hệ hỗ trợ ứng dụng. Yêu thích OK @@ -595,17 +613,17 @@ Bài kiểm tra này chỉ dành cho các nhà phát triển và không xác nhận hay phủ nhận việc hoạt động của bất kỳ tiện ích mở rộng nào. Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn Phương tiện - Tên và URL kho nguồn phim + Tên và URL kho tiện ích Đặt lại Để đảm bảo quá trình tải xuống và thông báo cho các chương trình truyền hình đã đăng ký không bị gián đoạn, CloudStream cần có quyền chạy ở chế độ nền. Bằng cách nhấn OK, một hộp thoại yêu cầu sẽ hiển thị. Vui lòng nhấn \"Cho phép\".\n\nXin lưu ý, quyền này không có nghĩa là CS3 sẽ làm hao pin của bạn. Nó sẽ chỉ hoạt động ở chế độ nền khi cần thiết, chẳng hạn như khi nhận được thông báo hoặc tải xuống video từ các tiện ích mở rộng chính thức. Mùa %1$d Tập %2$d sẽ được phát hành vào - Sắp ra mắt sau %s + Sắp tới sau %s Chọn thiết bị truyền Bảo mật Tài khoản Mã QR Bỏ qua - Mở kho nguồn phim + Mở kho tiện ích CloudStream Wiki Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên Mã PIN đã hết hạn! @@ -613,7 +631,7 @@ Không lấy được mã PIN, vui lòng thử xác thực cục bộ Không có tải xuống nào. Xác thực cục bộ - Truyền nguồn thay thế + Phản chiếu màn hình Phát từ đầu Mở video có sẵn Cảnh báo @@ -624,10 +642,16 @@ Bỏ chọn tất cả Xoá các tệp Xoá (%1$d | %2$s) - Bạn có chắc chắn muốn xóa vĩnh viễn các mục sau không? \n \n%s - Bạn có chắc chắn muốn xóa vĩnh viễn các tập trong %1$s? \n \n%2$s + Bạn có chắc chắn muốn xóa vĩnh viễn các mục sau không? +\n +\n%s + Bạn có chắc chắn muốn xóa vĩnh viễn các tập trong %1$s? +\n +\n%2$s Bạn cũng sẽ xóa vĩnh viễn tất cả các tập trong loạt phim: \n \n%s - Bạn có chắc chắn muốn xóa vĩnh viễn tất cả các tập trong loạt phim này không? \n \n%s + Bạn có chắc chắn muốn xóa vĩnh viễn tất cả các tập trong loạt phim này không? +\n +\n%s Xóa tiện ích mở rộng Ngày phát hành (Cũ đến mới) Ẩn tên các nút điều khiển trình phát @@ -635,7 +659,7 @@ Xem trước trên thanh tua Chưa tải phụ đề nào Xác nhận trước khi thoát - Hiển thị hộp thoại xác nhận trước khi thoát ứng dụng + Hiện hộp thoại xác nhận trước khi thoát ứng dụng Không hiển thị Hiển thị Vị trí thư mục sao lưu @@ -644,8 +668,8 @@ Video này là Torrent, điều này có nghĩa là hoạt động video của bạn có thể được theo dõi.\nHãy đảm bảo rằng bạn hiểu về Torrent trước khi tiếp tục. Lỗi mã hóa Giải mã phần mềm cho phép phát các tệp video không được thiết bị của bạn hỗ trợ, nhưng có thể gây ra phản hồi chậm hoặc phát lại không ổn định ở độ phân giải cao. - Giải mã phần mềm - Khởi động lại ứng dụng và chấp nhận cửa sổ bật lên của Stream Torrent để tiếp tục. + Bộ giải mã ứng dụng + Khởi động lại ứng dụng và chấp nhận cửa sổ Stream Torrent để tiếp tục. Kích hoạt torrent trong Cài đặt/Nguồn phim/Thể loại ưu tiên Tải phụ đề đầu tiên có sẵn Âm thanh @@ -685,7 +709,7 @@ Đến mục Tải xuống Không có kết nối Internet. \n\nVui lòng kết nối Internet rồi thử lại, hoặc xem các nội dung đã tải xuống khi đang ngoại tuyến. Thay đổi khung hiển thị màn hình - Quét chồng lấn + Vượt khung Thay đổi kích thước poster Kích thước poster Tăng tốc độ phát khi nhấn giữ @@ -700,12 +724,12 @@ URL hoặc ảnh không hợp lệ Đã cập nhật ảnh thành công Đánh dấu là đã xem đến tập này - Xóa đã xem đến tập này + Xóa những tập đã xem đến tập này Đã tải lại Tải lại nguồn phim Tên Độ phân giải và tên - Phát nguồn thay thế" + Phản chiếu màn hình" Căn chỉnh phụ đề Dưới trái Dưới giữa @@ -725,13 +749,13 @@ Cài đặt bản phát hành trước thất bại. Tập Bật bộ lọc độ sáng khi độ sáng màn hình vượt quá 100% - Hiển thị bảng dàn diễn viên + Hiển thị bảng diễn viên Thông tin video Độ sáng bổ sung Tên nguồn Hàng đợi tải xuống Không có tải xuống đang chờ nào. - Quyết định cách sắp xếp các nguồn video trong trình phát + Quyết định cách sắp xếp các nguồn video trong trình phát. Ưu tiên nguồn Tải xuống tất cả Hủy tất cả @@ -745,7 +769,4 @@ Đã bật độ sáng bổ sung Hiển thị lớp phủ siêu dữ liệu trình phát - Trực tiếp - Video - Xem trước diff --git a/app/src/main/res/values-b+zh+TW/strings.xml b/app/src/main/res/values-b+zh+TW/strings.xml index cfd8adf05..e5879ae37 100644 --- a/app/src/main/res/values-b+zh+TW/strings.xml +++ b/app/src/main/res/values-b+zh+TW/strings.xml @@ -19,7 +19,8 @@ 速度(%.2fx) 評分:%.1f - 發現新版本! \n%1$s -> %2$s + 發現新版本! +\n%1$s -> %2$s 填充 %d 分鐘 CloudStream @@ -186,8 +187,10 @@ 繼續 -30 +30 - 這將永遠刪除 %s \n你確定嗎? - 剩下 \n%d 分鐘 + 這將永遠刪除 %s +\n你確定嗎? + 剩下 +\n%d 分鐘 連載中 已完結 狀態 @@ -410,7 +413,9 @@ 已停用:%d 未下載:%d 已更新 %d 外掛程式 - CloudStream 預設沒有安裝網站。你需要從儲存庫安裝網站。 \n \n加入我們的 Discord 或在網路上搜尋。 + CloudStream 預設沒有安裝網站。你需要從儲存庫安裝網站。 +\n +\n加入我們的 Discord 或在網路上搜尋。 查看 公開清單 字幕全大寫 @@ -498,7 +503,8 @@ 按字母順序(A 到 Z) 按字母順序(Z 到 A) 選擇媒體庫 - 找到安全模式檔案! \n在刪除此檔案之前,將不會在啟動時載入任何擴充功能。 + 找到安全模式檔案! +\n在刪除此檔案之前,將不會在啟動時載入任何擴充功能。 日誌 失敗 通過 @@ -516,7 +522,8 @@ 還原 無法存取 GitHub。 正在開啟 jsDelivr proxy… 使用 jsDelivr 繞過直接使用 GitHub 網址時的存取封鎖。 可能導致更新延遲數天。 - 您的媒體庫是空的 :( \n登入媒體庫帳號或將節目新增到您本機的媒體庫。 + 您的媒體庫是空的 :( +\n登入媒體庫帳號或將節目新增到您本機的媒體庫。 您的媒體庫是空的。可嘗試以不同的帳號登入。 正在更新訂閱節目 備份頻率 @@ -524,7 +531,11 @@ 我的最愛 %s 已加入我的最愛 以 %s 的身分登入 - 您的媒體庫中似乎有多個重覆的項目: \n \n%s \n \n您要強制加入、取代已有項目、還是取消操作? + 您的媒體庫中似乎有多個重覆的項目: +\n +\n%s +\n +\n您要強制加入、取代已有項目、還是取消操作? 輸入 %s 的 PIN 碼 行動數據 找到可能重覆的項目 @@ -556,11 +567,19 @@ 找不到資源庫,請檢查網址與 VPN 設定 您已完成投票 在資源庫中找不到外掛程式 - 您的媒體庫中似乎有重覆的項目:「%s」。 \n \n您要強制加入、取代已有項目、還是取消操作? + 您的媒體庫中似乎有重覆的項目:「%s」。 +\n +\n您要強制加入、取代已有項目、還是取消操作? 設回預設 輸入 PIN 碼 PIN 碼 - 您可在此調整來源的排序方式。具有愈小的優先值的影片,在來源選擇中顯示得愈前面。來源優先值與品質優先值的加總就是影片優先值。 \n例如: \n來源 A:3 \n品質 B: 7 \n則該來源的影片優先值為 10。 \n \n注意:如果加總達到 10 或更高,則載入該連結時播放器將自動跳過載入! + 您可在此調整來源的排序方式。具有愈小的優先值的影片,在來源選擇中顯示得愈前面。來源優先值與品質優先值的加總就是影片優先值。 +\n例如: +\n來源 A:3 +\n品質 B: 7 +\n則該來源的影片優先值為 10。 +\n +\n注意:如果加總達到 10 或更高,則載入該連結時播放器將自動跳過載入! 輸入目前的 PIN 碼 顯示切換畫面方向的按鈕 選擇篩選外掛程式下載的模式 @@ -580,7 +599,8 @@ 顯示推薦 在播放器中新增速度選項 即將在 %s 推出 - %s \n剩餘 + %s +\n剩餘 測試所有擴充功能 停用電池優化 有聲書 @@ -744,7 +764,4 @@ 軟體解碼使程式可以播放裝置不支援的影片,但可能導致播放高解析的影片時的延遲或不穩定。 音量已超過 100% 顯示播放器元資料遮罩層 - 影片 - 預覽 - 播放中 diff --git a/app/src/main/res/values-b+zh/strings.xml b/app/src/main/res/values-b+zh/strings.xml index 56ec8f43e..de0cb834a 100644 --- a/app/src/main/res/values-b+zh/strings.xml +++ b/app/src/main/res/values-b+zh/strings.xml @@ -19,7 +19,8 @@ 速度(%.2fx) 评分:%.1f - 发现新版本! \n%1$s -> %2$s + 发现新版本! +\n%1$s -> %2$s 填充 %d 分钟 CloudStream @@ -187,8 +188,10 @@ 继续 -30 +30 - 这将永久删除 %s \n您确定吗? - %d 分钟 \n剩余 + 这将永久删除 %s +\n您确定吗? + %d 分钟 +\n剩余 连载中 已完结 状态 @@ -411,7 +414,9 @@ 已禁用:%d 未下载:%d 已更新 %d 插件 - CloudStream 默认不安装片源。您需要从仓库中安装片源。 \n \n加入我们的 Discord 或在网上搜索。 + CloudStream 默认不安装片源。您需要从仓库中安装片源。 +\n +\n加入我们的 Discord 或在网上搜索。 查看社区仓库 公开列表 字幕全大写 @@ -481,7 +486,8 @@ 应用退出后将会更新 插件已下载 从已观看中移除 - 发现安全模式文件! \n启动时不加载任何扩展,直到文件被删除。 + 发现安全模式文件! +\n启动时不加载任何扩展,直到文件被删除。 浏览器 排序方式 @@ -494,7 +500,8 @@ 字母排序(从 Z 到 A) 选择库 打开方式 - 您的库是空的 :( \n登录库账户或添加节目到您的本地库。 + 您的库是空的 :( +\n登录库账户或添加节目到您的本地库。 此列表是空的,请尝试切换到另一个。 播放器显示 - 快进快退秒数 播放器可见时使用的快进快退秒数 @@ -527,7 +534,13 @@ 配置文件 帮助 移动流量 - 在这里,您可以更改源的排序方式。如果视频具有更高的优先级,它将在源选择中显示得更高。源优先级和质量优先级的总和就是视频优先级。 \n \n来源 A:3 \n质量 B: 7 \n组合视频优先级为 10。 \n \n注意:如果总和为 10 或更多,则加载该链接时播放器将自动跳过加载! + 在这里,您可以更改源的排序方式。如果视频具有更高的优先级,它将在源选择中显示得更高。源优先级和质量优先级的总和就是视频优先级。 +\n +\n来源 A:3 +\n质量 B: 7 +\n组合视频优先级为 10。 +\n +\n注意:如果总和为 10 或更多,则加载该链接时播放器将自动跳过加载! 质量 个人资料背景 PIN @@ -557,8 +570,14 @@ 您已投票 %s已添加到收藏夹 %s已从收藏夹中删除 - 您的资料库中似乎已经存在一个可能相同的项目:\'%s.\' \n \n您想添加该项目、替换现有项目还是取消操作? - 在您的资料库中发现了潜在的重复项目: \n \n%s \n \n您想添加此项目、替换现有项目还是取消操作? + 您的资料库中似乎已经存在一个可能相同的项目:\'%s.\' +\n +\n您想添加该项目、替换现有项目还是取消操作? + 在您的资料库中发现了潜在的重复项目: +\n +\n%s +\n +\n您想添加此项目、替换现有项目还是取消操作? 确认PIN 输入来自 %s 的 PIN 码 锁定个人资料 @@ -578,7 +597,8 @@ 解锁 CloudStream 使用生物识别技术锁定 密码或 PIN 验证 - %s \n剩余 + %s +\n剩余 测试所有扩展 已复制! 访问剪贴板出错,请重试。 @@ -624,10 +644,18 @@ 全不选 删除文件 删除 (%1$d | %2$s) - 您确定要永久删除以下项目吗? \n \n%s - 您确定要永久删除 %1$s中的下述剧集吗? \n \n%2$s - 您还将永久删除下述系列中的所有剧集: \n \n%s - 您确定要永久删除下述系列的所有剧集吗? \n \n%s + 您确定要永久删除以下项目吗? +\n +\n%s + 您确定要永久删除 %1$s中的下述剧集吗? +\n +\n%2$s + 您还将永久删除下述系列中的所有剧集: +\n +\n%s + 您确定要永久删除下述系列的所有剧集吗? +\n +\n%s 发布日期(从新至旧) 无法获取设备 PIN 码,尝试本地身份验证 PIN 码现已过期! @@ -744,7 +772,4 @@ 确定在播放器中如何排列视频源的顺序 已启用额外亮度 显示播放器元数据遮罩层 - 视频 - 预览 - 播放中 diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 3d9200faf..1a47a6a63 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -34,7 +34,7 @@ Шукаць па %s… Распазнаванне маўлення недаступна Пачніце размаўляць… - Няма дадзеных + Няма даных Больш параметраў Наступная серыя Жанры @@ -128,7 +128,7 @@ \@string/home_play Для карэктнай працы гэтага пастаўшчыка можа спатрэбіцца VPN Гэты пастаўшчык — Torrent, рэкамендуецца VPN - Вэб-сайт не пастаўляе метададзеных, загрузіць відэа не ўдасца, калі на сайце яго няма. + Вэб-сайт не пастаўляе метаданых, загрузіць відэа не ўдасца, калі на сайце яго няма. Апісанне Сюжэту не знойдзена Апісання не знойдзена @@ -159,12 +159,12 @@ Выкарыстоўваць сістэмную яркасць у прайгравальніку замест цёмнага накладання Абнаўляць працэс прагляду Аўтаматычна сінхранізаваць прагрэс бягучай серыі - Аднавіць дадзеныя з рэзервовай копіі - Рэзервовае капіраванне дадзеных + Аднавіць даныя з рэзервовай копіі + Рэзервовае капіраванне даных Частата рэзервовага капіравання Загружаны файл рэзервовай копіі - Не ўдалося аднавіць дадзеныя з файла %s - Дадзеныя захаваны + Не ўдалося аднавіць даныя з файла %s + Даныя захаваны Няма дазволу да сховішча. Паспрабуйце ізноў. Памылка пры рэзервовым капіраванні %s Пошук @@ -247,7 +247,7 @@ Сціслы агляд у чарзе Субцітраў няма - Прадвызначаны + Прадвызначанае Свабодна Ужыта Праграма @@ -315,7 +315,7 @@ Прапусціць гэта абнаўленне Абнавіць Прыярытэтная якасць прагляду (WiFi) - Прыярытэтная якасць прагляду (Мабільная перадача дадзеных) + Прыярытэтная якасць прагляду (Мабільная перадача даных) Максімальная колькасць сімвалаў у загалоўку праглядальніка Паказваць інфармацыю ў прайгравальніку Памер буфера відэа @@ -458,7 +458,7 @@ Раздзяляльнасць Звесткі пра медыя Памылковы ID - Памылковыя дадзеныя + Памылковыя даныя Памылковы URL-адрэс Памылка Прыбраць схаваныя цітры з субцітраў @@ -480,7 +480,7 @@ Назва рэпазіторыя (неабавязкова) URL-адрас рэпазіторыя або кароткі код Убудова загружана - Убудова спампавана + Убудава спампавана Убудова выдалена Не ўдалося загрузіць %s 18+ @@ -499,7 +499,7 @@ Спампавана: %d Выключана: %d Не спампавана: %d - Абноўлена %d убудоў + Абноўлена %d плагіна(ў) Прадвызначана на CloudStream няма ўсталяваных вэб-сайтаў. Вам трэба ўсталяваць вэб-сайты з рэпазіторыяў. \n \nДалучыцеся да нашага сервера Discord або пашукайце ў сетцы. Праглядзець рэпазіторыі ад супольнасці Публічны спіс @@ -595,7 +595,7 @@ Адпісацца Профіль %d Wi-Fi - Мабільная перадача дадзеных + Мабільная перадача даных Выбраць як прадвызначаны Выкарыстоўваць Рэдагаваць @@ -643,7 +643,7 @@ Праверка сапраўднасці біяметрыяй не падтрымліваецца на гэтай прыладзе Разблакіруйце праграму адбіткам пальца, Face ID, PIN-кодам, узорам разблакіроўкі або паролем. Праз некалькі няўдалых спроб акно з запытам закрыецца. Проста перазапусціце праграму, каб паўтарыць спробу. - Вашы дадзеныя CloudStream былібылі толькі што зарэзерваваныя. Нягледзячы на тое, што магчымасць вельмі маленькая, усе прылады могуць паводзіць сябе па-рознаму. У рэдкасным выпадку, калі вы страціце доступ да праграмы, поўнасцю ачысціце даныя і аднавіце іх праз рэзервовую копію. Выбачайце за любую нязручнасць, якая можа з гэтага атрымацца. + Вашы даныя CloudStream былі зарэзерваваныя. Нягледзячы на тое, што магчымасць вельмі маленькая, усе прылады могуць паводзіць сябе па-рознаму. У рэдкасным выпадку, калі вы страціце доступ да праграмы, поўнасцю ачысціце даныя і аднавіце іх праз рэзервовую копію. Выбачайце за любую нязручнасць, якая можа з гэтага атрымацца. Скінуць CloudStream-Вікі Наведайце %s на вашым смартфоне або камп\'ютары і ўвядзіце код вышэй @@ -735,8 +735,5 @@ %d спампоўванняў у чарзе %d спампоўванняў у чарзе - Паказваць накладанне з метададзенымі ў прайгравальніку - Відэа - Перадпрагляд - Ужывую + Паказваць накладанне з метаданымі ў прайгравальніку diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index a13f87bfa..51d2379de 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -75,7 +75,7 @@ E disponueshme për shikim offline Zgjidh të gjitha Çzgjidh të gjitha - Përditësimi filloi + Update-i filloi Transmetim në rrjet Hap video lokale Gabim gjatë ngarkimit të lidhjeve @@ -83,7 +83,7 @@ Memorie e brendshme Dublim Titra - Fshi skedarin + Fshi Skedarin Luaj skedarin Rifillo shkarkimin Ndalo shkarkimin @@ -224,8 +224,8 @@ E Nuk u gjet asnjë episod Fshij - Fshi skedarin - Fshi skedarët + Fshij skedarin + Fshij skedarët Fshij (%1$d | %2$s) Anulo Ndalo @@ -309,7 +309,7 @@ Teksti i episodit Zgjidh elementët e ndërfaqes mbi poster Nuk u gjet asnjë përditësim - Kontrollo për përditësime + Kontrollo për përditësim Çelës Ndrysho madhësinë Burimi @@ -494,245 +494,4 @@ Të gjitha %s janë shkarkuar tashmë Asnjë shtesë nuk u gjet në repository Repository nuk u gjet, verifiko URL-në ose provoje me VPN - Të gjitha shtesat u ndaluan për shkak të një gabimi për të ndihmuar në gjetjen e shkaktarit. - Lista publike - CloudStream nuk vjen me faqe të instaluara. Duhet ti shtosh faqet nga repository-t.\n\nNa u bashko në Discord ose kërko në internet. - Autorët - shtesë - Titrat vetëm me shkronja të mëdha - Luajtësi i preferuar i videove - Përshkrimi - Shiko repository-t e komunitetit - Modaliteti i sigurt është aktive - Të pa shkarkuara: %d - Instalo shtesën para - Rifillo aplikacionin për të parë ndryshimet. - Çaktivizuar: %d - Pauzë - Gjurmë - Shkarkimi në grup përfundoi - Kjo do të heqë të gjitha shtesat - Gjuha - Rifillo - Fshi shtesën - Madhësia - Shiko të dhënat e gabimit - Të suportuara - Shkarkuar: %d - Gjurmë video-je - U përditësuan %d shtesa - Lista e HLS - shtesa - Vlerësimi: %s - Paralajmërim: CloudStream nuk mban asnjë përgjegjësi për përdorimin e shtesave të palëve të treta dhe nuk ofron asnjë mbështetje për to! - Statusi - Fshi repository-n - Versioni - %s (Çaktivizuar) - Shkarko listën e faqeve që dëshiron të përdorësh - Gjurmë audio-je - Video - Luajtësi i integruar - Pyet gjithmonë - Zgjidh pajisjen për transmetim - Aplikacioni nuk u gjet - Të gjitha gjuhët - Kalo %s - Hapjen - Mbylljen - Përmbledhjen - Mbylljen e përzier - Hapjen e përzier - Kreditet - Parashikimin - Intron - Pastro historikun - Historia - Shfaq butonat për kapërcimin e hyrjes/mbylljes - Teksti është shumë i gjatë. Nuk mund të ruhet në Clipboard. - Gabim gjatë aksesimit të Clipboard-it. Ju lutem provoni përsëri. - Kopjimi dështoi. Kopjo logcat-in dhe kontakto support-in e aplikacionit. - Shënoje si e përfunduar - Hiqe nga të shikuarat - Jeni i sigurt që dëshironi të dilni? - Po - Jo - Në rregull - Largo - Hap repository-n - Çaktivizo kursimin e baterisë - Për të siguruar shkarkime të pandërprera dhe njoftime për serialet e abonuar, CloudStream ka nevojë për leje për të funksionuar në sfond. Duke shtypur OK, do t’ju shfaqet një dialog kërkese. Ju lutem shtypni “Lejo”.\n\nJu lutemi vini re: kjo leje nuk do të thotë që CS3 do të harxhojë baterinë tuaj. Do të funksionojë në sfond vetëm kur është e nevojshme, si p.sh. kur merr njoftime ose shkarkon video nga shtesat zyrtare. - Përdorimi i baterisë është i vendosur si i pakufizuar - Informacioni i aplikacionit CloudStream nuk mund të hapet. - Po shkarkohet përditësimi i aplikacionit… - Po instalohet përditësimi i aplikacionit… - Instalimi i versionit të ri dështoi - Version i vjetër - Instaluesi i paketës - Aplikacioni do të përditësohet sapo të mbyllet - Rendit nga - Rendit - Vlerësimi (nga më i larti te më i ulëti) - Vlerësimi (nga më i ulëti te më i larti) - Përditësuar (nga më i riu te më i vjetri) - Përditësuar (nga më i vjetri te më i riu) - Alfabetike (A–Z) - Alfabetike (Z–A) - Episodet (rritëse) - Episodet (zbritëse) - Vlerësimi (më i larti) - Vlerësimi (më i ulëti) - Data e publikimit (më e fundit) - Data e publikimit (më e hershmja) - Ep %s - Vlerësimi %s - Data %s - Hape me - Kjo listë është bosh. Provo një tjetër. - U gjet skedari i modalitetit të sigurt!\nNuk do të ngarkohen asnjë shtesë gjatë nisjes derisa skedari të hiqet. - Riktheje në gjendjen e mëparshme - Duke përditësuar serialet që ndiqni - I abonuar - I abonuar në %s - U çabonove nga %s - Episodi %d u publikua! - Abonohu - Çabonohu - Profili %d - Wi-Fi - Mobile data - Vendose si parazgjedhje - Përdor - Ndrysho - Prioriteti i burimit - Vendos si duhet të renditen burimet e videos në luajtës - Profilet - Ndihmë - Këtu mund të ndryshoni si renditen burimet. Nëse një video ka prioritet më të lartë, do të shfaqet më lart në përzgjedhjen e burimeve. Shuma e prioritetit të burimit dhe prioritetit të cilësisë është prioriteti i videos.\n\nBurimi A: 3\nCilësia B: 7\nDo të ketë një prioritet të kombinuar video-je prej 10.\n\nSHËNIM: Nëse shuma është 10 ose më shumë, luajtësi do të kapërcejë automatikisht ngarkimin kur të hapet ai link! - Cilësitë - Sfondi i profilit - UI nuk mundi të krijohej siç duhet, ky është një PROBLEM SERIOZ dhe duhet raportuar menjëherë %s - Ju keni votuar - Të preferuarat - %s u shtua te të preferuarat - %s u hoq nga të preferuarat - Shto te të preferuarat - Hiqe nga të preferuarat - U gjet dublikatë e mundshme - Shto - Zëvendëso - Zëvendëso të gjitha - Duket se një artikull i mundshëm i dyfishuar tashmë ekziston në bibliotekën tuaj: ‘%s’.\n\nDëshironi ta shtoni gjithsesi, ta zëvendësoni atë ekzistuesin, apo ta anuloni veprimin? - Janë gjetur artikuj të mundshëm të dyfishtë në bibliotekën tuaj:\n\n%s\n\nDëshironi ta shtoni gjithsesi, të zëvendësoni ato ekzistuesit, apo të anuloni veprimin? - Fut PIN-in - Fut PIN-in për %s - Fut PIN-in aktual - Kyç profilin - PIN - PIN-i i gabuar. Ju lutem provoni përsëri. - PIN-i duhet të ketë 4 karaktere - Zgjidh një Llogari - Asnjë llogari - Menaxho Llogaritë - Ndrysho llogarinë - I identifikuar si %s - Kapërce përzgjedhjen e llogarisë gjatë nisjes - Përdor llogarinë e paracaktuar - Rrotullo - Shfaq një buton për ndryshimin e orientimit të ekranit - Aktivizo rrotullimin automatik të ekranit sipas orientimit të videos - Rrotullim automatik - I preferuar - Hiq nga të preferuarat - Hap CloudStream-in - Kyç me biometrikë - Verifikim me fjalëkalim/PIN - Biometrikat nuk janë të disponueshme në këtë pajisje - Hap aplikacionin me gjurmë gishti, Face ID, PIN, Pattern ose fjalëkalim. - Pas disa përpjekjesh të dështuara, rifillo aplikacionin për ta provuar përsëri. - Të dhënat tuaja të CloudStream janë kopjuar tani. Edhe pse mundësia është shumë e ulët, disa pajisje mund të sillen ndryshe. Në raste të rralla, nëse bllokoheni nga hyrja në aplikacion, fshini plotësisht të dhënat e aplikacionit dhe rikthejini ato nga kopja rezervë. Na vjen shumë keq për çdo shqetësim që mund të shkaktohet nga kjo. - Rivendos - CloudStream Wiki - Vizitoni %s në telefonin ose kompjuterin tuaj dhe futni kodin e mësipërm - Nuk u mor kodi PIN i pajisjes, provo verifikim lokal - PIN-i ka skaduar! - Kodi skadon pas %1$dm %2$ds - Data e publikimit (nga më e reja te më e vjetra) - Data e publikimit (nga më e vjetra te më e reja) - Fshih emrat e kontrolleve të luajtësit - Shiriti i kërkimit - Aktivizo miniaturën e pamjes paraprake në shiritin kërkues - Nuk ka titra të ngarkuara ende - Vendndodhja e dosjes së kopjes rezervë - I personalizuar - Konfirmo para daljes - Shfaq dialogun para daljes nga aplikacioni - Shfaqe - Mos e shfaq - Madhësia e konturit - Aktivizo torrent-in në Cilësimet/Ofruesit/Media e preferuar - Rifillo aplikacionin dhe prano dritaren “Stream Torrent” për të vazhduar. - Dekodim me softuer - Dekodimi me softuer i lejon player-it të luajë video që nuk mbështeten nga pajisja juaj, por mund të shkaktojë vonesa ose paqëndrueshmëri në rezolucione të larta. - Volumi është mbi 100% - Rrëshqit lart përsëri për të kaluar mbi 100% - Përditëso shtesat - Përditëso shtesat manualisht - Filloi procesi i përditësimit të shtesave! - %d Shtes(at) u përditësuan me sukses! - Nuk u përditësua asnjë shtesë. - Njoftimet e luajtësit - Njoftimi për kontrollin e luajtjes nga sfondi - I integruar - Online - Bëji të gjitha titrat me shkronja të trasha - Bëji të gjitha titrat me shkronja të pjerrëta - Rrezja e sfondit - Sa elementë të ndryshëm mund të shkarkohen njëkohësisht - Shkarkimet paralele - Lidhje paralele - Numri i lidhjeve paralele për çdo shkarkim - Shko tek Shkarkimet - Nuk ka lidhje interneti.\n\nJu lutemi lidhuni me internetin dhe provojeni përsëri, ose shikoni shkarkimet tuaja offline. - Ndryshon kufijtë e ekranit - Tejskenim - Ndryshon madhësinë e posterave - Madhësia e posterit - Ndërrimi i shpejtësisë me shtypje të gjatë - Mbaj shtypur për shpejtësi 2x - Ndrysho foton e profilit - Fut URL-n e fotos së profilit - Nuk u gjet asnjë URL - URL ose imazh i pavlefshëm - Imazhi u përditësua me sukses - Shëno si të parë deri në këtë episod - Hiq të parët deri në këtë episod - U ringarkuan - Ringarko ofruesin - Emri - Emri i burimit - Rezolucioni dhe emri - Shkarko të gjitha - Anulo të gjitha - Dëshiron të shkarkosh episodin %s? - Dëshiron të anulosh të gjitha shkarkimet në radhë? - Pozicionimi i titrave - Poshtë majtas - Poshtë në mes - Poshtë djathtas - Në mes majtas - Në mes në qendër - Në mes djathtas - Lart majtas - Lart në mes - Lart djathtas - - %d shkarkim aktiv - %d shkarkime aktive - - - %d shkarkim në radhë - %d shkarkime në radhë - - Live diff --git a/build.gradle.kts b/build.gradle.kts index 609a94b3a..e35c1f611 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,6 @@ plugins { alias(libs.plugins.dokka) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.multiplatform) apply false - alias(libs.plugins.kotlin.serialization) apply false } allprojects { diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar-SA/short_description.txt index e56a2e8be..7ccd9743b 100644 --- a/fastlane/metadata/android/ar-SA/short_description.txt +++ b/fastlane/metadata/android/ar-SA/short_description.txt @@ -1 +1 @@ -بث وتحميل الأفلام, المسلسلات التلفزيونية والأنمي. +بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية. diff --git a/fastlane/metadata/android/pt/short_description.txt b/fastlane/metadata/android/pt/short_description.txt index 08ad5a778..d0392f34b 100644 --- a/fastlane/metadata/android/pt/short_description.txt +++ b/fastlane/metadata/android/pt/short_description.txt @@ -1 +1 @@ -Transmita e descarga filmes, séries de TV e anime. +Transmita e transfira filmes, séries de TV e anime. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80e342c2c..a97145c3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,8 @@ androidGradlePlugin = "9.1.1" animeDb = "1.0.2" annotation = "1.10.0" appcompat = "1.7.1" -biometric = "1.4.0-alpha07" -buildkonfigGradlePlugin = "0.21.2" +biometric = "1.4.0-alpha06" +buildkonfigGradlePlugin = "0.18.0" coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later colorpicker = "6b46b49" conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything @@ -17,26 +17,22 @@ desugar_jdk_libs_nio = "2.1.5" dokkaGradlePlugin = "2.2.0" espressoCore = "3.7.0" fragmentKtx = "1.8.9" -instancioCore = "5.6.0" +fuzzywuzzy = "1.4.0" jacksonModuleKotlin = { strictly = "2.13.1" } # Later versions don't support minSdk <26 (Crashes on Android TV's and FireSticks) -json = "20260522" +json = "20251224" jsoup = "1.22.1" junit = "4.13.2" junitKtx = "1.3.0" junitVersion = "1.3.0" juniversalchardet = "2.5.0" kotlinGradlePlugin = "2.3.20" -kotlinxAtomicfu = "0.33.0" kotlinxCollectionsImmutable = "0.4.0" -kotlinxCoroutinesCore = "1.11.0" -kotlinxDatetime = "0.8.0" -kotlinxSerializationJson = "1.11.0" -ktor = "3.5.0" +kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" -material = "1.14.0" +material = "1.14.0-beta01" media3 = "1.9.3" -navigationKtx = "2.9.8" -newpipeextractor = "v0.26.3" +navigationKtx = "2.9.7" +newpipeextractor = "v0.26.0" nextlibMedia3 = "1.9.3-0.12.0" nicehttp = "0.4.18" overlappingpanels = "0.1.5" @@ -60,9 +56,6 @@ minSdk = "23" compileSdk = "36" targetSdk = "36" -versionCode = "68" -versionName = "4.7.0" - [libraries] activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } anime-db = { module = "com.github.recloudstream:anime-db", version.ref = "animeDb" } @@ -81,20 +74,15 @@ desugar_jdk_libs_nio = { module = "com.android.tools:desugar_jdk_libs_nio", vers espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } -instancio-core = { group = "org.instancio", name = "instancio-core", version.ref = "instancioCore" } +fuzzywuzzy = { module = "me.xdrop:fuzzywuzzy", version.ref = "fuzzywuzzy" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } json = { module = "org.json:json", version.ref = "json" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit" } junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitKtx" } juniversalchardet = { module = "com.github.albfernandez:juniversalchardet", version.ref = "juniversalchardet" } -kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlinGradlePlugin" } -kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinxAtomicfu" } kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } -kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" } lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" } material = { module = "com.google.android.material:material", version.ref = "material" } @@ -137,7 +125,6 @@ buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfigG dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaGradlePlugin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm" , version.ref = "kotlinGradlePlugin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlinGradlePlugin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinGradlePlugin" } [bundles] coil = ["coil", "coil-network-okhttp"] diff --git a/library/build.gradle.kts b/library/build.gradle.kts index a1f30fede..073e49e64 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -12,7 +12,6 @@ plugins { alias(libs.plugins.android.multiplatform.library) alias(libs.plugins.buildkonfig) alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.serialization) } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) @@ -57,31 +56,12 @@ kotlin { implementation(libs.annotation) // Annotations implementation(libs.nicehttp) // HTTP Lib implementation(libs.jackson.module.kotlin) // JSON Parser - implementation(libs.kotlinx.atomicfu) implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.serialization.json) // JSON Parser - implementation(libs.ktor.http) + implementation(libs.fuzzywuzzy) // Match Extractors implementation(libs.jsoup) // HTML Parser implementation(libs.rhino) // Run JavaScript + implementation(libs.newpipeextractor) implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit - - // Deprecated; will be removed once extensions have time to migrate from using it - implementation("me.xdrop:fuzzywuzzy:1.4.0") - } - - commonTest.dependencies { - implementation(libs.kotlin.test) - } - - // We will eventually add a new jvmCommonMain source set - // for things shared between Android and JVM. - androidMain.dependencies { - implementation(libs.newpipeextractor) - } - - jvmMain.dependencies { - implementation(libs.newpipeextractor) } } } @@ -104,11 +84,6 @@ buildkonfig { "MDL_API_KEY", (System.getenv("MDL_API_KEY") ?: localProperties["mdl.key"]).toString() ) - - buildConfigField( - FieldSpec.Type.STRING, - "TRAKT_CLIENT_ID", (System.getenv("TRAKT_CLIENT_ID") ?: localProperties["trakt.id"]).toString() - ) } } diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.android.kt deleted file mode 100644 index 345aa7185..000000000 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.android.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.newAudioFile -import com.lagradost.cloudstream3.newSubtitleFile -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.newExtractorLink -import org.schabi.newpipe.extractor.stream.StreamInfo -import org.schabi.newpipe.extractor.stream.StreamType - -actual open class YoutubeExtractor actual constructor() : ExtractorApi() { - - actual override val mainUrl = "https://www.youtube.com" - actual override val name = "YouTube" - actual override val requiresReferer = false - - actual override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ) { - val videoId = extractYouTubeId(url) - val watchUrl = "$mainUrl/watch?v=$videoId" - - val info = StreamInfo.getInfo(watchUrl) - val isLive = info.streamType == StreamType.LIVE_STREAM - || info.streamType == StreamType.AUDIO_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_AUDIO_STREAM - - if (isLive && info.hlsUrl != null) { - callback( - newExtractorLink( - source = name, - name = "YouTube Live", - url = info.hlsUrl - ) { - type = ExtractorLinkType.M3U8 - } - ) - } else { - processVideo(info, subtitleCallback, callback) - } - } - - private suspend fun processVideo( - info: StreamInfo, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ): Boolean { - val videoStreams = info.videoOnlyStreams.orEmpty() - if (videoStreams.isEmpty()) return false - - val audioStreams = info.audioStreams.orEmpty() - videoStreams.forEach { video -> - callback( - newExtractorLink( - source = name, - name = "YouTube ${normalizeCodec(video.codec)}", - url = video.content - ) { - quality = video.height - audioTracks = audioStreams.map { newAudioFile(it.content) } - } - ) - } - - info.subtitles.forEach { subtitle -> - subtitleCallback( - newSubtitleFile( - lang = subtitle.displayLanguageName - ?: subtitle.languageTag - ?: "Unknown", - url = subtitle.content - ) - ) - } - - return true - } - - private fun extractYouTubeId(url: String): String { - val regex = Regex( - "(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})" - ) - - return regex.find(url)?.groupValues?.get(1) - ?: throw IllegalArgumentException("Invalid YouTube URL: $url") - } - - private fun normalizeCodec(codec: String?): String { - if (codec.isNullOrBlank()) return "" - val c = codec.lowercase() - return when { - c.startsWith("av01") -> "AV1" - c.startsWith("vp9") -> "VP9" - c.startsWith("avc1") || c.startsWith("h264") -> "H264" - c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265" - else -> codec.substringBefore('.').uppercase() - } - } -} diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index 2f9c9b628..975572d05 100644 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -10,18 +10,17 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.nicehttp.requestCreator -import io.ktor.http.Url -import io.ktor.http.decodeURLPart import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response +import java.net.URI /** * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) @@ -120,7 +119,7 @@ actual class WebViewResolver actual constructor( } var fixedRequest: Request? = null - val extraRequestList = atomicListOf() + val extraRequestList = threadSafeListOf() main { try { @@ -212,7 +211,7 @@ actual class WebViewResolver actual constructor( * */ return@runBlocking try { when { - blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith( + blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith( "/favicon.ico" ) -> WebResourceResponse( "image/png", diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index ffc0a938d..c590165a1 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -17,34 +17,21 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.mainWork +import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.nicehttp.RequestBodyTypes -import io.ktor.http.Url -import io.ktor.http.URLBuilder -import io.ktor.http.encodedPath -import io.ktor.http.takeFrom import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn -import kotlinx.datetime.format.DateTimeComponents -import kotlinx.datetime.format.FormatStringsInDatetimeFormats -import kotlinx.datetime.format.byUnicodePattern -import kotlinx.datetime.format.char -import kotlinx.datetime.format.parse -import kotlinx.datetime.toInstant -import kotlinx.serialization.json.Json +import java.net.URI +import java.text.SimpleDateFormat +import java.util.* import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.absoluteValue import kotlin.math.roundToInt -import kotlin.time.Clock -import kotlin.time.Instant /** * API available only on prerelease builds. @@ -87,27 +74,20 @@ const val USER_AGENT = class ErrorLoadingException(message: String? = null) : Exception(message) //val baseHeader = mapOf("User-Agent" to USER_AGENT) - -@Prerelease -val json = Json { - encodeDefaults = true - explicitNulls = false - ignoreUnknownKeys = true -} - val mapper = JsonMapper.builder().addModule(kotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! object APIHolder { - val unixTimeMS: Long - get() = Clock.System.now().toEpochMilliseconds() val unixTime: Long - get() = unixTimeMS / 1000L + get() = System.currentTimeMillis() / 1000L + val unixTimeMS: Long + get() = System.currentTimeMillis() - val allProviders = atomicListOf() + // ConcurrentModificationException is possible!!! + val allProviders = threadSafeListOf() fun initAll() { - allProviders.withLock { + synchronized(allProviders) { for (api in allProviders) { api.init() } @@ -117,28 +97,28 @@ object APIHolder { /** String extension function to Capitalize first char of string.*/ fun String.capitalize(): String { - return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } - var apis: AtomicList = atomicListOf() + var apis: List = threadSafeListOf() var apiMap: Map? = null fun addPluginMapping(plugin: MainAPI) { - apis.withLock { + synchronized(apis) { apis = apis + plugin } initMap(true) } fun removePluginMapping(plugin: MainAPI) { - apis.withLock { + synchronized(apis) { apis = apis.filter { it != plugin } } initMap(true) } private fun initMap(forcedUpdate: Boolean = false) { - apis.withLock { + synchronized(apis) { if (apiMap == null || forcedUpdate) apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() } @@ -146,21 +126,24 @@ object APIHolder { fun getApiFromNameNull(apiName: String?): MainAPI? { if (apiName == null) return null - return allProviders.withLock { + synchronized(allProviders) { initMap() - apis.withLock { - apiMap?.get(apiName)?.let { apis.getOrNull(it) } + synchronized(apis) { + return apiMap?.get(apiName)?.let { apis.getOrNull(it) } // Leave the ?. null check, it can crash regardless - ?: allProviders.firstOrNull { it.name == apiName } + ?: allProviders.firstOrNull { it.name == apiName } } } } fun getApiFromUrlNull(url: String?): MainAPI? { if (url == null) return null - return allProviders.withLock { - allProviders.firstOrNull { url.startsWith(it.mainUrl) } + synchronized(allProviders) { + allProviders.forEach { api -> + if (url.startsWith(api.mainUrl)) return api + } } + return null } /** @@ -178,9 +161,9 @@ object APIHolder { // To get the key suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { try { - val _url = Url(url) + val uri = URI.create(url) val domain = base64Encode( - (_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(), + (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), ).replace("\n", "").replace("=", ".") val vToken = @@ -489,7 +472,7 @@ abstract class MainAPI { } fun init() { - overrideData?.get(this::class.simpleName)?.let { data -> + overrideData?.get(this.javaClass.simpleName)?.let { data -> overrideWithNewData(data) } } @@ -703,22 +686,17 @@ abstract class MainAPI { } } +/** Might need a different implementation for desktop*/ fun base64Decode(string: String): String { - // ISO-8859-1 decoding: each byte maps directly to its Unicode code point (0-255), - // so we mask each byte to unsigned and convert to the corresponding Char manually. - // decodeToString() can't be used here as it assumes UTF-8. - val bytes = base64DecodeArray(string) - return buildString(bytes.size) { - for (b in bytes) { - append((b.toInt() and 0xFF).toChar()) - } - } + return String(base64DecodeArray(string), Charsets.ISO_8859_1) } +@OptIn(ExperimentalEncodingApi::class) fun base64DecodeArray(string: String): ByteArray { return Base64.decode(string) } +@OptIn(ExperimentalEncodingApi::class) fun base64Encode(array: ByteArray): String { return Base64.encode(array) } @@ -1330,23 +1308,23 @@ fun getQualityFromString(string: String?): SearchQuality? { * ``` */ fun MainAPI.updateUrl(url: String): String { - return try { - val original = Url(url) - val updated = Url(mainUrl) + try { + val original = URI(url) + val updated = URI(mainUrl) - URLBuilder().apply { - takeFrom(updated) - user = original.user - password = original.password - encodedPath = original.encodedPath - fragment = original.fragment - - parameters.clear() - parameters.appendAll(original.parameters) - }.buildString() + // URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment) + return URI( + updated.scheme, + original.userInfo, + updated.host, + updated.port, + original.path, + original.query, + original.fragment + ).toString() } catch (t: Throwable) { logError(t) - url + return url } } @@ -1510,7 +1488,7 @@ constructor( override var posterUrl: String? = null, var year: Int? = null, - var dubStatus: MutableSet? = null, + var dubStatus: EnumSet? = null, var otherName: String? = null, var episodes: MutableMap = mutableMapOf(), @@ -1519,10 +1497,46 @@ constructor( override var quality: SearchQuality? = null, override var posterHeaders: Map? = null, override var score: Score? = null, -) : SearchResponse +) : SearchResponse { + @Suppress("DEPRECATION_ERROR") + @Deprecated( + "Use newAnimeSearchResponse", + level = DeprecationLevel.ERROR + ) + constructor( + name: String, + url: String, + apiName: String, + type: TvType? = null, + + posterUrl: String? = null, + year: Int? = null, + dubStatus: EnumSet? = null, + + otherName: String? = null, + episodes: MutableMap = mutableMapOf(), + + id: Int? = null, + quality: SearchQuality? = null, + posterHeaders: Map? = null, + ) : this( + name, + url, + apiName, + type, + posterUrl, + year, + dubStatus, + otherName, + episodes, + id, + quality, + posterHeaders, null + ) +} fun AnimeSearchResponse.addDubStatus(status: DubStatus, episodes: Int? = null) { - this.dubStatus = dubStatus?.also { it.add(status) } ?: mutableSetOf(status) + this.dubStatus = dubStatus?.also { it.add(status) } ?: EnumSet.of(status) if (this.type?.isMovieType() != true) if (episodes != null && episodes > 0) this.episodes[status] = episodes @@ -2521,45 +2535,15 @@ constructor( get() = score?.toInt(100) } -@OptIn(FormatStringsInDatetimeFormats::class) fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") { - if (date == null) return - this.date = runCatching { - // First try standard ISO 8601 (e.g. "2026-01-01T12:30:00.000Z", "2026-05-17T14:35+02:00") - runCatching { Instant.parse(date).toEpochMilliseconds() } - .getOrElse { - val fmt = DateTimeComponents.Format { byUnicodePattern(format) } - val components = DateTimeComponents.parse(date, fmt) - /** - * Try multiple conversions in order of precision for non-ISO-8601 formats, - * since the date string may or may not include time and/or timezone offset: - * 1. If the custom format produced a UTC offset (e.g. "2026-05-17 14:35+02:00"), use it directly - * 2. If it has time but no offset (e.g. "2026-05-17 14:35"), fall back to device timezone - * 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone - */ - runCatching { components.toInstantUsingOffset().toEpochMilliseconds() } - .recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() } - .getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() } - } - }.onFailure { logError(it) }.getOrNull() + try { + this.date = SimpleDateFormat(format, Locale.getDefault()).parse(date ?: return)?.time + } catch (e: Exception) { + logError(e) + } } -@Prerelease -fun Episode.addDate(date: LocalDate?) { - this.date = date?.atStartOfDayIn(TimeZone.currentSystemDefault())?.toEpochMilliseconds() -} - -@Prerelease -fun Episode.addDate(date: Instant?) { - this.date = date?.toEpochMilliseconds() -} - -// Deprecate after next stable -/* @Deprecated( - message = "Use addDate with LocalDate, Instant, or String instead.", - level = DeprecationLevel.WARNING, -) */ -fun Episode.addDate(date: java.util.Date?) { +fun Episode.addDate(date: Date?) { this.date = date?.time } @@ -2696,27 +2680,6 @@ fun fetchUrls(text: String?): List { return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList() } -@Prerelease -fun isUpcoming(dateString: String?): Boolean { - return runCatching { - val fmt = DateTimeComponents.Format { - year(); char('-'); monthNumber(); char('-'); day() - } - val components = DateTimeComponents.parse(dateString ?: return false, fmt) - /** - * Try multiple conversions in order of precision, since the date string format - * may or may not include time and/or timezone offset information: - * 1. If the string has a UTC offset (e.g. "2026-05-17T14:35+02:00"), use it directly - * 2. If it has time but no offset (e.g. "2026-05-17T14:35"), fall back to device timezone - * 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone - */ - val instant = runCatching { components.toInstantUsingOffset() } - .recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()) } - .getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()) } - Clock.System.now() < instant - }.onFailure { logError(it) }.getOrElse { false } -} - @Deprecated( "toRatingInt() is deprecated. Use new score API instead.", level = DeprecationLevel.ERROR diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt index 127b075da..4b163867d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -1,33 +1,39 @@ package com.lagradost.cloudstream3 -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import kotlin.reflect.KClass // Short name for requests client to make it nicer to use -private val jsonResponseParser = object : ResponseParser { +private val jacksonResponseParser = object : ResponseParser { + val mapper: ObjectMapper = jacksonObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false + ) + override fun parse(text: String, kClass: KClass): T { - return parseJson(text, kClass) + return mapper.readValue(text, kClass.java) } override fun parseSafe(text: String, kClass: KClass): T? { return try { - parse(text, kClass) - } catch (_: Exception) { + mapper.readValue(text, kClass.java) + } catch (e: Exception) { null } } override fun writeValueAsString(obj: Any): String { - return obj.toJson() + return mapper.writeValueAsString(obj) } } /** The default networking helper. This helper performs SSL checks. * If you need to make requests to websites with invalid SSL certificates use insecureApp instead. */ -var app = Requests(responseParser = jsonResponseParser).apply { +var app = Requests(responseParser = jacksonResponseParser).apply { defaultHeaders = mapOf("user-agent" to USER_AGENT) } @@ -35,6 +41,6 @@ var app = Requests(responseParser = jsonResponseParser).apply { * This should NEVER be used for sensitive networking operations such as logins. Only use this when required. */ @Prerelease @UnsafeSSL -var insecureApp = Requests(responseParser = jsonResponseParser).apply { +var insecureApp = Requests(responseParser = jacksonResponseParser).apply { defaultHeaders = mapOf("user-agent" to USER_AGENT) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt index b29d29f5d..38d35da2e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByseSX.kt @@ -8,8 +8,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper -import io.ktor.http.Url -import io.ktor.http.decodeURLPart +import java.net.URI +import java.nio.charset.StandardCharsets import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec @@ -46,11 +46,11 @@ open class ByseSX : ExtractorApi() { } private fun getBaseUrl(url: String): String { - return Url(url).let { "${it.protocol.name}://${it.host}" } + return URI(url).let { "${it.scheme}://${it.host}" } } private fun getCodeFromUrl(url: String): String { - val path = Url(url).encodedPath.decodeURLPart() + val path = URI(url).path ?: "" return path.trimEnd('/').substringAfterLast('/') } @@ -94,7 +94,7 @@ open class ByseSX : ExtractorApi() { cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) val plainBytes = cipher.doFinal(cipherBytes) - var jsonStr = plainBytes.decodeToString() + var jsonStr = String(plainBytes, StandardCharsets.UTF_8) if (jsonStr.startsWith("\uFEFF")) jsonStr = jsonStr.substring(1) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt index 5c9f58efc..fc155bdd9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt @@ -6,14 +6,15 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl import com.lagradost.cloudstream3.utils.newExtractorLink +import java.net.URLDecoder open class Cda : ExtractorApi() { override var mainUrl = "https://ebd.cda.pl" override var name = "Cda" override val requiresReferer = false + override suspend fun getUrl(url: String, referer: String?): List? { val mediaId = url .split("/").last() @@ -64,10 +65,10 @@ open class Cda : ExtractorApi() { .replace("_QWE", "") .replace("_Q5", "") .replace("_IKSDE", "") - a = a.decodeUrl() + a = URLDecoder.decode(a, "UTF-8") a = a.map { char -> if (char.code in 33..126) { - return@map (33 + (char.code + 14) % 94).toChar().toString() + return@map String.format("%c", 33 + (char.code + 14) % 94) } else { return@map char } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt index a85aff8d4..62c450073 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CineMMRedirect.kt @@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.loadExtractor -import io.ktor.http.Url +import okhttp3.HttpUrl.Companion.toHttpUrl // deobfuscated from https://hglink.to/main.js?v=1.1.3 using https://deobfuscate.io/ private val mirrors = arrayOf( @@ -90,7 +90,7 @@ abstract class CineMMRedirect : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val videoId = Url(url).encodedPath + val videoId = url.toHttpUrl().encodedPath val mirror = mirrors.random() // re-use existing extractors by calling the ExtractorApi @@ -98,4 +98,4 @@ abstract class CineMMRedirect : ExtractorApi() { val mirrorUrlWithVideoId = "https://$mirror$videoId" loadExtractor(mirrorUrlWithVideoId, referer, subtitleCallback, callback) } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt index 7f6d98049..3c79baf3a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/CloudMailRuExtractor.kt @@ -27,7 +27,7 @@ open class CloudMailRu : ExtractorApi() { "Origin" to mainUrl, "User-Agent" to USER_AGENT, ) - val vidId = url.substringAfter("public/").encodeToByteArray() + val vidId = url.substringAfter("public/").toByteArray() val vidIdEnc = base64Encode(vidId) val videoReq = app.get(url, headers=headers).text val regex = Regex(pattern = "videowl_view\":\\{\"count\":\"1\",\"url\":\"([^\"]*)\"\\}", options = setOf(RegexOption.IGNORE_CASE)) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 4732cafcf..db6db39d5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -7,8 +7,9 @@ import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 -import io.ktor.http.Url -import io.ktor.http.decodeURLPart +import java.net.URI + + class Geodailymotion : Dailymotion() { override val name = "GeoDailymotion" @@ -56,6 +57,7 @@ open class Dailymotion : ExtractorApi() { } } + private fun getEmbedUrl(url: String): String? { if (url.contains("/embed/") || url.contains("/video/")) return url if (url.contains("geo.dailymotion.com")) { @@ -65,8 +67,9 @@ open class Dailymotion : ExtractorApi() { return null } + private fun getVideoId(url: String): String? { - val path = Url(url).encodedPath.decodeURLPart() + val path = URI(url).path val id = path.substringAfter("/video/") return if (id.matches(videoIdRegex)) id else null } @@ -79,6 +82,7 @@ open class Dailymotion : ExtractorApi() { return generateM3u8(name, streamLink, "").forEach(callback) } + data class MetaData( val qualities: Map>?, val subtitles: SubtitlesWrapper? @@ -98,4 +102,5 @@ open class Dailymotion : ExtractorApi() { val label: String, val urls: List ) + } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index bce017276..12bc5a0c5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url +import java.net.URI class Doodspro : DoodLaExtractor() { override var mainUrl = "https://doods.pro" @@ -138,6 +138,8 @@ open class DoodLaExtractor : ExtractorApi() { } private fun getBaseUrl(url: String): String { - return Url(url).let { "${it.protocol.name}://${it.host}" } + return URI(url).let { + "${it.scheme}://${it.host}" + } } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Flyfile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Flyfile.kt deleted file mode 100644 index eb6d474a5..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Flyfile.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.Prerelease -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.newExtractorLink -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Prerelease -open class Flyfile : ExtractorApi() { - override val name: String = "FlyFile" - override val mainUrl: String = "https://flyfile.app" - open val apiUrl: String = "https://api.flyfile.app" - override val requiresReferer: Boolean = false - - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val videoId = url.substringAfterLast("/") - val videoInfo = app.get("$apiUrl/api/streaming/assign/$videoId") - .parsed() - - val streamUrl = "${videoInfo.url}/hls/${videoInfo.token}/master.m3u8" - callback.invoke( - newExtractorLink( - source = name, - name = name, - url = streamUrl, - type = ExtractorLinkType.M3U8 - ) - ) - } - - @Serializable - private data class StreamInfo( - @SerialName("url") - val url: String, - @SerialName("token") - val token: String - ) -} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt index ba297067e..e0fefe8aa 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GDMirrorbot.kt @@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.loadExtractor -import io.ktor.http.Url +import java.net.URI class Techinmind: GDMirrorbot() { override var name = "Techinmind Cloud AIO" @@ -103,7 +103,7 @@ open class GDMirrorbot : ExtractorApi() { } private fun getBaseUrl(url: String): String { - return Url(url).let { "${it.protocol.name}://${it.host}" } + return URI(url).let { "${it.scheme}://${it.host}" } } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt index 5fc55aac6..61c22e929 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt @@ -82,7 +82,7 @@ open class Gdriveplayer : ExtractorApi() { val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) ?.split(Regex("\\D+")) ?.joinToString("") { - it.toInt().toChar().toString() + Char(it.toInt()).toString() }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } ?: throw ErrorLoadingException("can't find password") val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "") @@ -125,4 +125,4 @@ open class Gdriveplayer : ExtractorApi() { @JsonProperty("label") val label: String ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt index 2b42e42c7..ced827eec 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.newExtractorLink -import kotlin.math.round open class Gofile : ExtractorApi() { override val name = "Gofile" @@ -68,19 +67,10 @@ open class Gofile : ExtractorApi() { ?: Qualities.Unknown.value } - private fun roundTo2Decimals(value: Double): String { - val rounded = round(value * 100) / 100.0 - val intPart = rounded.toLong() - val decPart = round((rounded - intPart) * 100).toLong() - return "$intPart.${decPart.toString().padStart(2, '0')}" - } - private fun formatBytes(bytes: Long): String { - val mb = 1024L * 1024 - val gb = mb * 1024 return when { - bytes < gb -> "${roundTo2Decimals(bytes.toDouble() / mb)} MB" - else -> "${roundTo2Decimals(bytes.toDouble() / gb)} GB" + bytes < 1024L * 1024 * 1024 -> "%.2f MB".format(bytes.toDouble() / (1024 * 1024)) + else -> "%.2f GB".format(bytes.toDouble() / (1024 * 1024 * 1024)) } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 1ccd3e4d5..ea6fba73b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.api.Log import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.AesHelper import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.extractors.helper.AesHelper +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue open class HDMomPlayer : ExtractorApi() { override val name = "HDMomPlayer" @@ -23,7 +24,7 @@ open class HDMomPlayer : ExtractorApi() { if (bePlayer != null) { val bePlayerPass = bePlayer.get(1) val bePlayerData = bePlayer.get(2) - val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.encodeToByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") + val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) } else { @@ -31,7 +32,7 @@ open class HDMomPlayer : ExtractorApi() { val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) if (trackStr != null) { - val tracks:List = parseJson>("[${trackStr}]") + val tracks:List = jacksonObjectMapper().readValue("[${trackStr}]") for (track in tracks) { if (track.file == null || track.label == null) continue @@ -67,4 +68,4 @@ open class HDMomPlayer : ExtractorApi() { @JsonProperty("language") val language: String?, @JsonProperty("default") val default: String? ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt index a974df15c..4f83bad25 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HubCloud.kt @@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url +import java.net.URI class HubCloud : ExtractorApi() { override val name = "Hub-Cloud" @@ -24,7 +24,7 @@ class HubCloud : ExtractorApi() { ) { val tag = "HubCloud" val realUrl = url.takeIf { - try { Url(it); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false } + try { URI(it).toURL(); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false } } ?: return val baseUrl=getBaseUrl(realUrl) @@ -161,7 +161,7 @@ class HubCloud : ExtractorApi() { private fun getBaseUrl(url: String): String { return try { - Url(url).let { "${it.protocol.name}://${it.host}" } + URI(url).let { "${it.scheme}://${it.host}" } } catch (_: Exception) { "" } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt index 1ac6c789c..40d817e99 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/InternetArchive.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl +import com.lagradost.cloudstream3.utils.StringUtils.decodeUri import com.lagradost.cloudstream3.utils.newExtractorLink import org.jsoup.nodes.Document @@ -96,7 +96,7 @@ open class InternetArchive : ExtractorApi() { if (mediaUrl.isNotEmpty()) { val name = if (mediaUrl.count() > 1) { val fileExtension = mediaUrl.substringAfterLast(".") - val fileNameCleaned = fileName.decodeUrl().substringBeforeLast('.') + val fileNameCleaned = fileName.decodeUri().substringBeforeLast('.') "$fileNameCleaned ($fileExtension)" } else this.name callback( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt index 84b818723..76f14d33b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt @@ -14,10 +14,11 @@ open class Mvidoo : ExtractorApi() { private fun String.decodeHex(): String { require(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() - .decodeToString() + return String( + chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + ) } override suspend fun getUrl( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 69c3b7759..8af77c1df 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -32,7 +32,7 @@ open class Odnoklassniki : ExtractorApi() { val embedUrl = url.replace("/video/","/videoembed/") val videoReq = app.get(embedUrl, headers=headers).text.replace("\\"", "\"").replace("\\\\", "\\") .replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> - matchResult.groupValues[1].toInt(16).toChar().toString() + Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString() } val videosStr = Regex(""""videos":(\[[^]]*])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") val videos = AppUtils.tryParseJson>(videosStr) ?: throw ErrorLoadingException("Video not found") diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index 3f7408eb3..98598dd28 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import java.nio.charset.StandardCharsets import java.security.MessageDigest import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec @@ -170,7 +171,7 @@ open class Rabbitstream : ExtractorApi() { IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) ) val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found") - return decryptedData.decodeToString() + return String(decryptedData, StandardCharsets.UTF_8) } data class Tracks( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index 822a5eed7..9654e5f38 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -35,14 +35,14 @@ open class RapidVid : ExtractorApi() { if (extractedValue != null) { val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - decoded = bytes.decodeToString() + decoded = String(bytes, Charsets.UTF_8) } else { val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\") extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "") val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() - decoded = bytes?.decodeToString() ?: throw ErrorLoadingException("File not found") + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") } callback.invoke( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt index 58aa25c8c..c721db6b9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log -import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app @@ -13,6 +12,7 @@ import com.lagradost.cloudstream3.utils.getAndUnpack import com.lagradost.cloudstream3.utils.getPacked import com.lagradost.cloudstream3.network.WebViewResolver + class Mwish : StreamWishExtractor() { override val name = "Mwish" override val mainUrl = "https://mwish.pro" @@ -28,12 +28,6 @@ class Ewish : StreamWishExtractor() { override val mainUrl = "https://embedwish.com" } -@Prerelease -class Hgcloudto : StreamWishExtractor() { - override val name = "Hgcloud" - override val mainUrl = "https://Hgcloud.to" -} - class WishembedPro : StreamWishExtractor() { override val name = "Wishembed" override val mainUrl = "https://wishembed.pro" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt index 2cb9a5e5d..611711e39 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt @@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.JsUnpacker import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.newExtractorLink +import java.net.URI open class Streamhub : ExtractorApi() { override var mainUrl = "https://streamhub.to" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt index 9886300aa..98481970b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import io.ktor.http.Url +import java.net.URI open class Streamplay : ExtractorApi() { override val name = "Streamplay" @@ -22,7 +22,9 @@ open class Streamplay : ExtractorApi() { ) { val request = app.get(url, referer = referer) val redirectUrl = request.url - val mainServer = Url(redirectUrl).let { "${it.protocol.name}://${it.host}" } + val mainServer = URI(redirectUrl).let { + "${it.scheme}://${it.host}" + } val key = redirectUrl.substringAfter("embed-").substringBefore(".html") val token = request.document.select("script").find { it.data().contains("sitekey:") }?.data() @@ -77,4 +79,4 @@ open class Streamplay : ExtractorApi() { @JsonProperty("label") val label: String? = null, ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt index 08dcb634e..582be8afb 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt @@ -6,6 +6,8 @@ import com.lagradost.cloudstream3.utils.* import org.mozilla.javascript.Context import org.mozilla.javascript.EvaluatorException import org.mozilla.javascript.Scriptable +import java.util.* + open class Userload : ExtractorApi() { override var name = "Userload" @@ -14,7 +16,7 @@ open class Userload : ExtractorApi() { private fun splitInput(input: String): List { var counter = 0 - val array = mutableListOf() + val array = ArrayList() var buffer = "" for (c in input) { when (c) { @@ -69,7 +71,7 @@ open class Userload : ExtractorApi() { } var txtresult = "" subchar.forEach{ - txtresult = txtresult.plus(it.toInt(8).toChar()) + txtresult = txtresult.plus(Char(it.toInt(8))) } val val1 = Regex(""""morocco="((.|\\n)*?)"&mycountry="""").find(txtresult)?.groups?.get(1)?.value.toString().drop(1).dropLast(1) val val2 = txtresult.substringAfter("""&mycountry="+""").substringBefore(")") diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt index 6b82ee454..974549fcb 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi @@ -22,7 +21,7 @@ open class Vicloud : ExtractorApi() { ) { val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1) app.get( - "$mainUrl/api/?$id=&_=$unixTimeMS", + "$mainUrl/api/?$id=&_=${System.currentTimeMillis()}", headers = mapOf( "X-Requested-With" to "XMLHttpRequest" ), diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index a23f6b683..36acf7f7a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -35,14 +35,14 @@ open class VidMoxy : ExtractorApi() { if (extractedValue != null) { val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - decoded = bytes.decodeToString() + decoded = String(bytes, Charsets.UTF_8) } else { val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\") extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "") val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() - decoded = bytes?.decodeToString() ?: throw ErrorLoadingException("File not found") + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") } callback.invoke( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt index 63ceb1f3d..846fd851d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidStack.kt @@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.fixUrl import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url +import java.net.URI import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @@ -84,7 +84,7 @@ open class VidStack : ExtractorApi() { private fun getBaseUrl(url: String): String { return try { - Url(url).let { "${it.protocol.name}://${it.host}" } + URI(url).let { "${it.scheme}://${it.host}" } } catch (e: Exception) { Log.e("Vidstack", "getBaseUrl fallback: ${e.message}") mainUrl diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt index 59d7a7f2e..47840a08a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt @@ -47,7 +47,7 @@ class Videa : ExtractorApi() { rawBytes[4] == 0x6C.toByte() // 'l' val videaXml = if (isXml) { - rawBytes.decodeToString() + String(rawBytes, Charsets.UTF_8) } else { // Handle encrypted XML response val xsHeader = response.headers["X-Videa-Xs"] ?: return @@ -179,7 +179,7 @@ class Videa : ExtractorApi() { } val actualEncryptedBytes = if (isBase64) { - val base64String = encryptedBytes.decodeToString() + val base64String = String(encryptedBytes, Charsets.UTF_8) .replace("\r", "") .replace("\n", "") .replace(" ", "") @@ -189,7 +189,7 @@ class Videa : ExtractorApi() { encryptedBytes } - val keyBytes = key.encodeToByteArray() + val keyBytes = key.toByteArray(Charsets.UTF_8) // RC4 key-scheduling algorithm (KSA) val s = IntArray(256) { it } @@ -211,6 +211,6 @@ class Videa : ExtractorApi() { result[k] = ((actualEncryptedBytes[k].toInt() and 0xFF) xor keyStreamByte).toByte() } - return result.decodeToString() + return String(result, Charsets.UTF_8) } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index bc94ae0cc..583d92322 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -2,11 +2,12 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue open class VideoSeyred : ExtractorApi() { override val name = "VideoSeyred" @@ -19,7 +20,7 @@ open class VideoSeyred : ExtractorApi() { val videoUrl = "${mainUrl}/playlist/${videoId}.json" val responseRaw = app.get(videoUrl) - val responseList: List = tryParseJson>(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") + val responseList:List = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") val response = responseList[0] for (track in response.tracks) { @@ -67,4 +68,4 @@ open class VideoSeyred : ExtractorApi() { @JsonProperty("label") val label: String? = null, @JsonProperty("default") val default: String? = null ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt index 49560d456..5c871b54b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt @@ -38,13 +38,13 @@ class Vidsonic() : ExtractorApi() { .substringBefore(";") .replace("'", "") - // (improved) Kotlin implementation of the JavaScript code from above + // (improved) Java implementation of the JavaScript code from above val streamUrl = encodedStreamUrl .replace("|", "") // always two base16 digits together build one ASCII char .chunked(2) .map { - it.toInt(16).toChar() + Integer.parseInt(it, 16).toChar() } .joinToString("") .reversed() diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt index fb310401a..dd8511eae 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt @@ -1,20 +1,14 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.newAudioFile +import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink - -expect open class YoutubeExtractor() : ExtractorApi { - override val mainUrl: String - override val name: String - override val requiresReferer: Boolean - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ) -} +import com.lagradost.cloudstream3.utils.newExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamType class YoutubeShortLinkExtractor : YoutubeExtractor() { override val mainUrl = "https://youtu.be" @@ -27,3 +21,107 @@ class YoutubeMobileExtractor : YoutubeExtractor() { class YoutubeNoCookieExtractor : YoutubeExtractor() { override val mainUrl = "https://www.youtube-nocookie.com" } + +open class YoutubeExtractor : ExtractorApi() { + + override val mainUrl = "https://www.youtube.com" + override val name = "YouTube" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val videoId = extractYouTubeId(url) + val watchUrl = "$mainUrl/watch?v=$videoId" + + val info = StreamInfo.getInfo(watchUrl) + + val isLive = + info.streamType == StreamType.LIVE_STREAM + || info.streamType == StreamType.AUDIO_LIVE_STREAM + || info.streamType == StreamType.POST_LIVE_STREAM + || info.streamType == StreamType.POST_LIVE_AUDIO_STREAM + + if (isLive && info.hlsUrl != null) { + callback( + newExtractorLink( + source = name, + name = "YouTube Live", + url = info.hlsUrl + ) { + type = ExtractorLinkType.M3U8 + } + ) + } else { + processVideo(info, subtitleCallback, callback) + } + } + + private suspend fun processVideo( + info: StreamInfo, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val videoStreams = info.videoOnlyStreams.orEmpty() + + if (videoStreams.isEmpty()) return false + + val audioStreams = info.audioStreams.orEmpty() + + videoStreams.forEach { video -> + + callback( + newExtractorLink( + source = name, + name = "YouTube ${normalizeCodec(video.codec)}", + url = video.content + ) { + quality = video.height + audioTracks = audioStreams.map { newAudioFile(it.content) } + } + ) + } + + + info.subtitles.forEach { subtitle -> + subtitleCallback( + newSubtitleFile( + lang = subtitle.displayLanguageName + ?: subtitle.languageTag + ?: "Unknown", + url = subtitle.content + ) + ) + } + + return true + } + + // ---------------- HELPERS ---------------- + + private fun extractYouTubeId(url: String): String { + val regex = Regex( + "(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})" + ) + return regex.find(url)?.groupValues?.get(1) + ?: throw IllegalArgumentException("Invalid YouTube URL: $url") + } + + private fun normalizeCodec(codec: String?): String { + if (codec.isNullOrBlank()) return "" + + val c = codec.lowercase() + + return when { + c.startsWith("av01") -> "AV1" + c.startsWith("vp9") -> "VP9" + c.startsWith("avc1") || c.startsWith("h264") -> "H264" + c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265" + else -> codec.substringBefore('.').uppercase() + } + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt index 6ad0524e8..af59b6f7d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/CryptoJSHelper.kt @@ -2,11 +2,13 @@ package com.lagradost.cloudstream3.extractors.helper import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.base64Encode +import java.util.Arrays import java.security.MessageDigest import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.IvParameterSpec +import java.nio.charset.StandardCharsets import kotlin.math.min /** @@ -46,9 +48,9 @@ object CryptoJS { // Create CryptoJS-like encrypted! val sBytes = APPEND.toByteArray() val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size) - sBytes.copyInto(destination = b, destinationOffset = 0) - saltBytes.copyInto(destination = b, destinationOffset = sBytes.size) - cipherText.copyInto(destination = b, destinationOffset = sBytes.size + saltBytes.size) + System.arraycopy(sBytes, 0, b, 0, sBytes.size) + System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size) + System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size) return base64Encode(b) } @@ -61,8 +63,8 @@ object CryptoJS { */ fun decrypt(password: String, cipherText: String): String { val ctBytes = base64DecodeArray(cipherText) - val saltBytes = ctBytes.copyOfRange(8, 16) - val cipherTextBytes = ctBytes.copyOfRange(16, ctBytes.size) + val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16) + val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size) val key = ByteArray(KEY_SIZE / 8) val iv = ByteArray(IV_SIZE / 8) @@ -105,18 +107,16 @@ object CryptoJS { hash.reset() } - block!!.copyInto( - destination = derivedBytes, - destinationOffset = numberOfDerivedWords * 4, - startIndex = 0, - endIndex = min(block.size, (targetKeySize - numberOfDerivedWords) * 4) + System.arraycopy( + block!!, 0, derivedBytes, numberOfDerivedWords * 4, + min(block.size, (targetKeySize - numberOfDerivedWords) * 4) ) numberOfDerivedWords += block.size / 4 } - derivedBytes.copyInto(destination = resultKey, destinationOffset = 0, startIndex = 0, endIndex = keySize * 4) - derivedBytes.copyInto(destination = resultIv, destinationOffset = 0, startIndex = keySize * 4, endIndex = (keySize * 4) + (ivSize * 4)) + System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4) + System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4) return derivedBytes // key + iv } @@ -126,4 +126,4 @@ object CryptoJS { SecureRandom().nextBytes(this) } } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt index 31618a32b..a16d41943 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt @@ -12,8 +12,8 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.newExtractorLink -import io.ktor.http.Url import org.jsoup.nodes.Document +import java.net.URI import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @@ -88,8 +88,8 @@ object GogoHelper { val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall val foundDecryptKey = secretDecryptKey ?: foundKey - val url = Url(iframeUrl) - val mainUrl = "https://${url.host}" + val uri = URI(iframeUrl) + val mainUrl = "https://" + uri.host val encryptedId = cryptoHandler(id, foundIv, foundKey) val encryptRequestData = if (isUsingAdaptiveData) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt index c3b50c7a5..2563d40e1 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt @@ -1,7 +1,7 @@ package com.lagradost.cloudstream3.extractors.helper -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl -import com.lagradost.cloudstream3.utils.StringUtils.encodeUrl +import com.lagradost.cloudstream3.utils.StringUtils.decodeUri +import com.lagradost.cloudstream3.utils.StringUtils.encodeUri // Taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/NineAnime.kt // GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md @@ -108,6 +108,8 @@ object NineAnimeHelper { } } - fun encode(input: String): String = input.encodeUrl() - private fun decode(input: String): String = input.decodeUrl() + fun encode(input: String): String = + input.encodeUri().replace("+", "%20") + + private fun decode(input: String): String = input.decodeUri() } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt index 7076e407f..6fde6efe3 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt @@ -30,9 +30,11 @@ class CrossTmdbProvider : TmdbProvider() { } private val validApis - get() = apis.filter { it.lang == this.lang && it::class != this::class } + get() = + synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } } //.distinctBy { it.uniqueId } + data class CrossMetaData( @JsonProperty("isSuccess") val isSuccess: Boolean, @JsonProperty("movies") val movies: List>? = null, @@ -119,4 +121,4 @@ class CrossTmdbProvider : TmdbProvider() { return base } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt index e7e1175f0..cf3e28a8d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/MyDramaList.kt @@ -18,7 +18,6 @@ import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.addDate import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.isUpcoming import com.lagradost.cloudstream3.mainPageOf import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.newEpisode @@ -31,6 +30,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import okhttp3.Interceptor import okhttp3.Response +import java.text.SimpleDateFormat +import java.util.Locale //Reference: https://mydramalist.github.io/MDL-API/ abstract class MyDramaListAPI : MainAPI() { @@ -191,6 +192,17 @@ abstract class MyDramaListAPI : MainAPI() { return this } + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } + } + private fun getStatus(status: String?): ShowStatus? { return when (status) { "Airing" -> ShowStatus.Ongoing @@ -439,4 +451,4 @@ abstract class MyDramaListAPI : MainAPI() { @JsonProperty("date") val date: String? = null, @JsonProperty("airedDate") val airedDate: String? = null, ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 59dcd2711..63f6d564c 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.metaproviders import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.api.BuildConfig import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.Actor import com.lagradost.cloudstream3.ActorData @@ -23,8 +22,8 @@ import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.addDate import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.isUpcoming import com.lagradost.cloudstream3.mainPageOf +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.newEpisode import com.lagradost.cloudstream3.newHomePageResponse import com.lagradost.cloudstream3.newMovieLoadResponse @@ -34,6 +33,8 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse import com.lagradost.cloudstream3.newTvSeriesSearchResponse import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson +import java.text.SimpleDateFormat +import java.util.Locale open class TraktProvider : MainAPI() { override var name = "Trakt" @@ -45,10 +46,10 @@ open class TraktProvider : MainAPI() { TvType.Anime, ) + private val traktClientId = + "d9f434f48b55683a279ffe88ddc68351cc04c9dc9372bd95af5de780b794e770" private val traktApiUrl = "https://api.trakt.tv" - val traktClientId: String = BuildConfig.TRAKT_CLIENT_ID - override val mainPage = mainPageOf( "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time @@ -113,7 +114,6 @@ open class TraktProvider : MainAPI() { val posterUrl = fixPath(mediaDetails?.images?.poster?.firstOrNull()) val backDropUrl = fixPath(mediaDetails?.images?.fanart?.firstOrNull()) - val logoUrl = fixPath(mediaDetails?.images?.logo?.firstOrNull()) val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=full,images") @@ -183,7 +183,6 @@ open class TraktProvider : MainAPI() { this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders this.backgroundPosterUrl = backDropUrl - this.logoUrl = logoUrl this.contentRating = mediaDetails.certification addTrailer(mediaDetails.trailer) addImdbId(mediaDetails.ids?.imdb) @@ -274,7 +273,6 @@ open class TraktProvider : MainAPI() { //posterHeaders this.nextAiring = nextAir this.backgroundPosterUrl = backDropUrl - this.logoUrl = logoUrl this.contentRating = mediaDetails.certification addTrailer(mediaDetails.trailer) addImdbId(mediaDetails.ids?.imdb) @@ -291,7 +289,18 @@ open class TraktProvider : MainAPI() { "trakt-api-version" to "2", "trakt-api-key" to traktClientId, ) - ).text + ).toString() + } + + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } } private fun getStatus(t: String?): ShowStatus { @@ -446,4 +455,4 @@ open class TraktProvider : MainAPI() { @JsonProperty("is_bollywood") val isBollywood: Boolean = false, @JsonProperty("is_cartoon") val isCartoon: Boolean = false, ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt index f4fce2ef3..61f87b8ba 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt @@ -17,7 +17,10 @@ abstract class BasePlugin { fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") element.sourcePlugin = this.filename - APIHolder.allProviders.add(element) + // Race condition causing which would case duplicates if not for distinctBy + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.add(element) + } APIHolder.addPluginMapping(element) } @@ -28,7 +31,9 @@ abstract class BasePlugin { fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") element.sourcePlugin = this.filename - extractorApis.add(element) + synchronized(extractorApis) { + extractorApis.add(element) + } } /** diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt index 180aee11a..e07f32c0a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.InternalAPI -import kotlin.concurrent.Volatile @InternalAPI object AppDebug { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt index 1c635013f..6832ab8d2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -1,91 +1,21 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.module.kotlin.readValue -import com.lagradost.cloudstream3.InternalAPI -import com.lagradost.cloudstream3.json import com.lagradost.cloudstream3.mapper -import com.lagradost.cloudstream3.mvvm.logError -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.serializer -import kotlinx.serialization.serializerOrNull -import kotlin.reflect.KClass +import java.io.Reader -@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) object AppUtils { - /** Any object as a JSON string */ + /** Any object as json string */ fun Any.toJson(): String { if (this is String) return this - return toJsonLiteral() + return mapper.writeValueAsString(this) } - /** Sometimes we want to encode as JSON even if it is already a String. */ - @InternalAPI - fun Any.toJsonLiteral(): String { - // @Serializable generates a serializer at compile time; contextual serializers are - // registered manually in serializersModule, we need both to support all cases - val serializer = - this::class.serializerOrNull() ?: json.serializersModule.getContextual(this::class) - return if (serializer != null) { - try { - @Suppress("UNCHECKED_CAST") - json.encodeToString(serializer as KSerializer, this) - } catch (e: SerializationException) { - logError(e) - mapper.writeValueAsString(this) - } - } else { - mapper.writeValueAsString(this) - } - } - - @InternalAPI - fun parseJson(value: String, kClass: KClass): T { - val serializer = kClass.serializerOrNull() ?: json.serializersModule.getContextual(kClass) - if (serializer != null) { - try { - return json.decodeFromString(serializer, value) - } catch (e: SerializationException) { - logError(e) - } - } - - return mapper.readValue(value, kClass.java) - } - - // This is inlined code and can easily cause breakage in extensions! - // Watch out when editing this to make sure stable also supports all inlined code! - inline fun parseJson(value: String): T { - // @Serializable generates a serializer at compile time; contextual serializers are - // registered manually in serializersModule, we need both to support all cases - val serializer = runCatching { serializer() } - .recoverCatching { json.serializersModule.getContextual(T::class) } - .getOrNull() - - // Prefer Kotlin Serialization over Jackson - if (serializer != null) { - try { - return json.decodeFromString(serializer, value) - } catch (e: SerializationException) { - logError(e) - } catch (_: Throwable) { - // Pass, the above code will trigger a NoSuchMethodError on stable due to our previously undefined json variable - } - } - + inline fun parseJson(value: String): T { return mapper.readValue(value) } - @Deprecated( - "This overload was only ever used for BasePlugin.Manifest which has since been migrated. " + - "No other code should be using this. Use reader.readText() and call parseJson(String) instead.", - level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("parseJson(reader.readText())") - ) - inline fun parseJson(reader: java.io.Reader, valueType: Class): T { - // Reader-based parsing has no kotlinx equivalent, fall back to Jackson + inline fun parseJson(reader: Reader, valueType: Class): T { return mapper.readValue(reader, valueType) } @@ -96,4 +26,4 @@ object AppUtils { null } } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt deleted file mode 100644 index 0be02ac6f..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import kotlinx.atomicfu.locks.SynchronizedObject -import kotlinx.atomicfu.locks.synchronized - -/** - * A thread-safe list backed by [SynchronizedObject]. - * - * For iteration, wrap block in [withLock] to hold the lock for the duration: - * list.withLock { list.forEach { ... } } - */ -open class AtomicList( - private val delegate: List = emptyList(), -) : List, SynchronizedObject() { - - fun withLock(block: () -> R): R = synchronized(this) { block() } - - fun filter(predicate: (T) -> Boolean): AtomicList = synchronized(this) { AtomicList(delegate.filter(predicate)) } - fun distinctBy(selector: (T) -> Any?): AtomicList = synchronized(this) { AtomicList(delegate.distinctBy(selector)) } - - override val size: Int get() = synchronized(this) { delegate.size } - override fun isEmpty(): Boolean = synchronized(this) { delegate.isEmpty() } - override fun contains(element: T): Boolean = synchronized(this) { delegate.contains(element) } - override fun containsAll(elements: Collection): Boolean = synchronized(this) { delegate.containsAll(elements) } - override fun get(index: Int): T = synchronized(this) { delegate[index] } - override fun indexOf(element: T): Int = synchronized(this) { delegate.indexOf(element) } - override fun lastIndexOf(element: T): Int = synchronized(this) { delegate.lastIndexOf(element) } - - // Iterators intentionally NOT synchronized, callers must use withLock { } for safe iteration. - override fun iterator(): Iterator = delegate.iterator() - override fun listIterator(): ListIterator = delegate.listIterator() - override fun listIterator(index: Int): ListIterator = delegate.listIterator(index) - override fun subList(fromIndex: Int, toIndex: Int): List = delegate.subList(fromIndex, toIndex) - - operator fun plus(element: T): AtomicList = synchronized(this) { AtomicList(delegate + element) } - operator fun plus(elements: Collection): AtomicList = synchronized(this) { AtomicList(delegate + elements) } -} - -class AtomicMutableList( - private val mutableDelegate: MutableList = mutableListOf(), -) : AtomicList(mutableDelegate), MutableList { - - override fun add(element: T): Boolean = synchronized(this) { mutableDelegate.add(element) } - override fun add(index: Int, element: T) = synchronized(this) { mutableDelegate.add(index, element) } - override fun addAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.addAll(elements) } - override fun addAll(index: Int, elements: Collection): Boolean = synchronized(this) { mutableDelegate.addAll(index, elements) } - override fun remove(element: T): Boolean = synchronized(this) { mutableDelegate.remove(element) } - override fun removeAt(index: Int): T = synchronized(this) { mutableDelegate.removeAt(index) } - override fun removeAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.removeAll(elements) } - override fun retainAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.retainAll(elements) } - override fun set(index: Int, element: T): T = synchronized(this) { mutableDelegate.set(index, element) } - override fun clear() = synchronized(this) { mutableDelegate.clear() } - - // Iterators intentionally NOT synchronized, callers must use withLock { } for safe iteration. - override fun iterator(): MutableIterator = mutableDelegate.iterator() - override fun listIterator(): MutableListIterator = mutableDelegate.listIterator() - override fun listIterator(index: Int): MutableListIterator = mutableDelegate.listIterator(index) - override fun subList(fromIndex: Int, toIndex: Int): MutableList = mutableDelegate.subList(fromIndex, toIndex) -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index d15ea129c..c525a1f36 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.annotation.WorkerThread -import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* +import java.util.Collections.synchronizedList @AnyThread expect fun runOnMainThreadNative(@MainThread work: (() -> Unit)) @@ -64,18 +64,9 @@ object Coroutines { /** * Safe to add and remove how you want * If you want to iterate over the list then you need to do: - * list.withLock { code here } + * synchronized(allProviders) { code here } */ - @Prerelease - fun atomicListOf(vararg items: T): AtomicMutableList { - return AtomicMutableList(items.toMutableList()) + fun threadSafeListOf(vararg items: T): MutableList { + return synchronizedList(items.toMutableList()) } - - // Deprecate after next stable - /*@Deprecated( - message = "Use atomicListOf() instead.", - replaceWith = ReplaceWith("atomicListOf(*items)"), - level = DeprecationLevel.WARNING, - )*/ - fun threadSafeListOf(vararg items: T): MutableList = atomicListOf(*items) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index f9167d08c..ab80cf2ca 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,11 +1,8 @@ -@file:OptIn(ExperimentalUuidApi::class) - package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonIgnore import com.lagradost.cloudstream3.AudioFile import com.lagradost.cloudstream3.IDownloadableMinimum -import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app @@ -81,7 +78,6 @@ import com.lagradost.cloudstream3.extractors.FilemoonV2 import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Multimoviesshg import com.lagradost.cloudstream3.extractors.FlaswishCom -import com.lagradost.cloudstream3.extractors.Flyfile import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.FourPlayRu @@ -110,7 +106,6 @@ import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.HDStreamAble import com.lagradost.cloudstream3.extractors.Habetar import com.lagradost.cloudstream3.extractors.Haxloppd -import com.lagradost.cloudstream3.extractors.Hgcloudto import com.lagradost.cloudstream3.extractors.HglinkTo import com.lagradost.cloudstream3.extractors.HgplayCDN import com.lagradost.cloudstream3.extractors.Hotlinger @@ -312,18 +307,14 @@ import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf -import io.ktor.http.Url -import io.ktor.http.decodeURLPart import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive +import me.xdrop.fuzzywuzzy.FuzzySearch import org.jsoup.Jsoup +import java.net.URI +import java.util.UUID import kotlin.coroutines.cancellation.CancellationException -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid -import kotlin.uuid.toJavaUuid -import kotlin.uuid.toKotlinUuid /** * For use in the ConcatenatingMediaSource. @@ -422,7 +413,7 @@ enum class ExtractorLinkType { private fun inferTypeFromUrl(url: String): ExtractorLinkType { val path = try { - Url(url).encodedPath.decodeURLPart() + URI(url).path } catch (_: Throwable) { // don't log magnet links as errors null @@ -439,43 +430,29 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { val INFER_TYPE: ExtractorLinkType? = null /** - * [Uuid] for the ClearKey DRM scheme. + * UUID for the ClearKey DRM scheme. * * * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. */ -@Prerelease -val CLEARKEY_DRM_UUID = Uuid.fromLongs(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) +val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) /** - * [Uuid] for the Widevine DRM scheme. + * UUID for the Widevine DRM scheme. * * * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. */ -@Prerelease -val WIDEVINE_DRM_UUID = Uuid.fromLongs(-0x121074568629b532L, -0x5c37d8232ae2de13L) +val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) /** - * [Uuid] for the PlayReady DRM scheme. + * UUID for the PlayReady DRM scheme. * * * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not * provide PlayReady support. */ -@Prerelease -val PLAYREADY_DRM_UUID = Uuid.fromLongs(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) - -// Deprecate after next stable - -// @Deprecated("Use CLEARKEY_DRM_UUID", ReplaceWith("CLEARKEY_DRM_UUID"), level = DeprecationLevel.WARNING) -val CLEARKEY_UUID = CLEARKEY_DRM_UUID.toJavaUuid() - -// @Deprecated("Use WIDEVINE_DRM_UUID", ReplaceWith("WIDEVINE_DRM_UUID"), level = DeprecationLevel.WARNING) -val WIDEVINE_UUID = WIDEVINE_DRM_UUID.toJavaUuid() - -// @Deprecated("Use PLAYREADY_DRM_UUID", ReplaceWith("PLAYREADY_DRM_UUID"), level = DeprecationLevel.WARNING) -val PLAYREADY_UUID = PLAYREADY_DRM_UUID.toJavaUuid() +val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) suspend fun newExtractorLink( source: String, @@ -498,42 +475,15 @@ suspend fun newExtractorLink( return builder } -// Deprecate after next stable -/* @Deprecated( - message = "Use Kotlin Uuid (kotlin.uuid.Uuid) instead of Java UUID.", - level = DeprecationLevel.WARNING, -) */ suspend fun newDrmExtractorLink( source: String, name: String, url: String, type: ExtractorLinkType? = null, - uuid: java.util.UUID, + uuid: UUID, initializer: suspend DrmExtractorLink.() -> Unit = { } ): DrmExtractorLink { - @Suppress("DEPRECATION_ERROR") - val builder = - DrmExtractorLink( - source = source, - name = name, - url = url, - uuid = uuid.toKotlinUuid(), - type = type ?: INFER_TYPE - ) - builder.initializer() - return builder -} - -@Prerelease -suspend fun newDrmExtractorLink( - source: String, - name: String, - url: String, - type: ExtractorLinkType? = null, - uuid: Uuid, - initializer: suspend DrmExtractorLink.() -> Unit = {}, -): DrmExtractorLink { @Suppress("DEPRECATION_ERROR") val builder = DrmExtractorLink( @@ -559,7 +509,7 @@ suspend fun newDrmExtractorLink( * @property type the type of the media, use [INFER_TYPE] if you want to auto infer the type from the url * @property kid Base64 value of The KID element (Key Id) contains the identifier of the key associated with a license. * @property key Base64 value of Key to be used to decrypt the media file. - * @property uuid Drm [Uuid] [WIDEVINE_DRM_UUID], [PLAYREADY_DRM_UUID], [CLEARKEY_DRM_UUID] (by default) .. etc + * @property uuid Drm UUID [WIDEVINE_UUID], [PLAYREADY_UUID], [CLEARKEY_UUID] (by default) .. etc * @property kty Key type "oct" (octet sequence) by default * @property keyRequestParameters Parameters that will used to request the key. * @see newDrmExtractorLink @@ -577,7 +527,7 @@ open class DrmExtractorLink private constructor( override var type: ExtractorLinkType, open var kid: String? = null, open var key: String? = null, - open var uuid: Uuid, + open var uuid: UUID, open var kty: String? = null, open var keyRequestParameters: HashMap, open var licenseUrl: String? = null, @@ -599,7 +549,7 @@ open class DrmExtractorLink private constructor( extractorData: String? = null, kid: String? = null, key: String? = null, - uuid: Uuid = CLEARKEY_DRM_UUID, + uuid: UUID = CLEARKEY_UUID, kty: String? = "oct", keyRequestParameters: HashMap = hashMapOf(), licenseUrl: String? = null, @@ -634,7 +584,7 @@ open class DrmExtractorLink private constructor( extractorData: String? = null, kid: String? = null, key: String? = null, - uuid: Uuid = CLEARKEY_DRM_UUID, + uuid: UUID = CLEARKEY_UUID, kty: String? = "oct", keyRequestParameters: HashMap = hashMapOf(), licenseUrl: String? = null, @@ -654,14 +604,6 @@ open class DrmExtractorLink private constructor( kty = kty, licenseUrl = licenseUrl, ) - - @Deprecated(message = "Use Kotlin Uuid", level = DeprecationLevel.HIDDEN) - fun setUuid(uuid: java.util.UUID) { - this.uuid = uuid.toKotlinUuid() - } - - @Deprecated(message = "Use Kotlin Uuid", level = DeprecationLevel.HIDDEN) - fun getUuid(): java.util.UUID = this.uuid.toJavaUuid() } /** Class holds extracted media info to be passed to the player. @@ -821,7 +763,7 @@ constructor( /** * Removes https:// and www. - * To match urls regardless of schema, perhaps Url() can be used? + * To match urls regardless of schema, perhaps Uri() can be used? */ val schemaStripRegex = Regex("""^(https:|)//(www\.|)""") @@ -940,7 +882,7 @@ suspend fun loadExtractor( // this is to match mirror domains - like example.com, example.net for (index in extractorApis.lastIndex downTo 0) { val extractor = extractorApis[index] - if (Levenshtein.partialRatio( + if (FuzzySearch.partialRatio( extractor.mainUrl, currentUrl ) > 80 @@ -961,7 +903,7 @@ suspend fun loadExtractor( return false } -val extractorApis: AtomicMutableList = atomicListOf( +val extractorApis: MutableList = arrayListOf( //AllProvider(), Mp4Upload(), StreamTape(), @@ -1252,7 +1194,6 @@ val extractorApis: AtomicMutableList = atomicListOf( MetaGnathTuggers(), Geodailymotion(), Mwish(), - Hgcloudto(), Dwish(), Ewish(), Kswplayer(), @@ -1299,7 +1240,6 @@ val extractorApis: AtomicMutableList = atomicListOf( GUpload(), HlsWish(), ByseQekaho(), - Flyfile() ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt index cd5d752b3..01e5bb862 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/HlsPlaylistParser.kt @@ -19,11 +19,12 @@ */ package com.lagradost.cloudstream3.utils -import com.lagradost.cloudstream3.base64DecodeArray -import io.ktor.http.Url import java.io.IOException +import java.net.URI import java.nio.ByteBuffer import java.util.UUID +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi @Suppress("unused") object HlsPlaylistParser { @@ -233,7 +234,7 @@ object HlsPlaylistParser { if (codecArray.isEmpty()) { return null } - val builder = StringBuilder() + val builder = java.lang.StringBuilder() for (codec in codecArray) { if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { if (builder.isNotEmpty()) { @@ -262,7 +263,7 @@ object HlsPlaylistParser { if (codecArray.isEmpty()) { return null } - val builder = StringBuilder() + val builder = java.lang.StringBuilder() for (codec in codecArray) { if (trackType != MimeTypes.getTrackTypeOfCodec(codec)) { if (builder.isNotEmpty()) { @@ -275,29 +276,29 @@ object HlsPlaylistParser { } } - object UrlUtil { - fun resolveToUrl(baseUrl: String?, referenceUrl: String?): Url { - return Url(resolve(baseUrl, referenceUrl)) + object UriUtil { + fun resolveToUri(baseUri: String?, referenceUri: String?): URI { + return URI.create(resolve(baseUri, referenceUri)) } - /** The length of arrays returned by [.getUrlIndices]. */ + /** The length of arrays returned by [.getUriIndices]. */ private const val INDEX_COUNT: Int = 4 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the ':' after the scheme. Equals -1 - * if the URL is a relative reference (no scheme). The hier-part starts at (schemeColon + 1), - * including when the URL has no scheme. + * if the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1), + * including when the URI has no scheme. */ private const val SCHEME_COLON: Int = 0 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the path part. Equals (schemeColon + @@ -309,7 +310,7 @@ object HlsPlaylistParser { const val PATH: Int = 1 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the query part, including the '?' @@ -320,87 +321,87 @@ object HlsPlaylistParser { const val QUERY: Int = 2 /** - * An index into an array returned by [.getUrlIndices]. + * An index into an array returned by [.getUriIndices]. * * * The value at this position in the array is the index of the fragment part, including the '#' - * before the fragment. Equal to the length of the URL if no fragment part, and (length - 1) if + * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if * the fragment part is a single '#' with no data. */ private const val FRAGMENT: Int = 3 /** - * Performs relative resolution of a `referenceUrl` with respect to a `baseUrl`. + * Performs relative resolution of a `referenceUri` with respect to a `baseUri`. * * * The resolution is performed as specified by RFC-3986. * - * @param baseUrl The base URL. - * @param referenceUrl The reference URL to resolve. + * @param baseUri The base URI. + * @param referenceUri The reference URI to resolve. */ - private fun resolve(baseUrl: String?, referenceUrl: String?): String { - var baseUrl = baseUrl - var referenceUrl = referenceUrl - val url = StringBuilder() + private fun resolve(baseUri: String?, referenceUri: String?): String { + var baseUri = baseUri + var referenceUri = referenceUri + val uri = StringBuilder() // Map null onto empty string, to make the following logic simpler. - baseUrl = baseUrl ?: "" - referenceUrl = referenceUrl ?: "" + baseUri = baseUri ?: "" + referenceUri = referenceUri ?: "" - val refIndices: IntArray = getUrlIndices(referenceUrl) + val refIndices: IntArray = getUriIndices(referenceUri) if (refIndices[SCHEME_COLON] != -1) { - // The reference is absolute. The target Url is the reference. - url.append(referenceUrl) - removeDotSegments(url, refIndices[PATH], refIndices[QUERY]) - return url.toString() + // The reference is absolute. The target Uri is the reference. + uri.append(referenceUri) + removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]) + return uri.toString() } - val baseIndices: IntArray = getUrlIndices(baseUrl) + val baseIndices: IntArray = getUriIndices(baseUri) if (refIndices[FRAGMENT] == 0) { - // The reference is empty or contains just the fragment part, then the target Url is the - // concatenation of the base Url without its fragment, and the reference. - return url.append(baseUrl, 0, baseIndices[FRAGMENT]).append(referenceUrl).toString() + // The reference is empty or contains just the fragment part, then the target Uri is the + // concatenation of the base Uri without its fragment, and the reference. + return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString() } if (refIndices[QUERY] == 0) { // The reference starts with the query part. The target is the base up to (but excluding) the // query, plus the reference. - return url.append(baseUrl, 0, baseIndices[QUERY]).append(referenceUrl).toString() + return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString() } if (refIndices[PATH] != 0) { // The reference has authority. The target is the base scheme plus the reference. val baseLimit = baseIndices[SCHEME_COLON] + 1 - url.append(baseUrl, 0, baseLimit).append(referenceUrl) + uri.append(baseUri, 0, baseLimit).append(referenceUri) return removeDotSegments( - url, + uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY] ) } - if (referenceUrl[refIndices[PATH]] == '/') { + if (referenceUri[refIndices[PATH]] == '/') { // The reference path is rooted. The target is the base scheme and authority (if any), plus // the reference. - url.append(baseUrl, 0, baseIndices[PATH]).append(referenceUrl) + uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri) return removeDotSegments( - url, + uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] ) } - // The target Url is the concatenation of the base Url up to (but excluding) the last segment, + // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment, // and the reference. This can be split into 2 cases: if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH] && baseIndices[PATH] == baseIndices[QUERY] ) { // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is // needed after the authority, before appending the reference. - url.append(baseUrl, 0, baseIndices[PATH]).append('/').append(referenceUrl) + uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri) return removeDotSegments( - url, + uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1 ) @@ -409,22 +410,22 @@ object HlsPlaylistParser { // it. If base hier-part has no '/', it could only mean that it is completely empty or // contains only one segment, in which case the whole hier-part is excluded and the reference // is appended right after the base scheme colon without an added '/'. - val lastSlashIndex = baseUrl.lastIndexOf('/', baseIndices[QUERY] - 1) + val lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1) val baseLimit = if (lastSlashIndex == -1) baseIndices[PATH] else lastSlashIndex + 1 - url.append(baseUrl, 0, baseLimit).append(referenceUrl) - return removeDotSegments(url, baseIndices[PATH], baseLimit + refIndices[QUERY]) + uri.append(baseUri, 0, baseLimit).append(referenceUri) + return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]) } } /** - * Removes dot segments from the path of a URL. + * Removes dot segments from the path of a URI. * - * @param url A [StringBuilder] containing the URL. - * @param offset The index of the start of the path in `url`. - * @param limit The limit (exclusive) of the path in `url`. + * @param uri A [StringBuilder] containing the URI. + * @param offset The index of the start of the path in `uri`. + * @param limit The limit (exclusive) of the path in `uri`. */ private fun removeDotSegments( - url: StringBuilder, + uri: java.lang.StringBuilder, offset: Int, limit: Int ): String { @@ -432,9 +433,9 @@ object HlsPlaylistParser { var limit = limit if (offset >= limit) { // Nothing to do. - return url.toString() + return uri.toString() } - if (url[offset] == '/') { + if (uri[offset] == '/') { // If the path starts with a /, always retain it. offset++ } @@ -444,7 +445,7 @@ object HlsPlaylistParser { while (i <= limit) { val nextSegmentStart = if (i == limit) { i - } else if (url[i] == '/') { + } else if (uri[i] == '/') { i + 1 } else { i++ @@ -452,16 +453,16 @@ object HlsPlaylistParser { } // We've encountered the end of a segment or the end of the path. If the final segment was // "." or "..", remove the appropriate segments of the path. - if (i == segmentStart + 1 && url[segmentStart] == '.') { + if (i == segmentStart + 1 && uri[segmentStart] == '.') { // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". - url.delete(segmentStart, nextSegmentStart) + uri.delete(segmentStart, nextSegmentStart) limit -= nextSegmentStart - segmentStart i = segmentStart - } else if (i == segmentStart + 2 && url[segmentStart] == '.' && url[segmentStart + 1] == '.') { + } else if (i == segmentStart + 2 && uri[segmentStart] == '.' && uri[segmentStart + 1] == '.') { // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi". - val prevSegmentStart = url.lastIndexOf("/", segmentStart - 2) + 1 + val prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1 val removeFrom = if (prevSegmentStart > offset) prevSegmentStart else offset - url.delete(removeFrom, nextSegmentStart) + uri.delete(removeFrom, nextSegmentStart) limit -= nextSegmentStart - removeFrom segmentStart = prevSegmentStart i = prevSegmentStart @@ -470,41 +471,41 @@ object HlsPlaylistParser { segmentStart = i } } - return url.toString() + return uri.toString() } /** - * Calculates indices of the constituent components of a URL. + * Calculates indices of the constituent components of a URI. * - * @param urlString The URL as a string. + * @param uriString The URI as a string. * @return The corresponding indices. */ - private fun getUrlIndices(urlString: String?): IntArray { + private fun getUriIndices(uriString: String?): IntArray { val indices = IntArray(INDEX_COUNT) - if (urlString.isNullOrEmpty()) { + if (uriString.isNullOrEmpty()) { indices[SCHEME_COLON] = -1 return indices } // Determine outer structure from right to left. - // Url = scheme ":" hier-part [ "?" query ] [ "#" fragment ] - val length = urlString.length - var fragmentIndex = urlString.indexOf('#') + // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + val length = uriString.length + var fragmentIndex = uriString.indexOf('#') if (fragmentIndex == -1) { fragmentIndex = length } - var queryIndex = urlString.indexOf('?') + var queryIndex = uriString.indexOf('?') if (queryIndex == -1 || queryIndex > fragmentIndex) { // '#' before '?': '?' is within the fragment. queryIndex = fragmentIndex } // Slashes are allowed only in hier-part so any colon after the first slash is part of the // hier-part, not the scheme colon separator. - var schemeIndexLimit = urlString.indexOf('/') + var schemeIndexLimit = uriString.indexOf('/') if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) { schemeIndexLimit = queryIndex } - var schemeIndex = urlString.indexOf(':') + var schemeIndex = uriString.indexOf(':') if (schemeIndex > schemeIndexLimit) { // '/' before ':' schemeIndex = -1 @@ -513,10 +514,10 @@ object HlsPlaylistParser { // Determine hier-part structure: hier-part = "//" authority path / path // This block can also cope with schemeIndex == -1. val hasAuthority = - schemeIndex + 2 < queryIndex && urlString[schemeIndex + 1] == '/' && urlString[schemeIndex + 2] == '/' + schemeIndex + 2 < queryIndex && uriString[schemeIndex + 1] == '/' && uriString[schemeIndex + 2] == '/' var pathIndex: Int if (hasAuthority) { - pathIndex = urlString.indexOf('/', schemeIndex + 3) // find first '/' after "://" + pathIndex = uriString.indexOf('/', schemeIndex + 3) // find first '/' after "://" if (pathIndex == -1 || pathIndex > queryIndex) { pathIndex = queryIndex } @@ -805,7 +806,7 @@ object HlsPlaylistParser { const val APPLICATION_MEDIA3_CUES: String = "$BASE_TYPE_APPLICATION/x-media3-cues" - /** MIME type for an image URL loaded from an external image management framework. */ + /** MIME type for an image URI loaded from an external image management framework. */ const val APPLICATION_EXTERNALLY_LOADED_IMAGE: String = "$BASE_TYPE_APPLICATION/x-image-uri" @@ -1168,6 +1169,7 @@ object HlsPlaylistParser { return parseOptionalStringAttr(line, pattern, null, variableDefinitions) } + @OptIn(ExperimentalEncodingApi::class) @Throws(ParserException::class) private fun parseDrmSchemeData( line: String, keyFormat: String, variableDefinitions: Map @@ -1175,22 +1177,22 @@ object HlsPlaylistParser { val keyFormatVersions = parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions) if (KEYFORMAT_WIDEVINE_PSSH_BINARY == keyFormat) { - val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions) + val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions) return SchemeData( - uuid = C.WIDEVINE_UUID, + uuid = WIDEVINE_UUID, mimeType = MimeTypes.VIDEO_MP4, - data = base64DecodeArray(urlString.substring(urlString.indexOf(','))) + data = Base64.Default.decode(uriString.substring(uriString.indexOf(','))) ) } else if (KEYFORMAT_WIDEVINE_PSSH_JSON == keyFormat) { return SchemeData( uuid = C.WIDEVINE_UUID, mimeType = "hls", - data = line.encodeToByteArray() + data = line.toByteArray(charset = Charsets.UTF_8) ) } else if (KEYFORMAT_PLAYREADY == keyFormat && "1" == keyFormatVersions) { - val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions) + val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions) val data: ByteArray = - base64DecodeArray(urlString.substring(urlString.indexOf(','))) + Base64.Default.decode(uriString.substring(uriString.indexOf(','))) val psshData: ByteArray = PsshAtomUtil.buildPsshAtom( systemId = C.PLAYREADY_UUID, @@ -1268,7 +1270,7 @@ object HlsPlaylistParser { } data class Variant( - val url: Url, + val url: URI, val format: Format, val videoGroupId: String?, val audioGroupId: String?, @@ -1321,7 +1323,7 @@ object HlsPlaylistParser { data class Rendition( /** The rendition's url, or null if the tag does not have a URI attribute. */ - val url: Url?, + val url: URI?, /** Format information associated with this rendition. */ val format: Format, @@ -1334,14 +1336,14 @@ object HlsPlaylistParser { ) data class HlsMultivariantPlaylist( - /** The base url. Used to resolve relative paths. */ + /** The base uri. Used to resolve relative paths. */ val baseUri: String, /** The list of tags in the playlist. */ val tags: List, /** All of the media playlist URLs referenced by the playlist. */ - //val mediaPlaylistUrls: List, + //val mediaPlaylistUrls: List, /** The variants declared by the playlist. */ val variants: List, @@ -1727,8 +1729,8 @@ object HlsPlaylistParser { private fun parseMultivariantPlaylist( iterator: Iterator, baseUri: String ): HlsMultivariantPlaylist { - val urlToVariantInfos: HashMap?> = - HashMap?>() + val urlToVariantInfos: HashMap?> = + HashMap?>() val variableDefinitions = HashMap() val variants: ArrayList = ArrayList() val videos: ArrayList = ArrayList() @@ -1851,10 +1853,10 @@ object HlsPlaylistParser { parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions) val closedCaptionsGroupId: String? = parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions) - val url: Url + val uri: URI if (isIFrameOnlyVariant) { - url = - UrlUtil.resolveToUrl( + uri = + UriUtil.resolveToUri( baseUri, parseStringAttr(line, REGEX_URI, variableDefinitions) ) @@ -1863,14 +1865,14 @@ object HlsPlaylistParser { "#EXT-X-STREAM-INF must be followed by another line", /* cause= */null ) } else { - // The following line contains #EXT-X-STREAM-INF's URL. + // The following line contains #EXT-X-STREAM-INF's URI. line = replaceVariableReferences(iterator.next(), variableDefinitions) - url = UrlUtil.resolveToUrl(baseUri, line) + uri = UriUtil.resolveToUri(baseUri, line) } val variant = Variant( - url = url, + url = uri, format = Format( id = variants.size.toString(), containerMimeType = MimeTypes.APPLICATION_M3U8, @@ -1888,10 +1890,10 @@ object HlsPlaylistParser { captionGroupId = closedCaptionsGroupId ) variants.add(variant) - var variantInfosForUrl: ArrayList? = urlToVariantInfos[url] + var variantInfosForUrl: ArrayList? = urlToVariantInfos[uri] if (variantInfosForUrl == null) { variantInfosForUrl = ArrayList() - urlToVariantInfos[url] = variantInfosForUrl + urlToVariantInfos[uri] = variantInfosForUrl } variantInfosForUrl.add( VariantInfo( @@ -1909,7 +1911,7 @@ object HlsPlaylistParser { // TODO: Don't deduplicate variants by URL. val deduplicatedVariants = variants.distinctBy { it.url } /*val deduplicatedVariants: ArrayList = ArrayList() - val urlsInDeduplicatedVariants = HashSet() + val urlsInDeduplicatedVariants = HashSet() for (i in variants.indices) { val variant: Variant = variants[i] if (urlsInDeduplicatedVariants.add(variant.url)) { @@ -1943,10 +1945,10 @@ object HlsPlaylistParser { containerMimeType = MimeTypes.APPLICATION_M3U8, ) - val referenceUrl: String? = + val referenceUri: String? = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions) - val url: Url? = - if (referenceUrl == null) null else UrlUtil.resolveToUrl(baseUri, referenceUrl) + val uri: URI? = + if (referenceUri == null) null else UriUtil.resolveToUri(baseUri, referenceUri) //val metadata = // Metadata(HlsTrackMetadataEntry(groupId, name, emptyList())) when (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) { @@ -1961,11 +1963,11 @@ object HlsPlaylistParser { codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO) ) } - if (url == null) { - // TODO: Remove this case and add a Rendition with a null url to videos. + if (uri == null) { + // TODO: Remove this case and add a Rendition with a null uri to videos. } else { //formatBuilder.setMetadata(metadata) - videos.add(Rendition(url = url, format = formatBuilder, groupId, name)) + videos.add(Rendition(url = uri, format = formatBuilder, groupId, name)) } } @@ -1993,11 +1995,11 @@ object HlsPlaylistParser { } } val format = formatBuilder.copy(sampleMimeType = sampleMimeType) - if (url != null) { + if (uri != null) { //formatBuilder.setMetadata(metadata) - audios.add(Rendition(url, format, groupId, name)) + audios.add(Rendition(uri, format, groupId, name)) } else if (variant != null) { - // TODO: Remove muxedAudioFormat and add a Rendition with a null url to audios. + // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios. muxedAudioFormat = format } } @@ -2016,10 +2018,10 @@ object HlsPlaylistParser { if (sampleMimeType == null) { sampleMimeType = MimeTypes.TEXT_VTT } - if (url != null) { + if (uri != null) { subtitles.add( Rendition( - url, + uri, formatBuilder.copy(sampleMimeType = sampleMimeType), groupId, name @@ -2076,4 +2078,4 @@ object HlsPlaylistParser { sessionKeyDrmInitData = sessionKeyDrmInitData ) } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt index 8aabeb1f1..1ec442d7b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt @@ -1,9 +1,11 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.mvvm.logError +import java.util.regex.Matcher +import java.util.regex.Pattern import kotlin.math.pow -// author: https://github.com/daarkdemon +//author: https://github.com/daarkdemon class JsHunter(private val hunterJS: String) { /** @@ -12,8 +14,9 @@ class JsHunter(private val hunterJS: String) { * @return true if it's H.U.N.T.E.R coded. */ fun detect(): Boolean { - val regex = Regex("eval\\(function\\(h,u,n,t,e,r\\)") - return regex.containsMatchIn(hunterJS) + val p = Pattern.compile("eval\\(function\\(h,u,n,t,e,r\\)") + val searchResults = p.matcher(hunterJS) + return searchResults.find() } /** @@ -21,15 +24,20 @@ class JsHunter(private val hunterJS: String) { * * @return the javascript unhunt or null. */ + fun dehunt(): String? { try { - val regex = Regex("""\}\("([^"]+)",[^,]+,\s*"([^"]+)",\s*(\d+),\s*(\d+)""", RegexOption.DOT_MATCHES_ALL) - val match = regex.find(hunterJS) - if (match != null && match.groupValues.size == 5) { - val h = match.groupValues[1] - val n = match.groupValues[2] - val t = match.groupValues[3].toInt() - val e = match.groupValues[4].toInt() + val p: Pattern = + Pattern.compile( + """\}\("([^"]+)",[^,]+,\s*"([^"]+)",\s*(\d+),\s*(\d+)""", + Pattern.DOTALL + ) + val searchResults: Matcher = p.matcher(hunterJS) + if (searchResults.find() && searchResults.groupCount() == 4) { + val h = searchResults.group(1)!! + val n = searchResults.group(2)!! + val t = searchResults.group(3)!!.toInt() + val e = searchResults.group(4)!!.toInt() return hunter(h, n, t, e) } } catch (e: Exception) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 2dbbfddb0..7ed2e9be2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.mvvm.logError +import java.util.regex.Pattern import kotlin.math.pow // https://github.com/cylonu87/JsUnpacker @@ -14,7 +15,9 @@ class JsUnpacker(packedJS: String?) { */ fun detect(): Boolean { val js = packedJS!!.replace(" ", "") - return Regex("eval\\(function\\(p,a,c,k,e,[rd]").containsMatchIn(js) + val p = Pattern.compile("eval\\(function\\(p,a,c,k,e,[rd]") + val m = p.matcher(js) + return m.find() } /** @@ -25,42 +28,41 @@ class JsUnpacker(packedJS: String?) { fun unpack(): String? { val js = packedJS ?: return null try { - val match = Regex( - """\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", - RegexOption.DOT_MATCHES_ALL - ).find(js) - if (match != null && match.groupValues.size == 5) { - val payload = match.groupValues[1].replace("\\'", "'") - val radixStr = match.groupValues[2] - val countStr = match.groupValues[3] - val symtab = match.groupValues[4].split("|").toTypedArray() + var p = + Pattern.compile("""\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", Pattern.DOTALL) + var m = p.matcher(js) + if (m.find() && m.groupCount() == 4) { + val payload = m.group(1)?.replace("\\'", "'") ?: "" + val radixStr = m.group(2) + val countStr = m.group(3) + val symtab = (m.group(4)?.split("\\|".toRegex()) ?: emptyList()).toTypedArray() var radix = 36 var count = 0 try { - radix = radixStr.toIntOrNull() ?: radix + radix = radixStr?.toIntOrNull() ?: radix } catch (_: Exception) { } try { - count = countStr.toIntOrNull() ?: 0 + count = countStr?.toIntOrNull() ?: 0 } catch (_: Exception) { } if (symtab.size != count) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - val wordRegex = Regex("""\b[a-zA-Z0-9_]+\b""") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") + m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 - wordRegex.findAll(payload).forEach { wordMatch -> - val word = wordMatch.value - val x = unbase.unbase(word) - val value = if (x in symtab.indices) symtab[x] else null - if (!value.isNullOrEmpty()) { - decoded.replace( - wordMatch.range.first + replaceOffset, - wordMatch.range.last + 1 + replaceOffset, - value - ) + while (m.find()) { + val word = m.group(0) + val x = if (word == null) 0 else unbase.unbase(word) + var value: String? = null + if (x < symtab.size && x >= 0) { + value = symtab[x] + } + if (!value.isNullOrEmpty() && !word.isNullOrEmpty()) { + decoded.replace(m.start() + replaceOffset, m.end() + replaceOffset, value) replaceOffset += value.length - word.length } } @@ -78,7 +80,6 @@ class JsUnpacker(packedJS: String?) { " !\"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" private var alphabet: String? = null private var dictionary: HashMap? = null - fun unbase(str: String): Int { var ret = 0 if (alphabet == null) { @@ -95,10 +96,18 @@ class JsUnpacker(packedJS: String?) { init { if (radix > 36) { when { - radix < 62 -> alphabet = ALPHABET_62.substring(0, radix) - radix in 63..94 -> alphabet = ALPHABET_95.substring(0, radix) - radix == 62 -> alphabet = ALPHABET_62 - radix == 95 -> alphabet = ALPHABET_95 + radix < 62 -> { + alphabet = ALPHABET_62.substring(0, radix) + } + radix in 63..94 -> { + alphabet = ALPHABET_95.substring(0, radix) + } + radix == 62 -> { + alphabet = ALPHABET_62 + } + radix == 95 -> { + alphabet = ALPHABET_95 + } } dictionary = HashMap(95) for (i in 0 until alphabet!!.length) { @@ -115,20 +124,74 @@ class JsUnpacker(packedJS: String?) { this.packedJS = packedJS } + companion object { - val c = listOf( - 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x61, 0x6e, 0x64, - 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x67, 0x6d, 0x73, 0x2e, 0x61, 0x64, 0x73, 0x2e, 0x4d, - 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x41, 0x64, 0x73 - ) - val z = listOf( - 0x63, 0x6f, 0x6d, 0x2e, 0x66, 0x61, 0x63, 0x65, 0x62, 0x6f, 0x6f, 0x6b, 0x2e, 0x61, - 0x64, 0x73, 0x2e, 0x41, 0x64 - ) + val c = + listOf( + 0x63, + 0x6f, + 0x6d, + 0x2e, + 0x67, + 0x6f, + 0x6f, + 0x67, + 0x6c, + 0x65, + 0x2e, + 0x61, + 0x6e, + 0x64, + 0x72, + 0x6f, + 0x69, + 0x64, + 0x2e, + 0x67, + 0x6d, + 0x73, + 0x2e, + 0x61, + 0x64, + 0x73, + 0x2e, + 0x4d, + 0x6f, + 0x62, + 0x69, + 0x6c, + 0x65, + 0x41, + 0x64, + 0x73 + ) + val z = + listOf( + 0x63, + 0x6f, + 0x6d, + 0x2e, + 0x66, + 0x61, + 0x63, + 0x65, + 0x62, + 0x6f, + 0x6f, + 0x6b, + 0x2e, + 0x61, + 0x64, + 0x73, + 0x2e, + 0x41, + 0x64 + ) fun String.load(): String? { return try { var load = this + for (q in c.indices) { if (c[q % 4] > 270) { load += c[q % 3] @@ -136,6 +199,7 @@ class JsUnpacker(packedJS: String?) { load += c[q].toChar() } } + Class.forName(load.substring(load.length - c.size, load.length)).name } catch (_: Exception) { try { @@ -143,7 +207,7 @@ class JsUnpacker(packedJS: String?) { for (w in z.indices) { f += z[w].toChar() } - Class.forName(f.substring(0b001, f.length)).name + return Class.forName(f.substring(0b001, f.length)).name } catch (_: Exception) { null } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Levenshtein.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Levenshtein.kt deleted file mode 100644 index 2f3957630..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Levenshtein.kt +++ /dev/null @@ -1,515 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2026 Konstantin Tskhovrebov - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.lagradost.cloudstream3.utils - -import com.lagradost.cloudstream3.Prerelease -import kotlin.math.round - -// Taken from https://github.com/terrakok/FuzzyKot/blob/f794d43/fuzzykot/src/commonMain/kotlin/com/github/terrakok/fuzzykot/Levenshtein.kt -@Prerelease -object Levenshtein { - fun ratio(s1: String, s2: String, processor: (String) -> String = { it }): Int { - val p1 = processor(s1) - val p2 = processor(s2) - return round(100 * basicRatio(p1, p2)).toInt() - } - - fun partialRatio(s1: String, s2: String, processor: (String) -> String = { it }): Int { - val p1 = processor(s1) - val p2 = processor(s2) - - val shorter: String - val longer: String - - if (p1.length < p2.length) { - shorter = p1 - longer = p2 - } else { - shorter = p2 - longer = p1 - } - - if (shorter.isEmpty()) return if (longer.isEmpty()) 100 else 0 - - val matchingBlocks = getMatchingBlocks(shorter.length, longer.length, getEditOps(shorter, longer)) - val scores = mutableListOf() - - for (mb in matchingBlocks) { - val dist = mb.dpos - mb.spos - val longStart = if (dist > 0) dist else 0 - var longEnd = longStart + shorter.length - if (longEnd > longer.length) longEnd = longer.length - - val longSubstr = longer.substring(longStart, longEnd) - val ratio = basicRatio(shorter, longSubstr) - - if (ratio > .995) return 100 - scores.add(ratio) - } - - return round(100 * (scores.maxOrNull() ?: 0.0)).toInt() - } - - private fun basicRatio(s1: String, s2: String): Double { - val lensum = s1.length + s2.length - if (lensum == 0) return 1.0 - val editDistance = levEditDistance(s1, s2, 1) - return (lensum - editDistance) / lensum.toDouble() - } -} - -private enum class EditType { - DELETE, - EQUAL, - INSERT, - REPLACE, - KEEP -} - -private data class EditOp( - var type: EditType? = null, - var spos: Int = 0, - var dpos: Int = 0 -) { - override fun toString(): String = "${type?.name ?: "null"}($spos,$dpos)" -} - -private data class MatchingBlock( - val spos: Int = 0, - val dpos: Int = 0, - val length: Int = 0 -) { - override fun toString(): String = "($spos,$dpos,$length)" -} - -private fun getEditOps(s1: String, s2: String): Array { - var len1Copy = s1.length - var len2Copy = s2.length - - var len1o = 0 - var i = 0 - - val matrix: IntArray - - val c1 = s1 - val c2 = s2 - - var p1 = 0 - var p2 = 0 - - while (len1Copy > 0 && len2Copy > 0 && c1[p1] == c2[p2]) { - len1Copy-- - len2Copy-- - p1++ - p2++ - len1o++ - } - - val len2o = len1o - - while (len1Copy > 0 && len2Copy > 0 && c1[p1 + len1Copy - 1] == c2[p2 + len2Copy - 1]) { - len1Copy-- - len2Copy-- - } - - len1Copy++ - len2Copy++ - - matrix = IntArray(len2Copy * len1Copy) - - while (i < len2Copy) { - matrix[i] = i - i++ - } - i = 1 - while (i < len1Copy) { - matrix[len2Copy * i] = i - i++ - } - - i = 1 - while (i < len1Copy) { - var ptrPrev = (i - 1) * len2Copy - var ptrC = i * len2Copy - val ptrEnd = ptrC + len2Copy - 1 - - val char1 = c1[p1 + i - 1] - var ptrChar2 = p2 - - var x = i - ptrC++ - - while (ptrC <= ptrEnd) { - var c3 = matrix[ptrPrev++] + if (char1 != c2[ptrChar2++]) 1 else 0 - x++ - if (x > c3) x = c3 - c3 = matrix[ptrPrev] + 1 - if (x > c3) x = c3 - matrix[ptrC++] = x - } - i++ - } - - return editOpsFromCostMatrix(len1Copy, c1, p1, len1o, len2Copy, c2, p2, len2o, matrix) -} - -private fun editOpsFromCostMatrix( - len1: Int, c1: String, p1: Int, o1: Int, - len2: Int, c2: String, p2: Int, o2: Int, - matrix: IntArray -): Array { - var i: Int = len1 - 1 - var j: Int = len2 - 1 - var pos: Int = matrix[len1 * len2 - 1] - var ptr: Int = len1 * len2 - 1 - val ops: Array = arrayOfNulls(pos) - var dir = 0 - - while (i > 0 || j > 0) { - if (dir < 0 && j != 0 && matrix[ptr] == matrix[ptr - 1] + 1) { - val eop = EditOp() - pos-- - ops[pos] = eop - eop.type = EditType.INSERT - eop.spos = i + o1 - eop.dpos = --j + o2 - ptr-- - continue - } - - if (dir > 0 && i != 0 && matrix[ptr] == matrix[ptr - len2] + 1) { - val eop = EditOp() - pos-- - ops[pos] = eop - eop.type = EditType.DELETE - eop.spos = --i + o1 - eop.dpos = j + o2 - ptr -= len2 - continue - } - - if (i != 0 && j != 0 && matrix[ptr] == matrix[ptr - len2 - 1] && c1[p1 + i - 1] == c2[p2 + j - 1]) { - i-- - j-- - ptr -= len2 + 1 - dir = 0 - continue - } - - if (i != 0 && j != 0 && matrix[ptr] == matrix[ptr - len2 - 1] + 1) { - pos-- - val eop = EditOp() - ops[pos] = eop - eop.type = EditType.REPLACE - eop.spos = --i + o1 - eop.dpos = --j + o2 - ptr -= len2 + 1 - dir = 0 - continue - } - - if (dir == 0 && j != 0 && matrix[ptr] == matrix[ptr - 1] + 1) { - pos-- - val eop = EditOp() - ops[pos] = eop - eop.type = EditType.INSERT - eop.spos = i + o1 - eop.dpos = --j + o2 - ptr-- - dir = -1 - continue - } - - if (dir == 0 && i != 0 && matrix[ptr] == matrix[ptr - len2] + 1) { - pos-- - val eop = EditOp() - ops[pos] = eop - eop.type = EditType.DELETE - eop.spos = --i + o1 - eop.dpos = j + o2 - ptr -= len2 - dir = 1 - continue - } - } - - return ops.requireNoNulls() -} - -private fun getMatchingBlocks(len1: Int, len2: Int, ops: Array): Array { - val n = ops.size - var numberOfMatchingBlocks = 0 - var i: Int - var spos: Int - var dpos: Int - var o = 0 - - dpos = 0 - spos = dpos - - i = n - while (i != 0) { - while (ops[o].type === EditType.KEEP && --i != 0) { - o++ - } - if (i == 0) break - if (spos < ops[o].spos || dpos < ops[o].dpos) { - numberOfMatchingBlocks++ - spos = ops[o].spos - dpos = ops[o].dpos - } - val type = ops[o].type!! - when (type) { - EditType.REPLACE -> do { - spos++ - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.DELETE -> do { - spos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.INSERT -> do { - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - else -> {} - } - } - - if (spos < len1 || dpos < len2) numberOfMatchingBlocks++ - - val matchingBlocks = arrayOfNulls(numberOfMatchingBlocks + 1) - o = 0 - dpos = 0 - spos = dpos - var mbIndex = 0 - - i = n - while (i != 0) { - while (ops[o].type === EditType.KEEP && --i != 0) o++ - if (i == 0) break - if (spos < ops[o].spos || dpos < ops[o].dpos) { - val mb = MatchingBlock( - spos = spos, - dpos = dpos, - length = ops[o].spos - spos - ) - spos = ops[o].spos - dpos = ops[o].dpos - matchingBlocks[mbIndex++] = mb - } - val type = ops[o].type!! - when (type) { - EditType.REPLACE -> do { - spos++ - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.DELETE -> do { - spos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - EditType.INSERT -> do { - dpos++ - i-- - o++ - } while (i != 0 && ops[o].type === type && spos == ops[o].spos && dpos == ops[o].dpos) - - else -> {} - } - } - - if (spos < len1 || dpos < len2) { - val mb = MatchingBlock( - spos = spos, - dpos = dpos, - length = len1 - spos - ) - matchingBlocks[mbIndex++] = mb - } - - val finalBlock = MatchingBlock( - spos = len1, - dpos = len2, - length = 0 - ) - matchingBlocks[mbIndex] = finalBlock - - return matchingBlocks.filterNotNull().toTypedArray() -} - -private fun levEditDistance(s1: String, s2: String, xcost: Int): Int { - var i: Int - val half: Int - - var c1 = s1 - var c2 = s2 - - var str1 = 0 - var str2 = 0 - - var len1 = s1.length - var len2 = s2.length - - while (len1 > 0 && len2 > 0 && c1[str1] == c2[str2]) { - len1-- - len2-- - str1++ - str2++ - } - - while (len1 > 0 && len2 > 0 && c1[str1 + len1 - 1] == c2[str2 + len2 - 1]) { - len1-- - len2-- - } - - if (len1 == 0) return len2 - if (len2 == 0) return len1 - - if (len1 > len2) { - val nx = len1 - val temp = str1 - len1 = len2 - len2 = nx - str1 = str2 - str2 = temp - val t = c2 - c2 = c1 - c1 = t - } - - if (len1 == 1) { - return if (xcost != 0) { - len2 + 1 - 2 * memchr(c2, str2, c1[str1], len2) - } else { - len2 - memchr(c2, str2, c1[str1], len2) - } - } - - len1++ - len2++ - half = len1 shr 1 - - val row = IntArray(len2) - var end = len2 - 1 - - i = 0 - while (i < len2 - if (xcost != 0) 0 else half) { - row[i] = i - i++ - } - - if (xcost != 0) { - i = 1 - while (i < len1) { - var p = 1 - val ch1 = c1[str1 + i - 1] - var c2p = str2 - var D = i - var x = i - while (p <= end) { - if (ch1 == c2[c2p++]) { - x = --D - } else { - x++ - } - D = row[p] - D++ - if (x > D) x = D - row[p++] = x - } - i++ - } - } else { - row[0] = len1 - half - 1 - i = 1 - while (i < len1) { - var p: Int - val ch1 = c1[str1 + i - 1] - var c2p: Int - var D: Int - var x: Int - - if (i >= len1 - half) { - val offset = i - (len1 - half) - c2p = str2 + offset - p = offset - val c3 = row[p++] + if (ch1 != c2[c2p++]) 1 else 0 - x = row[p] - x++ - D = x - if (x > c3) x = c3 - row[p++] = x - } else { - p = 1 - c2p = str2 - x = i - D = x - } - if (i <= half + 1) end = len2 + i - half - 2 - while (p <= end) { - val c3 = --D + if (ch1 != c2[c2p++]) 1 else 0 - x++ - if (x > c3) x = c3 - D = row[p] - D++ - if (x > D) x = D - row[p++] = x - } - if (i <= half) { - val c3 = --D + if (ch1 != c2[c2p]) 1 else 0 - x++ - if (x > c3) x = c3 - row[p] = x - } - i++ - } - } - - return row[end] -} - -private fun memchr(haystack: String, offset: Int, needle: Char, num: Int): Int { - var numCopy = num - if (numCopy != 0) { - var p = 0 - do { - if (haystack[offset + p] == needle) return 1 - p++ - } while (--numCopy != 0) - } - return 0 -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 00d448d94..23226418b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -112,8 +112,8 @@ object M3u8Helper2 { return c.doFinal(data) } - private fun getParentLink(url: String): String { - val split = url.split("/").toMutableList() + private fun getParentLink(uri: String): String { + val split = uri.split("/").toMutableList() split.removeAt(split.lastIndex) return split.joinToString("/") } @@ -322,15 +322,15 @@ object M3u8Helper2 { if (!match.isNullOrEmpty()) { encryptionState = true - var encryptionUrl = match[2] + var encryptionUri = match[2] - if (isNotCompleteUrl(encryptionUrl)) { - encryptionUrl = "${getParentLink(playlistStream.streamUrl)}/$encryptionUrl" + if (isNotCompleteUrl(encryptionUri)) { + encryptionUri = "${getParentLink(playlistStream.streamUrl)}/$encryptionUri" } - encryptionIv = match[3].encodeToByteArray() + encryptionIv = match[3].toByteArray() val encryptionKeyResponse = - app.get(encryptionUrl, headers = playlistStream.headers, verify = false) + app.get(encryptionUri, headers = playlistStream.headers, verify = false) val body = encryptionKeyResponse.body encryptionData = body.bytes() body.close() diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt index b9233490a..1e3a2ffb7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/StringUtils.kt @@ -1,32 +1,14 @@ package com.lagradost.cloudstream3.utils -import com.lagradost.cloudstream3.Prerelease -import io.ktor.http.decodeURLQueryComponent -import io.ktor.http.encodeURLParameter +import java.net.URLDecoder +import java.net.URLEncoder object StringUtils { - @Prerelease - fun String.decodeUrl(): String { - return this.decodeURLQueryComponent() + fun String.encodeUri(): String { + return URLEncoder.encode(this, "UTF-8") } - @Prerelease - fun String.encodeUrl(): String { - return this.encodeURLParameter() + fun String.decodeUri(): String { + return URLDecoder.decode(this, "UTF-8") } - - // Deprecate after next stable - - /* @Deprecated( - message = "Use Ktor 'Url' naming convention instead.", - replaceWith = ReplaceWith("this.encodeUrl()") - ) */ - fun String.encodeUri(): String = encodeUrl() - - /* @Deprecated( - message = "Use Ktor 'Url' naming convention instead.", - replaceWith = ReplaceWith("this.decodeUrl()") - ) */ - fun String.decodeUri(): String = decodeUrl() -} - +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt index a6362efd0..8d5479cc0 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.utils +import me.xdrop.fuzzywuzzy.FuzzySearch import java.util.Locale // If you find a way to use SettingsGeneral getCurrentLocale() @@ -111,8 +112,8 @@ object SubtitleHelper { for (lang in languages) { val score = maxOf( - Levenshtein.ratio(lowLangName, lang.languageName.lowercase()), - Levenshtein.ratio( + FuzzySearch.ratio(lowLangName, lang.languageName.lowercase()), + FuzzySearch.ratio( lowLangName, lang.nativeName.lowercase() ) ) @@ -314,23 +315,10 @@ object SubtitleHelper { val flagOffset = 0x1F1E6 // regional indicator "[A]" val offset = flagOffset - asciiOffset - /** - * Unicode surrogate pairs encode code points above U+FFFF (outside the Basic Multilingual Plane). - * The code point is offset by 0x10000, then split into two 10-bit halves: - * high surrogate: upper 10 bits, biased into the range 0xD800-0xDBFF - * low surrogate: lower 10 bits (masked with 0x3FF), biased into the range 0xDC00-0xDFFF - */ - fun toSurrogatePair(codePoint: Int): String { - val high = ((codePoint - 0x10000) shr 10) + 0xD800 - val low = ((codePoint - 0x10000) and 0x3FF) + 0xDC00 - return "${high.toChar()}${low.toChar()}" - } + val firstChar: Int = Character.codePointAt(countryLetters, 0) + offset + val secondChar: Int = Character.codePointAt(countryLetters, 1) + offset - val upperLetters = countryLetters.uppercase() - val first = upperLetters[0].code + offset - val second = upperLetters[1].code + offset - - return toSurrogatePair(first) + toSurrogatePair(second) + return String(Character.toChars(firstChar)) + String(Character.toChars(secondChar)) } // when (langTag = country) or (langTag contains country) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt index 8bdbf3788..206b0f29f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt @@ -2,9 +2,9 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Decode -import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl import com.lagradost.nicehttp.NiceResponse -import io.ktor.http.Url +import java.net.URI +import java.net.URLDecoder // Code heavily based on unshortenit.py form kodiondemand /addon @@ -48,8 +48,8 @@ object ShortLink { } } - suspend fun unshorten(url: String, type: String? = null): String { - var currentUrl = url + suspend fun unshorten(uri: String, type: String? = null): String { + var currentUrl = uri val visitedUrls = mutableSetOf() var count = 10 @@ -57,7 +57,9 @@ object ShortLink { visitedUrls += currentUrl count -= 1 - val domain = Url(currentUrl.trim()).host + val domain = + URI(currentUrl.trim()).host + ?: throw IllegalArgumentException("No domain found in URI!") currentUrl = shortList.firstOrNull { it.regex.find(domain) != null || type == it.type }?.function?.let { it(currentUrl) } ?: break @@ -65,8 +67,8 @@ object ShortLink { return currentUrl.trim() } - suspend fun unshortenAdfly(url: String): String { - val html = app.get(url).text + suspend fun unshortenAdfly(uri: String): String { + val html = app.get(uri).text val ysmm = Regex("""var ysmm =.*;?""").find(html)!!.value if (ysmm.isNotEmpty()) { @@ -79,46 +81,46 @@ object ShortLink { left += c[0] right = c[1] + right } - val encodedUrl = (left + right).toMutableList() + val encodedUri = (left + right).toMutableList() val numbers = - encodedUrl.mapIndexed { i, n -> Pair(i, n) }.filter { it.second.isDigit() } + encodedUri.mapIndexed { i, n -> Pair(i, n) }.filter { it.second.isDigit() } for (el in numbers.chunked(2).dropLastWhile { it.size == 1 }) { val xor = (el[0].second).code.xor(el[1].second.code) if (xor < 10) { - encodedUrl[el[0].first] = xor.digitToChar() + encodedUri[el[0].first] = xor.digitToChar() } } - val encodedbytearray = encodedUrl.map { it.code.toByte() }.toByteArray() - var decodedUrl = + val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray() + var decodedUri = base64Decode(encodedbytearray.toString()).dropLast(16) .drop(16) - if (Regex("""go\.php\?u=""").find(decodedUrl) != null) { - decodedUrl = - base64Decode(decodedUrl.replace(Regex("""(.*?)u="""), "")) + if (Regex("""go\.php\?u=""").find(decodedUri) != null) { + decodedUri = + base64Decode(decodedUri.replace(Regex("""(.*?)u="""), "")) } - return decodedUrl + return decodedUri } else { - return url + return uri } } - suspend fun unshortenLinkup(url: String): String { + suspend fun unshortenLinkup(uri: String): String { var r: NiceResponse? = null - var url = url + var uri = uri when { - url.contains("/tv/") -> url = url.replace("/tv/", "/tva/") - url.contains("delta") -> url = url.replace("/delta/", "/adelta/") - (url.contains("/ga/") || url.contains("/ga2/")) -> url = - base64Decode(url.split('/').last()).trim() + uri.contains("/tv/") -> uri = uri.replace("/tv/", "/tva/") + uri.contains("delta") -> uri = uri.replace("/delta/", "/adelta/") + (uri.contains("/ga/") || uri.contains("/ga2/")) -> uri = + base64Decode(uri.split('/').last()).trim() - url.contains("/speedx/") -> url = - url.replace("http://linkup.pro/speedx", "http://speedvideo.net") + uri.contains("/speedx/") -> uri = + uri.replace("http://linkup.pro/speedx", "http://speedvideo.net") else -> { - r = app.get(url, allowRedirects = true) - url = r.url + r = app.get(uri, allowRedirects = true) + uri = r.url val link = Regex("]*src=\\'([^'>]*)\\'[^<>]*>").find(r.text)?.value ?: Regex("""action="(?:[^/]+.*?/[^/]+/([a-zA-Z0-9_]+))">""").find(r.text)?.value @@ -126,40 +128,40 @@ object ShortLink { .elementAtOrNull(1)?.groupValues?.get(1) if (link != null) { - url = link + uri = link } } } - val short = Regex("""^https?://.*?(https?://.*)""").find(url)?.value + val short = Regex("""^https?://.*?(https?://.*)""").find(uri)?.value if (short != null) { - url = short + uri = short } if (r == null) { r = app.get( - url, + uri, allowRedirects = false ) if (r.headers["location"] != null) { - url = r.headers["location"].toString() + uri = r.headers["location"].toString() } } - if (url.contains("snip.")) { - if (url.contains("out_generator")) { - url = Regex("url=(.*)\$").find(url)!!.value - } else if (url.contains("/decode/")) { - url = app.get(url, allowRedirects = true).url + if (uri.contains("snip.")) { + if (uri.contains("out_generator")) { + uri = Regex("url=(.*)\$").find(uri)!!.value + } else if (uri.contains("/decode/")) { + uri = app.get(uri, allowRedirects = true).url } } - return url + return uri } - fun unshortenLinksafe(url: String): String { - return base64Decode(url.split("?url=").last()) + fun unshortenLinksafe(uri: String): String { + return base64Decode(uri.split("?url=").last()) } - suspend fun unshortenNuovoIndirizzo(url: String): String { - val soup = app.get(url, allowRedirects = true) + suspend fun unshortenNuovoIndirizzo(uri: String): String { + val soup = app.get(uri, allowRedirects = true) val header = soup.headers["refresh"] val link: String = if (header != null) { soup.headers["refresh"]!!.substringAfter("=") @@ -169,29 +171,29 @@ object ShortLink { return link } - suspend fun unshortenNuovoLink(url: String): String { - return app.get(url, allowRedirects = true).document.selectFirst("a")!!.attr("href") + suspend fun unshortenNuovoLink(uri: String): String { + return app.get(uri, allowRedirects = true).document.selectFirst("a")!!.attr("href") } - suspend fun unshortenUprot(url: String): String { - val page = app.get(url).text + suspend fun unshortenUprot(uri: String): String { + val page = app.get(uri).text Regex("""]+href="([^"]+)".*Continue""").findAll(page) .map { it.value.replace(""" - if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != url) { + if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != uri) { return link } } - return url + return uri } - fun unshortenDavisonbarker(url: String): String { - return url.substringAfter("dest=").decodeUrl() + fun unshortenDavisonbarker(uri: String): String { + return URLDecoder.decode(uri.substringAfter("dest="), "UTF-8") } - suspend fun unshortenIsecure(url: String): String { - val doc = app.get(url).document - return doc.selectFirst("iframe")?.attr("src")?.trim() ?: url + suspend fun unshortenIsecure(uri: String): String { + val doc = app.get(uri).document + return doc.selectFirst("iframe")?.attr("src")?.trim() ?: uri } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/NonEmptySerializer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/NonEmptySerializer.kt deleted file mode 100644 index 82de9f7f7..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/NonEmptySerializer.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import com.lagradost.cloudstream3.Prerelease -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.JsonTransformingSerializer - -/** - * Replicates Jackson's @JsonInclude(JsonInclude.Include.NON_EMPTY) behaviour. - * Strips null, empty strings, empty arrays, and empty objects from the serialized - * output. Requires the enclosing Json instance to have encodeDefaults = true, - * which is already in our default global Json instance. - * - * Usage: - * - * @OptIn(ExperimentalSerializationApi::class) - * @KeepGeneratedSerializer - * @Serializable(with = MyData.Serializer::class) - * data class MyData( - * val tags: List = emptyList(), - * val title: String = "", - * val meta: Map = emptyMap(), - * ) { - * object Serializer : NonEmptySerializer(MyData.generatedSerializer()) - * } - */ -@Prerelease -abstract class NonEmptySerializer(tSerializer: KSerializer) : - JsonTransformingSerializer(tSerializer) { - - override fun transformSerialize(element: JsonElement): JsonElement { - if (element !is JsonObject) return element - - return JsonObject(element.filterValues { value -> - when (value) { - is JsonPrimitive -> value.content.isNotEmpty() - is JsonArray -> value.isNotEmpty() - is JsonObject -> value.isNotEmpty() - JsonNull -> false - } - }) - } -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/WriteOnlySerializer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/WriteOnlySerializer.kt deleted file mode 100644 index c7f412eaa..000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/serializers/WriteOnlySerializer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.lagradost.cloudstream3.utils.serializers - -import com.lagradost.cloudstream3.Prerelease -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonTransformingSerializer - -/** - * Replicates Jackson's @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) behaviour. - * Properties in [keysToIgnore] are deserialized normally but omitted from serialized output. - * - * Usage: - * - * @OptIn(ExperimentalSerializationApi::class) - * @KeepGeneratedSerializer - * @Serializable(with = MyData.Serializer::class) - * data class MyData( - * val fieldA: String = "", - * val fieldB: String = "", - * ) { - * object Serializer : WriteOnlySerializer( - * MyData.generatedSerializer(), - * setOf("fieldB"), - * ) - * } - */ -@Prerelease -abstract class WriteOnlySerializer( - tSerializer: KSerializer, - private val keysToIgnore: Set, -) : JsonTransformingSerializer(tSerializer) { - - override fun transformSerialize(element: JsonElement): JsonElement { - if (element !is JsonObject) return element - return JsonObject(element.filterKeys { it !in keysToIgnore }) - } -} diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt deleted file mode 100644 index 4dc56978e..000000000 --- a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/EpisodeDateTest.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.lagradost.cloudstream3 - -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn -import kotlinx.datetime.toInstant -import kotlin.time.Instant -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class EpisodeDateTest { - - private val api = object : MainAPI() { - override var name = "Test" - override var mainUrl = "https://test.com" - } - - private fun episode() = api.newEpisode("") - - @Test - fun addDateDefaultFormatParsesIsoDate() { - val ep = episode() - ep.addDate("2026-05-17") - val expected = LocalDate(2026, 5, 17) - .atStartOfDayIn(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - assertEquals(expected, ep.date) - } - - @Test - fun addDateNullDoesNotSetDate() { - val ep = episode() - ep.addDate(null as String?) - assertNull(ep.date) - } - - @Test - fun addDateInvalidStringLeavesDateNull() { - val ep = episode() - ep.addDate("not-a-date") - assertNull(ep.date) - } - - @Test - fun addDateCustomFormatParsesSlashDate() { - val ep = episode() - ep.addDate("17/05/2026", "dd/MM/yyyy") - val expected = LocalDate(2026, 5, 17) - .atStartOfDayIn(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - assertEquals(expected, ep.date) - } - - @Test - fun addDateIsoDateTimeWithOffsetUsesExactInstant() { - val ep = episode() - ep.addDate("2026-05-17T10:30:00.000+05:00", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - val expected = Instant.parse("2026-05-17T10:30:00.000+05:00").toEpochMilliseconds() - assertEquals(expected, ep.date) - } - - @Test - fun addDateUtcDateTimeUsesExactInstant() { - val ep = episode() - ep.addDate("2026-05-17T10:30:00.000Z", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - val expected = Instant.parse("2026-05-17T10:30:00.000Z").toEpochMilliseconds() - assertEquals(expected, ep.date) - } - - @Test - fun addDateDateTimeNoOffsetUsesSystemTimezone() { - val ep = episode() - ep.addDate("2026-05-17T10:30:00", "yyyy-MM-dd'T'HH:mm:ss") - val expected = LocalDateTime(2026, 5, 17, 10, 30, 0) - .toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - assertEquals(expected, ep.date) - } - - @Test - fun addDateLocalDateSetsCorrectEpochMillis() { - val ep = episode() - ep.addDate(LocalDate(2026, 5, 17)) - val expected = LocalDate(2026, 5, 17) - .atStartOfDayIn(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - assertEquals(expected, ep.date) - } - - @Test - fun addDateNullLocalDateLeavesDateNull() { - val ep = episode() - ep.addDate(null as LocalDate?) - assertNull(ep.date) - } - - @Test - fun addDateInstantSetsCorrectEpochMillis() { - val ep = episode() - val instant = Instant.parse("2026-05-17T10:30:00Z") - ep.addDate(instant) - assertEquals(instant.toEpochMilliseconds(), ep.date) - } - - @Test - fun addDateNullInstantLeavesDateNull() { - val ep = episode() - ep.addDate(null as Instant?) - assertNull(ep.date) - } - - @Test - fun addDateIsoWithMillisAndZUsesExactInstant() { - val ep = episode() - ep.addDate("2026-01-01T12:30:00.000Z") - assertEquals(1767270600000L, ep.date) - } - - @Test - fun addDateIsoWithZNoMillisUsesExactInstant() { - val ep = episode() - ep.addDate("2026-01-01T12:30:00Z") - assertEquals(1767270600000L, ep.date) - } - - @Test - fun addDateIsoWithPositiveOffsetUsesExactInstant() { - val ep = episode() - ep.addDate("2026-05-17T14:35:00+02:00") - // 14:35 +02:00 = 12:35 UTC = 2026-05-17T12:35:00Z - assertEquals(1779021300000L, ep.date) - } - - @Test - fun addDateIsoWithNegativeOffsetUsesExactInstant() { - val ep = episode() - ep.addDate("2026-05-17T09:35:00-05:00") - // 09:35 -05:00 = 14:35 UTC = 2026-05-17T14:35:00Z - assertEquals(1779028500000L, ep.date) - } - - @Test - fun addDateCustomFormatWithOffsetUsesExactInstant() { - val ep = episode() - ep.addDate("17/05/2026 14:35+02:00", "dd/MM/yyyy HH:mmXXX") - // 14:35 +02:00 = 12:35 UTC = 2026-05-17T12:35:00Z - assertEquals(1779021300000L, ep.date) - } - - @Test - fun addDateCustomFormatDateTimeNoOffsetUsesSystemTimezone() { - val ep = episode() - ep.addDate("17/05/2026 14:35", "dd/MM/yyyy HH:mm") - val expected = LocalDateTime(2026, 5, 17, 14, 35, 0) - .toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - assertEquals(expected, ep.date) - } - - @Test - fun addDateCustomFormatDateOnlyUsesStartOfDay() { - val ep = episode() - ep.addDate("17/05/2026", "dd/MM/yyyy") - val expected = LocalDate(2026, 5, 17) - .atStartOfDayIn(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - assertEquals(expected, ep.date) - } -} - -class IsUpcomingTest { - - @Test - fun isUpcomingFutureDate() { - assertTrue(isUpcoming("2099-01-01")) - } - - @Test - fun isUpcomingPastDate() { - assertFalse(isUpcoming("2000-01-01")) - } - - @Test - fun isUpcomingNullReturnsFalse() { - assertFalse(isUpcoming(null)) - } - - @Test - fun isUpcomingInvalidStringReturnsFalse() { - assertFalse(isUpcoming("not-a-date")) - } -} diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.jvm.kt deleted file mode 100644 index 345aa7185..000000000 --- a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.jvm.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.newAudioFile -import com.lagradost.cloudstream3.newSubtitleFile -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.newExtractorLink -import org.schabi.newpipe.extractor.stream.StreamInfo -import org.schabi.newpipe.extractor.stream.StreamType - -actual open class YoutubeExtractor actual constructor() : ExtractorApi() { - - actual override val mainUrl = "https://www.youtube.com" - actual override val name = "YouTube" - actual override val requiresReferer = false - - actual override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ) { - val videoId = extractYouTubeId(url) - val watchUrl = "$mainUrl/watch?v=$videoId" - - val info = StreamInfo.getInfo(watchUrl) - val isLive = info.streamType == StreamType.LIVE_STREAM - || info.streamType == StreamType.AUDIO_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_STREAM - || info.streamType == StreamType.POST_LIVE_AUDIO_STREAM - - if (isLive && info.hlsUrl != null) { - callback( - newExtractorLink( - source = name, - name = "YouTube Live", - url = info.hlsUrl - ) { - type = ExtractorLinkType.M3U8 - } - ) - } else { - processVideo(info, subtitleCallback, callback) - } - } - - private suspend fun processVideo( - info: StreamInfo, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ): Boolean { - val videoStreams = info.videoOnlyStreams.orEmpty() - if (videoStreams.isEmpty()) return false - - val audioStreams = info.audioStreams.orEmpty() - videoStreams.forEach { video -> - callback( - newExtractorLink( - source = name, - name = "YouTube ${normalizeCodec(video.codec)}", - url = video.content - ) { - quality = video.height - audioTracks = audioStreams.map { newAudioFile(it.content) } - } - ) - } - - info.subtitles.forEach { subtitle -> - subtitleCallback( - newSubtitleFile( - lang = subtitle.displayLanguageName - ?: subtitle.languageTag - ?: "Unknown", - url = subtitle.content - ) - ) - } - - return true - } - - private fun extractYouTubeId(url: String): String { - val regex = Regex( - "(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})" - ) - - return regex.find(url)?.groupValues?.get(1) - ?: throw IllegalArgumentException("Invalid YouTube URL: $url") - } - - private fun normalizeCodec(codec: String?): String { - if (codec.isNullOrBlank()) return "" - val c = codec.lowercase() - return when { - c.startsWith("av01") -> "AV1" - c.startsWith("vp9") -> "VP9" - c.startsWith("avc1") || c.startsWith("h264") -> "H264" - c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265" - else -> codec.substringBefore('.').uppercase() - } - } -}