mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-06-19 20:05:41 +00:00
Compare commits
91 commits
fixlifecyc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c6bf2984e |
||
|
|
6f458fc9b5 |
||
|
|
b4100dbfca |
||
|
|
943bc551e9 |
||
|
|
c045bfdc0d |
||
|
|
2c03a3d976 |
||
|
|
3417fe0160 |
||
|
|
55450a02fa |
||
|
|
6f9646e52f |
||
|
|
b222911e29 |
||
|
|
5667f52648 |
||
|
|
b2a02a174f |
||
|
|
18a857723b |
||
|
|
292d3f1442 |
||
|
|
8012c58069 |
||
|
|
4f8a79669c |
||
|
|
2181243dd1 |
||
|
|
eae18bb50d |
||
|
|
f7cbf25b30 |
||
|
|
fd579fcc18 |
||
|
|
79cc3fb501 |
||
|
|
d78b991d66 |
||
|
|
70053ebbae |
||
|
|
a4a4c31f8d |
||
|
|
3844c896f1 |
||
|
|
11f77fbe11 |
||
|
|
62662cb064 |
||
|
|
8e0c664b1e |
||
|
|
4836e2b371 |
||
|
|
0e16f429af |
||
|
|
5de7f207f2 |
||
|
|
e1aacce93d |
||
|
|
8e1b41ea61 |
||
|
|
b7f5826a19 |
||
|
|
8e7569df53 |
||
|
|
0728dd06a1 |
||
|
|
041d21a486 |
||
|
|
a124450ddc |
||
|
|
028a794ea5 |
||
|
|
c1b6fc2eeb |
||
|
|
647c274944 |
||
|
|
22be73619e |
||
|
|
a3c100e75b |
||
|
|
d24f8bca0f |
||
|
|
4c3c463a19 |
||
|
|
007c0ff9bc |
||
|
|
c8bc999d22 |
||
|
|
b353cf2017 |
||
|
|
70ed1c753d |
||
|
|
00e943ebc4 |
||
|
|
0afb23eb2e |
||
|
|
0b642bb47f |
||
|
|
c6c70d5751 |
||
|
|
c1b49d0dcb |
||
|
|
85cc10c2e0 |
||
|
|
dd016341c0 |
||
|
|
ac0a0d2941 |
||
|
|
4ab97e4605 |
||
|
|
f894b8f7ec |
||
|
|
72386cb98c |
||
|
|
419b902ead |
||
|
|
638d749945 |
||
|
|
0f41ca2641 |
||
|
|
a6000fbe04 |
||
|
|
862e2590d2 |
||
|
|
9bc5027ea7 |
||
|
|
7e406cb5eb |
||
|
|
a24dc2600e |
||
|
|
89cc63673b |
||
|
|
ab85737637 |
||
|
|
9a53e267ac |
||
|
|
03eb6ccd45 |
||
|
|
7425d283cd |
||
|
|
6eb833130d |
||
|
|
3d1852ba04 | ||
|
|
59ae579b7c |
||
|
|
aa6e702b59 |
||
|
|
0d16a636e2 |
||
|
|
bfc926814c |
||
|
|
a45f1d9ab1 |
||
|
|
948a2c1725 |
||
|
|
4e24aa5db1 |
||
|
|
7476d24db3 |
||
|
|
c82fec0862 |
||
|
|
e36e9e8d24 |
||
|
|
e64136db8a |
||
|
|
104ab26790 |
||
|
|
2400e6ab45 |
||
|
|
4cc76ee6c5 |
||
|
|
8523a4bd90 |
||
|
|
58f45c7bda |
193 changed files with 4846 additions and 3406 deletions
1
.github/workflows/build_to_archive.yml
vendored
1
.github/workflows/build_to_archive.yml
vendored
|
|
@ -71,6 +71,7 @@ jobs:
|
|||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
|
||||
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
|
|
|||
98
.github/workflows/issue_action.yml
vendored
98
.github/workflows/issue_action.yml
vendored
|
|
@ -1,98 +0,0 @@
|
|||
name: Issue automatic actions
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
issue-moderator:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
|
||||
- name: Similarity analysis
|
||||
id: similarity
|
||||
uses: actions-cool/issues-similarity-analysis@v1
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
filter-threshold: 0.60
|
||||
title-excludes: ''
|
||||
comment-title: |
|
||||
### Your issue looks similar to these issues:
|
||||
Please close if duplicate.
|
||||
comment-body: '${index}. ${similarity} #${number}'
|
||||
|
||||
- name: Label if possible duplicate
|
||||
if: steps.similarity.outputs.similar-issues-found =='true'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ["possible duplicate"]
|
||||
})
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Automatically close issues that dont follow the issue template
|
||||
uses: lucasbento/auto-close-issues@v1.0.2
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
issue-close-message: |
|
||||
@${issue.user.login}: hello! :wave:
|
||||
This issue is being automatically closed because it does not follow the issue template."
|
||||
closed-issues-label: "invalid"
|
||||
|
||||
- name: Check if issue mentions a provider
|
||||
id: provider_check
|
||||
env:
|
||||
GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
|
||||
run: |
|
||||
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
|
||||
pip3 install httpx
|
||||
RES="$(python3 ./check_issue.py)"
|
||||
echo "name=${RES}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment if issue mentions a provider
|
||||
if: steps.provider_check.outputs.name != 'none'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
body: |
|
||||
Hello ${{ github.event.issue.user.login }}.
|
||||
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
|
||||
|
||||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||
|
||||
- name: Label if mentions provider
|
||||
if: steps.provider_check.outputs.name != 'none'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ["possible provider issue"]
|
||||
})
|
||||
|
||||
- name: Add eyes reaction to all issues
|
||||
uses: actions-cool/emoji-helper@v1.0.0
|
||||
with:
|
||||
type: 'issue'
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
emoji: 'eyes'
|
||||
1
.github/workflows/prerelease.yml
vendored
1
.github/workflows/prerelease.yml
vendored
|
|
@ -62,6 +62,7 @@ jobs:
|
|||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
|
||||
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
|
||||
|
||||
- name: Create pre-release
|
||||
|
|
|
|||
2
.github/workflows/pull_request.yml
vendored
2
.github/workflows/pull_request.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
cache-read-only: false
|
||||
|
||||
- name: Run Gradle
|
||||
run: ./gradlew assemblePrereleaseDebug lint
|
||||
run: ./gradlew assemblePrereleaseDebug lint check
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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())
|
||||
|
|
@ -103,8 +104,8 @@ android {
|
|||
applicationId = "com.lagradost.cloudstream3"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 68
|
||||
versionName = "4.7.0"
|
||||
versionCode = libs.versions.versionCode.get().toInt()
|
||||
versionName = libs.versions.versionName.get()
|
||||
|
||||
manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
|
||||
|
||||
|
|
@ -206,17 +207,22 @@ dependencies {
|
|||
testImplementation(libs.junit)
|
||||
testImplementation(libs.json)
|
||||
androidTestImplementation(libs.core)
|
||||
implementation(libs.junit.ktx)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.instancio.core)
|
||||
androidTestImplementation(libs.junit.ktx)
|
||||
androidTestImplementation(libs.kotlin.test)
|
||||
|
||||
// Android Core & Lifecycle
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.activity.ktx)
|
||||
implementation(libs.annotation)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.fragment.ktx)
|
||||
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)
|
||||
|
|
@ -253,13 +259,15 @@ 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)
|
||||
|
||||
|
|
@ -308,6 +316,7 @@ tasks.withType<KotlinJvmCompile> {
|
|||
optIn.addAll(
|
||||
"com.lagradost.cloudstream3.InternalAPI",
|
||||
"com.lagradost.cloudstream3.Prerelease",
|
||||
"kotlin.uuid.ExperimentalUuidApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import dalvik.system.DexFile
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlinx.serialization.serializerOrNull
|
||||
import org.instancio.Instancio
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SerializationClassTester {
|
||||
// Same as app, or using app reference
|
||||
val jacksonMapper = mapper
|
||||
val kotlinxMapper = json
|
||||
|
||||
@Test
|
||||
fun isIdenticalSerialization() {
|
||||
val serializableClasses = findSerializableClasses("com.lagradost")
|
||||
println("Number of serializable classes: ${serializableClasses.size}")
|
||||
|
||||
serializableClasses.forEach { kClass ->
|
||||
val instance = Instancio.create(kClass.java)
|
||||
|
||||
val jacksonJson = jacksonMapper.writeValueAsString(instance)
|
||||
val kotlinxJson = serializeWithKotlinx(kClass, instance)
|
||||
|
||||
assertEquals(
|
||||
jacksonJson,
|
||||
kotlinxJson,
|
||||
"""
|
||||
Serialization mismatch for:
|
||||
${kClass.qualifiedName}
|
||||
|
||||
Jackson:
|
||||
$jacksonJson
|
||||
|
||||
Kotlinx:
|
||||
$kotlinxJson
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
println("Identical serialization for: ${kClass.jvmName}")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
@Test
|
||||
fun isIdenticalDeserialization() {
|
||||
val serializableClasses = findSerializableClasses("com.lagradost")
|
||||
println("Number of serializable classes: ${serializableClasses.size}")
|
||||
|
||||
serializableClasses.forEach { kClass ->
|
||||
val instance = Instancio.create(kClass.java)
|
||||
// Convert to JSON to get example JSON object
|
||||
// We prefer jackson here because the app may have many jackson JSON strings in local storage
|
||||
val originalJson = jacksonMapper.writeValueAsString(instance)
|
||||
|
||||
// Create an object from the JSON using kotlinx
|
||||
val serializer =
|
||||
kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass)
|
||||
assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!")
|
||||
val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson)
|
||||
|
||||
// Create an object from the JSON using jackson
|
||||
val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java)
|
||||
|
||||
|
||||
// Deep inspect both object using the mapper toJson function.
|
||||
// This deep equality check can be performed using other methods, but this just works.
|
||||
val jacksonJson = mapperDecoded.toJson()
|
||||
val kotlinxJson = kotlinxDecoded.toJson()
|
||||
|
||||
assertEquals(
|
||||
jacksonJson,
|
||||
kotlinxJson,
|
||||
"""
|
||||
Serialization mismatch for:
|
||||
${kClass.qualifiedName}
|
||||
|
||||
Jackson:
|
||||
$jacksonJson
|
||||
|
||||
Kotlinx:
|
||||
$kotlinxJson
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
println("Identical deserialization for: ${kClass.jvmName}")
|
||||
}
|
||||
}
|
||||
|
||||
// DEX files are the best solution to read all our classes dynamically.
|
||||
// classgraph could be used instead, but it only gives results on the JVM, not Android.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun findSerializableClasses(packageName: String): List<KClass<*>> {
|
||||
val context = InstrumentationRegistry
|
||||
.getInstrumentation()
|
||||
.targetContext
|
||||
|
||||
val dexFile = DexFile(context.packageCodePath)
|
||||
return dexFile.entries()
|
||||
.toList()
|
||||
.filter { it.startsWith(packageName) }
|
||||
.mapNotNull {
|
||||
runCatching { Class.forName(it).kotlin }.getOrNull()
|
||||
}.filter { kClass ->
|
||||
// Not possible to use .hasAnnotation() on newer Android versions.
|
||||
kClass.java.annotations.any {
|
||||
it is Serializable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun serializeWithKotlinx(
|
||||
kClass: KClass<*>,
|
||||
value: Any
|
||||
): String {
|
||||
val serializer = kClass.serializer() as KSerializer<Any>
|
||||
return kotlinxMapper.encodeToString(serializer, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package com.lagradost.cloudstream3.utils.serializers
|
||||
|
||||
import android.net.Uri
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KeepGeneratedSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = NonEmptyData.Serializer::class)
|
||||
data class NonEmptyData(
|
||||
val title: String = "",
|
||||
val tags: List<String> = emptyList(),
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
val name: String = "hello",
|
||||
) {
|
||||
object Serializer : NonEmptySerializer<NonEmptyData>(NonEmptyData.generatedSerializer())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = WriteOnlyData.Serializer::class)
|
||||
data class WriteOnlyData(
|
||||
val fieldA: String = "",
|
||||
val fieldB: String = "",
|
||||
) {
|
||||
object Serializer : WriteOnlySerializer<WriteOnlyData>(
|
||||
WriteOnlyData.generatedSerializer(),
|
||||
setOf("fieldB"),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = MultiWriteOnly.Serializer::class)
|
||||
data class MultiWriteOnly(
|
||||
val fieldA: String = "",
|
||||
val fieldB: String = "",
|
||||
val fieldC: String = "",
|
||||
) {
|
||||
object Serializer : WriteOnlySerializer<MultiWriteOnly>(
|
||||
MultiWriteOnly.generatedSerializer(),
|
||||
setOf("fieldB", "fieldC"),
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UriData(
|
||||
@Serializable(with = UriSerializer::class)
|
||||
val uri: Uri = Uri.EMPTY,
|
||||
)
|
||||
|
||||
class SerializerTest {
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyStrings() {
|
||||
val data = NonEmptyData(title = "", name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("title"))
|
||||
assertTrue(result.contains("name"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyLists() {
|
||||
val data = NonEmptyData(tags = emptyList(), name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("tags"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyMaps() {
|
||||
val data = NonEmptyData(meta = emptyMap(), name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("meta"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerKeepsNonEmptyFields() {
|
||||
val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v"))
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("title"))
|
||||
assertTrue(result.contains("tags"))
|
||||
assertTrue(result.contains("meta"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerDoesNotAffectDeserialization() {
|
||||
val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}"""
|
||||
val result = parseJson<NonEmptyData>(input)
|
||||
assertEquals("hello", result.title)
|
||||
assertEquals(listOf("a"), result.tags)
|
||||
assertEquals(mapOf("k" to "v"), result.meta)
|
||||
assertEquals("world", result.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerOmitsFieldOnSerialize() {
|
||||
val data = WriteOnlyData(fieldA = "hello", fieldB = "secret")
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("fieldA"))
|
||||
assertFalse(result.contains("fieldB"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerDeserializesNormally() {
|
||||
val input = """{"fieldA":"hello","fieldB":"secret"}"""
|
||||
val result = parseJson<WriteOnlyData>(input)
|
||||
assertEquals("hello", result.fieldA)
|
||||
assertEquals("secret", result.fieldB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerDeserializesMissingAsDefault() {
|
||||
val input = """{"fieldA":"hello"}"""
|
||||
val result = parseJson<WriteOnlyData>(input)
|
||||
assertEquals("hello", result.fieldA)
|
||||
assertEquals("", result.fieldB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerHandlesMultipleKeys() {
|
||||
val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2")
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("fieldA"))
|
||||
assertFalse(result.contains("fieldB"))
|
||||
assertFalse(result.contains("fieldC"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerSerializesUriToString() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("https://example.com/path?query=1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerDeserializesStringToUri() {
|
||||
val input = """{"uri":"https://example.com/path?query=1"}"""
|
||||
val result = parseJson<UriData>(input)
|
||||
assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerRoundtripsCorrectly() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val encoded = data.toJson()
|
||||
val decoded = parseJson<UriData>(encoded)
|
||||
assertEquals(data.uri, decoded.uri)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,47 @@
|
|||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<queries>
|
||||
<!--
|
||||
QUERY_ALL_PACKAGES does not work on some devices running Android 11+ (like Google TV 14),
|
||||
so we must explicitly specify the packages and intent patterns we query to ensure visibility.
|
||||
-->
|
||||
<!-- For external video players -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:mimeType="application/x-mpegURL" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:mimeType="application/vnd.apple.mpegurl" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="magnet" />
|
||||
</intent>
|
||||
|
||||
<!-- Common players supported in actions/temp -->
|
||||
<package android:name="org.videolan.vlc" />
|
||||
<package android:name="org.videolan.vlc.debug" />
|
||||
<package android:name="is.xyz.mpv" />
|
||||
<package android:name="is.xyz.mpv.ytdl" />
|
||||
<package android:name="app.marlboroadvance.mpvex" />
|
||||
<package android:name="live.mehiz.mpvkt" />
|
||||
<package android:name="live.mehiz.mpvkt.preview" />
|
||||
<package android:name="com.brouken.player" />
|
||||
<package android:name="dev.anilbeesetti.nextplayer" />
|
||||
<package android:name="com.instantbits.cast.webvideo" />
|
||||
<package android:name="com.gianlu.aria2android" />
|
||||
|
||||
<!-- Torrent clients -->
|
||||
<package android:name="org.proninyaroslav.libretorrent" />
|
||||
<package android:name="com.biglybt.android.client" />
|
||||
</queries>
|
||||
|
||||
<!-- Fixes android tv fuckery -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter
|
||||
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerPipHelper.isPIPPossible
|
||||
import com.lagradost.cloudstream3.ui.player.Torrent
|
||||
import com.lagradost.cloudstream3.ui.result.ActorAdaptor
|
||||
|
|
@ -117,7 +116,6 @@ object CommonActivity {
|
|||
val onColorSelectedEvent = Event<Pair<Int, Int>>()
|
||||
val onDialogDismissedEvent = Event<Int>()
|
||||
|
||||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
||||
var appliedTheme: Int = 0
|
||||
var appliedColor: Int = 0
|
||||
|
|
@ -534,87 +532,7 @@ object CommonActivity {
|
|||
|
||||
|
||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?): Boolean? {
|
||||
|
||||
// 149 keycode_numpad 5
|
||||
val playerEvent = when (keyCode) {
|
||||
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||
PlayerEventType.SeekForward
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||
PlayerEventType.SeekBack
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_CHANNEL_UP -> {
|
||||
PlayerEventType.NextEpisode
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_CHANNEL_DOWN -> {
|
||||
PlayerEventType.PrevEpisode
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||
PlayerEventType.Pause
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
||||
PlayerEventType.Play
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
||||
PlayerEventType.Lock
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
||||
PlayerEventType.ToggleHide
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
||||
PlayerEventType.ToggleMute
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
||||
PlayerEventType.ShowMirrors
|
||||
}
|
||||
// OpenSubtitles shortcut
|
||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
||||
PlayerEventType.SearchSubtitlesOnline
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
||||
PlayerEventType.ShowSpeed
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
||||
PlayerEventType.Resize
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
||||
PlayerEventType.SkipOp
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
||||
PlayerEventType.SkipCurrentChapter
|
||||
}
|
||||
|
||||
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
|
||||
PlayerEventType.PlayPauseToggle
|
||||
}
|
||||
|
||||
else -> return null
|
||||
}
|
||||
val listener = playerEventListener
|
||||
if (listener != null) {
|
||||
listener.invoke(playerEvent)
|
||||
return true
|
||||
}
|
||||
return null
|
||||
|
||||
//when (keyCode) {
|
||||
// KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
// println("DPAD PRESSED")
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
/** overrides focus and custom key events */
|
||||
|
|
@ -661,8 +579,10 @@ object CommonActivity {
|
|||
|
||||
// TODO: Figure out why removing the check for SearchAutoComplete seems
|
||||
// to break focus on TV as it shouldn't need to be used.
|
||||
// Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote)
|
||||
// send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button.
|
||||
@SuppressLint("RestrictedApi")
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
|
||||
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) &&
|
||||
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
|
||||
) {
|
||||
showInputMethod(act.currentFocus?.findFocus())
|
||||
|
|
@ -683,4 +603,4 @@ object CommonActivity {
|
|||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -362,7 +362,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
|||
LinkGenerator(
|
||||
listOf(BasicLink(url, name)),
|
||||
extract = true,
|
||||
)
|
||||
id = url.hashCode()
|
||||
), 0
|
||||
)
|
||||
)
|
||||
} else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
|
||||
|
|
@ -407,13 +408,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
|||
return true
|
||||
}
|
||||
|
||||
synchronized(apis) {
|
||||
for (api in apis) {
|
||||
if (str.startsWith(api.mainUrl)) {
|
||||
loadResult(str, api.name, "")
|
||||
return true
|
||||
}
|
||||
}
|
||||
val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull()
|
||||
if (matchedApi != null) {
|
||||
loadResult(str, matchedApi.name, "")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -559,9 +557,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
|||
navView.isVisible = isNavVisible && !isLandscape()
|
||||
navHostFragment.apply {
|
||||
val marginPx = resources.getDimensionPixelSize(R.dimen.nav_rail_view_width)
|
||||
layoutParams = (navHostFragment.layoutParams as ViewGroup.MarginLayoutParams).apply {
|
||||
marginStart = if (isNavVisible && isLandscape() && isLayout(TV or EMULATOR)) marginPx else 0
|
||||
}
|
||||
layoutParams =
|
||||
(navHostFragment.layoutParams as ViewGroup.MarginLayoutParams).apply {
|
||||
marginStart =
|
||||
if (isNavVisible && isLandscape() && isLayout(TV or EMULATOR)) marginPx else 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -570,7 +570,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
|||
* highlight the wrong one in UI.
|
||||
*/
|
||||
when (destination.id) {
|
||||
in listOf(R.id.navigation_downloads, R.id.navigation_download_child, R.id.navigation_download_queue) -> {
|
||||
in listOf(
|
||||
R.id.navigation_downloads,
|
||||
R.id.navigation_download_child,
|
||||
R.id.navigation_download_queue
|
||||
) -> {
|
||||
navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true
|
||||
navView.menu.findItem(R.id.navigation_downloads).isChecked = true
|
||||
}
|
||||
|
|
@ -802,12 +806,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private val pluginsLock = Mutex()
|
||||
private fun onAllPluginsLoaded(success: Boolean = false) {
|
||||
ioSafe {
|
||||
pluginsLock.withLock {
|
||||
synchronized(allProviders) {
|
||||
allProviders.withLock {
|
||||
// Load cloned sites after plugins have been loaded since clones depend on plugins.
|
||||
try {
|
||||
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
|
||||
|
|
@ -1650,9 +1653,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
|||
ioSafe {
|
||||
initAll()
|
||||
// No duplicates (which can happen by registerMainAPI)
|
||||
apis = synchronized(allProviders) {
|
||||
allProviders.distinctBy { it }
|
||||
}
|
||||
apis = allProviders.distinctBy { it }
|
||||
}
|
||||
|
||||
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
||||
|
|
@ -1960,7 +1961,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
|
|||
|
||||
if (BuildConfig.DEBUG) {
|
||||
var providersAndroidManifestString = "Current androidmanifest should be:\n"
|
||||
synchronized(allProviders) {
|
||||
allProviders.withLock {
|
||||
for (api in allProviders) {
|
||||
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
|
||||
api.mainUrl.removePrefix(
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage
|
|||
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
|
||||
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
|
||||
import com.lagradost.cloudstream3.actions.temp.MpvPackage
|
||||
import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
|
||||
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
|
||||
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage
|
||||
import com.lagradost.cloudstream3.actions.temp.OnlyPlayer
|
||||
import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction
|
||||
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
|
||||
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
|
||||
|
|
@ -32,8 +34,8 @@ import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import com.lagradost.cloudstream3.utils.UiText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -43,7 +45,7 @@ import java.util.concurrent.FutureTask
|
|||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
object VideoClickActionHolder {
|
||||
val allVideoClickActions = threadSafeListOf(
|
||||
val allVideoClickActions = atomicListOf(
|
||||
// Default
|
||||
PlayInBrowserAction(),
|
||||
CopyClipboardAction(),
|
||||
|
|
@ -64,6 +66,8 @@ object VideoClickActionHolder {
|
|||
MpvYTDLPackage(),
|
||||
MpvKtPackage(),
|
||||
MpvKtPreviewPackage(),
|
||||
OnlyPlayer(),
|
||||
MpvRxPackage(),
|
||||
// Always Ask option
|
||||
AlwaysAskAction(),
|
||||
// added by plugins
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package com.lagradost.cloudstream3.actions.temp
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.actions.OpenInAppAction
|
||||
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
|
||||
import com.lagradost.cloudstream3.isEpisodeBased
|
||||
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||
import com.lagradost.cloudstream3.utils.txt
|
||||
|
||||
/** https://github.com/Riteshp2001/mpvRx
|
||||
*
|
||||
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L132
|
||||
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L56
|
||||
* */
|
||||
class MpvRxPackage : OpenInAppAction(
|
||||
appName = txt("mpvRx"),
|
||||
packageName = "app.gyrolet.mpvrx",
|
||||
intentClass = "app.gyrolet.mpvrx.ui.player.PlayerActivity"
|
||||
) {
|
||||
override val oneSource = true
|
||||
override suspend fun putExtra(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
video: ResultEpisode,
|
||||
result: LinkLoadingResult,
|
||||
index: Int?
|
||||
) {
|
||||
intent.apply {
|
||||
putExtra("title", video.name)
|
||||
val link = result.links[index!!]
|
||||
val headers = link.headers
|
||||
|
||||
setData(link.url.toUri())
|
||||
if (headers.isNotEmpty()) {
|
||||
// PlayerActivity expects a flat array: [key1, value1, key2, value2, ...]
|
||||
val flat = headers.entries.flatMap { listOf(it.key, it.value) }.toTypedArray()
|
||||
intent.putExtra("headers", flat)
|
||||
}
|
||||
/*val subs = result.subs // disabled due to https://github.com/Riteshp2001/mpvRx/issues/146
|
||||
intent.putExtra("subs", subs.map { it.url.toUri() }.toTypedArray())
|
||||
intent.putExtra(
|
||||
"subs.titles",
|
||||
subs.map { it.name }.toTypedArray(),
|
||||
)
|
||||
intent.putExtra(
|
||||
"subs.langs",
|
||||
subs.map { it.languageCode }.toTypedArray(),
|
||||
)
|
||||
val selected = subs.firstOrNull { it.matchesLanguageCode("en") }?.url?.toUri()
|
||||
intent.putExtra("subs.enable", selected?.let { arrayOf(it) } ?: arrayOf<Uri>() )*/
|
||||
|
||||
if (video.tvType.isEpisodeBased()) {
|
||||
video.season?.let { intent.putExtra("introdb_season", it) }
|
||||
video.episode.let { intent.putExtra("introdb_episode", it) }
|
||||
}
|
||||
|
||||
val position = getViewPos(video.id)?.position
|
||||
if (position != null)
|
||||
putExtra("position", position.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResult(activity: Activity, intent: Intent?) {
|
||||
val position = intent?.getIntExtra("position", -1) ?: -1
|
||||
val duration = intent?.getIntExtra("duration", -1) ?: -1
|
||||
Log.d("MPV", "Position: $position, Duration: $duration")
|
||||
updateDurationAndPosition(position.toLong(), duration.toLong())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -35,9 +35,11 @@ class PlayMirrorAction : VideoClickAction() {
|
|||
) {
|
||||
//Implemented a generator to handle the single
|
||||
val activity = context as? Activity ?: return
|
||||
val link = index?.let { result.links[it] }
|
||||
val generatorMirror = object : VideoGenerator<ResultEpisode>(listOf(video)) {
|
||||
override val hasCache: Boolean = false
|
||||
override val canSkipLoading: Boolean = false
|
||||
override fun getId(index: Int): Int = video.id
|
||||
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
|
|
@ -47,7 +49,7 @@ class PlayMirrorAction : VideoClickAction() {
|
|||
offset: Int,
|
||||
isCasting: Boolean
|
||||
): Boolean {
|
||||
index?.let { callback(result.links[it] to null) }
|
||||
index?.let { callback(link to null) }
|
||||
result.subs.forEach { subtitle -> subtitleCallback(subtitle) }
|
||||
return true
|
||||
}
|
||||
|
|
@ -56,7 +58,7 @@ class PlayMirrorAction : VideoClickAction() {
|
|||
activity.navigate(
|
||||
R.id.global_to_navigation_player,
|
||||
GeneratorPlayer.newInstance(
|
||||
generatorMirror, result.syncData
|
||||
generatorMirror, 0, result.syncData
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,68 @@
|
|||
package com.lagradost.cloudstream3.mvvm
|
||||
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.ui.BaseFragment
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.removeObservers(this)
|
||||
liveData.observe(this) { it?.let { t -> action(t) } }
|
||||
fun <T> ComponentActivity.observe(liveData: LiveData<T>, action: (T) -> Unit) {
|
||||
observeNullable(liveData) { t -> t?.run(action) }
|
||||
}
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
fun <T> ComponentActivity.observeNullable(liveData: LiveData<T>, action: (T?) -> Unit) {
|
||||
liveData.removeObservers(this)
|
||||
liveData.observe(this) { action(it) }
|
||||
liveData.observe(this, action)
|
||||
}
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T, V : ViewBinding> BaseFragment<V>.observe(liveData: LiveData<T>, action: (T) -> Unit) {
|
||||
observeNullable(liveData) { t -> t?.run(action) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches an observable to the root binding, instead of the fragment. This is more efficient as
|
||||
* it will not call observe if the view is in the background.
|
||||
*
|
||||
* NOTE: Only one observer at a time per value
|
||||
* */
|
||||
fun <T, V : ViewBinding> BaseFragment<V>.observeNullable(
|
||||
liveData: LiveData<T>, action: (T?) -> Unit
|
||||
) {
|
||||
val root = this.binding?.root
|
||||
if (root == null) {
|
||||
liveData.removeObservers(this)
|
||||
liveData.observe(this, action)
|
||||
} else {
|
||||
root.doOnAttach { view ->
|
||||
// On attach should make findViewTreeLifecycleOwner non-null, but use "this" just in case
|
||||
val owner: LifecycleOwner = view.findViewTreeLifecycleOwner() ?: this@observeNullable
|
||||
liveData.removeObservers(owner)
|
||||
liveData.observe(owner, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> View.observe(liveData: LiveData<T>, action: (T) -> Unit) {
|
||||
observeNullable(liveData) { t -> t?.run(action) }
|
||||
}
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> View.observeNullable(liveData: LiveData<T>, action: (T?) -> Unit) {
|
||||
doOnAttach { view ->
|
||||
// On attach should make findViewTreeLifecycleOwner non-null
|
||||
val owner: LifecycleOwner? = view.findViewTreeLifecycleOwner()
|
||||
if(owner == null) {
|
||||
debugException { "Expected non-null findViewTreeLifecycleOwner" }
|
||||
return@doOnAttach
|
||||
}
|
||||
liveData.removeObservers(owner)
|
||||
liveData.observe(owner, action)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ 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
|
||||
|
|
@ -26,9 +25,7 @@ abstract class Plugin : BasePlugin() {
|
|||
fun registerVideoClickAction(element: VideoClickAction) {
|
||||
Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction")
|
||||
element.sourcePlugin = this.filename
|
||||
synchronized(VideoClickActionHolder.allVideoClickActions) {
|
||||
VideoClickActionHolder.allVideoClickActions.add(element)
|
||||
}
|
||||
VideoClickActionHolder.allVideoClickActions.add(element)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -40,4 +37,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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -610,7 +610,7 @@ object PluginManager {
|
|||
return false
|
||||
}
|
||||
InputStreamReader(stream).use { reader ->
|
||||
manifest = parseJson(reader, BasePlugin.Manifest::class.java)
|
||||
manifest = parseJson<BasePlugin.Manifest>(reader.readText())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -651,9 +651,15 @@ object PluginManager {
|
|||
context.resources.configuration
|
||||
)
|
||||
}
|
||||
plugins[filePath] = pluginInstance
|
||||
classLoaders[loader] = pluginInstance
|
||||
urlPlugins[data.url ?: filePath] = pluginInstance
|
||||
synchronized(plugins) {
|
||||
plugins[filePath] = pluginInstance
|
||||
}
|
||||
synchronized(classLoaders) {
|
||||
classLoaders[loader] = pluginInstance
|
||||
}
|
||||
synchronized(urlPlugins) {
|
||||
urlPlugins[data.url ?: filePath] = pluginInstance
|
||||
}
|
||||
if (pluginInstance is Plugin) {
|
||||
pluginInstance.load(context)
|
||||
} else {
|
||||
|
|
@ -689,21 +695,20 @@ object PluginManager {
|
|||
}
|
||||
|
||||
// remove all registered apis
|
||||
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.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
|
||||
removePluginMapping(it)
|
||||
}
|
||||
|
||||
synchronized(extractorApis) {
|
||||
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
|
||||
APIHolder.allProviders.withLock {
|
||||
APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename }
|
||||
}
|
||||
|
||||
synchronized(VideoClickActionHolder.allVideoClickActions) {
|
||||
VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename }
|
||||
extractorApis.withLock {
|
||||
extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename }
|
||||
}
|
||||
|
||||
VideoClickActionHolder.allVideoClickActions.withLock {
|
||||
VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename }
|
||||
}
|
||||
|
||||
synchronized(classLoaders) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ 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
|
||||
|
|
@ -186,6 +187,16 @@ 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
|
||||
|
|
|
|||
|
|
@ -36,11 +36,9 @@ 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
|
||||
|
|
|
|||
|
|
@ -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.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
|
||||
|
||||
/** Stateless safe abstraction of SubtitleAPI */
|
||||
class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
|
||||
|
|
@ -24,26 +24,30 @@ 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 = threadSafeListOf<SavedSearchResponse>()
|
||||
private val searchCache = atomicListOf<SavedSearchResponse>()
|
||||
private var searchCacheIndex: Int = 0
|
||||
private val resourceCache = threadSafeListOf<SavedResourceResponse>()
|
||||
private val resourceCache = atomicListOf<SavedResourceResponse>()
|
||||
private var resourceCacheIndex: Int = 0
|
||||
const val CACHE_SIZE = 20
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun resource(data: SubtitleEntity): Result<SubtitleResource> = runCatching {
|
||||
synchronized(resourceCache) {
|
||||
val cached = resourceCache.withLock {
|
||||
var found: SubtitleResource? = null
|
||||
for (item in resourceCache) {
|
||||
// 20 min save
|
||||
if (item.query == data && (unixTime - item.unixTime) < 60 * 20) {
|
||||
return@runCatching item.response
|
||||
found = item.response
|
||||
break
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
if (cached != null) return@runCatching cached
|
||||
|
||||
val returnValue = api.resource(freshAuth(), data)
|
||||
synchronized(resourceCache) {
|
||||
resourceCache.withLock {
|
||||
val add = SavedResourceResponse(unixTime, returnValue, data)
|
||||
if (resourceCache.size > CACHE_SIZE) {
|
||||
resourceCache[resourceCacheIndex] = add // rolling cache
|
||||
|
|
@ -58,22 +62,25 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
|
|||
@WorkerThread
|
||||
suspend fun search(query: SubtitleSearch): Result<List<SubtitleEntity>> {
|
||||
return runCatching {
|
||||
synchronized(searchCache) {
|
||||
val cached = searchCache.withLock {
|
||||
var found: List<SubtitleEntity>? = null
|
||||
for (item in searchCache) {
|
||||
// 120 min save
|
||||
if (item.query == query && (unixTime - item.unixTime) < 60 * 120) {
|
||||
return@runCatching item.response
|
||||
found = item.response
|
||||
break
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
val returnValue =
|
||||
api.search(freshAuth(), query) ?: emptyList()
|
||||
if (cached != null) return@runCatching cached
|
||||
val returnValue = api.search(freshAuth(), query) ?: emptyList()
|
||||
|
||||
// only cache valid return values
|
||||
if (returnValue.isNotEmpty()) {
|
||||
val add = SavedSearchResponse(unixTime, returnValue, query)
|
||||
synchronized(searchCache) {
|
||||
searchCache.withLock {
|
||||
if (searchCache.size > CACHE_SIZE) {
|
||||
searchCache[searchCacheIndex] = add // rolling cache
|
||||
searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE
|
||||
|
|
@ -86,4 +93,3 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import com.lagradost.cloudstream3.ShowStatus
|
|||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.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 {
|
||||
-FuzzySearch.partialRatio(
|
||||
-Levenshtein.partialRatio(
|
||||
query.lowercase(), it.name.lowercase()
|
||||
)
|
||||
}
|
||||
|
|
@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() {
|
|||
override var score: Score? = null,
|
||||
val tags: List<String>? = null
|
||||
) : SearchResponse
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ 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(),
|
||||
)
|
||||
|
|
@ -83,8 +84,8 @@ class AniListApi : SyncAPI() {
|
|||
return "$mainUrl/anime/$id"
|
||||
}
|
||||
|
||||
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||
val data = searchShows(name) ?: return null
|
||||
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||
val data = searchShows(query) ?: return null
|
||||
return data.data?.page?.media?.map {
|
||||
SyncAPI.SyncSearchResult(
|
||||
it.title.romaji ?: return null,
|
||||
|
|
@ -96,7 +97,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
|
||||
|
|
@ -158,7 +159,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
|
||||
|
||||
|
|
@ -459,7 +460,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)
|
||||
|
|
@ -506,7 +507,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(
|
||||
|
|
@ -638,7 +639,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 ->
|
||||
|
|
@ -666,7 +667,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"
|
||||
|
||||
|
|
@ -714,7 +715,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 {
|
||||
|
|
@ -737,7 +738,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?,
|
||||
|
|
@ -786,7 +787,7 @@ class AniListApi : SyncAPI() {
|
|||
return data != ""
|
||||
}
|
||||
|
||||
private suspend fun getUser(token : AuthToken): AniListUser? {
|
||||
private suspend fun getUser(token: AuthToken): AniListUser? {
|
||||
val q = """
|
||||
{
|
||||
Viewer {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,8 @@ 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.format.DateTimeFormatter
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
|
|
@ -202,7 +201,7 @@ class KitsuApi: SyncAPI() {
|
|||
id = id,
|
||||
totalEpisodes = anime.episodeCount,
|
||||
title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(),
|
||||
publicScore = Score.from(anime.ratingTwenty.toString(), 20),
|
||||
publicScore = Score.from(anime.ratingTwenty, 20),
|
||||
duration = anime.episodeLength,
|
||||
synopsis = anime.synopsis,
|
||||
airStatus = when(anime.status) {
|
||||
|
|
@ -250,7 +249,7 @@ class KitsuApi: SyncAPI() {
|
|||
}
|
||||
|
||||
return SyncStatus(
|
||||
score = Score.from(anime.ratingTwenty.toString(), 20),
|
||||
score = Score.from(anime.ratingTwenty, 20),
|
||||
status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)),
|
||||
isFavorite = null,
|
||||
watchedEpisodes = anime.progress,
|
||||
|
|
@ -454,8 +453,8 @@ class KitsuApi: SyncAPI() {
|
|||
|
||||
private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array<KitsuNode> {
|
||||
|
||||
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount")
|
||||
val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status")
|
||||
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount")
|
||||
val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","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(",")}"
|
||||
|
||||
|
|
@ -526,7 +525,7 @@ class KitsuApi: SyncAPI() {
|
|||
this.id,
|
||||
this.attributes.progress,
|
||||
numEpisodes,
|
||||
Score.from(this.attributes.ratingTwenty.toString(), 20),
|
||||
Score.from(this.attributes.ratingTwenty, 20),
|
||||
parseDateLong(this.attributes.updatedAt),
|
||||
"Kitsu",
|
||||
TvType.Anime,
|
||||
|
|
@ -535,12 +534,9 @@ class KitsuApi: SyncAPI() {
|
|||
null,
|
||||
plot = synopsis,
|
||||
releaseDate = if (startDate == null) null else try {
|
||||
Date.from(
|
||||
Instant.from(
|
||||
DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd")
|
||||
.parse(startDate)
|
||||
)
|
||||
)
|
||||
Date.from(LocalDate.parse(startDate).atStartOfDay()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant())
|
||||
} catch (_: RuntimeException) {
|
||||
null
|
||||
}
|
||||
|
|
@ -583,7 +579,7 @@ class KitsuApi: SyncAPI() {
|
|||
@JsonProperty("avatar") val avatar: KitsuUserAvatar?,
|
||||
/* User list anime attributes */
|
||||
@JsonProperty("progress") val progress: Int?,
|
||||
@JsonProperty("ratingTwenty") val ratingTwenty: Float?,
|
||||
@JsonProperty("ratingTwenty") val ratingTwenty: Int?,
|
||||
@JsonProperty("updatedAt") val updatedAt: String?,
|
||||
@JsonProperty("status") val status: String?,
|
||||
)
|
||||
|
|
@ -632,7 +628,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:ssZ", Locale.getDefault()).parse(
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(
|
||||
string ?: return null
|
||||
)?.time?.div(1000)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
|||
|
|
@ -98,9 +98,9 @@ class MALApi : SyncAPI() {
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||
val auth = auth?.token?.accessToken ?: return null
|
||||
val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
||||
val url = "$apiUrl/v2/anime?q=$query&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<Data>? {
|
||||
private suspend fun getMalAnimeListSmart(auth: AuthData): Array<Data>? {
|
||||
return if (requireLibraryRefresh) {
|
||||
val list = getMalAnimeList(auth.token)
|
||||
setKey(MAL_CACHED_LIST, auth.user.id.toString(), list)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import com.lagradost.cloudstream3.Score
|
|||
import com.lagradost.cloudstream3.SimklSyncServices
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mapper
|
||||
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
|
||||
|
|
@ -30,6 +29,7 @@ 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,13 +117,8 @@ class SimklApi : SyncAPI() {
|
|||
* Gets cached object, if object is not fresh returns null and removes it from cache
|
||||
*/
|
||||
inline fun <reified T : Any> getKey(path: String): T? {
|
||||
// Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject"
|
||||
val type = mapper.typeFactory.constructParametricType(
|
||||
SimklCacheWrapper::class.java,
|
||||
T::class.java
|
||||
)
|
||||
val cache = getKey<String>(SIMKL_CACHE_KEY, path)?.let {
|
||||
mapper.readValue<SimklCacheWrapper<T>>(it, type)
|
||||
tryParseJson<SimklCacheWrapper<T>>(it)
|
||||
}
|
||||
|
||||
return if (cache?.isFresh() == true) {
|
||||
|
|
@ -916,7 +911,7 @@ class SimklApi : SyncAPI() {
|
|||
|
||||
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||
return app.get(
|
||||
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name)
|
||||
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query)
|
||||
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.newSearchResponseList
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
|
||||
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<String, String>
|
||||
)
|
||||
|
||||
private val cache = threadSafeListOf<SavedLoadResponse>()
|
||||
private val cache = atomicListOf<SavedLoadResponse>()
|
||||
private var cacheIndex: Int = 0
|
||||
const val CACHE_SIZE = 20
|
||||
|
||||
|
|
@ -66,9 +66,7 @@ class APIRepository(val api: MainAPI) {
|
|||
|
||||
private fun afterPluginsLoaded(forceReload: Boolean) {
|
||||
if (forceReload) {
|
||||
synchronized(cache) {
|
||||
cache.clear()
|
||||
}
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,21 +89,25 @@ class APIRepository(val api: MainAPI) {
|
|||
val fixedUrl = api.fixUrl(url)
|
||||
val lookingForHash = Pair(api.name, fixedUrl)
|
||||
|
||||
synchronized(cache) {
|
||||
val cached = cache.withLock {
|
||||
var found: LoadResponse? = null
|
||||
for (item in cache) {
|
||||
// 10 min save
|
||||
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
|
||||
return@withTimeout item.response
|
||||
found = item.response
|
||||
break
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
synchronized(cache) {
|
||||
cache.withLock {
|
||||
if (cache.size > CACHE_SIZE) {
|
||||
cache[cacheIndex] = add // rolling cache
|
||||
cacheIndex = (cacheIndex + 1) % CACHE_SIZE
|
||||
|
|
@ -215,4 +217,4 @@ class APIRepository(val api: MainAPI) {
|
|||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ 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
|
||||
|
|
@ -105,9 +102,6 @@ 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 {
|
||||
|
|
@ -334,6 +328,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
}, subtitleCallback = {
|
||||
currentSubs.add(it)
|
||||
},
|
||||
offset = 0,
|
||||
isCasting = true
|
||||
)
|
||||
}
|
||||
|
|
@ -448,4 +443,4 @@ class ControllerActivity : ExpandedControllerActivity() {
|
|||
SkipNextEpisodeController(skipOpButton)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,8 @@ object DownloadButtonSetup {
|
|||
}
|
||||
act.navigate(
|
||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||
DownloadFileGenerator(items).apply { goto(items.indexOfFirst { it.id == click.data.id }) }
|
||||
DownloadFileGenerator(items),
|
||||
items.indexOfFirst { it.id == click.data.id }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class DownloadFragment : BaseFragment<FragmentDownloadsBinding>(
|
|||
val size = cards.currentDownloads.size + cards.queue.size
|
||||
val context = binding.root.context
|
||||
val baseText = context.getString(R.string.download_queue)
|
||||
binding.downloadQueueText.text = if (size > 0) {
|
||||
binding.downloadQueueText.text = if (size > 0) {
|
||||
"$baseText (${cards.currentDownloads.size}/$size)"
|
||||
} else {
|
||||
baseText
|
||||
|
|
@ -349,7 +349,8 @@ class DownloadFragment : BaseFragment<FragmentDownloadsBinding>(
|
|||
listOf(BasicLink(url)),
|
||||
extract = true,
|
||||
refererUrl = referer,
|
||||
)
|
||||
id = url.hashCode()
|
||||
), 0
|
||||
)
|
||||
)
|
||||
dialog.dismissSafe(activity)
|
||||
|
|
|
|||
|
|
@ -76,10 +76,10 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
currentMetaData.id = id
|
||||
|
||||
if (!doSetProgress) return
|
||||
val appContext = context.applicationContext
|
||||
|
||||
ioSafe {
|
||||
val savedData = VideoDownloadManager.getDownloadFileInfo(context, id)
|
||||
|
||||
val savedData = VideoDownloadManager.getDownloadFileInfo(appContext, id)
|
||||
mainWork {
|
||||
if (savedData != null) {
|
||||
val downloadedBytes = savedData.fileLength
|
||||
|
|
@ -216,4 +216,4 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
* Get a clean slate again, might be useful in recyclerview?
|
||||
* */
|
||||
abstract fun resetView()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,8 +304,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
override fun setStatus(status: DownloadStatusTell?) {
|
||||
currentStatus = status
|
||||
|
||||
// Runs on the main thread, but also instant if it already is
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
// Runs on the main thread, but also instant if it already is.
|
||||
if (Looper.getMainLooper().isCurrentThread) {
|
||||
try {
|
||||
setStatusInternal(status)
|
||||
} catch (t: Throwable) {
|
||||
|
|
|
|||
|
|
@ -651,7 +651,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
|
|||
}
|
||||
|
||||
homeMasterAdapter = HomeParentItemAdapterPreview(
|
||||
fragment = this@HomeFragment,
|
||||
homeViewModel, accountViewModel
|
||||
)
|
||||
homeMasterRecycler.setRecycledViewPool(ParentItemAdapter.sharedPool)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ import androidx.core.graphics.toColorInt
|
|||
import com.lagradost.cloudstream3.ui.setRecycledViewPool
|
||||
|
||||
class HomeParentItemAdapterPreview(
|
||||
val fragment: LifecycleOwner,
|
||||
private val viewModel: HomeViewModel,
|
||||
private val accountViewModel: AccountViewModel
|
||||
) : ParentItemAdapter(
|
||||
|
|
@ -105,7 +104,7 @@ class HomeParentItemAdapterPreview(
|
|||
)
|
||||
}
|
||||
|
||||
return HeaderViewHolder(binding, viewModel, accountViewModel, fragment)
|
||||
return HeaderViewHolder(binding, viewModel, accountViewModel)
|
||||
}
|
||||
|
||||
override fun onBindHeader(holder: ViewHolderState<Bundle>) {
|
||||
|
|
@ -132,7 +131,6 @@ class HomeParentItemAdapterPreview(
|
|||
val binding: ViewBinding,
|
||||
val viewModel: HomeViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
fragment: LifecycleOwner,
|
||||
) :
|
||||
ViewHolderState<Bundle>(binding) {
|
||||
|
||||
|
|
@ -544,7 +542,7 @@ class HomeParentItemAdapterPreview(
|
|||
headProfilePicCard?.isGone = isLayout(TV or EMULATOR)
|
||||
alternateHeadProfilePicCard?.isGone = isLayout(TV or EMULATOR)
|
||||
|
||||
fragment.observe(viewModel.currentAccount) { currentAccount ->
|
||||
(headProfilePic ?: alternateHeadProfilePic)?.observe(viewModel.currentAccount) { currentAccount ->
|
||||
headProfilePic?.loadImage(currentAccount?.image)
|
||||
alternateHeadProfilePic?.loadImage(currentAccount?.image)
|
||||
}
|
||||
|
|
@ -775,7 +773,7 @@ class HomeParentItemAdapterPreview(
|
|||
fun onViewAttachedToWindow() {
|
||||
previewViewpager.registerOnPageChangeCallback(previewCallback)
|
||||
|
||||
binding.root.findViewTreeLifecycleOwner()?.apply {
|
||||
previewViewpager.apply {
|
||||
observe(viewModel.preview) {
|
||||
updatePreview(it)
|
||||
}
|
||||
|
|
@ -800,7 +798,7 @@ class HomeParentItemAdapterPreview(
|
|||
}
|
||||
toggleListHolder?.isGone = visible.isEmpty()
|
||||
}
|
||||
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() {
|
|||
private var currentShuffledList: List<SearchResponse> = listOf()
|
||||
|
||||
private fun autoloadRepo(): APIRepository {
|
||||
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } })
|
||||
return APIRepository(apis.withLock { apis.first { it.hasMainPage } })
|
||||
}
|
||||
|
||||
private val _availableWatchStatusTypes =
|
||||
|
|
|
|||
|
|
@ -210,14 +210,13 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(
|
|||
syncId: SyncIdName,
|
||||
apiName: String? = null,
|
||||
) {
|
||||
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 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 baseOptions = listOf(
|
||||
LibraryOpenerType.Default,
|
||||
LibraryOpenerType.None,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import android.os.Looper
|
|||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
|
|
@ -95,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_UUID
|
||||
import com.lagradost.cloudstream3.utils.CLEARKEY_DRM_UUID
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
|
|
@ -103,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_UUID
|
||||
import com.lagradost.cloudstream3.utils.PLAYREADY_DRM_UUID
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName
|
||||
import com.lagradost.cloudstream3.utils.WIDEVINE_UUID
|
||||
import com.lagradost.cloudstream3.utils.WIDEVINE_DRM_UUID
|
||||
import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.Interceptor
|
||||
|
|
@ -117,6 +118,7 @@ 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"
|
||||
|
|
@ -206,16 +208,14 @@ class CS3IPlayer : IPlayer {
|
|||
private var requestedListeningPercentages: List<Int>? = null
|
||||
|
||||
private var eventHandler: ((PlayerEvent) -> Unit)? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
@AnyThread
|
||||
fun event(event: PlayerEvent) {
|
||||
// Ensure that all work is done on the main looper, aka main thread
|
||||
if (Looper.myLooper() == mainHandler.looper) {
|
||||
// Ensure that all work is done on the main thread.
|
||||
if (Looper.getMainLooper().isCurrentThread) {
|
||||
eventHandler?.invoke(event)
|
||||
} else runOnMainThread {
|
||||
eventHandler?.invoke(event)
|
||||
} else {
|
||||
mainHandler.post {
|
||||
eventHandler?.invoke(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,8 +235,9 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
override fun initCallbacks(
|
||||
eventHandler: ((PlayerEvent) -> Unit),
|
||||
@MainThread eventHandler: ((PlayerEvent) -> Unit),
|
||||
requestedListeningPercentages: List<Int>?,
|
||||
) {
|
||||
this.requestedListeningPercentages = requestedListeningPercentages
|
||||
|
|
@ -1278,7 +1279,7 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
item.drm?.let { drm ->
|
||||
when (drm.uuid) {
|
||||
CLEARKEY_UUID -> {
|
||||
CLEARKEY_DRM_UUID.toJavaUuid() -> {
|
||||
// Use headers from DrmMetadata for media requests
|
||||
val client = dataSourceFactory
|
||||
?: throw IllegalArgumentException("Must supply onlineSource")
|
||||
|
|
@ -1299,8 +1300,8 @@ class CS3IPlayer : IPlayer {
|
|||
.createMediaSource(item.mediaItem)
|
||||
}
|
||||
|
||||
WIDEVINE_UUID,
|
||||
PLAYREADY_UUID -> {
|
||||
WIDEVINE_DRM_UUID.toJavaUuid(),
|
||||
PLAYREADY_DRM_UUID.toJavaUuid() -> {
|
||||
// Use headers from DrmMetadata for media requests
|
||||
val client = dataSourceFactory
|
||||
?: throw IllegalArgumentException("Must supply onlineSource")
|
||||
|
|
@ -1770,7 +1771,6 @@ class CS3IPlayer : IPlayer {
|
|||
return exoPlayer != null
|
||||
}
|
||||
|
||||
|
||||
@MainThread
|
||||
private fun loadTorrent(context: Context, link: ExtractorLink) {
|
||||
ioSafe {
|
||||
|
|
@ -1915,7 +1915,7 @@ class CS3IPlayer : IPlayer {
|
|||
drm = DrmMetadata(
|
||||
kid = link.kid,
|
||||
key = link.key,
|
||||
uuid = link.uuid,
|
||||
uuid = link.uuid.toJavaUuid(),
|
||||
kty = link.kty,
|
||||
licenseUrl = link.licenseUrl,
|
||||
keyRequestParameters = link.keyRequestParameters,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getFol
|
|||
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo
|
||||
|
||||
class DownloadFileGenerator(
|
||||
episodes: List<ExtractorUri>,
|
||||
currentIndex: Int = 0
|
||||
) : VideoGenerator<ExtractorUri>(episodes, currentIndex) {
|
||||
episodes: List<ExtractorUri>
|
||||
) : VideoGenerator<ExtractorUri>(episodes) {
|
||||
override val hasCache = false
|
||||
override val canSkipLoading = false
|
||||
|
||||
override fun getId(index: Int): Int? = this.videos.getOrNull(index)?.id
|
||||
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
sourceTypes: Set<ExtractorLinkType>,
|
||||
|
|
@ -28,7 +29,7 @@ class DownloadFileGenerator(
|
|||
offset: Int,
|
||||
isCasting: Boolean
|
||||
): Boolean {
|
||||
val meta = getCurrent(offset) ?: return false
|
||||
val meta = videos.getOrNull(offset) ?: return false
|
||||
|
||||
if (meta.uri == Uri.EMPTY) {
|
||||
// We do this here so that we only load it when
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPres
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.enableEdgeToEdgeCompat
|
||||
|
||||
class DownloadedPlayerActivity : AppCompatActivity() {
|
||||
private val dTAG = "DownloadedPlayerAct"
|
||||
companion object {
|
||||
const val TAG = "DownloadedPlayerActivity"
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean =
|
||||
CommonActivity.dispatchKeyEvent(this, event) ?: super.dispatchKeyEvent(event)
|
||||
|
|
@ -27,53 +29,83 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
|||
CommonActivity.onUserLeaveHint(this)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
// Ignore same intent so the player doesnt totally
|
||||
// reload if you are playing the same thing.
|
||||
if (isSameIntent(intent)) return
|
||||
setIntent(intent)
|
||||
Log.i(TAG, "onNewIntent")
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun isSameIntent(newIntent: Intent): Boolean {
|
||||
val old = intent ?: return false
|
||||
// Compare URIs first
|
||||
val oldUri = old.data ?: old.clipData?.getItemAt(0)?.uri
|
||||
val newUri = newIntent.data ?: newIntent.clipData?.getItemAt(0)?.uri
|
||||
if (oldUri != null && oldUri == newUri) return true
|
||||
// Fall back to comparing EXTRA_TEXT links
|
||||
val oldText = safe { old.getStringExtra(Intent.EXTRA_TEXT) }
|
||||
val newText = safe { newIntent.getStringExtra(Intent.EXTRA_TEXT) }
|
||||
return oldText != null && oldText == newText
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
CommonActivity.loadThemes(this)
|
||||
CommonActivity.init(this)
|
||||
enableEdgeToEdgeCompat()
|
||||
setContentView(R.layout.empty_layout)
|
||||
Log.i(dTAG, "onCreate")
|
||||
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) }
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
val data = intent.data
|
||||
|
||||
if (OfflinePlaybackHelper.playIntent(activity = this, intent = intent)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_OPEN_DOCUMENT || intent?.action == Intent.ACTION_VIEW) {
|
||||
val extraText = safe { // I dont trust android
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
}
|
||||
if (
|
||||
intent.action == Intent.ACTION_SEND ||
|
||||
intent.action == Intent.ACTION_OPEN_DOCUMENT ||
|
||||
intent.action == Intent.ACTION_VIEW
|
||||
) {
|
||||
val extraText = safe { intent.getStringExtra(Intent.EXTRA_TEXT) }
|
||||
val cd = intent.clipData
|
||||
val item = if (cd != null && cd.itemCount > 0) cd.getItemAt(0) else null
|
||||
val url = item?.text?.toString()
|
||||
|
||||
// idk what I am doing, just hope any of these work
|
||||
if (item?.uri != null)
|
||||
playUri(this, item.uri)
|
||||
else if (url != null)
|
||||
playLink(this, url)
|
||||
else if (data != null)
|
||||
playUri(this, data)
|
||||
else if (extraText != null)
|
||||
playLink(this, extraText)
|
||||
else {
|
||||
finish()
|
||||
return
|
||||
when {
|
||||
item?.uri != null -> playUri(this, item.uri)
|
||||
url != null -> playLink(this, url)
|
||||
data != null -> playUri(this, data)
|
||||
extraText != null -> playLink(this, extraText)
|
||||
else -> finishAndRemoveTask()
|
||||
}
|
||||
} else if (data?.scheme == "content") {
|
||||
playUri(this, data)
|
||||
} else {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
attachBackPressedCallback("DownloadedPlayerActivity") { finish() }
|
||||
} else finishAndRemoveTask()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
CommonActivity.setActivityInstance(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
|||
class ExtractorLinkGenerator(
|
||||
private val links: List<ExtractorLink>,
|
||||
private val subtitles: List<SubtitleData>,
|
||||
) : NoVideoGenerator() {
|
||||
) : NoVideoGenerator(null) {
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
sourceTypes: Set<ExtractorLinkType>,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||
|
|
@ -435,7 +434,8 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
// Restore when lock is disabled.
|
||||
restoreOrientationWithSensor(this)
|
||||
} else {
|
||||
this.requestedOrientation = playerHostView?.dynamicOrientation() ?: return@apply
|
||||
this.requestedOrientation =
|
||||
playerHostView?.dynamicOrientation() ?: return@apply
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -443,14 +443,14 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
}
|
||||
|
||||
private fun setupKeyEventListener() {
|
||||
keyEventListener = { eventNav ->
|
||||
val (event, hasNavigated) = eventNav
|
||||
keyEventListener = { (event, hasNavigated) ->
|
||||
when {
|
||||
event == null -> false
|
||||
event.action == KeyEvent.ACTION_DOWN &&
|
||||
(event.keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
|
||||
event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) ->
|
||||
(event.keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
|
||||
event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) ->
|
||||
playerHostView?.handleVolumeKey(event.keyCode) ?: false
|
||||
|
||||
player.isActive() -> handleKeyEvent(event, hasNavigated)
|
||||
else -> false
|
||||
}
|
||||
|
|
@ -763,24 +763,23 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
}
|
||||
}
|
||||
playerBinding?.apply {
|
||||
|
||||
playerLockHolder.isGone = isGone
|
||||
playerVideoBar.isGone = isGone
|
||||
|
||||
playerPausePlay.isGone = isGone
|
||||
// player_buffering?.isGone = isGone
|
||||
playerPausePlayHolderHolder.isGone =
|
||||
isGone || currentPlayerStatus == CSPlayerLoading.IsBuffering
|
||||
playerTopHolder.isGone = isGone
|
||||
val showPlayerEpisodes = !isGone && isThereEpisodes()
|
||||
playerEpisodesButtonRoot.isVisible = showPlayerEpisodes
|
||||
playerEpisodesButton.isVisible = showPlayerEpisodes
|
||||
playerVideoTitleHolder.isGone = togglePlayerTitleGone
|
||||
playerVideoTitleRez.isGone = isGone
|
||||
playerVideoTitleRez.isGone = isGone || playerVideoTitleRez.text.isBlank()
|
||||
playerEpisodeFiller.isGone = isGone
|
||||
playerCenterMenu.isGone = isGone
|
||||
playerLock.isGone = !isShowing
|
||||
// player_media_route_button?.isClickable = !isGone
|
||||
playerGoBackHolder.isGone = isGone
|
||||
playerSourcesBtt.isGone = isGone
|
||||
shadowOverlay.isGone = isGone
|
||||
playerSkipEpisode.isClickable = !isGone
|
||||
}
|
||||
}
|
||||
|
|
@ -880,6 +879,145 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
playerHostView?.requestUpdateBrightnessOverlayOnNextLayout()
|
||||
}
|
||||
|
||||
private fun handleKeyDownEvent(keyCode: Int): Boolean? {
|
||||
// adb shell input keyevent [INT]
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||
player.handleEvent(CSPlayerEvent.SeekForward)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||
player.handleEvent(CSPlayerEvent.SeekBack)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_CHANNEL_UP -> {
|
||||
player.handleEvent(CSPlayerEvent.NextEpisode)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_CHANNEL_DOWN -> {
|
||||
player.handleEvent(CSPlayerEvent.PrevEpisode)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||
player.handleEvent(CSPlayerEvent.Pause)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
||||
toggleLock()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_H -> {
|
||||
onClickChange()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
||||
player.handleEvent(CSPlayerEvent.ToggleMute)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
||||
showMirrorsDialogue()
|
||||
}
|
||||
// OpenSubtitles shortcut
|
||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
||||
val context = context
|
||||
if (subsProvidersIsActive && context != null) {
|
||||
openOnlineSubPicker(context, null) {}
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
||||
showSpeedDialog()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
||||
nextResize()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
||||
skipOp()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
||||
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
|
||||
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()) {
|
||||
return null
|
||||
}
|
||||
// If UI is not shown make click instantly skip to next chapter even if locked
|
||||
if (timestampShowState) {
|
||||
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
|
||||
} else if (!isLocked) {
|
||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||
}
|
||||
onClickChange()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
if (isShowing || isShowingEpisodeOverlay) {
|
||||
return null
|
||||
}
|
||||
onClickChange()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
if (!isShowing && !isLocked && !isShowingEpisodeOverlay) {
|
||||
player.seekTime(-androidTVInterfaceOffSeekTime)
|
||||
return true
|
||||
} else if (playerBinding?.playerPausePlay?.isFocused == true) {
|
||||
player.seekTime(-androidTVInterfaceOnSeekTime)
|
||||
return true
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
if (!isShowing && !isLocked && !isShowingEpisodeOverlay) {
|
||||
player.seekTime(androidTVInterfaceOffSeekTime)
|
||||
} else if (playerBinding?.playerPausePlay?.isFocused == true) {
|
||||
player.seekTime(androidTVInterfaceOnSeekTime)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN,
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
// Handled entirely by PlayerView.handleVolumeKey (checks PHONE/EMULATOR).
|
||||
if (playerHostView?.handleVolumeKey(keyCode) != true) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MENU,
|
||||
KeyEvent.KEYCODE_SETTINGS -> {
|
||||
if (isLocked || !isThereEpisodes()) {
|
||||
return null
|
||||
}
|
||||
toggleEpisodesOverlay(true)
|
||||
}
|
||||
else -> return null // Avoid capturing all input
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
|
||||
if (hasNavigated) {
|
||||
autoHide()
|
||||
|
|
@ -888,53 +1026,9 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
val keyCode = event.keyCode
|
||||
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
if (!isShowing) {
|
||||
// If UI is not shown make click instantly skip to next chapter even if locked
|
||||
if (timestampShowState) {
|
||||
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
|
||||
} else if (!isLocked) {
|
||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||
}
|
||||
onClickChange()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
if (!isShowing && !isShowingEpisodeOverlay) {
|
||||
onClickChange()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
if (!isShowing && !isLocked && !isShowingEpisodeOverlay) {
|
||||
player.seekTime(-androidTVInterfaceOffSeekTime)
|
||||
return true
|
||||
} else if (playerBinding?.playerPausePlay?.isFocused == true) {
|
||||
player.seekTime(-androidTVInterfaceOnSeekTime)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
if (!isShowing && !isLocked && !isShowingEpisodeOverlay) {
|
||||
player.seekTime(androidTVInterfaceOffSeekTime)
|
||||
return true
|
||||
} else if (playerBinding?.playerPausePlay?.isFocused == true) {
|
||||
player.seekTime(androidTVInterfaceOnSeekTime)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN,
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
// Handled entirely by PlayerView.handleVolumeKey (checks PHONE/EMULATOR).
|
||||
if (playerHostView?.handleVolumeKey(keyCode) == true) return true
|
||||
}
|
||||
val value = handleKeyDownEvent(keyCode)
|
||||
if (value != null) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1000,7 +1094,8 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
override fun onBindingCreated(binding: FragmentPlayerBinding, savedInstanceState: Bundle?) {
|
||||
// Set up playerBinding before super initializes the player
|
||||
// (brightness overlay is now injected by PlayerView.initialize())
|
||||
playerBinding = PlayerCustomLayoutBinding.bind(binding.root.findViewById(R.id.player_holder))
|
||||
playerBinding =
|
||||
PlayerCustomLayoutBinding.bind(binding.root.findViewById(R.id.player_holder))
|
||||
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
|
|
@ -1018,81 +1113,6 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
subtitleDelay = it
|
||||
}
|
||||
|
||||
// handle tv controls
|
||||
playerEventListener = { eventType ->
|
||||
when (eventType) {
|
||||
PlayerEventType.Lock -> {
|
||||
toggleLock()
|
||||
}
|
||||
|
||||
PlayerEventType.NextEpisode -> {
|
||||
player.handleEvent(CSPlayerEvent.NextEpisode)
|
||||
}
|
||||
|
||||
PlayerEventType.Pause -> {
|
||||
player.handleEvent(CSPlayerEvent.Pause)
|
||||
}
|
||||
|
||||
PlayerEventType.PlayPauseToggle -> {
|
||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||
}
|
||||
|
||||
PlayerEventType.Play -> {
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
}
|
||||
|
||||
PlayerEventType.SkipCurrentChapter -> {
|
||||
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
|
||||
}
|
||||
|
||||
PlayerEventType.Resize -> {
|
||||
nextResize()
|
||||
}
|
||||
|
||||
PlayerEventType.PrevEpisode -> {
|
||||
player.handleEvent(CSPlayerEvent.PrevEpisode)
|
||||
}
|
||||
|
||||
PlayerEventType.SeekForward -> {
|
||||
player.handleEvent(CSPlayerEvent.SeekForward)
|
||||
}
|
||||
|
||||
PlayerEventType.ShowSpeed -> {
|
||||
showSpeedDialog()
|
||||
}
|
||||
|
||||
PlayerEventType.SeekBack -> {
|
||||
player.handleEvent(CSPlayerEvent.SeekBack)
|
||||
}
|
||||
|
||||
PlayerEventType.Restart -> {
|
||||
player.handleEvent(CSPlayerEvent.Restart)
|
||||
}
|
||||
|
||||
PlayerEventType.ToggleMute -> {
|
||||
player.handleEvent(CSPlayerEvent.ToggleMute)
|
||||
}
|
||||
|
||||
PlayerEventType.ToggleHide -> {
|
||||
onClickChange()
|
||||
}
|
||||
|
||||
PlayerEventType.ShowMirrors -> {
|
||||
showMirrorsDialogue()
|
||||
}
|
||||
|
||||
PlayerEventType.SearchSubtitlesOnline -> {
|
||||
if (subsProvidersIsActive) {
|
||||
openOnlineSubPicker(view.context, null) {}
|
||||
}
|
||||
}
|
||||
|
||||
PlayerEventType.SkipOp -> {
|
||||
skipOp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle tv controls directly based on player state
|
||||
setupKeyEventListener()
|
||||
|
||||
|
|
@ -1137,8 +1157,9 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
else QualityDataHelper.QualityProfileType.WiFi
|
||||
|
||||
currentQualityProfile =
|
||||
profiles.firstOrNull { it.types.contains(type) }?.id ?: profiles.firstOrNull()?.id
|
||||
?: currentQualityProfile
|
||||
profiles.firstOrNull { it.types.contains(type) }?.id
|
||||
?: profiles.firstOrNull()?.id
|
||||
?: currentQualityProfile
|
||||
}
|
||||
playerBinding?.apply {
|
||||
playerSpeedBtt.isVisible = playBackSpeedEnabled
|
||||
|
|
@ -1179,15 +1200,6 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
}
|
||||
}
|
||||
|
||||
playerPausePlay.setOnClickListener {
|
||||
autoHide()
|
||||
if (currentPlayerStatus == CSPlayerLoading.IsEnded && isLayout(PHONE)) {
|
||||
player.handleEvent(CSPlayerEvent.Restart)
|
||||
} else {
|
||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||
}
|
||||
}
|
||||
|
||||
skipChapterButton.setOnClickListener {
|
||||
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ import kotlinx.coroutines.isActive
|
|||
import kotlinx.coroutines.launch
|
||||
import java.io.Serializable
|
||||
import java.util.Calendar
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
|
|
@ -139,11 +142,18 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
const val CHANNEL_ID = 7340
|
||||
const val STOP_ACTION = "stopcs3"
|
||||
|
||||
private var lastUsedGenerator: IGenerator? = null
|
||||
fun newInstance(generator: IGenerator, syncData: HashMap<String, String>? = null): Bundle {
|
||||
private val generators = ConcurrentHashMap<String, VideoGenerator<*>>()
|
||||
fun newInstance(
|
||||
generator: VideoGenerator<*>,
|
||||
index: Int,
|
||||
syncData: HashMap<String, String>? = null
|
||||
): Bundle {
|
||||
Log.i(TAG, "newInstance = $syncData")
|
||||
lastUsedGenerator = generator
|
||||
val uuid = UUID.randomUUID().toString()
|
||||
generators[uuid] = generator
|
||||
return Bundle().apply {
|
||||
putString("uuid", uuid)
|
||||
putInt("index", index)
|
||||
if (syncData != null) putSerializable("syncData", syncData)
|
||||
}
|
||||
}
|
||||
|
|
@ -162,26 +172,24 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
private lateinit var viewModel: PlayerGeneratorViewModel //by activityViewModels()
|
||||
private lateinit var sync: SyncViewModel
|
||||
private var currentLinks: Set<Pair<ExtractorLink?, ExtractorUri?>> = setOf()
|
||||
private var currentSubs: Set<SubtitleData> = setOf()
|
||||
|
||||
private var currentSelectedLink: Pair<ExtractorLink?, ExtractorUri?>? = null
|
||||
private var currentSelectedSubtitles: SubtitleData? = null
|
||||
private var currentMeta: Any? = null
|
||||
private var nextMeta: Any? = null
|
||||
private var isActive: Boolean = false
|
||||
private val currentMeta: Any? get() = viewModel.state.generatorState?.meta
|
||||
private val nextMeta: Any? get() = viewModel.state.generatorState?.nextMeta
|
||||
|
||||
private var isPlayerActive: AtomicBoolean = AtomicBoolean(false)
|
||||
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 var allMeta: List<ResultEpisode>? = null
|
||||
private fun startLoading() {
|
||||
player.release()
|
||||
currentSelectedSubtitles = null
|
||||
isActive = false
|
||||
binding?.overlayLoadingSkipButton?.isVisible = false
|
||||
binding?.playerLoadingOverlay?.isVisible = true
|
||||
}
|
||||
private val allMeta: List<ResultEpisode>?
|
||||
get() = viewModel.state.generatorState?.allMeta?.filterIsInstance<ResultEpisode>()
|
||||
?.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
|
||||
|
|
@ -213,7 +221,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
playerBinding?.playerTracksBtt?.isVisible =
|
||||
tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
|
||||
// Only set the preferred language if it is available.
|
||||
// Otherwise it may give some users audio track init failed!
|
||||
// Otherwise, it may give some users audio track init failed!
|
||||
if (tracks.allAudioTracks.any { it.language == preferredAudioTrackLanguage }) {
|
||||
player.setPreferredAudioTrack(preferredAudioTrackLanguage)
|
||||
}
|
||||
|
|
@ -232,7 +240,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
private fun getPos(): Long {
|
||||
val durPos = getViewPos(viewModel.getId()) ?: return 0L
|
||||
val durPos = getViewPos(viewModel.state.generatorState?.id) ?: return 0L
|
||||
if (durPos.duration == 0L) return 0L
|
||||
if (durPos.position * 100L / durPos.duration > 95L) {
|
||||
return 0L
|
||||
|
|
@ -383,9 +391,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
override fun onCustomAction(player: Player, action: String, intent: Intent) {
|
||||
when (action) {
|
||||
STOP_ACTION -> {
|
||||
playerHostView?.exitFullscreen()
|
||||
this@GeneratorPlayer.player.release()
|
||||
activity?.popCurrentPage()
|
||||
exitPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -485,9 +491,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadLink(link: Pair<ExtractorLink?, ExtractorUri?>?, sameEpisode: Boolean) {
|
||||
private fun loadLink(link: VideoLink?, sameEpisode: Boolean) {
|
||||
if (link == null) return
|
||||
|
||||
isPlayerActive.set(true)
|
||||
// manage UI
|
||||
binding?.playerLoadingOverlay?.isVisible = false
|
||||
val isTorrent =
|
||||
|
|
@ -503,16 +509,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
uiReset()
|
||||
currentSelectedLink = link
|
||||
currentMeta = viewModel.getMeta()
|
||||
nextMeta = viewModel.getNextMeta()
|
||||
allMeta = viewModel.getAllMeta()?.filterIsInstance<ResultEpisode>()?.map { episode ->
|
||||
// Refresh all the episodes watch duration
|
||||
getViewPos(episode.id)?.let { data ->
|
||||
episode.copy(position = data.position, duration = data.duration)
|
||||
} ?: episode
|
||||
}
|
||||
// setEpisodes(viewModel.getAllMeta() ?: emptyList())
|
||||
isActive = true
|
||||
setPlayerDimen(null)
|
||||
setTitle()
|
||||
if (!sameEpisode)
|
||||
|
|
@ -522,6 +519,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
// load player
|
||||
context?.let { ctx ->
|
||||
val (url, uri) = link
|
||||
val subtitles = viewModel.state.subtitles
|
||||
player.loadPlayer(
|
||||
ctx,
|
||||
sameEpisode,
|
||||
|
|
@ -530,9 +528,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
startPosition = if (sameEpisode) null else {
|
||||
if (isNextEpisode) 0L else getPos()
|
||||
},
|
||||
currentSubs,
|
||||
subtitles,
|
||||
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
|
||||
currentSubs, settings = true, downloads = true
|
||||
subtitles, settings = true, downloads = true
|
||||
),
|
||||
preview = true
|
||||
)
|
||||
|
|
@ -545,13 +543,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun sortLinks(qualityProfile: Int): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
||||
return currentLinks.sortedBy {
|
||||
// negative because we want to sort highest quality first
|
||||
-getLinkPriority(qualityProfile, it.first)
|
||||
}
|
||||
}
|
||||
|
||||
data class TempMetaData(
|
||||
var episode: Int? = null,
|
||||
var season: Int? = null,
|
||||
|
|
@ -877,20 +868,18 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
vararg subtitleData: SubtitleData
|
||||
) {
|
||||
if (subtitleData.isEmpty()) return
|
||||
val selectedSubtitle = subtitleData.first()
|
||||
val ctx = context ?: return
|
||||
|
||||
val subs = currentSubs + subtitleData
|
||||
val selectedSubtitle = subtitleData.first()
|
||||
viewModel.addSubtitles(subtitleData.toSet())
|
||||
|
||||
// this is used instead of observe(viewModel._currentSubs), because observe is too slow
|
||||
player.setActiveSubtitles(subs)
|
||||
player.setActiveSubtitles(viewModel.state.subtitles)
|
||||
|
||||
// Save current time as to not reset player to 00:00
|
||||
player.saveData()
|
||||
player.reloadPlayer(ctx)
|
||||
|
||||
setSubtitles(selectedSubtitle, false)
|
||||
viewModel.addSubtitles(subtitleData.toSet())
|
||||
|
||||
selectSourceDialog?.dismissSafe()
|
||||
selectSourceDialog = null
|
||||
|
|
@ -989,7 +978,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
// checks for both a race condition and if any of the subs generated is new
|
||||
if (this.isActive && !currentSubs.containsAll(subtitles) && !hasSelectASubtitle) {
|
||||
if (this.isActive && !viewModel.state.subtitles.containsAll(subtitles) && !hasSelectASubtitle) {
|
||||
hasSelectASubtitle = true
|
||||
runOnMainThread {
|
||||
addAndSelectSubtitles(*subtitles.toTypedArray())
|
||||
|
|
@ -1012,7 +1001,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
context?.let { ctx ->
|
||||
val isPlaying = player.getIsPlaying()
|
||||
player.handleEvent(CSPlayerEvent.Pause, PlayerEventSource.UI)
|
||||
val currentSubtitles = sortSubs(currentSubs)
|
||||
val currentSubtitles = sortSubs(viewModel.state.subtitles)
|
||||
|
||||
val sourceDialog = Dialog(ctx, R.style.DialogFullscreenPlayer)
|
||||
val binding =
|
||||
|
|
@ -1054,7 +1043,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
if (subsProvidersIsActive) {
|
||||
val currentLoadResponse = viewModel.getLoadResponse()
|
||||
val currentLoadResponse = viewModel.state.generatorState?.response
|
||||
|
||||
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
|
||||
R.layout.sort_bottom_footer_add_choice, null
|
||||
|
|
@ -1112,7 +1101,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||
|
||||
fun refreshLinks(qualityProfile: Int) {
|
||||
sortedUrls = sortLinks(qualityProfile)
|
||||
sortedUrls = viewModel.state.sortLinks(qualityProfile)
|
||||
if (sortedUrls.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
|
||||
true
|
||||
|
|
@ -1277,16 +1266,28 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
binding.profilesClickSettings.setOnClickListener {
|
||||
val activity = activity ?: return@setOnClickListener
|
||||
QualityProfileDialog(
|
||||
val dialog = QualityProfileDialog(
|
||||
activity,
|
||||
R.style.DialogFullscreenPlayer,
|
||||
currentLinks.mapNotNull { it.first?.let { extractorLink -> LinkSource(extractorLink) } },
|
||||
viewModel.state.links.mapNotNull {
|
||||
it.first?.let { extractorLink ->
|
||||
LinkSource(
|
||||
extractorLink
|
||||
)
|
||||
}
|
||||
},
|
||||
currentQualityProfile
|
||||
) { profile ->
|
||||
currentQualityProfile = profile.id
|
||||
setProfileName(profile.id)
|
||||
refreshLinks(profile.id)
|
||||
}.show()
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
viewModel.state.clearSortedLinksCache()
|
||||
refreshLinks(currentQualityProfile)
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
binding.subtitlesEncodingFormat.apply {
|
||||
|
|
@ -1430,11 +1431,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
var audioIndexStart = currentAudioTracks.indexOfFirst { track ->
|
||||
track.id == tracks.currentAudioTrack?.id &&
|
||||
track.formatIndex == tracks.currentAudioTrack?.formatIndex
|
||||
track.id == tracks.currentAudioTrack?.id &&
|
||||
track.formatIndex == tracks.currentAudioTrack?.formatIndex
|
||||
}.coerceAtLeast(0)
|
||||
|
||||
val audioArrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
val audioArrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
audioArrayAdapter.addAll(
|
||||
currentAudioTracks.mapIndexed { _, track ->
|
||||
|
|
@ -1442,7 +1444,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val language = (
|
||||
track.language?.trim()?.let { raw ->
|
||||
fromTagToLanguageName(raw)
|
||||
?: fromTagToLanguageName(raw.replace('_','-').substringBefore('-').lowercase())
|
||||
?: fromTagToLanguageName(
|
||||
raw.replace('_', '-').substringBefore('-').lowercase()
|
||||
)
|
||||
?: raw
|
||||
}
|
||||
?: track.label
|
||||
|
|
@ -1464,7 +1468,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
listOfNotNull(
|
||||
language.takeIf { it.isNotBlank() }?.replaceFirstChar { it.uppercaseChar() },
|
||||
language.takeIf { it.isNotBlank() }
|
||||
?.replaceFirstChar { it.uppercaseChar() },
|
||||
channels.takeIf { it.isNotBlank() },
|
||||
codec.takeIf { it.isNotBlank() }?.uppercase()
|
||||
).joinToString(" • ")
|
||||
|
|
@ -1492,7 +1497,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
binding.applyBtt.setOnClickListener {
|
||||
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
|
||||
player.setPreferredAudioTrack(
|
||||
currentTrack?.language,
|
||||
currentTrack?.language,
|
||||
currentTrack?.id,
|
||||
currentTrack?.formatIndex,
|
||||
)
|
||||
|
|
@ -1541,13 +1546,20 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
private fun startPlayer() {
|
||||
if (isActive) return // we don't want double load when you skip loading
|
||||
// We don't want double load when you skip loading
|
||||
if (isPlayerActive.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
val links = viewModel.state.sortLinks(currentQualityProfile)
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
}
|
||||
// Atomic operation to prevent double loading
|
||||
if (!isPlayerActive.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
loadLink(links.first(), false)
|
||||
showPlayerMetadata()
|
||||
}
|
||||
|
|
@ -1560,7 +1572,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val metaView = overlay.findViewById<TextView>(R.id.player_movie_meta)
|
||||
val descView = overlay.findViewById<TextView>(R.id.player_movie_overview)
|
||||
|
||||
val load = viewModel.getLoadResponse() ?: return
|
||||
val load = viewModel.state.generatorState?.response ?: return
|
||||
val episode = currentMeta as? ResultEpisode
|
||||
titleView.text = load.name
|
||||
|
||||
|
|
@ -1602,7 +1614,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
override fun nextEpisode() {
|
||||
if (viewModel.hasNextEpisode() == true) {
|
||||
isNextEpisode = true
|
||||
player.release()
|
||||
releasePlayer()
|
||||
viewModel.loadLinksNext()
|
||||
}
|
||||
}
|
||||
|
|
@ -1610,18 +1622,18 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
override fun prevEpisode() {
|
||||
if (viewModel.hasPrevEpisode() == true) {
|
||||
isNextEpisode = true
|
||||
player.release()
|
||||
releasePlayer()
|
||||
viewModel.loadLinksPrev()
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasNextMirror(): Boolean {
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
val links = viewModel.state.sortLinks(currentQualityProfile)
|
||||
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
|
||||
}
|
||||
|
||||
override fun nextMirror() {
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
val links = viewModel.state.sortLinks(currentQualityProfile)
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
|
|
@ -1668,7 +1680,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val percentage = position * 100L / duration
|
||||
|
||||
DataStoreHelper.setViewPosAndResume(
|
||||
viewModel.getId(),
|
||||
viewModel.state.generatorState?.id,
|
||||
position,
|
||||
duration,
|
||||
currentMeta,
|
||||
|
|
@ -1720,14 +1732,18 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
): SubtitleData? {
|
||||
val langCode = preferredAutoSelectSubtitles ?: return null
|
||||
if (downloads) {
|
||||
return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode(langCode) }
|
||||
sortSubs(subtitles).firstOrNull {
|
||||
it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode(
|
||||
langCode
|
||||
)
|
||||
}?.let { return it }
|
||||
}
|
||||
|
||||
if (!settings) return null
|
||||
|
||||
return sortSubs(subtitles).firstOrNull { it.matchesLanguageCode(langCode) }
|
||||
}
|
||||
|
||||
|
||||
private fun autoSelectFromSettings(): Boolean {
|
||||
// auto select subtitle based on settings
|
||||
val langCode = preferredAutoSelectSubtitles
|
||||
|
|
@ -1744,7 +1760,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
} else if (!langCode.isNullOrEmpty()) {
|
||||
getAutoSelectSubtitle(
|
||||
currentSubs, settings = true, downloads = false
|
||||
viewModel.state.subtitles, settings = true, downloads = false
|
||||
)?.let { sub ->
|
||||
if (setSubtitles(sub, false)) {
|
||||
player.saveData()
|
||||
|
|
@ -1758,20 +1774,20 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
return false
|
||||
}
|
||||
|
||||
private fun autoSelectFromDownloads(): Boolean {
|
||||
if (player.getCurrentPreferredSubtitle() == null) {
|
||||
getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub ->
|
||||
context?.let { ctx ->
|
||||
if (setSubtitles(sub, false)) {
|
||||
player.saveData()
|
||||
player.reloadPlayer(ctx)
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun autoSelectFromDownloads() {
|
||||
if (player.getCurrentPreferredSubtitle() != null) {
|
||||
return
|
||||
}
|
||||
return false
|
||||
val sub =
|
||||
getAutoSelectSubtitle(viewModel.state.subtitles, settings = false, downloads = true)
|
||||
?: return
|
||||
val ctx = context ?: return
|
||||
if (!setSubtitles(sub, false)) {
|
||||
return
|
||||
}
|
||||
player.saveData()
|
||||
player.reloadPlayer(ctx)
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
}
|
||||
|
||||
private fun autoSelectSubtitles() {
|
||||
|
|
@ -1855,7 +1871,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
playerBinding?.playerEpisodeFillerHolder?.isVisible = isFiller ?: false
|
||||
playerBinding?.playerVideoTitle?.text = playerVideoTitle
|
||||
playerBinding?.offlinePin?.isVisible = lastUsedGenerator is DownloadFileGenerator
|
||||
playerBinding?.offlinePin?.isVisible = viewModel.generator is DownloadFileGenerator
|
||||
}
|
||||
|
||||
fun setPlayerDimen(widthHeight: Pair<Int, Int>?) {
|
||||
|
|
@ -1996,6 +2012,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
skipAnimator?.cancel()
|
||||
isVisible = true
|
||||
|
||||
/** Focus instantly to make the focus color appear instantly */
|
||||
if (show && !isShowing) {
|
||||
// Automatically request focus if the menu is not opened
|
||||
playerBinding?.skipChapterButton?.requestFocus()
|
||||
}
|
||||
|
||||
// just in case
|
||||
val lay = layoutParams
|
||||
lay.width = from
|
||||
|
|
@ -2004,12 +2026,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
from, to
|
||||
).apply {
|
||||
addListener(onEnd = {
|
||||
if (show) {
|
||||
if (!isShowing) {
|
||||
// Automatically request focus if the menu is not opened
|
||||
playerBinding?.skipChapterButton?.requestFocus()
|
||||
}
|
||||
} else {
|
||||
if (!show) {
|
||||
playerBinding?.skipChapterButton?.isVisible = false
|
||||
if (!isShowing) {
|
||||
// Automatically return focus to play pause
|
||||
|
|
@ -2048,8 +2065,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun isThereEpisodes(): Boolean {
|
||||
val meta = allMeta
|
||||
return !meta.isNullOrEmpty() && meta.size > 1
|
||||
// Checks if there is a second episode of type ResultEpisode
|
||||
// => There exists more than 1 episode, and they are all ResultEpisode
|
||||
return viewModel.state.generatorState?.allMeta?.getOrNull(1) as? ResultEpisode != null
|
||||
}
|
||||
|
||||
override fun showEpisodesOverlay() {
|
||||
|
|
@ -2061,7 +2079,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
{ episodeClick ->
|
||||
if (episodeClick.action == ACTION_CLICK_DEFAULT) {
|
||||
isNextEpisode = false
|
||||
player.release()
|
||||
releasePlayer()
|
||||
playerEpisodeOverlay.isGone = true
|
||||
episodeClick.position?.let { viewModel.loadThisEpisode(it) }
|
||||
}
|
||||
|
|
@ -2080,7 +2098,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
(playerEpisodeList.adapter as? EpisodeAdapter)?.submitList(episodes)
|
||||
|
||||
// Scroll to current episode
|
||||
viewModel.getCurrentIndex()?.let { index ->
|
||||
viewModel.state.generatorState?.index?.let { index ->
|
||||
playerEpisodeList.scrollToPosition(index)
|
||||
// Ensure focus on tv
|
||||
if (isLayout(TV)) {
|
||||
|
|
@ -2124,32 +2142,64 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun releasePlayer() {
|
||||
player.release()
|
||||
currentSelectedSubtitles = null
|
||||
currentSelectedLink = null
|
||||
isPlayerActive.set(false)
|
||||
binding?.overlayLoadingSkipButton?.isVisible = false
|
||||
binding?.playerLoadingOverlay?.isVisible = true
|
||||
uiReset()
|
||||
}
|
||||
|
||||
fun exitPlayer() {
|
||||
playerHostView?.exitFullscreen()
|
||||
player.release()
|
||||
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]
|
||||
viewModel.attachGenerator(lastUsedGenerator)
|
||||
|
||||
val uuid = savedInstanceState?.getString("uuid") ?: arguments?.getString("uuid")
|
||||
val index = savedInstanceState?.getInt("index") ?: arguments?.getInt("index")
|
||||
val generator = generators[uuid]
|
||||
|
||||
unwrapBundle(savedInstanceState)
|
||||
unwrapBundle(arguments)
|
||||
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
var langFilterList = listOf<String>()
|
||||
var filterSubByLang = false
|
||||
// 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)
|
||||
showResolution = settingsManager.getBoolean(ctx.getString(R.string.show_resolution_key), true)
|
||||
showMediaInfo = settingsManager.getBoolean(ctx.getString(R.string.show_media_info_key), false)
|
||||
showResolution =
|
||||
settingsManager.getBoolean(ctx.getString(R.string.show_resolution_key), true)
|
||||
showMediaInfo =
|
||||
settingsManager.getBoolean(ctx.getString(R.string.show_media_info_key), false)
|
||||
limitTitle = settingsManager.getInt(ctx.getString(R.string.prefer_title_limit_key), 0)
|
||||
updateForcedEncoding(ctx)
|
||||
filterSubByLang =
|
||||
viewModel.filterSubByLang =
|
||||
settingsManager.getBoolean(getString(R.string.filter_sub_lang_key), false)
|
||||
if (filterSubByLang) {
|
||||
if (viewModel.filterSubByLang) {
|
||||
val langFromPrefMedia = settingsManager.getStringSet(
|
||||
this.getString(R.string.provider_lang_key), mutableSetOf("en")
|
||||
)
|
||||
langFilterList = langFromPrefMedia?.mapNotNull {
|
||||
viewModel.langFilterList = langFromPrefMedia?.mapNotNull {
|
||||
fromTagToEnglishLanguageName(it)?.lowercase() ?: return@mapNotNull null
|
||||
} ?: listOf()
|
||||
}
|
||||
|
|
@ -2162,18 +2212,23 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
preferredAutoSelectSubtitles = getAutoSelectLanguageTagIETF()
|
||||
|
||||
if (currentSelectedLink == null) {
|
||||
val selectedLink = currentSelectedLink
|
||||
if (selectedLink == null) {
|
||||
viewModel.loadLinks()
|
||||
} else {
|
||||
// Recreated view, so we need to recreate the
|
||||
loadLink(selectedLink, true)
|
||||
}
|
||||
|
||||
binding.overlayLoadingSkipButton.setOnClickListener {
|
||||
startPlayer()
|
||||
// Mark as "success" early
|
||||
viewModel.modifyState {
|
||||
copy(loading = Resource.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
binding.playerLoadingGoBack.setOnClickListener {
|
||||
playerHostView?.exitFullscreen()
|
||||
player.release()
|
||||
activity?.popCurrentPage()
|
||||
exitPlayer()
|
||||
}
|
||||
|
||||
playerBinding?.downloadHeader?.setOnClickListener {
|
||||
|
|
@ -2186,14 +2241,29 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
observe(viewModel.currentStamps) { stamps ->
|
||||
observe(viewModel.currentStamps) { (stamps, instance) ->
|
||||
if (instance != viewModel.state.instance) return@observe // Outdated observe
|
||||
player.addTimeStamps(stamps)
|
||||
}
|
||||
|
||||
observe(viewModel.loadingLinks) {
|
||||
when (it) {
|
||||
observe(viewModel.currentSubtitles) { (subtitles, instance) ->
|
||||
if (instance != viewModel.state.instance) return@observe // Outdated observe
|
||||
player.setActiveSubtitles(subtitles)
|
||||
|
||||
// If the file is downloaded then do not select auto select the subtitles
|
||||
// Downloaded subtitles cannot be selected immediately after loading since
|
||||
// player.getCurrentPreferredSubtitle() cannot fetch data from non-loaded subtitles
|
||||
// Resulting in unselecting the downloaded subtitle
|
||||
if (subtitles.lastOrNull()?.origin != SubtitleOrigin.DOWNLOADED_FILE) {
|
||||
autoSelectSubtitles()
|
||||
}
|
||||
}
|
||||
observe(viewModel.loadingLinks) { (loading, instance) ->
|
||||
if (instance != viewModel.state.instance) return@observe // Outdated observe
|
||||
|
||||
when (loading) {
|
||||
is Resource.Loading -> {
|
||||
startLoading()
|
||||
releasePlayer()
|
||||
}
|
||||
|
||||
is Resource.Success -> {
|
||||
|
|
@ -2205,30 +2275,30 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
is Resource.Failure -> {
|
||||
showToast(it.errorString, Toast.LENGTH_LONG)
|
||||
showToast(loading.errorString, Toast.LENGTH_LONG)
|
||||
startPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.currentLinks) {
|
||||
currentLinks = it
|
||||
val turnVisible = it.isNotEmpty() && lastUsedGenerator?.canSkipLoading == true
|
||||
observe(viewModel.currentLinks) { (links, instance) ->
|
||||
if (instance != viewModel.state.instance) return@observe // Outdated observe
|
||||
|
||||
val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true
|
||||
val wasGone = binding.overlayLoadingSkipButton.isGone
|
||||
|
||||
binding.overlayLoadingSkipButton.apply {
|
||||
isVisible = turnVisible
|
||||
val value = viewModel.currentLinks.value
|
||||
if (value.isNullOrEmpty()) {
|
||||
if (links.isEmpty()) {
|
||||
setText(R.string.skip_loading)
|
||||
} else {
|
||||
@SuppressLint("SetTextI18n")
|
||||
text = "${context.getString(R.string.skip_loading)} (${value.size})"
|
||||
text = "${context.getString(R.string.skip_loading)} (${links.size})"
|
||||
}
|
||||
}
|
||||
|
||||
safe {
|
||||
if (currentLinks.any { link ->
|
||||
if (!isPlayerActive.get() && viewModel.state.links.any { link ->
|
||||
getLinkPriority(currentQualityProfile, link.first) >=
|
||||
QualityDataHelper.AUTO_SKIP_PRIORITY
|
||||
}
|
||||
|
|
@ -2241,34 +2311,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
binding.overlayLoadingSkipButton.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.currentSubs) { set ->
|
||||
val setOfSub = mutableSetOf<SubtitleData>()
|
||||
if (langFilterList.isNotEmpty() && filterSubByLang) {
|
||||
Log.i("subfilter", "Filtering subtitle")
|
||||
langFilterList.forEach { lang ->
|
||||
Log.i("subfilter", "Lang: $lang")
|
||||
setOfSub += set.filter {
|
||||
it.originalName.contains(lang, ignoreCase = true) ||
|
||||
it.origin != SubtitleOrigin.URL
|
||||
}
|
||||
}
|
||||
currentSubs = setOfSub
|
||||
} else {
|
||||
currentSubs = set
|
||||
}
|
||||
player.setActiveSubtitles(set)
|
||||
|
||||
// If the file is downloaded then do not select auto select the subtitles
|
||||
// Downloaded subtitles cannot be selected immediately after loading since
|
||||
// player.getCurrentPreferredSubtitle() cannot fetch data from non-loaded subtitles
|
||||
// Resulting in unselecting the downloaded subtitle
|
||||
if (set.lastOrNull()?.origin != SubtitleOrigin.DOWNLOADED_FILE) {
|
||||
autoSelectSubtitles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
val LOADTYPE_INAPP = setOf(
|
||||
ExtractorLinkType.VIDEO,
|
||||
|
|
@ -28,71 +25,27 @@ val LOADTYPE_CHROMECAST = setOf(
|
|||
val LOADTYPE_ALL = ExtractorLinkType.entries.toSet()
|
||||
|
||||
|
||||
abstract class NoVideoGenerator : VideoGenerator<Nothing>(emptyList(), 0) {
|
||||
abstract class NoVideoGenerator(val id : Int?) : VideoGenerator<Nothing>(emptyList()) {
|
||||
override val hasCache = false
|
||||
override val canSkipLoading = false
|
||||
override fun getId(index: Int): Int? = id
|
||||
}
|
||||
|
||||
abstract class VideoGenerator<T : Any>(val videos: List<T>, var videoIndex: Int = 0) :
|
||||
IGenerator {
|
||||
abstract class VideoGenerator<T : Any>(val videos: List<T>) {
|
||||
abstract val hasCache: Boolean
|
||||
abstract val canSkipLoading: Boolean
|
||||
abstract fun getId(index : Int) : Int?
|
||||
|
||||
override fun hasNext(): Boolean = videoIndex < videos.lastIndex
|
||||
override fun hasPrev(): Boolean = videoIndex > 0
|
||||
override fun getAll(): List<T>? = videos
|
||||
override fun getCurrent(offset: Int): T? = videos.getOrNull(videoIndex + offset)
|
||||
override fun next() {
|
||||
if (hasNext()) {
|
||||
videoIndex += 1
|
||||
}
|
||||
}
|
||||
fun hasNext(videoIndex : Int): Boolean = videoIndex < videos.lastIndex
|
||||
fun hasPrev(videoIndex : Int): Boolean = videoIndex > 0
|
||||
|
||||
override fun prev() {
|
||||
if (hasPrev()) {
|
||||
videoIndex -= 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun goto(index: Int) {
|
||||
videoIndex = min(videos.lastIndex, max(0, index))
|
||||
}
|
||||
|
||||
override fun getCurrentId(): Int? {
|
||||
return when (val current = getCurrent()) {
|
||||
is ResultEpisode -> {
|
||||
current.id
|
||||
}
|
||||
|
||||
is ExtractorUri -> {
|
||||
current.id
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO deprecate/remove IGenerator in favor of a more ergonomic and correct implementation
|
||||
interface IGenerator {
|
||||
val hasCache: Boolean
|
||||
val canSkipLoading: Boolean
|
||||
|
||||
fun hasNext(): Boolean
|
||||
fun hasPrev(): Boolean
|
||||
fun next()
|
||||
fun prev()
|
||||
fun goto(index: Int)
|
||||
|
||||
fun getCurrentId(): Int? // this is used to save data or read data about this id
|
||||
fun getCurrent(offset: Int = 0): Any? // this is used to get metadata about the current playing, can return null
|
||||
fun getAll(): List<Any>? // this us used to get the metadata about all entries, not needed
|
||||
|
||||
/* not safe, must use try catch */
|
||||
suspend fun generateLinks(
|
||||
@Throws
|
||||
abstract suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
sourceTypes: Set<ExtractorLinkType>,
|
||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||
subtitleCallback: (SubtitleData) -> Unit,
|
||||
offset: Int = 0,
|
||||
isCasting: Boolean = false
|
||||
offset: Int,
|
||||
isCasting: Boolean
|
||||
): Boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,12 @@ package com.lagradost.cloudstream3.ui.player
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Rational
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.MainThread
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp
|
||||
|
||||
enum class PlayerEventType(val value: Int) {
|
||||
Pause(0),
|
||||
Play(1),
|
||||
SeekForward(2),
|
||||
SeekBack(3),
|
||||
|
||||
SkipCurrentChapter(4),
|
||||
NextEpisode(5),
|
||||
PrevEpisode(6),
|
||||
PlayPauseToggle(7),
|
||||
ToggleMute(8),
|
||||
Lock(9),
|
||||
ToggleHide(10),
|
||||
ShowSpeed(11),
|
||||
ShowMirrors(12),
|
||||
Resize(13),
|
||||
SearchSubtitlesOnline(14),
|
||||
SkipOp(15),
|
||||
Restart(16),
|
||||
}
|
||||
|
||||
enum class CSPlayerEvent(val value: Int) {
|
||||
Pause(0),
|
||||
Play(1),
|
||||
|
|
@ -220,8 +201,6 @@ data class CurrentTracks(
|
|||
val allTextTracks: List<TextTrack>,
|
||||
)
|
||||
|
||||
class InvalidFileException(msg: String) : Exception(msg)
|
||||
|
||||
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
|
||||
const val ACTION_MEDIA_CONTROL = "media_control"
|
||||
const val EXTRA_CONTROL_TYPE = "control_type"
|
||||
|
|
@ -243,8 +222,9 @@ interface IPlayer {
|
|||
fun getSubtitleOffset(): Long // in ms
|
||||
fun setSubtitleOffset(offset: Long) // in ms
|
||||
|
||||
@AnyThread
|
||||
fun initCallbacks(
|
||||
eventHandler: ((PlayerEvent) -> Unit),
|
||||
@MainThread eventHandler: ((PlayerEvent) -> Unit),
|
||||
/** this is used to request when the player should report back view percentage */
|
||||
requestedListeningPercentages: List<Int>? = null,
|
||||
)
|
||||
|
|
@ -311,4 +291,4 @@ interface IPlayer {
|
|||
|
||||
/** Get the current subtitle cues, for use with syncing */
|
||||
fun getSubtitleCues(): List<SubtitleCue>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class LinkGenerator(
|
|||
private val links: List<BasicLink>,
|
||||
private val extract: Boolean = true,
|
||||
private val refererUrl: String? = null,
|
||||
) : NoVideoGenerator() {
|
||||
id: Int?
|
||||
) : NoVideoGenerator(id) {
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
sourceTypes: Set<ExtractorLinkType>,
|
||||
|
|
@ -78,10 +79,8 @@ class LinkGenerator(
|
|||
class MinimalLinkGenerator(
|
||||
private val links: List<CloudStreamPackage.MinimalVideoLink>,
|
||||
private val subs: List<CloudStreamPackage.MinimalSubtitleLink>,
|
||||
private val id: Int? = null
|
||||
) : NoVideoGenerator() {
|
||||
override fun getCurrentId(): Int? = id
|
||||
|
||||
id: Int?
|
||||
) : NoVideoGenerator(id) {
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
sourceTypes: Set<ExtractorLinkType>,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentUris
|
||||
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 +13,25 @@ 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(
|
||||
LinkGenerator(
|
||||
listOf(
|
||||
BasicLink(url)
|
||||
)
|
||||
)
|
||||
)
|
||||
), id = url.hashCode()
|
||||
), 0
|
||||
),
|
||||
replacePlayerNavOptions
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -52,8 +62,9 @@ object OfflinePlaybackHelper {
|
|||
links,
|
||||
subs,
|
||||
if (id != -1) id else null,
|
||||
)
|
||||
)
|
||||
), 0
|
||||
),
|
||||
replacePlayerNavOptions
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
|
@ -73,12 +84,12 @@ object OfflinePlaybackHelper {
|
|||
name = name ?: getString(activity, R.string.downloaded_file),
|
||||
// well not the same as a normal id, but we take it as users may want to
|
||||
// play downloaded files and save the location
|
||||
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()
|
||||
?.hashCode()
|
||||
id = uri.lastPathSegment?.toLongOrNull()?.hashCode() ?: uri.lastPathSegment?.hashCode()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
), 0
|
||||
),
|
||||
replacePlayerNavOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,35 +9,188 @@ import com.lagradost.cloudstream3.LoadResponse
|
|||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safe
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import com.lagradost.cloudstream3.utils.videoskip.SkipAPI
|
||||
import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.PersistentSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.Contract
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
typealias VideoLink = Pair<ExtractorLink?, ExtractorUri?>
|
||||
|
||||
data class GeneratorState(
|
||||
val meta: Any?,
|
||||
val nextMeta: Any?,
|
||||
val allMeta: List<*>?,
|
||||
val response: LoadResponse?,
|
||||
val index: Int,
|
||||
val id: Int?,
|
||||
)
|
||||
|
||||
/** Immutable state of all current links relevant to displaying the video */
|
||||
// @MustUseReturnValues
|
||||
// @Immutable
|
||||
data class VideoState(
|
||||
val subtitles: PersistentSet<SubtitleData> = persistentSetOf(),
|
||||
val links: PersistentSet<VideoLink> = persistentSetOf(),
|
||||
val stamps: PersistentList<VideoSkipStamp> = persistentListOf(),
|
||||
val loading: Resource<Unit> = Resource.Loading(),
|
||||
val generatorState: GeneratorState? = null,
|
||||
val instance: Int,
|
||||
) {
|
||||
/**
|
||||
* This acts as a local cache for sorted links that are not copied over by the copy constructor.
|
||||
*
|
||||
* sortedBy is not exactly expensive, but each hasNextMirror does it again, so this alleviates unnecessary recomputation
|
||||
* */
|
||||
private val sortedLinks: ConcurrentHashMap<Int, List<VideoLink>> = ConcurrentHashMap()
|
||||
|
||||
fun clearSortedLinksCache() = sortedLinks.clear()
|
||||
|
||||
// Modifying sortedLinks is not considered a "visible" side effect, and rerunning it does not change the result
|
||||
// It is by all standards, idempotent and by extension also pure as it has no "visible" side effect
|
||||
/** Returns .links in the sorted order according to the qualityProfile.
|
||||
* Use .links if order is not needed */
|
||||
@Contract(pure = true)
|
||||
fun sortLinks(qualityProfile: Int): List<VideoLink> {
|
||||
return sortedLinks[qualityProfile] ?: links.sortedBy { link ->
|
||||
// negative because we want to sort highest quality first
|
||||
-getLinkPriority(qualityProfile, link.first)
|
||||
}.also { value -> sortedLinks[qualityProfile] = value }
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
fun add(item: SubtitleData): VideoState = copy(subtitles = subtitles.add(item))
|
||||
|
||||
@Contract(pure = true)
|
||||
fun add(item: VideoLink): VideoState = copy(links = links.add(item))
|
||||
|
||||
@Contract(pure = true)
|
||||
fun add(item: VideoSkipStamp): VideoState = copy(stamps = stamps.add(item))
|
||||
|
||||
@JvmName("addSubtitleData")
|
||||
@Contract(pure = true)
|
||||
fun add(items: Collection<SubtitleData>): VideoState = copy(subtitles = subtitles.addAll(items))
|
||||
|
||||
@JvmName("addVideoLink")
|
||||
@Contract(pure = true)
|
||||
fun add(items: Collection<VideoLink>): VideoState = copy(links = links.addAll(items))
|
||||
|
||||
@JvmName("addVideoSkipStamp")
|
||||
@Contract(pure = true)
|
||||
fun add(items: Collection<VideoSkipStamp>): VideoState = copy(stamps = stamps.addAll(items))
|
||||
|
||||
@Contract(pure = true)
|
||||
fun set(item: SubtitleData): VideoState = copy(subtitles = persistentSetOf(item))
|
||||
|
||||
@Contract(pure = true)
|
||||
fun set(item: VideoLink): VideoState = copy(links = persistentSetOf(item))
|
||||
|
||||
@Contract(pure = true)
|
||||
fun set(item: VideoSkipStamp): VideoState = copy(stamps = persistentListOf(item))
|
||||
|
||||
@JvmName("setSubtitleData")
|
||||
@Contract(pure = true)
|
||||
fun set(items: Collection<SubtitleData>): VideoState = copy(subtitles = items.toPersistentSet())
|
||||
|
||||
@JvmName("setVideoLink")
|
||||
@Contract(pure = true)
|
||||
fun set(items: Collection<VideoLink>): VideoState = copy(links = items.toPersistentSet())
|
||||
|
||||
@JvmName("setVideoSkipStamp")
|
||||
@Contract(pure = true)
|
||||
fun set(items: Collection<VideoSkipStamp>): VideoState = copy(stamps = items.toPersistentList())
|
||||
}
|
||||
|
||||
data class VideoLive<T>(
|
||||
val value: T,
|
||||
val instance: Int,
|
||||
)
|
||||
|
||||
class PlayerGeneratorViewModel : ViewModel() {
|
||||
companion object {
|
||||
const val TAG = "PlayViewGen"
|
||||
}
|
||||
|
||||
private var generator: IGenerator? = null
|
||||
@Volatile
|
||||
var generator: VideoGenerator<*>? = null
|
||||
|
||||
private val _currentLinks = MutableLiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>>(setOf())
|
||||
val currentLinks: LiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>> = _currentLinks
|
||||
@Volatile
|
||||
var episodeIndex: Int = 0
|
||||
|
||||
private val _currentSubs = MutableLiveData<Set<SubtitleData>>(setOf())
|
||||
val currentSubs: LiveData<Set<SubtitleData>> = _currentSubs
|
||||
/**
|
||||
* The state of the video player, only modify it by modifyState to make sure observe is called,
|
||||
* and avoid concurrency issues.
|
||||
*
|
||||
* This value can be used without Synchronized or locking when reading, as all fields are immutable.
|
||||
* */
|
||||
@Volatile
|
||||
var state = VideoState(instance = 0)
|
||||
private set
|
||||
|
||||
private val _loadingLinks = MutableLiveData<Resource<Boolean?>>()
|
||||
val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks
|
||||
private val _currentLinks =
|
||||
MutableLiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>>(null)
|
||||
val currentLinks: LiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>> = _currentLinks
|
||||
|
||||
private val _currentStamps = MutableLiveData<List<VideoSkipStamp>>(emptyList())
|
||||
val currentStamps: LiveData<List<VideoSkipStamp>> = _currentStamps
|
||||
private val _currentSubtitles = MutableLiveData<VideoLive<Set<SubtitleData>>>(null)
|
||||
val currentSubtitles: LiveData<VideoLive<Set<SubtitleData>>> = _currentSubtitles
|
||||
|
||||
private val _loadingLinks = MutableLiveData<VideoLive<Resource<Unit>>>()
|
||||
val loadingLinks: LiveData<VideoLive<Resource<Unit>>> = _loadingLinks
|
||||
|
||||
private val _currentStamps = MutableLiveData<VideoLive<List<VideoSkipStamp>>>(null)
|
||||
val currentStamps: LiveData<VideoLive<List<VideoSkipStamp>>> = _currentStamps
|
||||
|
||||
/**
|
||||
* Modifies the `state` variable safely, and with the correct observe behavior.
|
||||
*
|
||||
* Synchronized to avoid concurrency issues, and make this operation atomic.
|
||||
* Otherwise, one update may be lost if they are done in parallel.
|
||||
* */
|
||||
@Synchronized
|
||||
fun modifyState(op: VideoState.() -> VideoState) {
|
||||
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"
|
||||
*
|
||||
* We do this by "Referential equality" https://kotlinlang.org/docs/equality.html#referential-equality
|
||||
* 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))
|
||||
if (state.stamps !== oldState.stamps)
|
||||
_currentStamps.postValue(VideoLive(state.stamps, state.instance))
|
||||
if (state.subtitles !== oldState.subtitles)
|
||||
_currentSubtitles.postValue(VideoLive(state.subtitles, state.instance))
|
||||
|
||||
/** Normal equality here as it is not a collection */
|
||||
if (state.loading != oldState.loading)
|
||||
_loadingLinks.postValue(VideoLive(state.loading, state.instance))
|
||||
}
|
||||
|
||||
private val _currentSubtitleYear = MutableLiveData<Int?>(null)
|
||||
val currentSubtitleYear: LiveData<Int?> = _currentSubtitleYear
|
||||
|
|
@ -53,41 +206,32 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
_currentSubtitleYear.postValue(year)
|
||||
}
|
||||
|
||||
fun getId(): Int? {
|
||||
return generator?.getCurrentId()
|
||||
}
|
||||
|
||||
fun loadLinks(episode: Int) {
|
||||
generator?.goto(episode)
|
||||
loadLinks()
|
||||
}
|
||||
|
||||
fun loadLinksPrev() {
|
||||
Log.i(TAG, "loadLinksPrev")
|
||||
if (generator?.hasPrev() == true) {
|
||||
generator?.prev()
|
||||
if (generator?.hasPrev(episodeIndex) == true) {
|
||||
episodeIndex += 1
|
||||
loadLinks()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLinksNext() {
|
||||
Log.i(TAG, "loadLinksNext")
|
||||
if (generator?.hasNext() == true) {
|
||||
generator?.next()
|
||||
if (generator?.hasNext(episodeIndex) == true) {
|
||||
episodeIndex += 1
|
||||
loadLinks()
|
||||
}
|
||||
}
|
||||
|
||||
fun hasNextEpisode(): Boolean? {
|
||||
return generator?.hasNext()
|
||||
return generator?.hasNext(episodeIndex)
|
||||
}
|
||||
|
||||
fun hasPrevEpisode(): Boolean? {
|
||||
return generator?.hasPrev()
|
||||
return generator?.hasPrev(episodeIndex)
|
||||
}
|
||||
|
||||
fun preLoadNextLinks() {
|
||||
val id = getId()
|
||||
val id = generator?.getId(episodeIndex)
|
||||
// Do not preload if already loading
|
||||
if (id == currentLoadingEpisodeId) return
|
||||
|
||||
|
|
@ -97,14 +241,15 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
||||
if (generator?.hasCache == true && generator?.hasNext(episodeIndex) == true) {
|
||||
safeApiCall {
|
||||
generator?.generateLinks(
|
||||
sourceTypes = LOADTYPE_INAPP,
|
||||
clearCache = false,
|
||||
isCasting = false,
|
||||
callback = {},
|
||||
subtitleCallback = {},
|
||||
offset = 1
|
||||
offset = episodeIndex + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -118,129 +263,137 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getLoadResponse(): LoadResponse? {
|
||||
return safe { (generator as? RepoLinkGenerator?)?.page }
|
||||
}
|
||||
|
||||
fun getMeta(): Any? {
|
||||
return safe { generator?.getCurrent() }
|
||||
}
|
||||
|
||||
fun getAllMeta(): List<Any>? {
|
||||
return safe { generator?.getAll() }
|
||||
}
|
||||
|
||||
fun getNextMeta(): Any? {
|
||||
return safe {
|
||||
if (generator?.hasNext() == false) return@safe null
|
||||
generator?.getCurrent(offset = 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadThisEpisode(index:Int) {
|
||||
generator?.goto(index)
|
||||
fun loadThisEpisode(index: Int) {
|
||||
episodeIndex = index
|
||||
loadLinks()
|
||||
}
|
||||
|
||||
fun getCurrentIndex():Int?{
|
||||
val repoGen = generator as? RepoLinkGenerator ?: return null
|
||||
return repoGen.videoIndex
|
||||
fun attachGenerator(newGenerator: VideoGenerator<*>, index: Int) {
|
||||
Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index")
|
||||
generator = newGenerator
|
||||
episodeIndex = index
|
||||
}
|
||||
|
||||
fun attachGenerator(newGenerator: IGenerator?) {
|
||||
if (generator == null) {
|
||||
generator = newGenerator
|
||||
}
|
||||
}
|
||||
|
||||
private var extraSubtitles : MutableSet<SubtitleData> = mutableSetOf()
|
||||
|
||||
/**
|
||||
* If duplicate nothing will happen
|
||||
* */
|
||||
fun addSubtitles(file: Set<SubtitleData>) = synchronized(extraSubtitles) {
|
||||
extraSubtitles += file
|
||||
val current = _currentSubs.value ?: emptySet()
|
||||
val next = extraSubtitles + current
|
||||
|
||||
// if it is of a different size then we have added distinct items
|
||||
if (next.size != current.size) {
|
||||
// Posting will refresh subtitles which will in turn
|
||||
// make the subs to english if previously unselected
|
||||
_currentSubs.postValue(next)
|
||||
}
|
||||
fun addSubtitles(file: Set<SubtitleData>) {
|
||||
val validFile = file.filter(::isValidSubtitle)
|
||||
if (validFile.isNotEmpty())
|
||||
modifyState {
|
||||
add(validFile)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentJob: Job? = null
|
||||
private var currentStampJob: Job? = null
|
||||
|
||||
fun loadStamps(duration: Long) {
|
||||
//currentStampJob?.cancel()
|
||||
currentStampJob = ioSafe {
|
||||
val meta = generator?.getCurrent()
|
||||
val page = (generator as? RepoLinkGenerator?)?.page
|
||||
if (page != null && meta is ResultEpisode) {
|
||||
_currentStamps.postValue(listOf())
|
||||
_currentStamps.postValue(
|
||||
SkipAPI.videoStamps(
|
||||
page,
|
||||
meta,
|
||||
duration,
|
||||
hasNextEpisode() ?: false
|
||||
)
|
||||
)
|
||||
val genState = state.generatorState ?: return@ioSafe
|
||||
val meta = genState.meta
|
||||
val page = genState.response
|
||||
val id = genState.id
|
||||
if (page == null || meta !is ResultEpisode) {
|
||||
return@ioSafe
|
||||
}
|
||||
val stamps = SkipAPI.videoStamps(
|
||||
page,
|
||||
meta,
|
||||
duration,
|
||||
hasNextEpisode() ?: false
|
||||
)
|
||||
|
||||
/** Avoid adding stamps to the wrong video */
|
||||
modifyState {
|
||||
if (id != this.generatorState?.id) {
|
||||
this
|
||||
} else {
|
||||
set(stamps)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var langFilterList = listOf<String>()
|
||||
var filterSubByLang = false
|
||||
|
||||
fun isValidSubtitle(subtitle: SubtitleData): Boolean {
|
||||
if (langFilterList.isEmpty() || !filterSubByLang) {
|
||||
return true
|
||||
}
|
||||
|
||||
/** Only filter out subtitles fetched online */
|
||||
if (subtitle.origin != SubtitleOrigin.URL) {
|
||||
return true
|
||||
}
|
||||
|
||||
return langFilterList.any { lang ->
|
||||
subtitle.originalName.contains(lang, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLinks(sourceTypes: Set<ExtractorLinkType> = LOADTYPE_INAPP) {
|
||||
Log.i(TAG, "loadLinks")
|
||||
Log.i(TAG, "loadLinks with generator=$generator and index=$episodeIndex")
|
||||
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 {
|
||||
// if we load links then we clear the prev loaded links
|
||||
synchronized(extraSubtitles) {
|
||||
extraSubtitles.clear()
|
||||
}
|
||||
val currentLinks = mutableSetOf<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
|
||||
// clear old data
|
||||
_currentSubs.postValue(emptySet())
|
||||
_currentLinks.postValue(emptySet())
|
||||
|
||||
// load more data
|
||||
_loadingLinks.postValue(Resource.Loading())
|
||||
// Load more data
|
||||
val loadingState = safeApiCall {
|
||||
generator?.generateLinks(
|
||||
sourceTypes = sourceTypes,
|
||||
clearCache = forceClearCache,
|
||||
callback = {
|
||||
synchronized(currentLinks) {
|
||||
currentLinks.add(it)
|
||||
// Clone to prevent ConcurrentModificationException
|
||||
safe {
|
||||
// Extra safe since .toSet() iterates.
|
||||
_currentLinks.postValue(currentLinks.toSet())
|
||||
callback = { link ->
|
||||
if (isActive)
|
||||
modifyState {
|
||||
add(link)
|
||||
}
|
||||
}
|
||||
},
|
||||
subtitleCallback = {
|
||||
synchronized(extraSubtitles) {
|
||||
currentSubs.add(it)
|
||||
safe {
|
||||
_currentSubs.postValue(currentSubs + extraSubtitles)
|
||||
isCasting = false,
|
||||
offset = index,
|
||||
subtitleCallback = { link ->
|
||||
if (isActive && isValidSubtitle(link))
|
||||
modifyState {
|
||||
add(link)
|
||||
}
|
||||
}
|
||||
})
|
||||
Unit
|
||||
}
|
||||
|
||||
_loadingLinks.postValue(loadingState)
|
||||
_currentLinks.postValue(currentLinks)
|
||||
synchronized(extraSubtitles) {
|
||||
_currentSubs.postValue(currentSubs + extraSubtitles)
|
||||
if (!isActive) {
|
||||
return@launchSafe
|
||||
}
|
||||
|
||||
/** 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1055,7 +1055,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) {
|
|||
return validHeight && validWidth
|
||||
}
|
||||
|
||||
return rawY > (context.getStatusBarHeight() ?: 0) && rawX < screenWidthWithOrientation
|
||||
return rawY > context.getStatusBarHeight() && rawX < screenWidthWithOrientation
|
||||
}
|
||||
|
||||
private fun handleGesture(view: View, event: MotionEvent): Boolean {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import android.widget.ProgressBar
|
|||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
|
|
@ -44,7 +45,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|||
import com.github.rubensousa.previewseekbar.PreviewBar
|
||||
import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
|
||||
import com.lagradost.cloudstream3.CommonActivity.isInPIPMode
|
||||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
|
|
@ -287,7 +287,13 @@ class PlayerView @JvmOverloads constructor(
|
|||
val previewFrameLayout: FrameLayout? =
|
||||
exoPlayerView?.findViewById(R.id.previewFrameLayout)
|
||||
|
||||
if (progressBar != null && previewImageView != null && previewFrameLayout != null) {
|
||||
/** Hide the previewFrameLayout on TV to make the skip op button not float,
|
||||
* as previewFrameLayout is normally invisible */
|
||||
if(isLayout(TV)) {
|
||||
previewFrameLayout?.isVisible = false
|
||||
}
|
||||
|
||||
if (progressBar != null && previewImageView != null && previewFrameLayout != null && !isLayout(TV)) {
|
||||
var resume = false
|
||||
progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener {
|
||||
override fun onScrubStart(previewBar: PreviewBar?) {
|
||||
|
|
@ -369,7 +375,8 @@ class PlayerView @JvmOverloads constructor(
|
|||
exoFfwdText?.text = context.getString(R.string.ffw_text_regular_format).format(seekSecs)
|
||||
|
||||
playerPausePlay?.setOnClickListener {
|
||||
if (currentPlayerStatus == CSPlayerLoading.IsEnded) {
|
||||
scheduleAutoHide()
|
||||
if (currentPlayerStatus == CSPlayerLoading.IsEnded && isLayout(PHONE)) {
|
||||
player.handleEvent(CSPlayerEvent.Restart, PlayerEventSource.UI)
|
||||
} else {
|
||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI)
|
||||
|
|
@ -460,7 +467,6 @@ class PlayerView @JvmOverloads constructor(
|
|||
player.releaseCallbacks()
|
||||
player = CS3IPlayer()
|
||||
|
||||
playerEventListener = null
|
||||
// keyEventListener is deregistered in onPause so that the incoming player's
|
||||
// onResume can register its own listener without racing against release().
|
||||
|
||||
|
|
@ -614,9 +620,10 @@ class PlayerView @JvmOverloads constructor(
|
|||
|
||||
/** Error handling */
|
||||
|
||||
@MainThread
|
||||
fun playerError(exception: Throwable) {
|
||||
fun showErrorToast(message: String, gotoNext: Boolean = false) {
|
||||
if (gotoNext && callbacks?.hasNextMirror() == true) {
|
||||
fun showErrorToast(message: String) {
|
||||
if (callbacks?.hasNextMirror() == true) {
|
||||
showToast(message, Toast.LENGTH_SHORT)
|
||||
callbacks?.nextMirror()
|
||||
} else {
|
||||
|
|
@ -638,7 +645,7 @@ class PlayerView @JvmOverloads constructor(
|
|||
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
|
||||
PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED ->
|
||||
showErrorToast("${context.getString(R.string.source_error)}\n$errorName ($code)\n$msg", gotoNext = true)
|
||||
showErrorToast("${context.getString(R.string.source_error)}\n$errorName ($code)\n$msg")
|
||||
|
||||
PlaybackException.ERROR_CODE_REMOTE_ERROR,
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
|
||||
|
|
@ -646,7 +653,7 @@ class PlayerView @JvmOverloads constructor(
|
|||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
|
||||
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE ->
|
||||
showErrorToast("${context.getString(R.string.remote_error)}\n$errorName ($code)\n$msg", gotoNext = true)
|
||||
showErrorToast("${context.getString(R.string.remote_error)}\n$errorName ($code)\n$msg")
|
||||
|
||||
PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED,
|
||||
PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER,
|
||||
|
|
@ -654,43 +661,31 @@ class PlayerView @JvmOverloads constructor(
|
|||
PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED,
|
||||
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED,
|
||||
PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED ->
|
||||
showErrorToast("${context.getString(R.string.render_error)}\n$errorName ($code)\n$msg", gotoNext = true)
|
||||
showErrorToast("${context.getString(R.string.render_error)}\n$errorName ($code)\n$msg")
|
||||
|
||||
PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED,
|
||||
PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES ->
|
||||
showErrorToast("${context.getString(R.string.unsupported_error)}\n$errorName ($code)\n$msg", gotoNext = true)
|
||||
showErrorToast("${context.getString(R.string.unsupported_error)}\n$errorName ($code)\n$msg")
|
||||
|
||||
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
|
||||
PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED ->
|
||||
showErrorToast("${context.getString(R.string.encoding_error)}\n$errorName ($code)\n$msg", gotoNext = true)
|
||||
showErrorToast("${context.getString(R.string.encoding_error)}\n$errorName ($code)\n$msg")
|
||||
|
||||
else ->
|
||||
showErrorToast("${context.getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg", gotoNext = false)
|
||||
showErrorToast("${context.getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg")
|
||||
}
|
||||
}
|
||||
|
||||
is InvalidFileException ->
|
||||
showErrorToast("${context.getString(R.string.source_error)}\n${exception.message}", gotoNext = true)
|
||||
|
||||
is SocketTimeoutException -> {
|
||||
/**
|
||||
* Ensures this is run on the UI thread to prevent issues
|
||||
* caused by SocketTimeoutException in torrents. Running
|
||||
* on another thread can break player interactions or
|
||||
* prevent switching to the next source.
|
||||
*/
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
showErrorToast("${context.getString(R.string.remote_error)}\n${exception.message}", gotoNext = true)
|
||||
}
|
||||
}
|
||||
is SocketTimeoutException ->
|
||||
showErrorToast("${context.getString(R.string.remote_error)}\n${exception.message}")
|
||||
|
||||
is ErrorLoadingException ->
|
||||
exception.message?.let { showErrorToast(it, gotoNext = true) }
|
||||
?: showErrorToast(exception.toString(), gotoNext = true)
|
||||
exception.message?.let { showErrorToast(it) }
|
||||
?: showErrorToast(exception.toString())
|
||||
|
||||
else ->
|
||||
exception.message?.let { showErrorToast(it, gotoNext = false) }
|
||||
?: showErrorToast(exception.toString(), gotoNext = false)
|
||||
exception.message?.let { showErrorToast(it) }
|
||||
?: showErrorToast(exception.toString())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -729,8 +724,7 @@ class PlayerView @JvmOverloads constructor(
|
|||
if (isLayout(TV or EMULATOR)) return ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
return if (autoPlayerRotateEnabled && isVerticalOrientation)
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||
else
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
else ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
}
|
||||
|
||||
/** Event dispatch */
|
||||
|
|
@ -741,6 +735,7 @@ class PlayerView @JvmOverloads constructor(
|
|||
* and returning early WON'T stop it from changing in e.g. the player time
|
||||
* or pause status.
|
||||
*/
|
||||
@MainThread
|
||||
fun mainCallback(event: PlayerEvent) {
|
||||
// We don't want to spam DownloadEvent.
|
||||
if (event !is DownloadEvent) Log.i(TAG, "Handle event: $event")
|
||||
|
|
@ -761,11 +756,7 @@ class PlayerView @JvmOverloads constructor(
|
|||
is TimestampInvokedEvent -> callbacks?.onTimestamp(event.timestamp)
|
||||
is TracksChangedEvent -> callbacks?.onTracksInfoChanged()
|
||||
is EmbeddedSubtitlesFetchedEvent -> callbacks?.embeddedSubtitlesFetched(event.tracks)
|
||||
is ErrorEvent -> {
|
||||
val cb = callbacks
|
||||
if (cb != null) cb.playerError(event.error)
|
||||
else playerError(event.error)
|
||||
}
|
||||
is ErrorEvent -> callbacks?.playerError(event.error) ?: playerError(event.error)
|
||||
is RequestAudioFocusEvent -> requestAudioFocus()
|
||||
is EpisodeSeekEvent -> when (event.offset) {
|
||||
-1 -> callbacks?.prevEpisode()
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
|||
import com.lagradost.cloudstream3.utils.AppContextUtils.html
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
data class Cache(
|
||||
val linkCache: MutableSet<ExtractorLink>,
|
||||
|
|
@ -23,9 +23,8 @@ data class Cache(
|
|||
|
||||
class RepoLinkGenerator(
|
||||
episodes: List<ResultEpisode>,
|
||||
currentIndex: Int = 0,
|
||||
val page: LoadResponse? = null,
|
||||
) : VideoGenerator<ResultEpisode>(episodes, currentIndex) {
|
||||
) : VideoGenerator<ResultEpisode>(episodes) {
|
||||
companion object {
|
||||
const val TAG = "RepoLink"
|
||||
val cache: HashMap<Pair<String, Int>, Cache> =
|
||||
|
|
@ -34,6 +33,7 @@ class RepoLinkGenerator(
|
|||
|
||||
override val hasCache = true
|
||||
override val canSkipLoading = true
|
||||
override fun getId(index: Int): Int? = videos.getOrNull(index)?.id
|
||||
|
||||
// this is a simple array that is used to instantly load links if they are already loaded
|
||||
//var linkCache = Array<Set<ExtractorLink>>(size = episodes.size, init = { setOf() })
|
||||
|
|
@ -48,7 +48,7 @@ class RepoLinkGenerator(
|
|||
offset: Int,
|
||||
isCasting: Boolean,
|
||||
): Boolean {
|
||||
val current = getCurrent(offset) ?: return false
|
||||
val current = videos.getOrNull(offset) ?: return false
|
||||
|
||||
val currentCache = synchronized(cache) {
|
||||
cache[current.apiName to current.id] ?: Cache(
|
||||
|
|
@ -61,10 +61,12 @@ class RepoLinkGenerator(
|
|||
}
|
||||
}
|
||||
|
||||
// these act as a general filter to prevent duplication of links or names
|
||||
val currentLinksUrls = mutableSetOf<String>() // makes all urls unique
|
||||
val currentSubsUrls = mutableSetOf<String>() // makes all subs urls unique
|
||||
val lastCountedSuffix = mutableMapOf<String, UInt>()
|
||||
// These act as a general filter to prevent duplication of links or names
|
||||
// Avoid any possible ConcurrentModificationException
|
||||
val currentLinksUrls = ConcurrentHashMap.newKeySet<String>()
|
||||
val currentSubsUrls = ConcurrentHashMap.newKeySet<String>()
|
||||
// Use atomics as otherwise we get race conditions when incrementing, while rare it did actually happen!
|
||||
val lastCountedSuffix = ConcurrentHashMap<String, AtomicInteger>()
|
||||
|
||||
synchronized(currentCache) {
|
||||
val outdatedCache =
|
||||
|
|
@ -75,7 +77,10 @@ class RepoLinkGenerator(
|
|||
currentCache.subtitleCache.clear()
|
||||
currentCache.saturated = false
|
||||
} else if (currentCache.linkCache.isNotEmpty()) {
|
||||
Log.d(TAG, "Resumed previous loading from ${unixTime - currentCache.lastCachedTimestamp}s ago")
|
||||
Log.d(
|
||||
TAG,
|
||||
"Resumed previous loading from ${unixTime - currentCache.lastCachedTimestamp}s ago"
|
||||
)
|
||||
}
|
||||
|
||||
// call all callbacks
|
||||
|
|
@ -88,8 +93,7 @@ class RepoLinkGenerator(
|
|||
|
||||
currentCache.subtitleCache.forEach { sub ->
|
||||
currentSubsUrls.add(sub.url)
|
||||
val suffixCount = lastCountedSuffix.getOrDefault(sub.originalName, 0u) + 1u
|
||||
lastCountedSuffix[sub.originalName] = suffixCount
|
||||
lastCountedSuffix.getOrPut(sub.originalName) { AtomicInteger(0) }.incrementAndGet()
|
||||
subtitleCallback(sub)
|
||||
}
|
||||
|
||||
|
|
@ -108,17 +112,15 @@ class RepoLinkGenerator(
|
|||
subtitleCallback = { file ->
|
||||
Log.d(TAG, "Loaded SubtitleFile: $file")
|
||||
val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
|
||||
if (correctFile.url.isBlank() || currentSubsUrls.contains(correctFile.url)) {
|
||||
if (correctFile.url.isBlank() || !currentSubsUrls.add(correctFile.url)) {
|
||||
return@loadLinks
|
||||
}
|
||||
currentSubsUrls.add(correctFile.url)
|
||||
|
||||
// this part makes sure that all names are unique for UX
|
||||
|
||||
val nameDecoded = correctFile.originalName.html().toString().trim() // `%3Ch1%3Esub%20name…` → `<h1>sub name…` → `sub name…`
|
||||
|
||||
val suffixCount = lastCountedSuffix.getOrDefault(nameDecoded, 0u) +1u
|
||||
lastCountedSuffix[nameDecoded] = suffixCount
|
||||
val nameDecoded = correctFile.originalName.html().toString()
|
||||
.trim() // `%3Ch1%3Esub%20name…` → `<h1>sub name…` → `sub name…`
|
||||
val suffixCount =
|
||||
lastCountedSuffix.getOrPut(nameDecoded) { AtomicInteger(0) }.incrementAndGet()
|
||||
|
||||
val updatedFile =
|
||||
correctFile.copy(originalName = nameDecoded, nameSuffix = "$suffixCount")
|
||||
|
|
@ -132,10 +134,9 @@ class RepoLinkGenerator(
|
|||
},
|
||||
callback = { link ->
|
||||
Log.d(TAG, "Loaded ExtractorLink: $link")
|
||||
if (link.url.isBlank() || currentLinksUrls.contains(link.url)) {
|
||||
if (link.url.isBlank() || !currentLinksUrls.add(link.url)) {
|
||||
return@loadLinks
|
||||
}
|
||||
currentLinksUrls.add(link.url)
|
||||
|
||||
synchronized(currentCache) {
|
||||
if (currentCache.linkCache.add(link)) {
|
||||
|
|
|
|||
|
|
@ -559,7 +559,7 @@ class ResultFragmentTv : BaseFragment<FragmentResultTvBinding>(
|
|||
ExtractorLinkGenerator(
|
||||
extractedTrailerLinks,
|
||||
emptyList()
|
||||
)
|
||||
), 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -925,8 +925,12 @@ class ResultFragmentTv : BaseFragment<FragmentResultTvBinding>(
|
|||
resultTvComingSoon.isVisible = d.comingSoon
|
||||
|
||||
populateChips(resultTag, d.tags)
|
||||
val prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(root.context)
|
||||
val showCast = prefs.getBoolean(root.context.getString(R.string.show_cast_in_details_key), true)
|
||||
val prefs =
|
||||
androidx.preference.PreferenceManager.getDefaultSharedPreferences(root.context)
|
||||
val showCast = prefs.getBoolean(
|
||||
root.context.getString(R.string.show_cast_in_details_key),
|
||||
true
|
||||
)
|
||||
|
||||
resultCastItems.isGone = !showCast || d.actors.isNullOrEmpty()
|
||||
(resultCastItems.adapter as? ActorAdaptor)?.submitList(if (showCast) d.actors else emptyList())
|
||||
|
|
|
|||
|
|
@ -38,9 +38,8 @@ class ResultTrailerPlayer : ResultFragmentPhone() {
|
|||
|
||||
// Single-tap on empty player area: toggle controls.
|
||||
override fun onSingleTap() {
|
||||
if (!introVisible) {
|
||||
if (isShowing) uiReset() else showControls()
|
||||
}
|
||||
if (introVisible) return
|
||||
if (isShowing) uiReset() else showControls()
|
||||
}
|
||||
|
||||
private fun showControls() {
|
||||
|
|
@ -58,6 +57,19 @@ class ResultTrailerPlayer : ResultFragmentPhone() {
|
|||
|
||||
override fun onHidePlayerUI() = uiReset()
|
||||
|
||||
// When the hold-speedup gesture fires, hide controls so the video is unobstructed.
|
||||
// The speedup button show/hide and speed change are handled by PlayerView.
|
||||
override fun onHoldSpeedUp(show: Boolean) {
|
||||
if (show && isShowing) uiReset()
|
||||
}
|
||||
|
||||
override fun onGestureEnd(hadSwipe: Boolean, wasUiShowing: Boolean) {
|
||||
if (!player.getIsPlaying() && hadSwipe && wasUiShowing && !isShowing) {
|
||||
isShowing = true
|
||||
showControls()
|
||||
} else playerHostView?.scheduleAutoHide()
|
||||
}
|
||||
|
||||
override fun nextEpisode() {}
|
||||
override fun prevEpisode() {}
|
||||
override fun playerPositionChanged(position: Long, duration: Long) {}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
|
|
@ -10,24 +11,50 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.actions.AlwaysAskAction
|
||||
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
|
||||
import com.lagradost.cloudstream3.APIHolder
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.ActorData
|
||||
import com.lagradost.cloudstream3.AnimeLoadResponse
|
||||
import com.lagradost.cloudstream3.CloudStreamApp.Companion.context
|
||||
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.EpisodeResponse
|
||||
import com.lagradost.cloudstream3.IDownloadableMinimum
|
||||
import com.lagradost.cloudstream3.LiveStreamLoadResponse
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getKitsuId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.MovieLoadResponse
|
||||
import com.lagradost.cloudstream3.ProviderType
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.Score
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.SeasonData
|
||||
import com.lagradost.cloudstream3.ShowStatus
|
||||
import com.lagradost.cloudstream3.SimklSyncServices
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.TorrentLoadResponse
|
||||
import com.lagradost.cloudstream3.TrackerType
|
||||
import com.lagradost.cloudstream3.TrailerData
|
||||
import com.lagradost.cloudstream3.TvSeriesLoadResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.VPNStatus
|
||||
import com.lagradost.cloudstream3.actions.AlwaysAskAction
|
||||
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.isEpisodeBased
|
||||
import com.lagradost.cloudstream3.isLiveStream
|
||||
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
|
|
@ -44,9 +71,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|||
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.LOADTYPE_ALL
|
||||
import com.lagradost.cloudstream3.ui.player.LOADTYPE_CHROMECAST
|
||||
import com.lagradost.cloudstream3.ui.player.LOADTYPE_INAPP
|
||||
|
|
@ -58,6 +83,7 @@ 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
|
||||
|
|
@ -105,22 +131,20 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
|||
import com.lagradost.cloudstream3.utils.UiText
|
||||
import com.lagradost.cloudstream3.utils.VIDEO_WATCH_STATE
|
||||
import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.sanitizeFilename
|
||||
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadEpisodeMetadata
|
||||
import com.lagradost.cloudstream3.utils.downloader.DownloadObjects
|
||||
import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
|
||||
import com.lagradost.cloudstream3.utils.downloader.DownloadUtils.downloadSubtitle
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.txt
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** This starts at 1 */
|
||||
|
|
@ -425,7 +449,7 @@ fun SelectPopup.getOptions(context: Context): List<String> {
|
|||
}
|
||||
|
||||
data class ExtractedTrailerData(
|
||||
var mirros: List<Pair<ExtractorLink,String>>,//Pair of extracted trailer link and original trailer link
|
||||
var mirros: List<Pair<ExtractorLink, String>>,//Pair of extracted trailer link and original trailer link
|
||||
var subtitles: List<SubtitleFile> = emptyList(),
|
||||
)
|
||||
|
||||
|
|
@ -456,7 +480,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
var currentRepo: APIRepository? = null
|
||||
private var currentId: Int? = null
|
||||
private var fillers: HashSet<Int> = hashSetOf()
|
||||
private var generator: IGenerator? = null
|
||||
private var generator: RepoLinkGenerator? = null
|
||||
private var preferDubStatus: DubStatus? = null
|
||||
private var preferStartEpisode: Int? = null
|
||||
private var preferStartSeason: Int? = null
|
||||
|
|
@ -1269,9 +1293,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
subs += sub
|
||||
updatePage()
|
||||
},
|
||||
isCasting = isCasting
|
||||
isCasting = isCasting,
|
||||
offset = 0
|
||||
)
|
||||
} catch (e: CancellationException) {
|
||||
} catch (_: CancellationException) {
|
||||
// Do nothing
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
|
|
@ -1300,7 +1325,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
episodeIds: Array<String>,
|
||||
watchState: VideoWatchState
|
||||
) {
|
||||
val watchStateString = DataStore.mapper.writeValueAsString(watchState)
|
||||
val watchStateString = watchState.toJson()
|
||||
episodeIds.forEach {
|
||||
if (getVideoWatchState(it.toInt()) != watchState) {
|
||||
editor.setKeyRaw(
|
||||
|
|
@ -1520,26 +1545,24 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
ACTION_PLAY_EPISODE_IN_PLAYER -> {
|
||||
val list = HashMap<String, String>(currentResponse?.syncData ?: emptyMap())
|
||||
val generator = generator ?: return
|
||||
|
||||
// I know kinda shit to iterate all, but it is 100% sure to work
|
||||
val index = generator.videos.indexOfFirst { value -> value.id == click.data.id }
|
||||
|
||||
generator?.also {
|
||||
it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work
|
||||
?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id }
|
||||
?.let { index ->
|
||||
if (index >= 0)
|
||||
it.goto(index)
|
||||
}
|
||||
}
|
||||
if (currentResponse?.type == TvType.CustomMedia) {
|
||||
generator?.generateLinks(
|
||||
generator.generateLinks(
|
||||
offset = index,
|
||||
clearCache = true,
|
||||
LOADTYPE_ALL,
|
||||
isCasting = false,
|
||||
sourceTypes = LOADTYPE_ALL,
|
||||
callback = {},
|
||||
subtitleCallback = {})
|
||||
} else {
|
||||
activity?.navigate(
|
||||
R.id.global_to_navigation_player,
|
||||
GeneratorPlayer.newInstance(
|
||||
generator ?: return, list
|
||||
generator, index,list
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -1663,14 +1686,13 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
val realRecommendations = ArrayList<SearchResponse>()
|
||||
val apiNames = synchronized(apis) {
|
||||
apis.filter {
|
||||
it.name.contains("gogoanime", true) ||
|
||||
it.name.contains("9anime", true)
|
||||
}.map {
|
||||
it.name
|
||||
}
|
||||
val apiNames = 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))
|
||||
|
|
@ -1809,11 +1831,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
|
||||
private suspend fun updateFillers(data : LoadResponse) {
|
||||
fillers =
|
||||
withContext(Dispatchers.IO) {
|
||||
safe { FillerEpisodeCheck.getFillerEpisodes(data) }
|
||||
} ?: hashSetOf()
|
||||
private suspend fun updateFillers(data: LoadResponse) {
|
||||
fillers = ioWorkSafe {
|
||||
FillerEpisodeCheck.getFillerEpisodes(data)
|
||||
} ?: hashSetOf()
|
||||
}
|
||||
|
||||
fun changeDubStatus(status: DubStatus) {
|
||||
|
|
@ -2432,26 +2453,34 @@ class ResultViewModel2 : ViewModel() {
|
|||
loadResponse.trailers.windowed(limit, limit, true).takeWhile { list ->
|
||||
list.amap { trailerData ->
|
||||
try {
|
||||
val links = arrayListOf<Pair<ExtractorLink,String>>()
|
||||
val links = arrayListOf<Pair<ExtractorLink, String>>()
|
||||
val subs = arrayListOf<SubtitleFile>()
|
||||
if (!loadExtractor(
|
||||
trailerData.extractorUrl,
|
||||
trailerData.referer,
|
||||
{ subs.add(it) },
|
||||
{ links.add(Pair(it,trailerData.extractorUrl))}) && trailerData.raw
|
||||
{
|
||||
links.add(
|
||||
Pair(
|
||||
it,
|
||||
trailerData.extractorUrl
|
||||
)
|
||||
)
|
||||
}) && trailerData.raw
|
||||
) {
|
||||
arrayListOf(
|
||||
Pair(
|
||||
newExtractorLink(
|
||||
"",
|
||||
"Trailer",
|
||||
trailerData.extractorUrl,
|
||||
type = INFER_TYPE
|
||||
) {
|
||||
this.referer = trailerData.referer ?: ""
|
||||
this.quality = Qualities.Unknown.value
|
||||
this.headers = trailerData.headers
|
||||
},trailerData.extractorUrl)
|
||||
"",
|
||||
"Trailer",
|
||||
trailerData.extractorUrl,
|
||||
type = INFER_TYPE
|
||||
) {
|
||||
this.referer = trailerData.referer ?: ""
|
||||
this.quality = Qualities.Unknown.value
|
||||
this.headers = trailerData.headers
|
||||
}, trailerData.extractorUrl
|
||||
)
|
||||
) to arrayListOf()
|
||||
} else {
|
||||
links to subs
|
||||
|
|
@ -2677,4 +2706,4 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() {
|
|||
|
||||
private var suggestionJob: Job? = null
|
||||
|
||||
private var repos = synchronized(apis) { apis.map { APIRepository(it) } }
|
||||
private var repos = apis.withLock { 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 = synchronized(apis) { apis.map { APIRepository(it) } }
|
||||
repos = apis.withLock { apis.map { APIRepository(it) } }
|
||||
}
|
||||
|
||||
fun searchAndCancel(
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
fun showAdd() {
|
||||
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
|
||||
val providers = allProviders.distinctBy { it::class }.sortedBy { it.name }
|
||||
activity?.showDialog(
|
||||
providers.map { "${it.name} (${it.mainUrl})" },
|
||||
-1,
|
||||
|
|
|
|||
|
|
@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() {
|
|||
|
||||
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
|
||||
activity?.getApiProviderLangSettings()?.let { currentLangTags ->
|
||||
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 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 currentIndexList = currentLangTags.map { langTag ->
|
||||
|
|
|
|||
|
|
@ -119,13 +119,14 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
}, { repo ->
|
||||
// Prompt user before deleting repo
|
||||
main {
|
||||
val builder = AlertDialog.Builder(context ?: binding.root.context)
|
||||
val uiContext = context ?: binding.root.context
|
||||
val builder = AlertDialog.Builder(uiContext)
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
ioSafe {
|
||||
RepositoryManager.removeRepository(binding.root.context, repo)
|
||||
RepositoryManager.removeRepository(uiContext.applicationContext, repo)
|
||||
extensionViewModel.loadStats()
|
||||
extensionViewModel.loadRepositories()
|
||||
}
|
||||
|
|
@ -136,9 +137,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
}
|
||||
|
||||
builder.setTitle(R.string.delete_repository)
|
||||
.setMessage(
|
||||
context?.getString(R.string.delete_repository_plugins)
|
||||
)
|
||||
.setMessage(uiContext.getString(R.string.delete_repository_plugins))
|
||||
.setPositiveButton(R.string.delete, dialogClickListener)
|
||||
.setNegativeButton(R.string.cancel, dialogClickListener)
|
||||
.show().setDefaultFocus()
|
||||
|
|
@ -210,9 +209,9 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
|
||||
binding.applyBtt.setOnClickListener secondListener@{
|
||||
val name = binding.repoNameInput.text?.toString()
|
||||
val urlInput = binding.repoUrlInput.text?.toString()
|
||||
ioSafe {
|
||||
val url = binding.repoUrlInput.text?.toString()
|
||||
?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
|
||||
val url = urlInput?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
|
||||
if (url.isNullOrBlank()) {
|
||||
main {
|
||||
showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
||||
|
|
|
|||
|
|
@ -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 me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import com.lagradost.cloudstream3.utils.Levenshtein
|
||||
import java.io.File
|
||||
|
||||
// String => repository url
|
||||
|
|
@ -246,7 +246,7 @@ class PluginsViewModel : ViewModel() {
|
|||
this.sortedBy { it.plugin.second.name }
|
||||
} else {
|
||||
this.sortedBy {
|
||||
-FuzzySearch.partialRatio(
|
||||
-Levenshtein.partialRatio(
|
||||
it.plugin.second.name.lowercase(),
|
||||
query.lowercase()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class TestFragment : BaseFragment<FragmentTestingBinding>(
|
|||
providerTest.setProgress(passed, failed, total)
|
||||
}
|
||||
|
||||
observeNullable(testViewModel.providerResults) {
|
||||
observe(testViewModel.providerResults) {
|
||||
safe {
|
||||
val newItems = it.sortedBy { api -> api.first.name }
|
||||
(providerTestRecyclerView.adapter as? TestResultAdapter)?.submitList(
|
||||
|
|
|
|||
|
|
@ -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.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
|
||||
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 = threadSafeListOf<Pair<MainAPI, TestingUtils.TestResultProvider>>()
|
||||
private val providers = atomicListOf<Pair<MainAPI, TestingUtils.TestResultProvider>>()
|
||||
private var passed = 0
|
||||
private var failed = 0
|
||||
private var total = 0
|
||||
|
|
@ -51,9 +51,9 @@ class TestViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private fun postProviders() {
|
||||
synchronized(providers) {
|
||||
providers.withLock {
|
||||
val filtered = when (filter) {
|
||||
ProviderFilter.All -> providers
|
||||
ProviderFilter.All -> providers.toList()
|
||||
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) {
|
||||
synchronized(providers) {
|
||||
providers.withLock {
|
||||
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 = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
|
||||
total = APIHolder.allProviders.withLock { APIHolder.allProviders.size }
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
fun startTest() {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
|
||||
val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() }
|
||||
total = apis.size
|
||||
failed = 0
|
||||
passed = 0
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
|
|||
if (isSetup)
|
||||
if (
|
||||
// If any available languages
|
||||
synchronized(apis) { apis.distinctBy { it.lang }.size > 1 }
|
||||
apis.distinctBy { it.lang }.size > 1
|
||||
) {
|
||||
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment<FragmentSetupProviderLanguage
|
|||
|
||||
val currentLangTags = ctx.getApiProviderLangSettings()
|
||||
|
||||
val languagesTagName = synchronized(APIHolder.apis) {
|
||||
listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) +
|
||||
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() } // name ignoring flag emoji
|
||||
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
|
||||
}
|
||||
|
||||
val currentIndexList = currentLangTags.map { langTag ->
|
||||
|
|
|
|||
|
|
@ -369,28 +369,10 @@ object AppContextUtils {
|
|||
}
|
||||
|
||||
fun Context.getApiSettings(): HashSet<String> {
|
||||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
val hashSet = HashSet<String>()
|
||||
val activeLangs = getApiProviderLangSettings()
|
||||
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
||||
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
|
||||
.map { it.name })
|
||||
|
||||
/*val set = settingsManager.getStringSet(
|
||||
this.getString(R.string.search_providers_list_key),
|
||||
hashSet
|
||||
)?.toHashSet() ?: hashSet
|
||||
|
||||
val list = HashSet<String>()
|
||||
for (name in set) {
|
||||
val api = getApiFromNameNull(name) ?: continue
|
||||
if (activeLangs.contains(api.lang)) {
|
||||
list.add(name)
|
||||
}
|
||||
}*/
|
||||
//if (list.isEmpty()) return hashSet
|
||||
//return list
|
||||
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name })
|
||||
return hashSet
|
||||
}
|
||||
|
||||
|
|
@ -481,9 +463,7 @@ object AppContextUtils {
|
|||
} ?: default
|
||||
val langs = this.getApiProviderLangSettings()
|
||||
val hasUniversal = langs.contains(AllLanguagesName)
|
||||
val allApis = synchronized(apis) {
|
||||
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
|
||||
}
|
||||
val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
|
||||
return if (currentPrefMedia.isEmpty()) {
|
||||
allApis
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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
|
||||
|
|
@ -21,11 +20,12 @@ 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,9 +133,7 @@ object BackupUtils {
|
|||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun getBackup(context: Context?): BackupFile? {
|
||||
if (context == null) return null
|
||||
|
||||
private fun getBackup(context: Context): BackupFile {
|
||||
val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
|
||||
|
|
@ -214,7 +212,7 @@ object BackupUtils {
|
|||
|
||||
fileStream = stream.openNew()
|
||||
printStream = PrintWriter(fileStream)
|
||||
printStream.print(mapper.writeValueAsString(backupFile))
|
||||
printStream.print(backupFile.toJson())
|
||||
|
||||
showToast(
|
||||
R.string.backup_success,
|
||||
|
|
@ -259,8 +257,8 @@ object BackupUtils {
|
|||
val input = activity.contentResolver.openInputStream(uri)
|
||||
?: return@ioSafe
|
||||
|
||||
val restoredValue =
|
||||
mapper.readValue<BackupFile>(input)
|
||||
val text = input.bufferedReader().readText()
|
||||
val restoredValue = parseJson<BackupFile>(text)
|
||||
|
||||
restore(
|
||||
activity,
|
||||
|
|
|
|||
|
|
@ -2,17 +2,16 @@ 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"
|
||||
|
|
@ -88,8 +87,18 @@ data class Editor(
|
|||
}
|
||||
|
||||
object DataStore {
|
||||
val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
|
||||
// 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
|
||||
|
||||
private fun getPreferences(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
|
|
@ -99,7 +108,6 @@ object DataStore {
|
|||
return getPreferences(this)
|
||||
}
|
||||
|
||||
|
||||
fun getFolderName(folder: String, path: String): String {
|
||||
return "${folder}/${path}"
|
||||
}
|
||||
|
|
@ -165,17 +173,17 @@ object DataStore {
|
|||
fun <T> Context.setKey(path: String, value: T) {
|
||||
try {
|
||||
getSharedPrefs().edit {
|
||||
putString(path, mapper.writeValueAsString(value))
|
||||
putString(path, value?.toJsonLiteral())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Context.getKey(path: String, valueType: Class<T>): T? {
|
||||
fun <T : Any> Context.getKey(path: String, valueType: Class<T>): T? {
|
||||
try {
|
||||
val json: String = getSharedPrefs().getString(path, null) ?: return null
|
||||
return json.toKotlinObject(valueType)
|
||||
return parseJson(json, valueType.kotlin)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -186,11 +194,11 @@ object DataStore {
|
|||
}
|
||||
|
||||
inline fun <reified T : Any> String.toKotlinObject(): T {
|
||||
return mapper.readValue(this, T::class.java)
|
||||
return parseJson(this)
|
||||
}
|
||||
|
||||
fun <T> String.toKotlinObject(valueType: Class<T>): T {
|
||||
return mapper.readValue(this, valueType)
|
||||
fun <T : Any> String.toKotlinObject(valueType: Class<T>): T {
|
||||
return parseJson(this, valueType.kotlin)
|
||||
}
|
||||
|
||||
// GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR
|
||||
|
|
@ -214,4 +222,4 @@ object DataStore {
|
|||
inline fun <reified T : Any> Context.getKey(folder: String, path: String, defVal: T?): T? {
|
||||
return getKey(getFolderName(folder, path), defVal) ?: defVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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
|
||||
|
|
@ -11,6 +12,7 @@ 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
|
||||
|
|
@ -22,82 +24,86 @@ 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 = ImageLoader.Builder(context)
|
||||
internal fun buildImageLoader(context: PlatformContext): ImageLoader {
|
||||
val isBrokenHardware = hasPotentialBrokenHardware()
|
||||
return ImageLoader.Builder(context)
|
||||
.crossfade(200)
|
||||
.allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder
|
||||
.allowHardware(SDK_INT >= 28 && !isBrokenHardware)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
.networkCachePolicy(CachePolicy.ENABLED)
|
||||
.memoryCache {
|
||||
MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching
|
||||
MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache
|
||||
.strongReferencesEnabled(false)
|
||||
.build()
|
||||
}
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath())
|
||||
.maxSizeBytes(512L * 1024 * 1024) // 512 MB
|
||||
.maxSizePercent(0.04) // Use 4 % of the device's storage space for disk caching
|
||||
.maxSizePercent(0.04) // max 4% of storage 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) })) }
|
||||
.also {
|
||||
it.setupCoilLogger()
|
||||
Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.")
|
||||
.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()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/** Use DebugLogger on debug builds which won't slow down release builds & use EventListener for
|
||||
/** 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, "Error loading image: ${result.throwable}")
|
||||
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.d(TAG, "setupCoilLogger: Activated EVENT_LISTENER FOR COIL")
|
||||
}
|
||||
}
|
||||
|
||||
/** we use coil's built in loader with our global synchronized instance, this way we achieve
|
||||
latest and complete functionality as well as stability **/
|
||||
/** coil's built in loader attached w/ global synchronized instance **/
|
||||
private fun ImageView.loadImageInternal(
|
||||
imageData: Any?,
|
||||
headers: Map<String, String>? = null,
|
||||
builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations
|
||||
) {
|
||||
// clear image to avoid loading & flickering issue at fast scrolling (e.g, an image recycler)
|
||||
// clear image to avoid loading & flickering issue at fast scrolling (~recycler view/lazy column)
|
||||
this.dispose()
|
||||
|
||||
if(imageData == null) return // Just in case
|
||||
|
||||
if (imageData == null) return
|
||||
// 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
|
||||
}
|
||||
|
||||
// Use Coil's built-in load method but with our custom module & a decent USER-AGENT always
|
||||
// which can be overridden by extensions.
|
||||
// headers can be overridden by extensions.
|
||||
this.load(imageData, SingletonImageLoader.get(context)) {
|
||||
this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder ->
|
||||
headerBuilder["User-Agent"] = USER_AGENT
|
||||
|
|
@ -105,11 +111,22 @@ 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?,
|
||||
|
|
@ -138,12 +155,6 @@ object ImageLoader {
|
|||
builder: ImageRequest.Builder.() -> Unit = {}
|
||||
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder)
|
||||
|
||||
fun ImageView.loadImage(
|
||||
imageData: HttpUrl?,
|
||||
headers: Map<String, String>? = null,
|
||||
builder: ImageRequest.Builder.() -> Unit = {}
|
||||
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder)
|
||||
|
||||
fun ImageView.loadImage(
|
||||
imageData: File?,
|
||||
builder: ImageRequest.Builder.() -> Unit = {}
|
||||
|
|
@ -173,4 +184,4 @@ object ImageLoader {
|
|||
imageData: ByteBuffer?,
|
||||
builder: ImageRequest.Builder.() -> Unit = {}
|
||||
) = loadImageInternal(imageData = imageData, builder = builder)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<GithubRelease>>(
|
||||
val response = parseJson<Array<GithubRelease>>(
|
||||
app.get(url, headers = headers).text
|
||||
)
|
||||
).toList()
|
||||
|
||||
val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""")
|
||||
val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""")
|
||||
|
|
@ -103,9 +103,7 @@ 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -150,9 +148,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<List<GithubRelease>>(
|
||||
val response = parseJson<Array<GithubRelease>>(
|
||||
app.get(releaseUrl, headers = headers).text
|
||||
)
|
||||
).toList()
|
||||
|
||||
val found = response.lastOrNull { rel ->
|
||||
rel.prerelease || rel.tagName == "pre-release"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
|||
//import com.lagradost.cloudstream3.animeproviders.AniflixProvider
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
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 = parseJson<MalSyncPage?>(response)
|
||||
val mapped = tryParseJson<MalSyncPage?>(response)
|
||||
|
||||
val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId
|
||||
val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id
|
||||
|
|
@ -96,10 +96,8 @@ object SyncUtil {
|
|||
.mapNotNull { it.url }.toMutableList()
|
||||
|
||||
if (type == "anilist") { // TODO MAKE BETTER
|
||||
synchronized(apis) {
|
||||
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
||||
current.add("${it.mainUrl}/anime/$id")
|
||||
}
|
||||
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
||||
current.add("${it.mainUrl}/anime/$id")
|
||||
}
|
||||
}
|
||||
return current
|
||||
|
|
@ -169,4 +167,4 @@ object SyncUtil {
|
|||
@JsonProperty("updatedAt") val updatedAt: String?,
|
||||
@JsonProperty("deletedAt") val deletedAt: String?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +48,10 @@ 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<SearchResponse>) : TestResult(true)
|
||||
class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true)
|
||||
|
|
@ -87,7 +91,7 @@ object TestingUtils {
|
|||
} catch (e: Throwable) {
|
||||
when (e) {
|
||||
is NotImplementedError -> {
|
||||
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
||||
fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
||||
}
|
||||
|
||||
is CancellationException -> {
|
||||
|
|
@ -115,7 +119,7 @@ object TestingUtils {
|
|||
api.search(query, 1)?.items?.takeIf { it.isNotEmpty() }
|
||||
} catch (e: Throwable) {
|
||||
if (e is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented search()")
|
||||
fail("Provider has not implemented search()")
|
||||
} else if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
|
|
@ -125,7 +129,7 @@ object TestingUtils {
|
|||
}
|
||||
|
||||
return if (searchResults.isNullOrEmpty()) {
|
||||
Assert.fail("Api ${api.name} did not return any search responses")
|
||||
fail("Api ${api.name} did not return any search responses")
|
||||
TestResult.Fail // Should not be reached
|
||||
} else {
|
||||
TestResultList(searchResults)
|
||||
|
|
@ -216,7 +220,7 @@ object TestingUtils {
|
|||
// return TestResult(validResults)
|
||||
} catch (e: Throwable) {
|
||||
if (e is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented load()")
|
||||
fail("Provider has not implemented load()")
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
|
@ -228,14 +232,14 @@ object TestingUtils {
|
|||
url: String?,
|
||||
logger: Logger
|
||||
): TestResult {
|
||||
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
|
||||
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}")
|
||||
Assert.assertTrue(
|
||||
assertTrue(
|
||||
"Api ${api.name} returns link with invalid url ${link.url}",
|
||||
link.url.length > 4
|
||||
)
|
||||
|
|
@ -245,12 +249,12 @@ object TestingUtils {
|
|||
logger.log("Links loaded: $linksLoaded")
|
||||
return TestResult(linksLoaded > 0)
|
||||
} else {
|
||||
Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded")
|
||||
fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
when (e) {
|
||||
is NotImplementedError -> {
|
||||
Assert.fail("Provider has not implemented loadLinks()")
|
||||
fail("Provider has not implemented loadLinks()")
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
|
@ -276,7 +280,7 @@ object TestingUtils {
|
|||
|
||||
// Test Homepage
|
||||
val homepage = testHomepage(api, logger)
|
||||
Assert.assertTrue("Homepage failed to load", homepage.success)
|
||||
assertTrue("Homepage failed to load", homepage.success)
|
||||
val homePageList = (homepage as? TestResultList)?.results ?: emptyList()
|
||||
|
||||
// Test Search Results
|
||||
|
|
@ -287,7 +291,7 @@ object TestingUtils {
|
|||
listOf("over", "iron", "guy")).take(3)
|
||||
|
||||
val searchResults = testSearch(api, searchQueries, logger)
|
||||
Assert.assertTrue("Failed to get search results", searchResults.success)
|
||||
assertTrue("Failed to get search results", searchResults.success)
|
||||
searchResults as TestResultList
|
||||
|
||||
// Test Load and LoadLinks
|
||||
|
|
@ -321,4 +325,4 @@ object TestingUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -804,6 +804,7 @@ 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
|
||||
|
|
@ -822,7 +823,6 @@ object VideoDownloadManager {
|
|||
)
|
||||
val requestStream = request.body.byteStream()
|
||||
|
||||
val buffer = ByteArray(bufferSize)
|
||||
var read: Int
|
||||
|
||||
try {
|
||||
|
|
@ -853,6 +853,7 @@ 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
|
||||
|
|
@ -861,7 +862,7 @@ object VideoDownloadManager {
|
|||
for (i in 0 until retries) {
|
||||
try {
|
||||
// in case
|
||||
start = resolve(start, end, callback)
|
||||
start = resolve(start, end, buffer, 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
|
||||
|
|
@ -1158,13 +1159,29 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
// this will take up the first available job and resolve
|
||||
// Reuse a download buffer to decrease unnecessary alloc
|
||||
val buffer = ByteArray(items.bufferSize)
|
||||
|
||||
// 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
|
||||
|
|
@ -1175,7 +1192,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, callback = callback)) {
|
||||
if (!items.resolveSafe(index, buffer = buffer, callback = callback)) {
|
||||
fileMutex.withLock {
|
||||
if (metadata.type != DownloadType.IsStopped) {
|
||||
metadata.type = DownloadType.IsFailed
|
||||
|
|
@ -1333,10 +1350,23 @@ 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
|
||||
|
|
@ -2000,6 +2030,8 @@ object VideoDownloadManager {
|
|||
|
||||
linkLoadingJob = ioSafe {
|
||||
generator.generateLinks(
|
||||
offset = 0,
|
||||
isCasting = false,
|
||||
clearCache = false,
|
||||
sourceTypes = LOADTYPE_INAPP_DOWNLOAD,
|
||||
callback = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.lagradost.cloudstream3.utils.serializers
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
/**
|
||||
* Custom KSerializer for Android's [Uri] type.
|
||||
*
|
||||
* Uri is an Android platform type and cannot be annotated with @Serializable directly.
|
||||
* Registering it in a SerializersModule globally would require a custom module passed to
|
||||
* every Json instance, which adds hidden coupling. This serializer is also used sparingly
|
||||
* across the codebase, so the overhead of a global registration isn't justified.
|
||||
* Instead, we keep it explicit so that each usage site opts in intentionally and the
|
||||
* serialization behavior remains visible.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* @Serializable
|
||||
* data class MyData(
|
||||
* @Serializable(with = UriSerializer::class)
|
||||
* val uri: Uri,
|
||||
* )
|
||||
*/
|
||||
object UriSerializer : KSerializer<Uri> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Uri) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Uri {
|
||||
return Uri.parse(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -12,17 +12,18 @@
|
|||
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"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/player_metadata_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="64dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingBottom="32dp">
|
||||
|
|
@ -39,23 +40,23 @@
|
|||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitStart"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/player_movie_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowDx="2"
|
||||
android:shadowDy="2"
|
||||
android:shadowRadius="4"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
tools:text="Zootopia 2"/>
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Zootopia 2" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- GENRES / YEAR / RATING -->
|
||||
|
|
@ -64,10 +65,10 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:maxLines="2"
|
||||
android:textColor="#B3FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:maxLines="2"
|
||||
tools:text="Animation • Action • Adventure • 2025 • ⭐ 7.6"/>
|
||||
tools:text="Animation • Action • Adventure • 2025 • ⭐ 7.6" />
|
||||
|
||||
<!-- SYNOPSIS -->
|
||||
<TextView
|
||||
|
|
@ -75,23 +76,24 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:textColor="#E6FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:lineSpacingExtra="8dp"
|
||||
android:maxLines="5"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowDx="2"
|
||||
android:shadowDy="2"
|
||||
android:shadowRadius="4"
|
||||
android:maxLines="5"
|
||||
tools:text="Brave rabbit cop Judy Hopps..."/>
|
||||
android:textColor="#E6FFFFFF"
|
||||
android:textSize="16sp"
|
||||
tools:text="Brave rabbit cop Judy Hopps..." />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/video_outline"
|
||||
android:visibility="gone"
|
||||
android:src="@drawable/video_outline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/video_outline"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
|
||||
<ProgressBar
|
||||
android:id="@+id/player_progressbar_left_level1"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
android:layout_width="5dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_centerInParent="true"
|
||||
|
|
@ -183,11 +186,11 @@
|
|||
android:progressDrawable="@drawable/progress_drawable_vertical"
|
||||
android:progressTint="@color/white"
|
||||
android:progressTintMode="src_in"
|
||||
tools:progress="30"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal" />
|
||||
tools:progress="30" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/player_progressbar_left_level2"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
android:layout_width="5dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_centerInParent="true"
|
||||
|
|
@ -199,8 +202,7 @@
|
|||
android:progressDrawable="@drawable/progress_drawable_vertical"
|
||||
android:progressTint="@color/colorPrimaryOrange"
|
||||
android:progressTintMode="src_in"
|
||||
tools:progress="0"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal" />
|
||||
tools:progress="0" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
|
|
@ -322,23 +324,18 @@
|
|||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/skip_chapter_button"
|
||||
style="@style/NiceButton"
|
||||
style="@style/VideoButtonTV"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="100dp"
|
||||
android:backgroundTint="@color/skipOpTransparent"
|
||||
android:maxLines="1"
|
||||
android:nextFocusLeft="@id/player_pause_play"
|
||||
android:nextFocusUp="@id/player_restart"
|
||||
android:nextFocusDown="@id/player_pause_play"
|
||||
android:padding="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="15sp"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="@dimen/rounded_button_radius"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:strokeColor="@color/white"
|
||||
app:strokeWidth="1dp"
|
||||
tools:text="Skip Opening"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
|
@ -360,28 +357,30 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end">
|
||||
|
||||
<TextView
|
||||
android:maxLines="2"
|
||||
android:id="@+id/player_video_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:gravity="end"
|
||||
android:maxWidth="600dp"
|
||||
android:maxLines="2"
|
||||
android:textAlignment="viewEnd"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Hello world" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/offline_pin"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="2dp"
|
||||
android:src="@drawable/ic_offline_pin_24"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_gravity="start"/>
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
|
|
@ -400,9 +399,9 @@
|
|||
android:id="@+id/player_video_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginBottom="2.5dp"
|
||||
android:layout_gravity="end"
|
||||
android:textColor="#B3FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
|
|
@ -581,6 +580,7 @@
|
|||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/player_episodes_button_root"
|
||||
android:layout_width="60dp"
|
||||
|
|
@ -964,19 +964,18 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center|center_vertical"
|
||||
android:text="@string/player_is_live"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="50dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:text="@string/player_is_live"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
/>
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_left"
|
||||
|
|
@ -1129,11 +1128,11 @@
|
|||
android:layout_height="45dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="70dp"
|
||||
app:iconGravity="top"
|
||||
android:clickable="false"
|
||||
android:textAllCaps="false"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/speedup"
|
||||
app:iconGravity="top"
|
||||
app:iconTint="@color/textColor"
|
||||
app:rippleColor="?attr/colorPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
|
@ -1141,34 +1140,34 @@
|
|||
|
||||
<LinearLayout
|
||||
android:id="@+id/player_episode_overlay"
|
||||
android:visibility="gone"
|
||||
android:padding="5dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
android:padding="5dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/player_episode_overlay_title"
|
||||
android:padding="10dp"
|
||||
style="@style/WatchHeaderText"
|
||||
android:textSize="15sp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:text="@string/episodes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:padding="10dp"
|
||||
android:text="@string/episodes"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:id="@+id/player_episode_list"
|
||||
tools:listitem="@layout/player_episodes"
|
||||
android:nextFocusLeft="@id/player_episodes_button"
|
||||
android:layout_width="400dp"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants">
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:nextFocusLeft="@id/player_episodes_button"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/player_episodes">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@
|
|||
<string name="subs_edge_type">Rand tipe</string>
|
||||
<string name="download_done">Klaar Afgelaai</string>
|
||||
<string name="continue_watching">Kyk verder</string>
|
||||
<string name="new_update_format" formatted="true">Nuwe opdatering gevind!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Nuwe opdatering gevind! \n%1$s -> %2$s</string>
|
||||
<string name="subs_download_languages">Laai Tale af</string>
|
||||
<string name="search_provider_text_providers">Soek deur verskaffers te gebruik</string>
|
||||
<string name="go_back_img_des">Gaan terug</string>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
<string name="next_episode_time_hour_format" formatted="true">%1$dሰዓት %2$dደቂቃ</string>
|
||||
<string name="search_poster_img_des">ፖስተር</string>
|
||||
<string name="title_downloads">የወረዱ</string>
|
||||
<string name="new_update_format" formatted="true">አዲስ ማሻሻያ ተገኝቷል!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">አዲስ ማሻሻያ ተገኝቷል! \n%1$s -> %2$s</string>
|
||||
<string name="go_back_img_des">ተመለስ</string>
|
||||
<string name="episode_more_options_des">ተጨማሪ አማራጮች</string>
|
||||
<string name="type_watching">በማየት ላይ</string>
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@
|
|||
<string name="show_log_cat">فرجي الـLogcat 🐈</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="continue_watching">كفي حضر</string>
|
||||
<string name="new_update_format" formatted="true">في أپدايت جديدة!
|
||||
\n%1$s ← %2$s</string>
|
||||
<string name="new_update_format" formatted="true">في أپدايت جديدة! \n%1$s ← %2$s</string>
|
||||
<string name="subs_download_languages">نزل الترجمات مع الڤيديو</string>
|
||||
<string name="search_provider_text_providers">عوزو المصادر لَ تنبّشو</string>
|
||||
<string name="go_back_img_des">رجاع</string>
|
||||
|
|
@ -96,8 +95,7 @@
|
|||
<string name="subs_text_color">لون الكتيبة</string>
|
||||
<string name="type_completed">مخلص</string>
|
||||
<string name="use_system_brightness_settings_des">عوز قوة ضوّ الشاشة تبع السيستام بدل من تغميئ الڤيديو</string>
|
||||
<string name="restore_failed_format" formatted="true">فشل ترجيع النسخة الإحتياطية من ملف
|
||||
\n%s</string>
|
||||
<string name="restore_failed_format" formatted="true">فشل ترجيع النسخة الإحتياطية من ملف \n%s</string>
|
||||
<string name="play_trailer_button">مشّي المقطع الدعائي</string>
|
||||
<string name="play_livestream_button">مشّي البث المباشر</string>
|
||||
<string name="no_episodes_found">م لقينا ولا حلقة</string>
|
||||
|
|
@ -166,8 +164,7 @@
|
|||
<string name="no_subtitles">طفي الترجمة</string>
|
||||
<string name="synopsis">القصة</string>
|
||||
<string name="used_storage">مستعمل</string>
|
||||
<string name="resume_time_left" formatted="true">%dد
|
||||
\nباقي</string>
|
||||
<string name="resume_time_left" formatted="true">%dد \nباقي</string>
|
||||
<string name="status_ongoing">عم ينعرض حاليًا</string>
|
||||
<string name="queued">بلايحة النَطر</string>
|
||||
<string name="status">حالة</string>
|
||||
|
|
@ -194,7 +191,7 @@
|
|||
<string name="render_error">في مشكلة بجهاز العرض (Renderer error)</string>
|
||||
<string name="show_title">العِنوان</string>
|
||||
<string name="jsdelivr_proxy">پروكسي \"گِت هَب\"</string>
|
||||
<string name="limit_title_rez">جودة مشغل الڤيديو</string>
|
||||
<string name="limit_title_rez">فرجي معلومات مشغل الڤيديو</string>
|
||||
<string name="show_sub">ملصق الترجمة</string>
|
||||
<string name="ova_singular">أوڤا</string>
|
||||
<string name="episode_action_download_mirror">نَزِل من مصادر وجودات مختلفة</string>
|
||||
|
|
@ -361,11 +358,7 @@
|
|||
<string name="home_next_random_img_des">العشوائي يللي بعده</string>
|
||||
<string name="subtitles_shadow">خيال</string>
|
||||
<string name="subscription_in_progress_notification">عم نجدِد المثلثلات يللي مشتركينلها</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن:
|
||||
\n
|
||||
\n%s
|
||||
\n
|
||||
\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">مبين إنو فيه عنصر متل هيدا موجود بال مكتبة عندكن: \n \n%s \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوه مع العنصر الموجود، أو تلغو الإجراء؟</string>
|
||||
<string name="batch_download_finish_format" formatted="true">نزلت %1$d %2$s</string>
|
||||
<string name="error_invalid_id">معرف مش صالح</string>
|
||||
<string name="skip_type_format" formatted="true">أفّي %s</string>
|
||||
|
|
@ -373,9 +366,7 @@
|
|||
<string name="enter_pin_with_name" formatted="true">حطو الأرقام السرية لـ\"%s\"</string>
|
||||
<string name="apk_installer_legacy">الطريقة القديمة</string>
|
||||
<string name="subtitles_raised">معلى</string>
|
||||
<string name="blank_repo_message">\"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات.
|
||||
\n
|
||||
\nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت.</string>
|
||||
<string name="blank_repo_message">\"كلود ستريم\" م بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. \n \nفيه معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت.</string>
|
||||
<string name="add_sync">زبد تتبع</string>
|
||||
<string name="mobile_data">3G/4G…</string>
|
||||
<string name="player_loaded_subtitles" formatted="true">نفَتح %s</string>
|
||||
|
|
@ -388,8 +379,7 @@
|
|||
<string name="uppercase_all_subtitles">دايمًا كتوب ب أحرف كاپيتال، A بدل a</string>
|
||||
<string name="player_pref">مشغل الڤيديو المفضل</string>
|
||||
<string name="quality_4k">4K</string>
|
||||
<string name="batch_download_start_format" formatted="true">بَلَش تنزيل %1$d %2$s
|
||||
\n…</string>
|
||||
<string name="batch_download_start_format" formatted="true">بَلَش تنزيل %1$d %2$s \n…</string>
|
||||
<string name="extension_description">الوصف</string>
|
||||
<string name="view_public_repositories_button">شوف الريپويات تبع مجتمع \"كلاود ستريم\"</string>
|
||||
<string name="safe_mode_title">إنت هلّق بال وضع الآمن</string>
|
||||
|
|
@ -419,9 +409,7 @@
|
|||
<string name="skip_setup">أفّى الإعداد</string>
|
||||
<string name="authenticated_user" formatted="true">فتت ع أكونت \"%s\" تبعك</string>
|
||||
<string name="subtitles_outline">حدود خطية</string>
|
||||
<string name="unable_to_inflate">في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا
|
||||
\n(UI was unable to be created correctly)
|
||||
\n%s</string>
|
||||
<string name="unable_to_inflate">في مشكلة بطعمير الـUI. هيدي مشكبة كبيرة. پليز بَلِغ عنّا \n(UI was unable to be created correctly) \n%s</string>
|
||||
<string name="edit">عَدِل</string>
|
||||
<string name="sort_updated_new">تجَدَد (من الجديد للقديم)</string>
|
||||
<string name="quality_tc">TC</string>
|
||||
|
|
@ -477,8 +465,7 @@
|
|||
<string name="quality_sd">SD</string>
|
||||
<string name="extensions">الإضافات</string>
|
||||
<string name="subtitles_remove_bloat">شيل الإعلانات من الترجمة</string>
|
||||
<string name="empty_library_no_accounts_message">رفّكن فاضي ☹
|
||||
\nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي.</string>
|
||||
<string name="empty_library_no_accounts_message">رفّكن فاضي ☹ \nفوتو على أكونت فيها رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي.</string>
|
||||
<string name="repository_name_hint">اسم الريپو (مش ضروري)</string>
|
||||
<string name="qualities">الجودات</string>
|
||||
<string name="error_invalid_data">بيانات مش صالحة</string>
|
||||
|
|
@ -505,8 +492,7 @@
|
|||
<string name="sort_rating_asc">رايتينگ (من الواطي للعالي)</string>
|
||||
<string name="player_load_subtitles">فتاح من ملف</string>
|
||||
<string name="disable">طفي</string>
|
||||
<string name="safe_mode_file">لقينا ملف الوضع الآمن!
|
||||
\nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف.</string>
|
||||
<string name="safe_mode_file">لقينا ملف الوضع الآمن! \nمش رح نعوز إضافيات وقت ما ينفَتَح الآپ حتّى ينشال الملف.</string>
|
||||
<string name="subtitle_offset_extra_hint_none_format">مش مغير وقت الترجمة</string>
|
||||
<string name="error">مشكلة</string>
|
||||
<string name="home_source">مصدر</string>
|
||||
|
|
@ -531,7 +517,7 @@
|
|||
<string name="plugin">إضافات</string>
|
||||
<string name="plugin_load_fail" formatted="true">م قدرنا نفتح %s</string>
|
||||
<string name="extension_rating" formatted="true">رايتينگ: %s</string>
|
||||
<string name="download_all_plugins_from_repo">تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا!</string>
|
||||
<string name="download_all_plugins_from_repo">تحزير: \"كلاود ستريم\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا!</string>
|
||||
<string name="extension_status">الحالة</string>
|
||||
<string name="delete_repository">محي الريپو</string>
|
||||
<string name="category_player">مشغل الڤيديو</string>
|
||||
|
|
@ -540,10 +526,7 @@
|
|||
<string name="already_voted">إنتو أصلًا مصوتين</string>
|
||||
<string name="quality_cam">كاميرا</string>
|
||||
<string name="no_plugins_found_error">م لقينا ولا إضافة بال ريپو</string>
|
||||
<string name="duplicate_message_single" formatted="true">مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن:
|
||||
\n\"%s\"
|
||||
\n
|
||||
\nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟</string>
|
||||
<string name="duplicate_message_single" formatted="true">مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: \n\"%s\" \n \nبدكن تزيدو هيدا العنصر بأيّ حال، أو تستبدلوّ مع العنصر الموجود، أو تلغو الإجراء؟</string>
|
||||
<string name="error_invalid_url">رايط مش صالح</string>
|
||||
<string name="subtitle_offset_hint">1000 مللي ثانية</string>
|
||||
<string name="extension_version">إصدار</string>
|
||||
|
|
@ -563,13 +546,7 @@
|
|||
<string name="action_open_play">@string/home_play</string>
|
||||
<string name="action_remove_from_watched">شيلو من لايحة المحتوى الحاضرينو</string>
|
||||
<string name="skip_type_credits">الإعتمادات</string>
|
||||
<string name="quality_profile_help">فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها.
|
||||
\n
|
||||
\nمتلًا:
|
||||
\nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8).
|
||||
\nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1).
|
||||
\n
|
||||
\nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر!</string>
|
||||
<string name="quality_profile_help">فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n \nمتلًا: \nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). \nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). \n \nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر!</string>
|
||||
<string name="enter_current_pin">حطو الأرقام السرية الحالية</string>
|
||||
<string name="audio_tracks">صوت</string>
|
||||
<string name="rotate_video_desc">حط كبسة لبرم إتجاه الشاشة</string>
|
||||
|
|
@ -589,16 +566,14 @@
|
|||
<string name="password_pin_authentication_title">رمز/كلمة مرور للمصادقة</string>
|
||||
<string name="biometric_setting_summary">فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، أو الپاسورد.</string>
|
||||
<string name="biometric_prompt_description">بعد كذا محاولة فاشلة، هيدا الشباك رح يسكر. بكل بساطة، سكر الآپ ورجاع فتحه حتى تجرب بعد مرة.</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nباقي</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nباقي</string>
|
||||
<string name="biometric_unsupported">المصادقة البيومترية مش مدعومة ع هالجهاز</string>
|
||||
<string name="unfavorite">شيله من المفضل</string>
|
||||
<string name="repo_copy_label">اسم وعنوان الريپو</string>
|
||||
<string name="toast_copied">نتسخ!</string>
|
||||
<string name="clipboard_permission_error">فيه ارور بال وصول ل الكليپ-بورد. پليز جرب مرة أخرى.</string>
|
||||
<string name="clipboard_unknown_error">فيه ارور بال نسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ.</string>
|
||||
<string name="biometric_warning">هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها.
|
||||
\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج.</string>
|
||||
<string name="biometric_warning">هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج.</string>
|
||||
<string name="ok">أوكي</string>
|
||||
<string name="battery_dialog_title">وقف اپتميزايشن بطارية جهازك</string>
|
||||
<string name="app_unrestricted_toast">بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\"</string>
|
||||
|
|
@ -634,21 +609,13 @@
|
|||
<string name="downloads_delete_select">نقي الإشيا اللي بدك تمحيها</string>
|
||||
<string name="offline_file">موجود لينحضر بلا إنترنت</string>
|
||||
<string name="delete_files">محي الفايلات</string>
|
||||
<string name="delete_message_multiple" formatted="true">متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_section" formatted="true">رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">متأكد إنو بدك تمحي هيدي الإشيا ل الأبد؟ \n \n%s</string>
|
||||
<string name="delete_message_series_section" formatted="true">رح كمان تمحي ل الأبد كل الحلقات اللي ب هيدا المسلسل؟ \n \n%s</string>
|
||||
<string name="select_all">نقي كل شي</string>
|
||||
<string name="deselect_all">شيل التنقاية</string>
|
||||
<string name="delete_format" formatted="true">محي (%1$d | %2$s)</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_only" formatted="true">متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">متأكد إنو بدك تمحي هيدي الحلقات من %1$s؟ \n \n%2$s</string>
|
||||
<string name="delete_message_series_only" formatted="true">متأكد إنو بدك تمحي كل الحلقات اللي بهيدا المسلسل؟ \n \n%s</string>
|
||||
<string name="preview_seekbar">صورة زغيرة مع التقريب وال تبعيد</string>
|
||||
<string name="preview_seekbar_desc">بت حط صورة زغير من الڤيديو إنت و عم بت قرب أو ترجع بال ڤيديو</string>
|
||||
<string name="no_subtitles_loaded">بعد مش معمول لود لولا ترجمة</string>
|
||||
|
|
@ -739,4 +706,33 @@
|
|||
<string name="top_left">فوق، عال شمال</string>
|
||||
<string name="top_center">فوق، بال نُص</string>
|
||||
<string name="top_right">فوق، عال يمين</string>
|
||||
<string name="download_queue">ليستة التنزيلات</string>
|
||||
<string name="queue_empty_message">مافي شي عم يتنزّل هلّق.</string>
|
||||
<string name="extra_brightness_settings">قوة ضو إضافية</string>
|
||||
<string name="extra_brightness_settings_des">بت حط فلتر للبرايتنس لمّا تعلي قوة الضو ل أكتر من 100%</string>
|
||||
<string name="extra_brightness_key">extra_brightness_enabled</string>
|
||||
<string name="search_suggestions">اقتراحات التنبيش</string>
|
||||
<string name="search_suggestions_des">بت فرجي اقتراحات إنتا و عم بت نَبّش</string>
|
||||
<string name="clear_suggestions">مساح الاقتراحات</string>
|
||||
<string name="show_player_metadata_overlay">فرجي ميتا-ديتا فوق الڤيديو</string>
|
||||
<string name="show_cast_in_details">فرجي ليستة الممثلين</string>
|
||||
<string name="video_singular">ڤيديو</string>
|
||||
<string name="video_info">معلومات الڤيديو</string>
|
||||
<string name="source_priority">أولوية المصدر</string>
|
||||
<string name="source_priority_help">حدد ترتيب المصادر بال مشغل</string>
|
||||
<string name="source_name">اسم المصدر</string>
|
||||
<string name="download_all">نزلن كلن</string>
|
||||
<string name="cancel_all">لغين كلن</string>
|
||||
<string name="download_episode_range">بدك تنزل الحلقة %s؟</string>
|
||||
<string name="cancel_queue_message">بدك تلغي كل شي عم يتنَزَّل؟</string>
|
||||
<plurals name="downloads_active">
|
||||
<item quantity="one">م شي عم يتنزل</item>
|
||||
<item quantity="other">شي واحد عم يتنزل</item>
|
||||
</plurals>
|
||||
<plurals name="downloads_queued">
|
||||
<item quantity="one">مافي شي بعد بده يبلش يتنزل</item>
|
||||
<item quantity="other">فيه شي واحد بعد بده يبلش يتنزل</item>
|
||||
</plurals>
|
||||
<string name="player_is_live">لايڤ</string>
|
||||
<string name="skip_type_preview">پريڤيو</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">سرعة (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">تقييم: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">يوجد تحديث جديد!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">يوجد تحديث جديد! \n%1$s -> %2$s</string>
|
||||
<string name="duration_format" formatted="true">%d دقيقة</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="play_with_app_name">تشغيل بواسطة CloudStream</string>
|
||||
|
|
@ -176,10 +175,8 @@
|
|||
<string name="resume">إستئناف</string>
|
||||
<string name="go_back_30">-٣٠</string>
|
||||
<string name="go_forward_30">+٣٠</string>
|
||||
<string name="delete_message">سوف يتم الحذف نهائيا %s
|
||||
\nهل أنت متأكد?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nمتبقية</string>
|
||||
<string name="delete_message">سوف يتم الحذف نهائيا %s \nهل أنت متأكد?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nمتبقية</string>
|
||||
<string name="status_ongoing">جاري التنفيذ</string>
|
||||
<string name="status_completed">اكتمل</string>
|
||||
<string name="status">الحالة</string>
|
||||
|
|
@ -401,9 +398,7 @@
|
|||
<string name="plugins_downloaded" formatted="true">تم تحميل: %d</string>
|
||||
<string name="plugins_disabled" formatted="true">مُعطل %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">غير مُحمل: %d</string>
|
||||
<string name="blank_repo_message">لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات.
|
||||
\n
|
||||
\nانضم إلى ديسكورد أو ابحث عبر الإنترنت.</string>
|
||||
<string name="blank_repo_message">لا يحتوي CloudStream على مواقع مثبتة افتراضيًا. تحتاج إلى تثبيت المواقع من المستودعات. \n \nانضم إلى ديسكورد أو ابحث عبر الإنترنت.</string>
|
||||
<string name="view_public_repositories_button">عرض مستودعات المجتمع</string>
|
||||
<string name="view_public_repositories_button_short">قائمة عامة</string>
|
||||
<string name="uppercase_all_subtitles">جميع الترجمات حروف كبيرة</string>
|
||||
|
|
@ -493,15 +488,13 @@
|
|||
<string name="sort_rating_desc">التقييم (من الأعلى إلى الأدنى)</string>
|
||||
<string name="sort_rating_asc">التقييم (من الأدنى إلى الأعلى)</string>
|
||||
<string name="sort_alphabetical_z">الترتيب الأبجدي (من ي إلى أ)</string>
|
||||
<string name="empty_library_no_accounts_message">مكتبتك فارغة :(
|
||||
\nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية.</string>
|
||||
<string name="empty_library_no_accounts_message">مكتبتك فارغة :( \nقم بتسجيل الدخول على حساب مكتبة أو أضف عروضا إلى مكتبتك المحلية.</string>
|
||||
<string name="sort_updated_old">محدث (من القديم إلى الجديد)</string>
|
||||
<string name="sort_by">فرز حسب</string>
|
||||
<string name="sort">افرز</string>
|
||||
<string name="open_with">فتح بواسطة</string>
|
||||
<string name="library">المكتبة</string>
|
||||
<string name="safe_mode_file">تم العثور على ملف الوضع الآمن!
|
||||
\nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف.</string>
|
||||
<string name="safe_mode_file">تم العثور على ملف الوضع الآمن! \nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف.</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">مدة التقديم عنما يكون المشغل مخفيا</string>
|
||||
<string name="android_tv_interface_off_seek_settings">مدة التقديم - المشغل مخفي</string>
|
||||
<string name="pref_category_android_tv">تلفزيون أندرويد</string>
|
||||
|
|
@ -533,13 +526,7 @@
|
|||
<string name="edit">تعديل</string>
|
||||
<string name="profiles">الملفات التعريفية</string>
|
||||
<string name="help">مساعدة</string>
|
||||
<string name="quality_profile_help">هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو.
|
||||
\n
|
||||
\nالمصدر أ: 3
|
||||
\nالجودة ب: 7
|
||||
\nسيكون لها أولوية فيديو مجمعة تبلغ 10.
|
||||
\n
|
||||
\nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط!</string>
|
||||
<string name="quality_profile_help">هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. \n \nالمصدر أ: 3 \nالجودة ب: 7 \nسيكون لها أولوية فيديو مجمعة تبلغ 10. \n \nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط!</string>
|
||||
<string name="qualities">النوعيات</string>
|
||||
<string name="profile_background_des">خلفية الملف الشخصي</string>
|
||||
<string name="unable_to_inflate">تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s</string>
|
||||
|
|
@ -552,11 +539,7 @@
|
|||
<string name="favorite_removed">تمت إزالة %s من المفضلة</string>
|
||||
<string name="favorites_list_name">المفضلة</string>
|
||||
<string name="favorite_added">تمت إضافة %s إلى المفضلة</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">احتمال وجود تكرارات في مكتبتك.
|
||||
\n
|
||||
\n%s
|
||||
\n
|
||||
\nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">احتمال وجود تكرارات في مكتبتك. \n \n%s \n \nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟</string>
|
||||
<string name="duplicate_title">احتمال أن يكون موجود بالفعل</string>
|
||||
<string name="lock_profile">قفل الحساب</string>
|
||||
<string name="action_add_to_favorites">اضافة الى المفضلة</string>
|
||||
|
|
@ -569,9 +552,7 @@
|
|||
<string name="action_subscribe">إشترك</string>
|
||||
<string name="action_remove_from_favorites">إزالة من المفضلة</string>
|
||||
<string name="select_an_account">اختار حساب</string>
|
||||
<string name="duplicate_message_single">يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'.
|
||||
\n
|
||||
\nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟</string>
|
||||
<string name="duplicate_message_single">يبدو أن هناك عنصرًا مكررًا موجود بالفعل في مكتبتك: \'%s\'. \n \nهل ترغب في إضافة هذا العنصر على أية حال، أو استبدال العنصر الموجود، أو إلغاء الإجراء؟</string>
|
||||
<string name="enter_pin">ادخال ال PIN</string>
|
||||
<string name="pin">PIN</string>
|
||||
<string name="enter_current_pin">أدخل ال PIN الحالي</string>
|
||||
|
|
@ -599,8 +580,7 @@
|
|||
<string name="password_pin_authentication_title">مصادقة كلمة المرور/رقم التعريف الشخصي</string>
|
||||
<string name="biometric_prompt_description">بعد عدة محاولات فاشلة، سيتم إغلاق المطالبة. ما عليك سوى إعادة تشغيل التطبيق للمحاولة مرة أخرى.</string>
|
||||
<string name="biometric_warning">لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا.</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nمتبقي</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nمتبقي</string>
|
||||
<string name="favorite">المفضلة</string>
|
||||
<string name="unfavorite">إزالة من المفضلة</string>
|
||||
<string name="repo_copy_label">اسم و عنوان المخزن</string>
|
||||
|
|
@ -642,21 +622,13 @@
|
|||
<string name="downloads_delete_select">الرجاء تحديد العناصر للحذف</string>
|
||||
<string name="select_all">تحديد الكل</string>
|
||||
<string name="delete_files">حذف الملفات</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">هل أنت متأكد أنك تريد حذف الحلقات التالية نهائيًا في %1$s ؟ \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">ستقوم أيضًا بحذف جميع الحلقات في السلسلة التالية نهائيًا: \n \n%s</string>
|
||||
<string name="delete_format" formatted="true">حذف (%1$d | %2$s)</string>
|
||||
<string name="offline_file">متاح للمشاهدة في وضع عدم الاتصال</string>
|
||||
<string name="deselect_all">إلغاء تحديد الكل</string>
|
||||
<string name="delete_message_multiple" formatted="true">هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">هل أنت متأكد أنك تريد حذف العناصر التالية نهائيًا ؟ \n \n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">هل أنت متأكد أنك تريد حذف جميع الحلقات في السلسلة التالية نهائيًا ؟ \n \n%s</string>
|
||||
<string name="preview_seekbar">معاينة شريط البحث</string>
|
||||
<string name="preview_seekbar_desc">تمكين معاينة الصورة المصغرة على شريط البحث</string>
|
||||
<string name="no_subtitles_loaded">لم يتم تحميل أي ترجمات بعد</string>
|
||||
|
|
@ -749,7 +721,7 @@
|
|||
<string name="search_suggestions">اقتراحات البحث</string>
|
||||
<string name="search_suggestions_des">عرض اقتراحات البحث أثناء الكتابة</string>
|
||||
<string name="clear_suggestions">مسح الاقتراحات</string>
|
||||
<string name="show_cast_in_details">عرض لوحة البث</string>
|
||||
<string name="show_cast_in_details">عرض لوحة فريق التمثيل</string>
|
||||
<string name="install_prerelease">تثبيت الإصدار التجريبي</string>
|
||||
<string name="prerelease_already_installed">تم تثبيت الإصدار التجريبي بالفعل.</string>
|
||||
<string name="prerelease_install_failed">فشل تثبيت الإصدار التجريبي.</string>
|
||||
|
|
@ -781,4 +753,7 @@
|
|||
<item quantity="other">%d تنزيل قيد الانتظار</item>
|
||||
</plurals>
|
||||
<string name="show_player_metadata_overlay">عرض واجهة منبثقة للبيانات الوصفية للمشغِّل</string>
|
||||
<string name="video_singular">مقطع</string>
|
||||
<string name="skip_type_preview">استعراض</string>
|
||||
<string name="player_is_live">البث قائم</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -35,8 +35,7 @@
|
|||
<string name="download_paused">توقف التنزيل</string>
|
||||
<string name="type_plan_to_watch">خطط للمشاهدة</string>
|
||||
<string name="type_re_watching">إعادة المشاهدة</string>
|
||||
<string name="new_update_format" formatted="true">!تم العثور على تحديث جديد
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">!تم العثور على تحديث جديد \n%1$s -> %2$s</string>
|
||||
<string name="rated_format" formatted="true">%.1f:قدر</string>
|
||||
<string name="duration_format" formatted="true">%dاقل</string>
|
||||
<string name="app_name">كلاودستريم</string>
|
||||
|
|
@ -157,23 +156,15 @@
|
|||
<string name="sort_updated_new">تم التحديث (من الجديد إلى القديم)</string>
|
||||
<string name="sort_updated_old">تم التحديث (القديم إلى الجديد)</string>
|
||||
<string name="sort_alphabetical_a">أبجديًا (من الألف إلى الياء)</string>
|
||||
<string name="empty_library_no_accounts_message">مكتبتك فارغة :(
|
||||
\nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية.</string>
|
||||
<string name="safe_mode_file">!تم العثور على ملف الوضع الآمن
|
||||
\n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف</string>
|
||||
<string name="empty_library_no_accounts_message">مكتبتك فارغة :( \nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية.</string>
|
||||
<string name="safe_mode_file">!تم العثور على ملف الوضع الآمن \n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف</string>
|
||||
<string name="revert">ارجع</string>
|
||||
<string name="subscription_in_progress_notification">تحديث العروض المشتركة</string>
|
||||
<string name="set_default">الوضع العادي</string>
|
||||
<string name="edit">حرر</string>
|
||||
<string name="profiles">ملفات تعريفية</string>
|
||||
<string name="help">مساعدة</string>
|
||||
<string name="quality_profile_help">.هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو
|
||||
\n
|
||||
\nالمصدر أ: 3
|
||||
\nالجودة ب: 7
|
||||
\nستكون أولوية الفيديو المدمجة .10
|
||||
\n
|
||||
\n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط</string>
|
||||
<string name="quality_profile_help">.هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو \n \nالمصدر أ: 3 \nالجودة ب: 7 \nستكون أولوية الفيديو المدمجة .10 \n \n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط</string>
|
||||
<string name="already_voted">لقد صوت بالفعل</string>
|
||||
<string name="sort_alphabetical_z">أبجديًا (ياء إلى ألف)</string>
|
||||
<string name="sort_by">ترتيب حسب</string>
|
||||
|
|
@ -227,8 +218,7 @@
|
|||
<string name="sort_apply">قدم</string>
|
||||
<string name="torrent_plot">وصف</string>
|
||||
<string name="picture_in_picture_des">يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى</string>
|
||||
<string name="delete_message" formatted="true">نهائيا %sسيؤدي هذا الى حذف
|
||||
\nهل أنت متأكد؟</string>
|
||||
<string name="delete_message" formatted="true">نهائيا %sسيؤدي هذا الى حذف \nهل أنت متأكد؟</string>
|
||||
<string name="subs_font">الخط</string>
|
||||
<string name="subs_font_size">حجم الخط</string>
|
||||
<string name="action_remove_watching">زيل</string>
|
||||
|
|
@ -255,8 +245,7 @@
|
|||
<string name="show_log_cat">🐈عرض لوجكات</string>
|
||||
<string name="test_log">سجل</string>
|
||||
<string name="picture_in_picture">صور في صور</string>
|
||||
<string name="resume_time_left" formatted="true">%d
|
||||
\nباقي</string>
|
||||
<string name="resume_time_left" formatted="true">%d \nباقي</string>
|
||||
<string name="video_source">مصدر</string>
|
||||
<string name="android_tv_interface_off_seek_settings">اللاعب مخفي - ابحث عن المبلغ</string>
|
||||
<string name="backup_frequency">تكرار النسخ الاحتياطي</string>
|
||||
|
|
|
|||
|
|
@ -252,12 +252,9 @@
|
|||
<string name="resume">পুনৰ আৰম্ভ কৰক</string>
|
||||
<string name="go_back_30">-৩০</string>
|
||||
<string name="go_forward_30">+৩০</string>
|
||||
<string name="delete_message" formatted="true">এইটো স্থায়ীভাৱে %s ডিলিট কৰিব।
|
||||
\nআপুনি নিশ্চিত নেকি?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nবাকী</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nবাকী</string>
|
||||
<string name="delete_message" formatted="true">এইটো স্থায়ীভাৱে %s ডিলিট কৰিব। \nআপুনি নিশ্চিত নেকি?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nবাকী</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nবাকী</string>
|
||||
<string name="status_ongoing">চলমান</string>
|
||||
<string name="status_completed">সম্পূৰ্ণ</string>
|
||||
<string name="status">স্থিতি</string>
|
||||
|
|
@ -456,9 +453,7 @@
|
|||
<string name="plugins_updated" formatted="true">%d প্লাগইন আপডেট কৰা হ\'ল</string>
|
||||
<string name="plugins_disabled" formatted="true">নিষ্ক্ৰিয় কৰা: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">ডাউনলোড কৰা নহয়: %d</string>
|
||||
<string name="blank_repo_message">CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব।
|
||||
\n
|
||||
\nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক।</string>
|
||||
<string name="blank_repo_message">CloudStreamত কোনো চাইট ডিফল্টভাৱে ইনষ্টল হোৱা নাই। আপুনিয়ে ৰিপ\'জিট\'ৰিবোৰৰ পৰা চাইটসমূহ ইনষ্টল কৰিব লাগিব। \n \nআমাৰ Discordত যোগদান কৰক বা অনলাইন বিচাৰক।</string>
|
||||
<string name="view_public_repositories_button">সম্প্ৰদায়ৰ ৰিপ\'জিট\'ৰিসমূহ চাওক</string>
|
||||
<string name="uppercase_all_subtitles">সকলো চাবটাইটল মুকলি আখৰত</string>
|
||||
<string name="download_all_plugins_from_repo">সতর্কতা: CloudStream 3 কোৱা নাই যে তৃতীয় পক্ষৰ বৃদ্ধিসমূহ ব্যৱহাৰ কৰিবলৈ আপুনি সম্পূৰ্ণ দায়িত্ব ল\'ব আৰু কোনো সহায় নাপাব!</string>
|
||||
|
|
@ -523,11 +518,9 @@
|
|||
<string name="sort_alphabetical_a">বৰ্ণানুক্ৰমিক (A ৰ পৰা Z)</string>
|
||||
<string name="select_library">পুথিভঁৰালী বাছক</string>
|
||||
<string name="open_with">ইয়াৰ সহায়ত খুলক</string>
|
||||
<string name="empty_library_no_accounts_message">আপোনাৰ পুথিভঁৰালী খালি আছে :(
|
||||
\nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক।</string>
|
||||
<string name="empty_library_no_accounts_message">আপোনাৰ পুথিভঁৰালী খালি আছে :( \nএখন পুথিভঁৰালী একাউণ্টত লগ ইন কৰক বা স্থানীয় পুথিভঁৰালীত শ্ব\'সমূহ যোগ কৰক।</string>
|
||||
<string name="empty_library_logged_in_message">এই তালিকা খালি। অন্য এটি তালিকালৈ সলনি কৰি চাওক।</string>
|
||||
<string name="safe_mode_file">নিরাপদ ম\'ড ফাইল পোৱা গৈছে!
|
||||
\nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে।</string>
|
||||
<string name="safe_mode_file">নিরাপদ ম\'ড ফাইল পোৱা গৈছে! \nফাইল আঁতৰোৱা নোহোৱালৈকে কোনো এক্সটেনশ্যন আৰম্ভ নকৰা হৈছে।</string>
|
||||
<string name="revert">ঘূৰাই দিয়া</string>
|
||||
<string name="subscription_in_progress_notification">সদস্যতা গ্ৰহণ কৰা শ্ব\'সমূহ আপডেট কৰিছে</string>
|
||||
<string name="subscription_new">%s-ত সদস্যতা গ্ৰহণ কৰা হৈছে</string>
|
||||
|
|
@ -539,13 +532,7 @@
|
|||
<string name="edit">সম্পাদনা কৰক</string>
|
||||
<string name="profiles">প্ৰ\'ফাইলসমূহ</string>
|
||||
<string name="help">সহায়</string>
|
||||
<string name="quality_profile_help">ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ।
|
||||
\n
|
||||
\nউৎস A: 3
|
||||
\nগুণ B: 7
|
||||
\nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10।
|
||||
\n
|
||||
\nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব!</string>
|
||||
<string name="quality_profile_help">ইয়াত আপুনি উৎসসমূহ কেনেকৈ ক্ৰম অনুযায়ী চিহ্নিত কৰিব সেই বিষয়ে সলনি কৰিব পাৰে। যদি এখন ভিডিঅ\'ৰ উচ্চ অগ্ৰাধিকাৰ থাকে তেন্তে সেইটো উৎস নিৰ্বাচনৰ সময়ত ওপৰত দেখা যাব। উৎস অগ্ৰাধিকাৰ আৰু গুণ অগ্ৰাধিকাৰৰ মুঠ যোগফল হৈছে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ। \n \nউৎস A: 3 \nগুণ B: 7 \nসৰহযোগে ভিডিঅ\'ৰ অগ্ৰাধিকাৰ হ\'ব 10। \n \nদ্ৰষ্টব্য: যদি যোগফল 10 বা তাৰ ওপৰত থাকে তেন্তে প্লেয়াৰটো নিজেই লিংক ল\'ড কৰাৰ সময়ত ল\'ডিং এড়াই যাব!</string>
|
||||
<string name="profile_background_des">প্ৰ\'ফাইলৰ পটভূমি</string>
|
||||
<string name="unable_to_inflate">UI সঠিকভাৱে সৃষ্টি কৰিব পৰা নগ\'ল, ই এটা গুৰুত্বপূৰ্ণ সমস্যা আৰু তাক অবিলম্বে জনোৱা উচিত %s</string>
|
||||
<string name="already_voted">আপুনি ইতিমধ্যে ভোট দিছে</string>
|
||||
|
|
@ -556,14 +543,8 @@
|
|||
<string name="action_remove_from_favorites">প্ৰিয় তালিকাৰ পৰা আঁতৰ কৰক</string>
|
||||
<string name="duplicate_title">সম্ভাৱ্য নকল বস্ত্ত পোৱা গৈছে</string>
|
||||
<string name="duplicate_replace_all">সকলো প্ৰতিস্থাপন কৰক</string>
|
||||
<string name="duplicate_message_single" formatted="true">আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\'
|
||||
\n
|
||||
\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে:
|
||||
\n
|
||||
\n%s
|
||||
\n
|
||||
\nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে?</string>
|
||||
<string name="duplicate_message_single" formatted="true">আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তু পোৱা গৈছে: \'%s.\' \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমান বস্তু প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">আপোনাৰ পুথিভঁৰালীতে সম্ভাৱ্য নকল বস্তুসমূহ পোৱা গৈছে: \n \n%s \n \nআপুনি এই বস্তু যিকোনো হ\'লে যোগ কৰিব বিচাৰেনে, বিদ্যমানবোৰ প্ৰতিস্থাপন কৰিব, বা কাৰ্য বাতিল কৰিব বিচাৰেনে?</string>
|
||||
<string name="enter_pin_with_name" formatted="true">%s ৰ বাবে পিন সন্নিবিষ্ট কৰক</string>
|
||||
<string name="enter_current_pin">বৰ্তমান পিন সন্নিবিষ্ট কৰক</string>
|
||||
<string name="lock_profile">প্ৰফাইল লক কৰক</string>
|
||||
|
|
@ -588,8 +569,7 @@
|
|||
<string name="download">ডাউনলোড</string>
|
||||
<string name="updates_settings_des">এপ্ আৰম্ভণিৰ পিছত নতুন আপডেটৰ সন্ধান কৰক।</string>
|
||||
<string name="anim">একেই ডেভেলপাৰৰ দ্বাৰা এনিম এপ্</string>
|
||||
<string name="new_update_format" formatted="true">নতুন আপডেট পোৱা গ’ল!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">নতুন আপডেট পোৱা গ’ল! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">ফিলাৰ</string>
|
||||
<string name="play_with_app_name">CloudStreamৰে প্লে কৰক</string>
|
||||
<string name="title_search">সন্ধান</string>
|
||||
|
|
@ -609,18 +589,10 @@
|
|||
<string name="sort_apply">প্ৰয়োগ কৰক</string>
|
||||
<string name="delete_files">ফাইলসমূহ ডিলিট কৰক</string>
|
||||
<string name="delete_format" formatted="true">ডিলিট (%1$d | %2$s)</string>
|
||||
<string name="delete_message_multiple" formatted="true">আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">%1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">আপুনি স্থায়ীভাৱে তলত দিয়া আইটেমসমূহ ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">%1$s ত তলত দিয়া এপিচ’ডসমূহ স্থায়ীভাৱে ডিলিট কৰিব নেকি? \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিব: \n \n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">আপুনি দিয়া ছিৰিজৰ সকলো এপিচ’ড স্থায়ীভাৱে ডিলিট কৰিবলৈ নিশ্চিত নেকি? \n \n%s</string>
|
||||
<string name="sort_release_date_old">মুক্তিৰ তাৰিখ (পুৰণাৰ পৰা নতুন)</string>
|
||||
<string name="test_warning">সতৰ্কবাৰ্তা</string>
|
||||
<string name="auth_locally">স্থানীয়ভাৱে প্ৰমাণীকৰণ কৰক</string>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
<string name="preview_background_img_des">Визуализация на фона</string>
|
||||
<string formatted="true" name="player_speed_text_format">Скорост (%.2fx)</string>
|
||||
<string formatted="true" name="rated_format">Оценка: %.1f</string>
|
||||
<string formatted="true" name="new_update_format">Намерена е нова актуализация!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string formatted="true" name="new_update_format">Намерена е нова актуализация! \n%1$s -> %2$s</string>
|
||||
<string formatted="true" name="filler">Шаблон</string>
|
||||
<string formatted="true" name="duration_format">%d мин</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
|
|
@ -183,10 +182,8 @@
|
|||
<string name="resume">Продължи</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">30</string>
|
||||
<string formatted="true" name="delete_message">Това ще изтрие за постоянно %s
|
||||
\nСигурни ли сте?</string>
|
||||
<string formatted="true" name="resume_time_left">%dm
|
||||
\nостава</string>
|
||||
<string formatted="true" name="delete_message">Това ще изтрие за постоянно %s \nСигурни ли сте?</string>
|
||||
<string formatted="true" name="resume_time_left">%dm \nостава</string>
|
||||
<string name="status_ongoing">Продължава</string>
|
||||
<string name="status_completed">Завършен</string>
|
||||
<string name="status">Статус</string>
|
||||
|
|
@ -405,9 +402,7 @@
|
|||
<string formatted="true" name="plugins_disabled">Деактивирано: %d</string>
|
||||
<string formatted="true" name="plugins_not_downloaded">Не е изтеглено: %d</string>
|
||||
<string formatted="true" name="plugins_updated">Актуализирани %d плъгини</string>
|
||||
<string name="blank_repo_message">CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища.
|
||||
\n
|
||||
\nПрисъединете се към нашия Дискорд или потърсете онлайн.</string>
|
||||
<string name="blank_repo_message">CloudStream няма инсталирани сайтове по подразбиране. Трябва да инсталирате сайтовете от хранилища. \n \nПрисъединете се към нашия Дискорд или потърсете онлайн.</string>
|
||||
<string name="view_public_repositories_button">Вижте хранилищата на общността</string>
|
||||
<string name="view_public_repositories_button_short">Публичен списък</string>
|
||||
<string name="uppercase_all_subtitles">Всички субтитри с главни букви</string>
|
||||
|
|
@ -519,12 +514,10 @@
|
|||
<string name="profile_number">Профил %d</string>
|
||||
<string name="sort_alphabetical_a">По азбучен ред (A до Z)</string>
|
||||
<string name="open_with">Отваряне с</string>
|
||||
<string name="empty_library_no_accounts_message">Вашата библиотека е празна :(
|
||||
\nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека.</string>
|
||||
<string name="empty_library_no_accounts_message">Вашата библиотека е празна :( \nВпишете се в акаунт с библиотеки или добавете сериали в локалната Ви библиотека.</string>
|
||||
<string name="use">Използване</string>
|
||||
<string name="subscription_episode_released">Епизод %d е публикуван!</string>
|
||||
<string name="safe_mode_file">Намерен е файл за безопасен режим!
|
||||
\nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат.</string>
|
||||
<string name="safe_mode_file">Намерен е файл за безопасен режим! \nНяма да се зареждат никакви разширения при стартиране, докато файлът не бъде премахнат.</string>
|
||||
<string name="already_voted">Вече сте гласували</string>
|
||||
<string name="set_default">Задаване по подразбиране</string>
|
||||
<string name="pin_error_length">ПИН трябва да е 4 символа</string>
|
||||
|
|
@ -551,23 +544,11 @@
|
|||
<string name="android_tv_interface_off_seek_settings">Скрит играч - сума за търсене</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Сумата за търсене, използвана, когато играчът е скрит</string>
|
||||
<string name="sort_updated_new">Актуализирано (от ново към старо)</string>
|
||||
<string name="quality_profile_help">Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото.
|
||||
\n
|
||||
\nИзточник A: 3
|
||||
\nКачество B: 7
|
||||
\nЩе има комбиниран видео приоритет от 10.
|
||||
\n
|
||||
\nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди!</string>
|
||||
<string name="quality_profile_help">Тук можете да промените начина на подреждане на източниците. Ако даден видеоклип има по-висок приоритет, той ще се показва по-високо в избора на източник. Сборът от приоритета на източника и приоритета на качеството е приоритетът на видеото. \n \nИзточник A: 3 \nКачество B: 7 \nЩе има комбиниран видео приоритет от 10. \n \nЗАБЕЛЕЖКА: Ако сумата е 10 или повече, играчът автоматично ще пропусне зареждането, когато тази връзка се зареди!</string>
|
||||
<string name="duplicate_replace">Замени</string>
|
||||
<string name="duplicate_replace_all">Замени Всички</string>
|
||||
<string name="duplicate_message_single" formatted="true">Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“.
|
||||
\n
|
||||
\nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">Във вашата библиотека са намерени потенциални дублиращи се елементи:
|
||||
\n
|
||||
\n%s
|
||||
\n
|
||||
\nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието?</string>
|
||||
<string name="duplicate_message_single" formatted="true">Изглежда, че потенциално дублиран елемент вече съществува във вашата библиотека: „%s“. \n \nИскате ли все пак да добавите този елемент, да замените съществуващия или да отмените действието?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">Във вашата библиотека са намерени потенциални дублиращи се елементи: \n \n%s \n \nИскате ли все пак да добавите този елемент, да замените съществуващите или да отмените действието?</string>
|
||||
<string name="lock_profile">Заключи Профил</string>
|
||||
<string name="enter_current_pin">Вкарай Сегашен ПИН</string>
|
||||
<string name="manage_accounts">Управлявай Профили</string>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@
|
|||
<string name="preview_background_img_des">ব্যাকগ্রাউন্ড দেখান</string>
|
||||
<string name="player_speed_text_format" formatted="true">গতি (%.2f গুণ)</string>
|
||||
<string name="rated_format" formatted="true">মূল্যায়নঃ %.1f</string>
|
||||
<string name="new_update_format" formatted="true">নতুন আপডেট এসেছে!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">নতুন আপডেট এসেছে! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">ফিলার</string>
|
||||
<string name="duration_format" formatted="true">%d মিনিট</string>
|
||||
<string name="app_name">ক্লাউডস্ট্রিম</string>
|
||||
|
|
@ -159,8 +158,7 @@
|
|||
<string name="movies">সিনেমা</string>
|
||||
<string name="discord">ডিসকর্ডে যোগ দিন</string>
|
||||
<string name="torrent">টরেন্টস</string>
|
||||
<string name="delete_message" formatted="true">এটি স্থায়ীভাবে মুছে ফেলা হবে %s
|
||||
\nআপনি কি নিশ্চিত?</string>
|
||||
<string name="delete_message" formatted="true">এটি স্থায়ীভাবে মুছে ফেলা হবে %s \nআপনি কি নিশ্চিত?</string>
|
||||
<string name="pause">থামুন</string>
|
||||
<string name="go_back_30">-৩০</string>
|
||||
<string name="github">গিটহাব</string>
|
||||
|
|
@ -184,8 +182,7 @@
|
|||
<string name="used_storage">ব্যবহৃত</string>
|
||||
<string name="library">লাইব্রেরী</string>
|
||||
<string name="lightnovel">আমাদের তৈরি ছোট উপন্যাস পড়ার অ্যাপ্লিকেশন</string>
|
||||
<string name="resume_time_left" formatted="true">%d মি
|
||||
\nবাকি</string>
|
||||
<string name="resume_time_left" formatted="true">%d মি \nবাকি</string>
|
||||
<string name="others">অন্যান্য</string>
|
||||
<string name="status_ongoing">চলমান</string>
|
||||
<string name="asian_drama">এশিয়ান নাটক</string>
|
||||
|
|
@ -308,8 +305,7 @@
|
|||
<string name="example_password">password123</string>
|
||||
<string name="episode_upcoming_format" formatted="true">আসছে %s সময়ের মধ্যে</string>
|
||||
<string name="cancel">বাতিল করুন</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nঅবশিষ্ট</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nঅবশিষ্ট</string>
|
||||
<string name="live_singular">লাইভ স্ট্রিম</string>
|
||||
<string name="source_error">সোর্স সমস্যা</string>
|
||||
<string name="remote_error">রিমোট সমস্যা</string>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">Rychlost (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">Hodnocení: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">Nalezena nová aktualizace!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Nalezena nová aktualizace! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">Výplň</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
|
|
@ -172,10 +171,8 @@
|
|||
<string name="resume">Pokračovat</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="delete_message" formatted="true">Toto nevratně smaže %s
|
||||
\nJste si jisti?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nzbývá</string>
|
||||
<string name="delete_message" formatted="true">Toto nevratně smaže %s \nJste si jisti?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nzbývá</string>
|
||||
<string name="status_ongoing">Probíhající</string>
|
||||
<string name="status_completed">Dokončena</string>
|
||||
<string name="status">Stav</string>
|
||||
|
|
@ -416,9 +413,7 @@
|
|||
<string name="plugin_downloaded">Doplněk stažen</string>
|
||||
<string name="is_adult">18+</string>
|
||||
<string name="batch_download_start_format" formatted="true">Spuštěno stahování %1$d %2$s…</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="plugins_disabled" formatted="true">Zakázáno: %d</string>
|
||||
<string name="plugins_updated" formatted="true">Aktualizováno %d doplňků</string>
|
||||
<string name="safe_mode_crash_info">Zobrazit informace o pádu</string>
|
||||
|
|
@ -446,8 +441,7 @@
|
|||
<string name="update_notification_failed">Nepodařilo se nainstalovat novou verzi aplikace</string>
|
||||
<string name="apk_installer_legacy">Původní</string>
|
||||
<string name="delayed_update_notice">Aplikace bude po ukončení aktualizována</string>
|
||||
<string name="empty_library_no_accounts_message">Vaše knihovna je prázdná :(
|
||||
\nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny.</string>
|
||||
<string name="empty_library_no_accounts_message">Vaše knihovna je prázdná :( \nPřihlaste se k účtu v knihovně nebo přidejte pořady do místní knihovny.</string>
|
||||
<string name="select_library">Vybrat knihovnu</string>
|
||||
<string name="sort_rating_desc">Hodnocení (od nejvyššího)</string>
|
||||
<string name="sort_rating_asc">Hodnocení (od nejnižšího)</string>
|
||||
|
|
@ -455,8 +449,7 @@
|
|||
<string name="sort_by">Seřadit podle</string>
|
||||
<string name="sort">Řazení</string>
|
||||
<string name="empty_library_logged_in_message">Tento seznam je prázdný. Zkuste přepnout na jiný.</string>
|
||||
<string name="safe_mode_file">Nalezen soubor bezpečného režimu!
|
||||
\nDo odebrání souboru nebudeme načítat žádná rozšíření.</string>
|
||||
<string name="safe_mode_file">Nalezen soubor bezpečného režimu! \nDo odebrání souboru nebudeme načítat žádná rozšíření.</string>
|
||||
<string name="sort_updated_new">Aktualizováno (od nejnovějšího)</string>
|
||||
<string name="sort_updated_old">Aktualizováno (od nejstaršího)</string>
|
||||
<string name="sort_alphabetical_a">Abecedně (od A do Z)</string>
|
||||
|
|
@ -537,13 +530,7 @@
|
|||
<string name="help">Nápověda</string>
|
||||
<string name="qualities">Kvality</string>
|
||||
<string name="profile_background_des">Pozadí profilu</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="unable_to_inflate">Nepodařilo se správně vytvořit rozhraní. Toto je VÁŽNÁ CHYBA, kterou je potřeba ihned nahlásit %s</string>
|
||||
<string name="disable">Vypnout</string>
|
||||
<string name="automatic_plugin_download_mode_title">Výběr režimu pro filtrování stahování doplňků</string>
|
||||
|
|
@ -553,11 +540,7 @@
|
|||
<string name="favorite_removed">%s odebráno z oblíbených</string>
|
||||
<string name="favorites_list_name">Oblíbené</string>
|
||||
<string name="favorite_added">%s přidáno do oblíbených</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="backup_frequency">Frekvence záloh</string>
|
||||
<string name="duplicate_title">Nalezena potenciální duplicita</string>
|
||||
<string name="lock_profile">Zamknout profil</string>
|
||||
|
|
@ -571,9 +554,7 @@
|
|||
<string name="action_subscribe">Odebírat</string>
|
||||
<string name="action_remove_from_favorites">Odebrat z oblíbených</string>
|
||||
<string name="select_an_account">Vyberte účet</string>
|
||||
<string name="duplicate_message_single">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?</string>
|
||||
<string name="duplicate_message_single">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?</string>
|
||||
<string name="enter_pin">Zadejte PIN</string>
|
||||
<string name="pin">PIN</string>
|
||||
<string name="enter_current_pin">Zadejte současný PIN</string>
|
||||
|
|
@ -602,8 +583,7 @@
|
|||
<string name="biometric_prompt_description">Po několika nezdařilých pokusech se okno zavře. Pro opětovný pokus restartujte aplikaci.</string>
|
||||
<string name="biometric_warning">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í.</string>
|
||||
<string name="unfavorite">Odebrat z oblíbených</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nzbývá</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nzbývá</string>
|
||||
<string name="favorite">Přidat do oblíbených</string>
|
||||
<string name="repo_copy_label">Název a adresa repozitáře</string>
|
||||
<string name="clipboard_unknown_error">Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace.</string>
|
||||
|
|
@ -644,20 +624,12 @@
|
|||
<string name="downloads_delete_select">Zvolte položky k odstranění</string>
|
||||
<string name="offline_file">Dostupné pro sledování offline</string>
|
||||
<string name="select_all">Vybrat vše</string>
|
||||
<string name="delete_message_multiple" formatted="true">Opravdu chcete trvale odstranit následující položky?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Opravdu chcete trvale odstranit následující epizody v %1$s?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Opravdu chcete trvale odstranit všechny epizody v následujících sériích?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">Opravdu chcete trvale odstranit následující položky? \n \n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Opravdu chcete trvale odstranit následující epizody v %1$s? \n \n%2$s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Opravdu chcete trvale odstranit všechny epizody v následujících sériích? \n \n%s</string>
|
||||
<string name="deselect_all">Zrušit výběr všeho</string>
|
||||
<string name="delete_files">Odstranit soubory</string>
|
||||
<string name="delete_message_series_section" formatted="true">Také trvale odstraníte všechny epizody v následujících sériích:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Také trvale odstraníte všechny epizody v následujících sériích: \n \n%s</string>
|
||||
<string name="delete_format" formatted="true">Odstranit (%1$d | %2$s)</string>
|
||||
<string name="preview_seekbar">Náhled v liště přehrávače</string>
|
||||
<string name="preview_seekbar_desc">Povolit náhled miniatur na liště přehrávače</string>
|
||||
|
|
@ -779,4 +751,7 @@
|
|||
<string name="source_priority">Priorita zdrojů</string>
|
||||
<string name="source_priority_help">Rozhodněte, jak mají být řazeny zdroje videí v přehrávači</string>
|
||||
<string name="show_player_metadata_overlay">Zobrazit překrytí metadat v přehrávači</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Náhled</string>
|
||||
<string name="player_is_live">Živě</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@
|
|||
<string name="preview_background_img_des">Hintergrundbildvorschau</string>
|
||||
<string name="player_speed_text_format" formatted="true">Geschwindigkeit (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">Bewertung: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">Neues Update gefunden!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Neues Update gefunden! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">Füller</string>
|
||||
<string name="duration_format" formatted="true">%d Min</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
|
|
@ -188,10 +187,8 @@
|
|||
<string name="resume">Fortsetzen</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="delete_message" formatted="true">Dadurch wird %s permanent gelöscht
|
||||
\nBist du dir sicher?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nverbleibend</string>
|
||||
<string name="delete_message" formatted="true">Dadurch wird %s permanent gelöscht \nBist du dir sicher?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nverbleibend</string>
|
||||
<string name="status_ongoing">Laufend</string>
|
||||
<string name="status_completed">Abgeschlossen</string>
|
||||
<string name="status">Status</string>
|
||||
|
|
@ -255,7 +252,7 @@
|
|||
<string name="update">Update</string>
|
||||
<string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string>
|
||||
<string name="limit_title">Videoplayertitel max. Zeichen</string>
|
||||
<string name="limit_title_rez">Playerinformationen anzeigen</string>
|
||||
<string name="limit_title_rez">Zeige Playerinformationen</string>
|
||||
<string name="video_buffer_size_settings">Videopuffergröße</string>
|
||||
<string name="video_buffer_length_settings">Videopufferlänge</string>
|
||||
<string name="video_buffer_disk_settings">Video-Cache in Speicher</string>
|
||||
|
|
@ -401,9 +398,7 @@
|
|||
<string name="plugins_downloaded" formatted="true">Heruntergeladen: %d</string>
|
||||
<string name="plugins_disabled" formatted="true">Deaktiviert: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Nicht heruntergeladen: %d</string>
|
||||
<string name="blank_repo_message">CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden.
|
||||
\n
|
||||
\nTrete unserem Discord Server bei oder suche online.</string>
|
||||
<string name="blank_repo_message">CloudStream hat standardmäßig keine Websites installiert. Websites müssen aus Repositories installiert werden. \n \nTrete unserem Discord Server bei oder suche online.</string>
|
||||
<string name="view_public_repositories_button">Community-Repositories anzeigen</string>
|
||||
<string name="view_public_repositories_button_short">Öffentliche Liste</string>
|
||||
<string name="uppercase_all_subtitles">Alle Untertitel in Großbuchstaben</string>
|
||||
|
|
@ -484,11 +479,9 @@
|
|||
<string name="sort_alphabetical_z">Alphabetisch (Z zu A)</string>
|
||||
<string name="select_library">Bibliothek auswählen</string>
|
||||
<string name="open_with">Öffnen mit</string>
|
||||
<string name="empty_library_no_accounts_message">Deine Bibliothek ist leer :(
|
||||
\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu.</string>
|
||||
<string name="empty_library_no_accounts_message">Deine Bibliothek ist leer :( \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu.</string>
|
||||
<string name="empty_library_logged_in_message">Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln.</string>
|
||||
<string name="safe_mode_file">Datei für den abgesicherten Modus gefunden!
|
||||
\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird.</string>
|
||||
<string name="safe_mode_file">Datei für den abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird.</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Player ausgeblendet - Betrag zum vor- und zurückspulen</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Der Betrag, welcher verwendet wird, wenn der Player ausgeblendet ist</string>
|
||||
|
|
@ -522,13 +515,7 @@
|
|||
<string name="help">Hilfe</string>
|
||||
<string name="qualities">Qualitäten</string>
|
||||
<string name="profile_background_des">Profil-Hintergrund</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="automatic_plugin_download_mode_title">Filtermodus für Plugin-Downloads auswählen</string>
|
||||
<string name="already_voted">Es wurde bereits abgestimmt</string>
|
||||
<string name="no_plugins_found_error">Keine Plugins im Repository gefunden</string>
|
||||
|
|
@ -560,14 +547,8 @@
|
|||
<string name="skip_startup_account_select_pref">Kontoauswahl beim Starten überspringen</string>
|
||||
<string name="manage_accounts">Konten verwalten</string>
|
||||
<string name="edit_account">Konto bearbeiten</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_message_single" formatted="true">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?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_message_single" formatted="true">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?</string>
|
||||
<string name="links_reloaded_toast">Links wurden neu geladen</string>
|
||||
<string name="rotate_video">Drehen</string>
|
||||
<string name="rotate_video_desc">Zeige einen Umschalter für Bildschirmorientierung an</string>
|
||||
|
|
@ -587,8 +568,7 @@
|
|||
<string name="unfavorite">kein Favorit</string>
|
||||
<string name="biometric_prompt_description">Dieser Bildschirm wurde nach einigen Fehlversuchen geschlossen. Starte die App neu.</string>
|
||||
<string name="biometric_warning">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.</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nausstehend</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nausstehend</string>
|
||||
<string name="favorite">Favorit</string>
|
||||
<string name="toast_copied">Kopiert!</string>
|
||||
<string name="clipboard_unknown_error">Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support.</string>
|
||||
|
|
@ -607,7 +587,7 @@
|
|||
<string name="pref_category_security">Sicherheit</string>
|
||||
<string name="pref_category_accounts">Konten</string>
|
||||
<string name="open_downloaded_repo">Repository öffnen</string>
|
||||
<string name="device_pin_url_message">Besuche<b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
|
||||
<string name="device_pin_url_message">Besuche <b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
|
||||
<string name="device_pin_error_message">PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung</string>
|
||||
<string name="downloads_empty">Zur Zeit sind keine Downloads verfügbar.</string>
|
||||
<string name="open_local_video">Lokales Video öffnen</string>
|
||||
|
|
@ -630,18 +610,10 @@
|
|||
<string name="play_from_beginning_img_des">Vom Beginn an spielen</string>
|
||||
<string name="downloads_delete_select">Elemente zum Löschen auswählen</string>
|
||||
<string name="offline_file">Zum Offline-Ansehen verfügbar</string>
|
||||
<string name="delete_message_multiple" formatted="true">Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">Bist du dir sicher, dass du die folgenden Elemente permanent löschen willst? \n \n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Bist du dir sicher, dass du die folgenden Episoden in %1$s permanent löschen willst? \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Du wirst ebenfalls alle Episoden der folgenden Serien permanent löschen: \n \n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Bist du dir sicher, dass du alle Episoden der folgenden Serien permanent löschen willst? \n \n%s</string>
|
||||
<string name="sort_release_date_new">Veröffentlichungsdatum (von neu nach alt)</string>
|
||||
<string name="sort_release_date_old">Veröffentlichungsdatum (von alt nach neu)</string>
|
||||
<string name="preview_seekbar">Suchleisten Vorschau</string>
|
||||
|
|
@ -740,8 +712,8 @@
|
|||
<string name="extra_brightness_settings">Zusätzliche Helligkeit</string>
|
||||
<string name="extra_brightness_settings_des">Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist</string>
|
||||
<string name="extra_brightness_key">Erhöhte Helligkeit aktiviert</string>
|
||||
<string name="show_cast_in_details">Cast-Panel zeigen</string>
|
||||
<string name="video_info">Medieninfo</string>
|
||||
<string name="show_cast_in_details">Zeige Cast-Panel</string>
|
||||
<string name="video_info">Mediainfo</string>
|
||||
<string name="source_name">Quellname</string>
|
||||
<string name="download_all">Alle herunterladen</string>
|
||||
<string name="download_episode_range">Möchtest du Episode %s herunter laden?</string>
|
||||
|
|
@ -759,4 +731,8 @@
|
|||
<string name="queue_empty_message">Es befinden sich keine Downloads in der Warteschlange.</string>
|
||||
<string name="source_priority">Quellpriorität</string>
|
||||
<string name="source_priority_help">Entscheide, wie Videoquellen im Player sortiert werden sollen</string>
|
||||
<string name="show_player_metadata_overlay">Zeige Player-Metadaten</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Vorschau</string>
|
||||
<string name="player_is_live">Live</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -110,8 +110,7 @@
|
|||
<string name="benene_des">Μπανάνα δόθηκε</string>
|
||||
<string name="player_speed_text_format" formatted="true">Ταχύτητα (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">Βαθμολογία: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">Νέα διαθέσιμη ενημέρωση!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Νέα διαθέσιμη ενημέρωση! \n%1$s -> %2$s</string>
|
||||
<string name="double_tap_to_pause_settings_des">Πατήστε δύο φορές στη μέση για παύση</string>
|
||||
<string name="use_system_brightness_settings">Χρήση φωτεινότητας συστήματος</string>
|
||||
<string name="use_system_brightness_settings_des">Χρήση φωτεινότητας συστήματος στο ενσωματωμένο πρόγραμμα αναπαραγωγής, αντί εφαρμογής προεπιλεγμένου σκούρου επικαλύμματος</string>
|
||||
|
|
@ -149,10 +148,8 @@
|
|||
<string name="cancel">Ακύρωση</string>
|
||||
<string name="pause">Παύση</string>
|
||||
<string name="resume">Συνέχιση</string>
|
||||
<string name="delete_message" formatted="true">Αυτό θα διαγράψει μόνιμα το %s
|
||||
\nΕίστε σίγουροι πως θέλετε να προχωρήσετε;</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nαπομένουν</string>
|
||||
<string name="delete_message" formatted="true">Αυτό θα διαγράψει μόνιμα το %s \nΕίστε σίγουροι πως θέλετε να προχωρήσετε;</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nαπομένουν</string>
|
||||
<string name="status_ongoing">Σε εξέλιξη</string>
|
||||
<string name="status">Κατάσταση</string>
|
||||
<string name="year">Έτος</string>
|
||||
|
|
@ -323,9 +320,7 @@
|
|||
<string name="plugins_disabled" formatted="true">Απενεργοποιήθηκε: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Δεν κατέβηκε: %d</string>
|
||||
<string name="plugins_updated" formatted="true">Ενημερώθηκαν %d πρόσθετα</string>
|
||||
<string name="blank_repo_message">Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων.
|
||||
\n
|
||||
\nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο.</string>
|
||||
<string name="blank_repo_message">Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. \n \nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο.</string>
|
||||
<string name="view_public_repositories_button">Προβολή αποθετηρίων κοινότητας</string>
|
||||
<string name="view_public_repositories_button_short">Δημόσια λίστα</string>
|
||||
<string name="uppercase_all_subtitles">Κεφαλοποίηση υποτίτλων</string>
|
||||
|
|
@ -487,23 +482,15 @@
|
|||
<string name="action_remove_from_watched">Αφαίρεση από παρακολουθημένα</string>
|
||||
<string name="browser">Περιηγητής</string>
|
||||
<string name="open_with">Άνοιγμα με</string>
|
||||
<string name="empty_library_no_accounts_message">Η βιβλιοθήκη σας είναι άδεια :(
|
||||
\nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας.</string>
|
||||
<string name="safe_mode_file">Βρέθηκε αρχείο Ασφαλούς Λειτουργίας!
|
||||
\nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο.</string>
|
||||
<string name="empty_library_no_accounts_message">Η βιβλιοθήκη σας είναι άδεια :( \nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας.</string>
|
||||
<string name="safe_mode_file">Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! \nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο.</string>
|
||||
<string name="test_log">Αρχείο Καταγραφής</string>
|
||||
<string name="test_failed">Απέτυχε</string>
|
||||
<string name="test_passed">Πέτυχε</string>
|
||||
<string name="start">Εκκίνηση</string>
|
||||
<string name="no_plugins_found_error">Δε βρέθηκαν επεκτάσεις στο αποθετήριο</string>
|
||||
<string name="no_repository_found_error">Δε βρέθηκε αποθετήριο, ελέγξτε την URL και δοκιμάστε VPN</string>
|
||||
<string name="quality_profile_help">Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο.
|
||||
\n
|
||||
\nΠηγή Α: 3
|
||||
\nΠοιότητα Β: 7
|
||||
\nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10.
|
||||
\n
|
||||
\nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος!</string>
|
||||
<string name="quality_profile_help">Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. \n \nΠηγή Α: 3 \nΠοιότητα Β: 7 \nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. \n \nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος!</string>
|
||||
<string name="category_provider_test">Δοκιμή παρόχου</string>
|
||||
<string name="watch_quality_pref_data">Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου)</string>
|
||||
<string name="jsdelivr_proxy">Διακομιστής μεσολάβησης GitHub</string>
|
||||
|
|
@ -549,9 +536,7 @@
|
|||
<string name="ok">Εντάξει</string>
|
||||
<string name="battery_dialog_title">Απενεργοποιήση της εξοικονόμησης της μπαταρίας</string>
|
||||
<string name="already_voted">Έχετε ήδη ψηφίσει</string>
|
||||
<string name="duplicate_message_single" formatted="true">Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\'
|
||||
\n
|
||||
\nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια;</string>
|
||||
<string name="duplicate_message_single" formatted="true">Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' \n \nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια;</string>
|
||||
<string name="enter_current_pin">Εισαγωγή Τρέχον Κωδικού</string>
|
||||
<string name="lock_profile">Κλείδωμα Προφίλ</string>
|
||||
<string name="biometric_authentication_title">Ξεκλείδωμα Cloudstream</string>
|
||||
|
|
@ -578,11 +563,7 @@
|
|||
<string name="duplicate_title">Πιθανό αντίγραφο βρέθηκε</string>
|
||||
<string name="duplicate_add">Προσθήκη</string>
|
||||
<string name="duplicate_replace">Αντικατάσταση</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη:
|
||||
\n
|
||||
\n%s
|
||||
\n
|
||||
\nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: \n \n%s \n \nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια?</string>
|
||||
<string name="enter_pin_with_name" formatted="true">Εισαγωγή Κωδικού για %s</string>
|
||||
<string name="pin">Κωδικός</string>
|
||||
<string name="pin_error_incorrect">Εσφαλμένος Κωδικός. Προσπαθήστε ξανά.</string>
|
||||
|
|
@ -595,8 +576,7 @@
|
|||
<string name="jsdelivr_proxy_summary">Παράκαμψη απαγόρευσης από raw github URLs χρησιμοποιώντας jsDelivr. Μπορεί να καθυστερήσει τις ενημερώσεις για μερικές μέρες.</string>
|
||||
<string name="rotate_video_desc">Εμφάνιση κουμπιού για περιστροφή οθόνης</string>
|
||||
<string name="favorite">Αγαπημένο</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nαπομένουν</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nαπομένουν</string>
|
||||
<string name="biometric_unsupported">Βιομετρική αυθεντικοποίηση δεν υποστηρίζεται από τη συσκευή</string>
|
||||
<string name="episode_action_cast_mirror">Καστ ταινίας</string>
|
||||
<string name="battery_dialog_message">Για να εξασφαλιστούν αδιάκοπες λήψεις και ειδοποιήσεις για αναγραφόμενες τηλεοπτικές εκπομπές, το CloudStream χρειάζεται άδεια για να τρέξει στο παρασκήνιο. Πατώντας OK, θα εμφανιστεί ένας διάλογος αιτήματος. Παρακαλώ πατήστε \\\"Επιτρέπω\\\".\n\nΠαρακαλώ σημειώστε, αυτή η άδεια δεν σημαίνει ότι το CS3 θα αποστραγγίσει την μπαταρία σας. Θα λειτουργεί στο παρασκήνιο μόνο όταν είναι απαραίτητο, όπως κατά τη λήψη ειδοποιήσεων ή τη λήψη βίντεο από επίσημες επεκτάσεις.</string>
|
||||
|
|
|
|||
|
|
@ -83,8 +83,7 @@
|
|||
<string name="next_episode_time_day_format" formatted="true">%1$dt %2$dh %3$dm</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||
<string name="new_update_format" formatted="true">Nova ĝisdatigo trovita!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Nova ĝisdatigo trovita! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">Speciala epizodo</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="download_started">Elŝuto Komencite</string>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
<string name="batch_download_finish_format" formatted="true">Descargado %1$d %2$s</string>
|
||||
<string name="delete_repository">Borrar repositorio</string>
|
||||
<string name="next_episode_format" formatted="true">El episodio %d se lanzará en</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$d h %2$d m</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d m</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$d h %2$d m</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d m</string>
|
||||
<string name="result_poster_img_des">Póster</string>
|
||||
<string name="extensions">Extensiones</string>
|
||||
<string name="downloaded_file">Archivo descargado</string>
|
||||
|
|
@ -80,8 +80,7 @@
|
|||
<string name="episode_action_reload_links">Recargar enlaces</string>
|
||||
<string name="sync_total_episodes_none">/??</string>
|
||||
<string name="sync_total_episodes_some" formatted="true">/%d</string>
|
||||
<string name="delete_message" formatted="true">Esto eliminará %s permanentemente
|
||||
\nEstá seguro?</string>
|
||||
<string name="delete_message" formatted="true">Esto eliminará %s permanentemente \nEstá seguro?</string>
|
||||
<string name="confirm_exit_dialog">¿Seguro que quieres salir?</string>
|
||||
<string name="popup_resume_download">Continuar Descarga</string>
|
||||
<string name="example_lang_name">Código de idioma (es_ES)</string>
|
||||
|
|
@ -104,7 +103,7 @@
|
|||
<string name="player_speed_text_format" formatted="true">Velocidad (%.2f×)</string>
|
||||
<string name="skip_loading">Omitir carga</string>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep. %2$d</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%1$d d %2$d h %3$d m</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%1$d d %2$d h %3$d m</string>
|
||||
<string name="cast_format" formatted="true">Reparto: %s</string>
|
||||
<string name="filler" formatted="true">Relleno</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
|
|
@ -249,8 +248,7 @@
|
|||
<string name="resume">Continuar</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nfaltante</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nfaltante</string>
|
||||
<string name="status_ongoing">En curso</string>
|
||||
<string name="status_completed">Completado</string>
|
||||
<string name="status">Estado</string>
|
||||
|
|
@ -482,10 +480,8 @@
|
|||
<string name="sort_alphabetical_z">Alfabéticamente (Z a A)</string>
|
||||
<string name="select_library">Seleccionar biblioteca</string>
|
||||
<string name="open_with">Abrir con</string>
|
||||
<string name="empty_library_no_accounts_message">Tu biblioteca está vacía :(
|
||||
\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local.</string>
|
||||
<string name="safe_mode_file">¡Se encontró un archivo en modo seguro!
|
||||
\nNo cargar ninguna extensión al inicio hasta que se elimine el archivo.</string>
|
||||
<string name="empty_library_no_accounts_message">Tu biblioteca está vacía :( \nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local.</string>
|
||||
<string name="safe_mode_file">¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo.</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Reproductor visible - buscar cantidad</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Reproductor oculto - buscar cantidad</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
|
|
@ -510,13 +506,7 @@
|
|||
<string name="pref_category_bypass">ISP Bypasses</string>
|
||||
<string name="watch_quality_pref_data">Calidad de visualización preferida (Datos móviles)</string>
|
||||
<string name="help">Ayuda</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="profile_number">Perfil %d</string>
|
||||
<string name="wifi">Wifi</string>
|
||||
<string name="edit">Editar</string>
|
||||
|
|
@ -536,11 +526,7 @@
|
|||
<string name="favorite_removed">%s eliminado de favoritos</string>
|
||||
<string name="favorites_list_name">Favoritos</string>
|
||||
<string name="favorite_added">%s añadido a favoritos</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_title">Posible duplicado encontrado</string>
|
||||
<string name="lock_profile">Bloquear perfil</string>
|
||||
<string name="action_add_to_favorites">Añadido a favoritos</string>
|
||||
|
|
@ -583,8 +569,7 @@
|
|||
<string name="biometric_warning">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.</string>
|
||||
<string name="favorite">Favorito</string>
|
||||
<string name="unfavorite">Eliminar de favoritos</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nrestante</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nrestante</string>
|
||||
<string name="repo_copy_label">Nombre y URL del repositorio</string>
|
||||
<string name="toast_copied">¡Copiado!</string>
|
||||
<string name="clipboard_unknown_error">Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación.</string>
|
||||
|
|
@ -609,7 +594,7 @@
|
|||
<string name="qr_image">Imagen del código QR</string>
|
||||
<string name="dismiss">Descartar</string>
|
||||
<string name="open_downloaded_repo">Abrir repositorio</string>
|
||||
<string name="device_pin_url_message">Visita <b> %s </b> en tu smartphone o ordenador e introduce el código anterior</string>
|
||||
<string name="device_pin_url_message">Visita <b>%s</b> en tu smartphone o equipo e introduce el código anterior</string>
|
||||
<string name="device_pin_expired_message">¡El código PIN ya ha caducado!</string>
|
||||
<string name="device_pin_counter_text">El código caduca en %1$d mín y %2$d s</string>
|
||||
<string name="device_pin_error_message">No puedo obtener el código PIN del dispositivo; intente con la autenticación local</string>
|
||||
|
|
@ -621,24 +606,16 @@
|
|||
<string name="hide_player_control_names">Ocultar los nombres de los controles del reproductor</string>
|
||||
<string name="sort_release_date_old">Fecha de lanzamiento (antigua a nueva)</string>
|
||||
<string name="sort_release_date_new">Fecha de lanzamiento (de nueva a antigua)</string>
|
||||
<string name="delete_message_series_only" formatted="true">¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">¿Estás seguro de que quieres borrar permanentemente todos los episodios de la serie? \n \n%s</string>
|
||||
<string name="downloads_delete_select">Seleccionar elementos para eliminar</string>
|
||||
<string name="offline_file">Disponible para visualizar sin conexión</string>
|
||||
<string name="select_all">Seleccionar todo</string>
|
||||
<string name="deselect_all">Deseleccionar todo</string>
|
||||
<string name="delete_files">Borrar archivos</string>
|
||||
<string name="delete_format" formatted="true">Borrar (%1$d | %2$s)</string>
|
||||
<string name="delete_message_multiple" formatted="true">¿Seguro que quieres borrar de forma permanente los siguientes elementos?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">También borrará permanentemente todos los episodios de las siguientes series:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">¿Seguro que quieres borrar de forma permanente los siguientes elementos? \n \n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">¿Estás seguro de que deseas eliminar permanentemente los siguientes episodios en %1$s? \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">También borrará permanentemente todos los episodios de las siguientes series: \n \n%s</string>
|
||||
<string name="preview_seekbar_desc">Activar la previsualización para las miniaturas en la barra de búsqueda</string>
|
||||
<string name="preview_seekbar">Previsualización de Seekbar</string>
|
||||
<string name="no_subtitles_loaded">Aún no hay subtítulos cargados</string>
|
||||
|
|
@ -695,9 +672,9 @@
|
|||
<string name="overscan_settings">Sobreexploración</string>
|
||||
<string name="poster_size_settings_des">Cambios en el tamaño de los pósteres</string>
|
||||
<string name="poster_size_settings">Tamaño del póster</string>
|
||||
<string name="download_time_left_hour_min_sec_format" formatted="true">%1$d h %2$d m %3$d s</string>
|
||||
<string name="download_time_left_min_sec_format" formatted="true">%1$d m %2$d s</string>
|
||||
<string name="download_time_left_sec_format" formatted="true">%1$d s</string>
|
||||
<string name="download_time_left_hour_min_sec_format" formatted="true">%1$d h %2$d m %3$d s</string>
|
||||
<string name="download_time_left_min_sec_format" formatted="true">%1$d m %2$d s</string>
|
||||
<string name="download_time_left_sec_format" formatted="true">%1$d s</string>
|
||||
<string name="show_rating">Etiqueta de valoración</string>
|
||||
<string name="speedup_summary">Mantenga presionado para duplicar la velocidad</string>
|
||||
<string name="no_account">Sin cuenta</string>
|
||||
|
|
@ -756,4 +733,8 @@
|
|||
<item quantity="many">%d descargas encoladas</item>
|
||||
<item quantity="other">%d descargas encoladas</item>
|
||||
</plurals>
|
||||
<string name="show_player_metadata_overlay">Mostrar superposición de metadatos del jugador</string>
|
||||
<string name="video_singular">Vídeo</string>
|
||||
<string name="skip_type_preview">Vista previa</string>
|
||||
<string name="player_is_live">En Vivo</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -113,8 +113,7 @@
|
|||
<string name="type_watching">در حال تماشا</string>
|
||||
<string name="title_downloads">بارگیریها</string>
|
||||
<string name="player_speed_text_format" formatted="true">سرعت (%.2f برابر)</string>
|
||||
<string name="new_update_format" formatted="true">بروزرسانی جدید پیدا شد!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">بروزرسانی جدید پیدا شد! \n%1$s -> %2$s</string>
|
||||
<string name="play_movie_button">پخش فیلم</string>
|
||||
<string name="browser">مرورگر</string>
|
||||
<string name="play_episode">پخش قسمت</string>
|
||||
|
|
@ -130,8 +129,7 @@
|
|||
<string name="subs_hold_to_reset_to_default">برای بازنشانی به پیشفرض نگهدارید</string>
|
||||
<string name="library">کتابخانه</string>
|
||||
<string name="status_ongoing">در ادامه</string>
|
||||
<string name="delete_message" formatted="true">این فرآیند بطور کامل %s را حذف میکند
|
||||
\nآیا از این کار اطمینان دارید؟</string>
|
||||
<string name="delete_message" formatted="true">این فرآیند بطور کامل %s را حذف میکند \nآیا از این کار اطمینان دارید؟</string>
|
||||
<string name="repo_copy_label">نام مخزن و نشانی</string>
|
||||
<string name="toast_copied">کپی شد!</string>
|
||||
<string name="settings_info">درباره</string>
|
||||
|
|
@ -180,13 +178,11 @@
|
|||
<string name="delete_file">حذف پرونده</string>
|
||||
<string name="show_trailers_settings">نمایش تریلر ها</string>
|
||||
<string name="episodes">قسمتها</string>
|
||||
<string name="resume_time_left" formatted="true">%dد
|
||||
\nباقیمانده</string>
|
||||
<string name="resume_time_left" formatted="true">%dد \nباقیمانده</string>
|
||||
<string name="github">گیتهاب</string>
|
||||
<string name="pref_filter_search_quality">پنهان کردن ویدیو مشخص شده از نتایج جستجو</string>
|
||||
<string name="cancel">لغو</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nباقیمانده</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nباقیمانده</string>
|
||||
<string name="action_default">پیشفرض</string>
|
||||
<string name="cartoons_singular">کارتون</string>
|
||||
<string name="torrent_singular">تورنت</string>
|
||||
|
|
|
|||
|
|
@ -79,8 +79,7 @@
|
|||
<string name="cancel">Annuler</string>
|
||||
<string name="pause">Pause</string>
|
||||
<string name="resume">Reprendre</string>
|
||||
<string name="delete_message">Cela va supprimer définitivement %s
|
||||
\nÊtes-vous sûr ?</string>
|
||||
<string name="delete_message">Cela va supprimer définitivement %s \nÊtes-vous sûr ?</string>
|
||||
<string name="status_ongoing">En cours</string>
|
||||
<string name="status_completed">Terminé</string>
|
||||
<string name="status">Statut</string>
|
||||
|
|
@ -122,8 +121,7 @@
|
|||
<string name="update">Mettre à jour</string>
|
||||
<string name="dns_pref_summary">Utile pour contourner les bloquages des FAI</string>
|
||||
<string name="download_path_pref">Emplacement de téléchargement</string>
|
||||
<string name="new_update_format" formatted="true">Nouvelle mise à jour trouvée !
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Nouvelle mise à jour trouvée ! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">Épisode spécial</string>
|
||||
<string name="watch_quality_pref">Qualité de visionnage préférée (WiFi)</string>
|
||||
<string name="video_buffer_size_settings">Taille de la mémoire cache</string>
|
||||
|
|
@ -137,7 +135,7 @@
|
|||
<string name="display_subbed_dubbed_settings">Afficher les animés en Anglais (Dub) / sous-titrés</string>
|
||||
<string name="phone_layout">Disposition en mode téléphone</string>
|
||||
<string name="app_dub_sub_episode_text_format">%1$s Ep %2$d</string>
|
||||
<string name="rated_format" formatted="true">Note : %.1f</string>
|
||||
<string name="rated_format" formatted="true">Note : %.1f</string>
|
||||
<string name="resize_zoom">Zoom</string>
|
||||
<string name="resize_fit">Adapter à l\'écran</string>
|
||||
<string name="app_layout">Disposition de l\'application</string>
|
||||
|
|
@ -145,7 +143,7 @@
|
|||
<string name="provider_lang_settings">Langues des extensions</string>
|
||||
<string name="preferred_media_settings">Médias préférées</string>
|
||||
<string name="automatic">Auto</string>
|
||||
<string name="cast_format">Distribution : %s</string>
|
||||
<string name="cast_format">Distribution : %s</string>
|
||||
<string name="duration_format">%d min</string>
|
||||
<string name="search_hint_site">Rechercher sur %s…</string>
|
||||
<string name="type_re_watching">À re-regarder</string>
|
||||
|
|
@ -291,8 +289,8 @@
|
|||
<string name="lightnovel">Application Light Novel par les mêmes devs</string>
|
||||
<string name="anim">Anime app by the same devs</string>
|
||||
<string name="discord">Rejoignez le Discord</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$d h %2$d min</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d min</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$d h %2$d min</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d min</string>
|
||||
<string name="play_with_app_name">Lire avec CloudStream</string>
|
||||
<string name="play_livestream_button">Lire en direct</string>
|
||||
<string name="skip_type_ed">Fin</string>
|
||||
|
|
@ -306,9 +304,9 @@
|
|||
<string name="skip_type_intro">Intro</string>
|
||||
<string name="clear_history">Effacer l\'historique</string>
|
||||
<string name="yes">Oui</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%1$d j %2$d h %3$d min</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%1$d j %2$d h %3$d min</string>
|
||||
<string name="stream">Stream</string>
|
||||
<string name="confirm_exit_dialog">Êtes-vous sûr·e de vouloir quitter ?</string>
|
||||
<string name="confirm_exit_dialog">Êtes-vous sûr·e de vouloir quitter ?</string>
|
||||
<string name="no">Non</string>
|
||||
<string name="update_notification_downloading">Téléchargement de la mise à jour…</string>
|
||||
<string name="next_episode_format" formatted="true">L\'épisode %d sera publié dans</string>
|
||||
|
|
@ -321,8 +319,7 @@
|
|||
<string name="example_site_name">Nouveau Nom du site</string>
|
||||
<string name="error_invalid_id">ID invalide</string>
|
||||
<string name="automatic_plugin_download_summary">Installer automatiquement les plugins qui sont dans les repository mais qui n\'ont pas encore été installés.</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nrestant</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nrestant</string>
|
||||
<string name="livestreams">En direct</string>
|
||||
<string name="others">Autres</string>
|
||||
<string name="live_singular">En direct</string>
|
||||
|
|
@ -363,7 +360,7 @@
|
|||
<string name="nsfw">NSFW</string>
|
||||
<string name="example_ip">127.0.0.1</string>
|
||||
<string name="sync_score_format" formatted="true">%d / 10</string>
|
||||
<string name="sync_total_episodes_none">/??</string>
|
||||
<string name="sync_total_episodes_none">/ ??</string>
|
||||
<string name="sync_total_episodes_some" formatted="true">/%d</string>
|
||||
<string name="quality_sd">SD</string>
|
||||
<string name="quality_uhd">UHD</string>
|
||||
|
|
@ -409,14 +406,14 @@
|
|||
<string name="batch_download_finish_format" formatted="true">Téléchargé %1$d %2$s</string>
|
||||
<string name="batch_download_nothing_to_download_format" formatted="true">Tous les %s déjà téléchargés</string>
|
||||
<string name="setup_extensions_subtext">Télécharger la liste de sites que vous voulez utiliser</string>
|
||||
<string name="plugins_downloaded" formatted="true">Téléchargé : %d</string>
|
||||
<string name="plugins_downloaded" formatted="true">Téléchargé : %d</string>
|
||||
<string name="video_tracks">Pistes vidéo</string>
|
||||
<string name="apply_on_restart">Redémarrez l\'application pour voir les changements.</string>
|
||||
<string name="safe_mode_description">Toutes les extensions ont été désactivé à cause d\'un crash pour vous aider à trouver l\'extension causant le problème.</string>
|
||||
<string name="safe_mode_title">Mode sans échec activé</string>
|
||||
<string name="extension_size">Taille</string>
|
||||
<string name="extension_version">Version</string>
|
||||
<string name="extension_rating" formatted="true">Note : %s</string>
|
||||
<string name="extension_rating" formatted="true">Note : %s</string>
|
||||
<string name="extension_description">Description</string>
|
||||
<string name="extension_status">Status</string>
|
||||
<string name="extension_install_first">Installer l\'extension d\'abord</string>
|
||||
|
|
@ -432,10 +429,10 @@
|
|||
<string name="repository_name_hint">Nom de dépôt (optionnel)</string>
|
||||
<string name="plugin_singular">plugin</string>
|
||||
<string name="delete_repository">Supprimer le repository</string>
|
||||
<string name="plugins_disabled" formatted="true">Désactivé : %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Non téléchargé : %d</string>
|
||||
<string name="plugins_disabled" formatted="true">Désactivé : %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Non téléchargé : %d</string>
|
||||
<string name="plugins_updated" formatted="true">%d plugins mis-à-jour</string>
|
||||
<string name="download_all_plugins_from_repo">Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci !</string>
|
||||
<string name="download_all_plugins_from_repo">Avertissement : CloudStream 3 décline toute responsabilité concernant l’utilisation d’extensions tierces et ne fournit aucun support pour celles-ci !</string>
|
||||
<string name="single_plugin_disabled" formatted="true">%s (Désactivé)</string>
|
||||
<string name="tracks">Pistes</string>
|
||||
<string name="audio_tracks">Pistes audio</string>
|
||||
|
|
@ -448,9 +445,7 @@
|
|||
<string name="apk_installer_package_installer">Installateur de paquet</string>
|
||||
<string name="plugin">plugins</string>
|
||||
<string name="delete_repository_plugins">Cela supprimera également tous les plugins du repository</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="extension_language">Langage</string>
|
||||
<string name="enable_skip_op_from_database_des">Afficher les popups skip pour les intro / fins</string>
|
||||
<string name="apk_installer_legacy">Ancienne méthode d\'installation</string>
|
||||
|
|
@ -479,8 +474,7 @@
|
|||
<string name="sort_rating_asc">Note (basse à haute)</string>
|
||||
<string name="sort_rating_desc">Note (haut à bas)</string>
|
||||
<string name="sort_alphabetical_a">Alphabétique (A à Z)</string>
|
||||
<string name="empty_library_no_accounts_message">Votre bibliothèque est vide :(
|
||||
\nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale.</string>
|
||||
<string name="empty_library_no_accounts_message">Votre bibliothèque est vide :( \nConnectez-vous à un compte de bibliothèque ou ajoutez des séries à votre bibliothèque locale.</string>
|
||||
<string name="empty_library_logged_in_message">Cette liste est vide. Essayez d\'en changer.</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="sort_by">Trier par</string>
|
||||
|
|
@ -489,8 +483,7 @@
|
|||
<string name="open_with">Ouvrir avec</string>
|
||||
<string name="sort_updated_new">Mis à jour (Nouveau vers ancien)</string>
|
||||
<string name="sort_updated_old">Mis à jour (ancien vers nouveau)</string>
|
||||
<string name="safe_mode_file">Fichier du mode sans échec trouvé !
|
||||
\nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé.</string>
|
||||
<string name="safe_mode_file">Fichier du mode sans échec trouvé ! \nAucune extension ne sera chargée au démarrage avant que le fichier ne soit enlevé.</string>
|
||||
<string name="stop">Arrêter</string>
|
||||
<string name="revert">Annuler</string>
|
||||
<string name="test_log">Enregistrer</string>
|
||||
|
|
@ -515,13 +508,7 @@
|
|||
<string name="jsdelivr_enabled">Impossible d\'atteindre GitHub. Activation du proxy jsDelivr…</string>
|
||||
<string name="already_voted">Vous avez déjà voté</string>
|
||||
<string name="disable">Désactivé</string>
|
||||
<string name="quality_profile_help">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é !</string>
|
||||
<string name="quality_profile_help">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é !</string>
|
||||
<string name="no_plugins_found_error">Aucun plugin trouvé dans ce dossier</string>
|
||||
<string name="no_repository_found_error">Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN</string>
|
||||
<string name="mobile_data">Données mobiles</string>
|
||||
|
|
@ -554,20 +541,14 @@
|
|||
<string name="pin">PIN</string>
|
||||
<string name="favorites_list_name">Favoris</string>
|
||||
<string name="logged_account" formatted="true">Connecté en tant que %s</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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 ?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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 ?</string>
|
||||
<string name="enter_pin_with_name" formatted="true">Saisir le code PIN pour %s</string>
|
||||
<string name="duplicate_title">Doublon potentiel trouvé</string>
|
||||
<string name="lock_profile">Verrouiller le profil</string>
|
||||
<string name="skip_startup_account_select_pref">Ignorer la sélection de compte au démarrage</string>
|
||||
<string name="action_unsubscribe">Se désabonner</string>
|
||||
<string name="action_subscribe">S\'abonner</string>
|
||||
<string name="duplicate_message_single" formatted="true">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 ?</string>
|
||||
<string name="duplicate_message_single" formatted="true">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 ?</string>
|
||||
<string name="enter_current_pin">Saisir le code PIN actuel</string>
|
||||
<string name="rotate_video">Pivoter</string>
|
||||
<string name="links_reloaded_toast">Les liens ont été rechargés</string>
|
||||
|
|
@ -580,7 +561,7 @@
|
|||
<string name="test_extensions">Testez toutes les extensions</string>
|
||||
<string name="recommendations_tooltip">Afficher les recommandations</string>
|
||||
<string name="test_extensions_summary">Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension.</string>
|
||||
<string name="toast_copied">Copié !</string>
|
||||
<string name="toast_copied">Copié !</string>
|
||||
<string name="repo_copy_label">Nom du dépôt et adresse internet</string>
|
||||
<string name="favorite">Favori</string>
|
||||
<string name="biometric_warning">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.</string>
|
||||
|
|
@ -619,10 +600,10 @@
|
|||
<string name="open_downloaded_repo">Ouvrir le dépôt</string>
|
||||
<string name="device_pin_counter_text">Code expire dans %1$dm %2$ds</string>
|
||||
<string name="cs3wiki">Wiki de CloudStream</string>
|
||||
<string name="device_pin_expired_message">Le code PIN est maintenant expiré !</string>
|
||||
<string name="device_pin_expired_message">Le code PIN est maintenant expiré !</string>
|
||||
<string name="qr_image">Image du code QR</string>
|
||||
<string name="delete_plugin">Supprimer l\'extension</string>
|
||||
<string name="delete_message_multiple" formatted="true">Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">Êtes-vous sûr de vouloir supprimer définitivement les éléments suivants ? \n \n%s</string>
|
||||
<string name="auth_locally">Authentification locale</string>
|
||||
<string name="sort_release_date_old">Date de sortie (du plus ancien au plus récent)</string>
|
||||
<string name="sort_release_date_new">Date de sortie (du plus récent au plus ancien)</string>
|
||||
|
|
@ -638,9 +619,9 @@
|
|||
<string name="pref_category_accounts">Comptes</string>
|
||||
<string name="torrent_info">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.</string>
|
||||
<string name="dismiss">Ignorer</string>
|
||||
<string name="delete_message_series_section" formatted="true">Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Vous allez également supprimer définitivement tous les épisodes des séries suivantes :\n\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Êtes-vous sûr de vouloir supprimer définitivement tous les épisodes des séries suivantes ?\n\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Êtes-vous sûr de vouloir supprimer définitivement les épisodes suivants dans %1$s ?\n\n%2$s</string>
|
||||
<string name="episode_action_cast_mirror">Recopier l’écran</string>
|
||||
<string name="no_subtitles_loaded">Aucun sous-titre n’a encore été chargé</string>
|
||||
<string name="backup_path_title">Emplacement du dossier de sauvegarde</string>
|
||||
|
|
@ -660,7 +641,7 @@
|
|||
<string name="sort_button_rating">Note %s</string>
|
||||
<string name="sort_button_date">Date %s</string>
|
||||
<string name="update_plugins">Mettre à jour les plugins</string>
|
||||
<string name="plugins_updated_manually">%d plugin(s) mis à jour avec succès !</string>
|
||||
<string name="plugins_updated_manually">%d plugin(s) mis à jour avec succès !</string>
|
||||
<string name="no_plugins_updated_manually">Aucun plugin n\'a été mis à jour.</string>
|
||||
<string name="sort_episodes_rating_high_low">Note (Plus Haute)</string>
|
||||
<string name="update_plugins_manually">Mettre à jour les plugins manuellement</string>
|
||||
|
|
@ -671,7 +652,7 @@
|
|||
<string name="player_notification_channel_description">La notification du lecteur pour contrôler la lecture en arrière-plan</string>
|
||||
<string name="sort_episodes_date_newest">Date (Plus Récent)</string>
|
||||
<string name="sort_episodes_rating_low_high">Note (Plus Basse)</string>
|
||||
<string name="starting_plugin_update_manually">Démarrage du processus de mise à jour du plugin !</string>
|
||||
<string name="starting_plugin_update_manually">Démarrage du processus de mise à jour du plugin !</string>
|
||||
<string name="subtitles_from_embedded">Intégré</string>
|
||||
<string name="subtitles_from_online">En ligne</string>
|
||||
<string name="speech_recognition_unavailable">La reconnaissance vocale n\'est pas disponible</string>
|
||||
|
|
@ -741,8 +722,8 @@
|
|||
<string name="source_priority_help">Déterminez comment les sources vidéo seront triées dans le lecteur</string>
|
||||
<string name="download_all">Télécharger tout</string>
|
||||
<string name="cancel_all">Tout annuler</string>
|
||||
<string name="download_episode_range">Voulez-vous télécharger l\'épisode %s ?</string>
|
||||
<string name="cancel_queue_message">Vous voulez annuler tous les téléchargements en file d\'attente ?</string>
|
||||
<string name="download_episode_range">Voulez-vous télécharger l\'épisode %s ?</string>
|
||||
<string name="cancel_queue_message">Vous voulez annuler tous les téléchargements en file d\'attente ?</string>
|
||||
<plurals name="downloads_active">
|
||||
<item quantity="one">%d téléchargement actif</item>
|
||||
<item quantity="many">%d téléchargements actifs</item>
|
||||
|
|
@ -754,4 +735,7 @@
|
|||
<item quantity="other">%d téléchargements en attente</item>
|
||||
</plurals>
|
||||
<string name="show_player_metadata_overlay">Afficher les métadata de l\'overlay du lecteur vidéo</string>
|
||||
<string name="video_singular">Vidéo</string>
|
||||
<string name="skip_type_preview">Prévisualisation</string>
|
||||
<string name="player_is_live">Direct</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@
|
|||
<string name="episode_poster_img_des">Póster do Episodio</string>
|
||||
<string name="go_back_img_des">Regresar</string>
|
||||
<string name="home_change_provider_img_des">Cambiar provedor</string>
|
||||
<string name="new_update_format" formatted="true">Nova actualización atopada!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Nova actualización atopada! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">Recheo</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
<string name="title_settings">Configuración</string>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
<resources>
|
||||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">स्पीड (%.2fx)</string>
|
||||
<string name="new_update_format" formatted="true">नया अपडेट आया है!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">नया अपडेट आया है! \n%1$s -> %2$s</string>
|
||||
<string name="title_home">होम</string>
|
||||
<string name="title_search">खोजें</string>
|
||||
<string name="title_downloads">डाउनलोडस</string>
|
||||
|
|
@ -87,8 +86,7 @@
|
|||
<string name="cancel">रद्द करें</string>
|
||||
<string name="pause">रोकें</string>
|
||||
<string name="resume">फिर से चलाएं</string>
|
||||
<string name="delete_message">इससे %s स्थायी रूप से हट जाएगा
|
||||
\nक्या आपका निर्णय निश्चित है ?</string>
|
||||
<string name="delete_message">इससे %s स्थायी रूप से हट जाएगा \nक्या आपका निर्णय निश्चित है ?</string>
|
||||
<string name="status_ongoing">अभी चालू है</string>
|
||||
<string name="status_completed">मुकम्मल हुया</string>
|
||||
<string name="status">स्थिति</string>
|
||||
|
|
@ -153,11 +151,7 @@
|
|||
<string name="duration_format" formatted="true">%d मिनट</string>
|
||||
<string name="app_name">क्लाउडस्ट्रीम</string>
|
||||
<string name="play_with_app_name">क्लाउडस्ट्रीम के साथ चलाएं</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं:
|
||||
\n
|
||||
\n%s
|
||||
\n
|
||||
\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">आपकी लाइब्रेरी में संभावित डुप्लिकेट आइटम पाए गए हैं: \n \n%s \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे?</string>
|
||||
<string name="enter_pin_with_name" formatted="true">%s के लिए पिन दर्ज करें</string>
|
||||
<string name="duplicate_title">संभावित डुप्लिकेट मिला</string>
|
||||
<string name="update_started">अपडेट शुरू हुआ</string>
|
||||
|
|
@ -177,9 +171,7 @@
|
|||
<string name="select_an_account">अकाउंट चुनिये</string>
|
||||
<string name="skip_loading">लोडिंग स्किप करे</string>
|
||||
<string name="loading">लोडिंग…</string>
|
||||
<string name="duplicate_message_single" formatted="true">ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\'
|
||||
\n
|
||||
\nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे?</string>
|
||||
<string name="duplicate_message_single" formatted="true">ऐसा प्रतीत होता है कि संभावित रूप से डुप्लिकेट आइटम आपकी लाइब्रेरी में पहले से मौजूद है: \'%s.\' \n \nक्या आप किसी भी तरह इस आइटम को जोड़ना चाहेंगे, मौजूदा आइटम को बदलना चाहेंगे, या कार्रवाई रद्द करना चाहेंगे?</string>
|
||||
<string name="enter_pin">पिन दर्ज करें</string>
|
||||
<string name="pin">पिन</string>
|
||||
<string name="links_reloaded_toast">लिंक पुन्ह खुली</string>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@
|
|||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">Brzina (%.2f×)</string>
|
||||
<string name="rated_format" formatted="true">Ocjena: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">Pronađeno je novo ažuriranje!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Pronađeno je novo ažuriranje! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">Umetak</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
|
|
@ -186,10 +185,8 @@
|
|||
<string name="resume">Nastavi</string>
|
||||
<string name="go_back_30">−30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="delete_message" formatted="true">Ovo će trajno izbrisati %s
|
||||
\nJeste li sigurni?</string>
|
||||
<string name="resume_time_left" formatted="true">%dmin
|
||||
\npreostalo</string>
|
||||
<string name="delete_message" formatted="true">Ovo će trajno izbrisati %s \nJeste li sigurni?</string>
|
||||
<string name="resume_time_left" formatted="true">%dmin \npreostalo</string>
|
||||
<string name="status_ongoing">U tijeku</string>
|
||||
<string name="status_completed">Završeno</string>
|
||||
<string name="status">Status</string>
|
||||
|
|
@ -411,9 +408,7 @@
|
|||
<string name="plugins_downloaded" formatted="true">Preuzeto: %d</string>
|
||||
<string name="plugins_disabled" formatted="true">Onemogućeno: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Nepreuzeto: %d</string>
|
||||
<string name="blank_repo_message">CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija.
|
||||
\n
|
||||
\nPridružite se našem Discordu ili tražite online.</string>
|
||||
<string name="blank_repo_message">CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. \n \nPridružite se našem Discordu ili tražite online.</string>
|
||||
<string name="view_public_repositories_button">Prikaži repozitorije zajednice</string>
|
||||
<string name="view_public_repositories_button_short">Javni popis</string>
|
||||
<string name="uppercase_all_subtitles">Koristi velika slova za sve titlove</string>
|
||||
|
|
@ -498,11 +493,9 @@
|
|||
<string name="sort_alphabetical_z">Abecedno (Ž do A)</string>
|
||||
<string name="select_library">Odaberite biblioteku</string>
|
||||
<string name="open_with">Otvori sa</string>
|
||||
<string name="empty_library_no_accounts_message">Vaša je biblioteka prazna :(
|
||||
\nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku.</string>
|
||||
<string name="empty_library_no_accounts_message">Vaša je biblioteka prazna :( \nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku.</string>
|
||||
<string name="empty_library_logged_in_message">Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu.</string>
|
||||
<string name="safe_mode_file">Pronađena je datoteka sigurnog načina rada!
|
||||
\nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni.</string>
|
||||
<string name="safe_mode_file">Pronađena je datoteka sigurnog načina rada! \nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni.</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Prikazan player – Količina pomicanja</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Količina pomicanja koja se koristi kada je player vidljiv</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Player skriven – Količina pomicanja</string>
|
||||
|
|
@ -541,23 +534,13 @@
|
|||
<string name="disable">Onemogući</string>
|
||||
<string name="no_plugins_found_error">U repozitoriju nisu pronađeni dodaci</string>
|
||||
<string name="no_repository_found_error">Repozitorij nije pronađen. Provjeri URL i pokušaj VPN</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="already_voted">Već si glasao/la</string>
|
||||
<string name="backup_frequency">Učestalost spremanja sigurnosne kopije</string>
|
||||
<string name="favorite_removed">%s uklonjeno iz favorita</string>
|
||||
<string name="favorites_list_name">Favoriti</string>
|
||||
<string name="favorite_added">%s dodano u favorite</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_title">Pronađen potencijalni duplikat</string>
|
||||
<string name="lock_profile">Zaključaj profil</string>
|
||||
<string name="action_add_to_favorites">Dodaj u favorite</string>
|
||||
|
|
@ -570,9 +553,7 @@
|
|||
<string name="action_subscribe">Pretplata</string>
|
||||
<string name="action_remove_from_favorites">Ukloni iz favorita</string>
|
||||
<string name="select_an_account">Odaberite račun</string>
|
||||
<string name="duplicate_message_single" formatted="true">Č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?</string>
|
||||
<string name="duplicate_message_single" formatted="true">Č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?</string>
|
||||
<string name="enter_pin">Unesite PIN</string>
|
||||
<string name="pin">PIN</string>
|
||||
<string name="enter_current_pin">Unesite trenutni PIN</string>
|
||||
|
|
@ -596,8 +577,7 @@
|
|||
<string name="repo_copy_label">Ime repozitorija i URL</string>
|
||||
<string name="toast_copied">kopirano!</string>
|
||||
<string name="biometric_setting">Zaključaj s biometrijskim podatcima</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\npreostalo</string>
|
||||
<string name="resume_remaining" formatted="true">%s \npreostalo</string>
|
||||
<string name="clipboard_permission_error">Pogreška pri pristupanju međuspremnika. Pokušaj ponovo.</string>
|
||||
<string name="biometric_authentication_title">Otključaj CloudStream</string>
|
||||
<string name="password_pin_authentication_title">Lozinka/PIN autentifikacija</string>
|
||||
|
|
@ -616,7 +596,7 @@
|
|||
<string name="biometric_setting_summary">Otključaj aplikaciju pomoću otiska prsta, ID-a lica, PIN-a, uzorka i lozinke.</string>
|
||||
<string name="episode_upcoming_format" formatted="true">Sljedeća u %s</string>
|
||||
<string name="clipboard_unknown_error">Pogreška pri kopiranju. Kopirajte zapisnik i kontaktirajte podršku aplikacije.</string>
|
||||
<string name="battery_dialog_message">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.</string>
|
||||
<string name="battery_dialog_message">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.</string>
|
||||
<string name="biometric_warning">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.</string>
|
||||
<string name="next_season_episode_format" formatted="true">Sezona %1$d epizoda %2$d izlazi za</string>
|
||||
<string name="episode_action_cast_mirror">Cast duplikat</string>
|
||||
|
|
@ -642,22 +622,14 @@
|
|||
<string name="delete_plugin">Izbriši dodatak</string>
|
||||
<string name="offline_file">Dostupno za gledanje offline</string>
|
||||
<string name="select_all">Označi sve</string>
|
||||
<string name="delete_message_multiple" formatted="true">Stvarno želite trajno izbrisati sljedeće stavke?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Stvarno želite trajno izbrisati sljedeće epizode u %1$s?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Trajno ćete izbrisati i sve epizode u sljedećim serijama:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">Stvarno želite trajno izbrisati sljedeće stavke? \n \n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Stvarno želite trajno izbrisati sljedeće epizode u %1$s? \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Trajno ćete izbrisati i sve epizode u sljedećim serijama: \n \n%s</string>
|
||||
<string name="downloads_delete_select">Odaberi stavke za brisanje</string>
|
||||
<string name="deselect_all">Odznači sve</string>
|
||||
<string name="delete_format" formatted="true">Izbriši (%1$d | %2$s)</string>
|
||||
<string name="delete_files">Izbriši datoteke</string>
|
||||
<string name="delete_message_series_only" formatted="true">Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Stvarno želite trajno izbrisati sve epizode u sljedećoj seriji? \n \n%s</string>
|
||||
<string name="no_subtitles_loaded">Još nije učitan nijedan titl</string>
|
||||
<string name="preview_seekbar">Pretpregled trake za traženje</string>
|
||||
<string name="preview_seekbar_desc">Omogući minijaturu pregleda na traci za pretraživanje</string>
|
||||
|
|
@ -749,7 +721,7 @@
|
|||
<string name="top_center">Gore u sredini</string>
|
||||
<string name="top_right">Gore desno</string>
|
||||
<string name="extra_brightness_settings">Dodatna svjetlina</string>
|
||||
<string name="extra_brightness_settings_des">Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana</string>
|
||||
<string name="extra_brightness_settings_des">Uključi filtar svjetline kada se prekorači 100 % svjetline ekrana</string>
|
||||
<string name="extra_brightness_key">dodatna_svjetlina_uključena</string>
|
||||
<string name="search_suggestions">Prijedlozi za pretraživanje</string>
|
||||
<string name="search_suggestions_des">Prikaži prijedloge za pretraživanje tijekom tipkanja</string>
|
||||
|
|
@ -775,4 +747,8 @@
|
|||
<string name="download_episode_range">Želiš li preuzeti epizodu %s?</string>
|
||||
<string name="cancel_queue_message">Želiš li otkazati sva preuzimanja u redu čekanja?</string>
|
||||
<string name="show_cast_in_details">Prikaži ploču glumačke postave</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Pregled</string>
|
||||
<string name="player_is_live">Uživo</string>
|
||||
<string name="show_player_metadata_overlay">Prikaži sloj metapodataka playera</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@
|
|||
<string name="home_change_provider_img_des">Szolgáltató Váltás</string>
|
||||
<string name="player_speed_text_format" formatted="true">Sebesség (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">Értékelés: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">Új frissítés található!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Új frissítés található! \n%1$s -> %2$s</string>
|
||||
<string name="duration_format" formatted="true">%d perc</string>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%1$sEp%2$d</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
|
|
@ -149,8 +148,7 @@
|
|||
<string name="episode_short">Ep</string>
|
||||
<string name="no_episodes_found">Nem található epizód</string>
|
||||
<string name="delete_file">Fájl törlése</string>
|
||||
<string name="resume_time_left" formatted="true">%dp
|
||||
\nhátra</string>
|
||||
<string name="resume_time_left" formatted="true">%dp \nhátra</string>
|
||||
<string name="duration">Időtartam</string>
|
||||
<string name="free_storage">Elérhető</string>
|
||||
<string name="used_storage">Használatban</string>
|
||||
|
|
@ -206,8 +204,7 @@
|
|||
<string name="season_format">%1$s %2$d%3$s</string>
|
||||
<string name="no_season">Nincs évad</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="delete_message" formatted="true">Ezzel véglegesen törli a %s
|
||||
\nBiztosan törli?</string>
|
||||
<string name="delete_message" formatted="true">Ezzel véglegesen törli a %s \nBiztosan törli?</string>
|
||||
<string name="status_ongoing">Folyamatban levő</string>
|
||||
<string name="year">Év</string>
|
||||
<string name="site">Webhely</string>
|
||||
|
|
@ -323,8 +320,7 @@
|
|||
<string name="extension_types">Támogatott</string>
|
||||
<string name="update_notification_downloading">Alkalmazásfrissítés letöltése…</string>
|
||||
<string name="sort_updated_new">Frissítve (újabbtól a régebbihez)</string>
|
||||
<string name="empty_library_no_accounts_message">Ú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.</string>
|
||||
<string name="empty_library_no_accounts_message">Ú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.</string>
|
||||
<string name="empty_library_logged_in_message">Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani.</string>
|
||||
<string name="max">Max</string>
|
||||
<string name="quality_4k">4K</string>
|
||||
|
|
@ -416,9 +412,7 @@
|
|||
<string name="subtitles_remove_captions">Zárt feliratok eltávolítása a feliratokból</string>
|
||||
<string name="is_adult">18+</string>
|
||||
<string name="delete_repository_plugins">Ez az összes tároló bővítményt is törli</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="extension_version">Verzió</string>
|
||||
<string name="action_mark_as_watched">Megjelölés megtekintettként</string>
|
||||
<string name="action_remove_from_watched">Eltávolítás a megnézettek közül</string>
|
||||
|
|
@ -472,8 +466,7 @@
|
|||
<string name="skip_type_credits">Közreműködők</string>
|
||||
<string name="sort_alphabetical_z">Betűrendben (Z-től az A-ig)</string>
|
||||
<string name="select_library">Könyvtár kiválasztása</string>
|
||||
<string name="safe_mode_file">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.</string>
|
||||
<string name="safe_mode_file">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.</string>
|
||||
<string name="normal">Normál</string>
|
||||
<string name="player_loaded_subtitles" formatted="true">%s betöltve</string>
|
||||
<string name="skip_setup">Beállítás kihagyása</string>
|
||||
|
|
@ -536,18 +529,8 @@
|
|||
<string name="profiles">Profilok</string>
|
||||
<string name="action_remove_from_favorites">Eltávolítás kedvencekből</string>
|
||||
<string name="enter_current_pin">Adja meg a jelenlegi PIN-t</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="skip_startup_account_select_pref">Fiók választás kihagyása belépéskor</string>
|
||||
<string name="use_default_account">Használjon alapértelmezett fiókot</string>
|
||||
<string name="rotate_video">Elforgatás</string>
|
||||
|
|
@ -556,9 +539,7 @@
|
|||
<string name="favorite_added">%s hozzáadva a kedvencekhez</string>
|
||||
<string name="favorite_removed">%s eltávolítva a kedvencekből</string>
|
||||
<string name="action_add_to_favorites">Hozzáadás a kedvencekhez</string>
|
||||
<string name="duplicate_message_single" formatted="true">Ú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?</string>
|
||||
<string name="duplicate_message_single" formatted="true">Ú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?</string>
|
||||
<string name="enter_pin">Adja meg a PIN-t</string>
|
||||
<string name="lock_profile">Profil Zárolása</string>
|
||||
<string name="select_an_account">Válasszon egy fiókot</string>
|
||||
|
|
|
|||
|
|
@ -169,10 +169,8 @@
|
|||
<string name="resume">Lanjutkan</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="delete_message" formatted="true">Ini akan secara permanen menghapus %s
|
||||
\nApakah anda yakin?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\ntersisa</string>
|
||||
<string name="delete_message" formatted="true">Ini akan secara permanen menghapus %s \nApakah anda yakin?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \ntersisa</string>
|
||||
<string name="status_ongoing">Masih Berlanjut</string>
|
||||
<string name="status_completed">Tamat</string>
|
||||
<string name="status">Status</string>
|
||||
|
|
@ -390,9 +388,7 @@
|
|||
<string name="plugins_updated" formatted="true">%d plugin diperbarui</string>
|
||||
<string name="view_public_repositories_button">Lihat repositori komunitas</string>
|
||||
<string name="view_public_repositories_button_short">Daftar publik</string>
|
||||
<string name="blank_repo_message">CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori.
|
||||
\n
|
||||
\nBergabunglah dengan Discord kami atau cari secara online.</string>
|
||||
<string name="blank_repo_message">CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. \n \nBergabunglah dengan Discord kami atau cari secara online.</string>
|
||||
<string name="repository_url_hint">URL Repositori atau Kode Pendek</string>
|
||||
<string name="create_account">Buat Akun</string>
|
||||
<string name="error">Error</string>
|
||||
|
|
@ -487,8 +483,7 @@
|
|||
<string name="action_remove_from_watched">Hapus dari tontonan</string>
|
||||
<string name="browser">Peramban</string>
|
||||
<string name="select_library">Pilih pustaka</string>
|
||||
<string name="empty_library_no_accounts_message">Yahh daftar pustaka kamu kosong :(
|
||||
\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu.</string>
|
||||
<string name="empty_library_no_accounts_message">Yahh daftar pustaka kamu kosong :( \nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu.</string>
|
||||
<string name="library">Pustaka</string>
|
||||
<string name="sort_by">Urutkan berdasarkan</string>
|
||||
<string name="sort">Urutkan</string>
|
||||
|
|
@ -500,8 +495,7 @@
|
|||
<string name="sort_alphabetical_z">Abjad (Z ke A)</string>
|
||||
<string name="open_with">Buka dengan</string>
|
||||
<string name="empty_library_logged_in_message">Yahh daftar ini kosong. Coba ganti ke yang lain.</string>
|
||||
<string name="safe_mode_file">Mode aman file ditemukan!
|
||||
\nTidak memuat ekstensi pada startup sampai berkas dihapus.</string>
|
||||
<string name="safe_mode_file">Mode aman file ditemukan! \nTidak memuat ekstensi pada startup sampai berkas dihapus.</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Sembunyikan Pemutaran - Geser</string>
|
||||
<string name="android_tv_interface_on_seek_settings">Pemutar terlihat - Geser</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Geser untuk menghilangkan</string>
|
||||
|
|
@ -527,13 +521,7 @@
|
|||
<string name="watch_quality_pref_data">Kualitas nonton yang diinginkan (Data Seluler)</string>
|
||||
<string name="mobile_data">Data seluler</string>
|
||||
<string name="help">Bantuan</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="profile_number">Profil %d</string>
|
||||
<string name="wifi">Wifi</string>
|
||||
<string name="set_default">Pengaturan default</string>
|
||||
|
|
@ -593,8 +581,7 @@
|
|||
<string name="biometric_prompt_description">Setelah beberapa kali gagal, perintah akan ditutup. Cukup mulai ulang aplikasi untuk mencoba lagi.</string>
|
||||
<string name="unfavorite">Batalkan favorit</string>
|
||||
<string name="biometric_authentication_title">Buka kunci CloudStream</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\ntersisa</string>
|
||||
<string name="resume_remaining" formatted="true">%s \ntersisa</string>
|
||||
<string name="favorite">Favorit</string>
|
||||
<string name="biometric_setting">Kunci dengan Biometrik</string>
|
||||
<string name="repo_copy_label">Nama dan URL repositori</string>
|
||||
|
|
@ -638,18 +625,10 @@
|
|||
<string name="select_all">Pilih Semua</string>
|
||||
<string name="deselect_all">Batal Pilih Semua</string>
|
||||
<string name="delete_files">Hapus File</string>
|
||||
<string name="delete_message_multiple" formatted="true">Apakah Anda yakin ingin menghapus item berikut secara permanen?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Anda juga akan menghapus semua episode dalam seri berikut secara permanen:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">Apakah Anda yakin ingin menghapus item berikut secara permanen? \n \n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Apakah Anda yakin ingin menghapus episode berikut di %1$s secara permanen? \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Anda juga akan menghapus semua episode dalam seri berikut secara permanen: \n \n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Apakah Anda yakin ingin menghapus semua episode dalam seri berikut secara permanen? \n \n%s</string>
|
||||
<string name="delete_format" formatted="true">Hapus (%1$d | %2$s)</string>
|
||||
<string name="delete_plugin">Hapus plugin</string>
|
||||
<string name="device_pin_error_message">Tidak bisa mendapatkan kode PIN perangkat, coba autentikasi lokal</string>
|
||||
|
|
@ -765,4 +744,7 @@
|
|||
<string name="download_episode_range">Apakah kamu ingin mengunduh episode %s?</string>
|
||||
<string name="cancel_queue_message">Apakah kamu ingin membatalkan semua unduhan dalam antrean?</string>
|
||||
<string name="show_player_metadata_overlay">Tampilkan overlay metadata pemutar</string>
|
||||
<string name="player_is_live">Live</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Pratinjau</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<string name="next_episode_format" formatted="true">L\'episodio %d uscirà in</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d min</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d min</string>
|
||||
<!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS -->
|
||||
<string name="result_poster_img_des">Poster</string>
|
||||
<string name="search_poster_img_des">Poster</string>
|
||||
|
|
@ -19,8 +19,7 @@
|
|||
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
|
||||
<string name="player_speed_text_format" formatted="true">Velocità (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">Valutato: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">Nuovo aggiornamento trovato!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Nuovo aggiornamento trovato! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">Filler</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
<!-- <string name="app_name">CloudStream</string> -->
|
||||
|
|
@ -186,10 +185,8 @@
|
|||
<string name="resume">Riprendi</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="delete_message" formatted="true">Stai per eliminare permanentemente %s
|
||||
\nSei sicuro?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm
|
||||
\nrimanenti</string>
|
||||
<string name="delete_message" formatted="true">Stai per eliminare permanentemente %s \nSei sicuro?</string>
|
||||
<string name="resume_time_left" formatted="true">%dm \nrimanenti</string>
|
||||
<string name="status_ongoing">In corso</string>
|
||||
<string name="status_completed">Completato</string>
|
||||
<string name="status">Stato</string>
|
||||
|
|
@ -412,9 +409,7 @@
|
|||
<string name="plugins_disabled" formatted="true">Disabilitato: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Non scaricato: %d</string>
|
||||
<string name="plugins_updated" formatted="true">Aggiornati %d plugin</string>
|
||||
<string name="blank_repo_message">CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository.
|
||||
\n
|
||||
\nUnisciti al nostro Discord o cerca online.</string>
|
||||
<string name="blank_repo_message">CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. \n \nUnisciti al nostro Discord o cerca online.</string>
|
||||
<string name="view_public_repositories_button">Visualizza i repository della comunità</string>
|
||||
<string name="view_public_repositories_button_short">Lista pubblica</string>
|
||||
<string name="uppercase_all_subtitles">Tutti i sottotitoli in maiuscolo</string>
|
||||
|
|
@ -502,15 +497,13 @@
|
|||
<string name="sort_updated_old">Aggiornato (Da vecchio a nuovo)</string>
|
||||
<string name="sort_alphabetical_a">Alfabetico (A - Z)</string>
|
||||
<string name="sort_alphabetical_z">Alfabetico (Z - A)</string>
|
||||
<string name="empty_library_no_accounts_message">La tua libreria è vuota :(
|
||||
\nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale.</string>
|
||||
<string name="empty_library_no_accounts_message">La tua libreria è vuota :( \nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale.</string>
|
||||
<string name="select_library">Seleziona libreria</string>
|
||||
<string name="open_with">Apri con</string>
|
||||
<string name="library">Libreria</string>
|
||||
<string name="sort">Ordina</string>
|
||||
<string name="empty_library_logged_in_message">Questo elenco è vuoto. Prova a passare a un altro.</string>
|
||||
<string name="safe_mode_file">File \"safe mode\" trovato!
|
||||
\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso.</string>
|
||||
<string name="safe_mode_file">File \"safe mode\" trovato! \nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso.</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Intervallo di ricerca utilizzato quando il lettore è nascosto</string>
|
||||
<string name="pref_category_android_tv">TV Android</string>
|
||||
<string name="android_tv_interface_on_seek_settings_summary">Intervallo di ricerca utilizzato quando il lettore è visibile</string>
|
||||
|
|
@ -534,13 +527,7 @@
|
|||
<string name="subscription_in_progress_notification">Aggiornando shows a cui sei iscritto</string>
|
||||
<string name="subscription_episode_released">L\'episodio %d è stato rilasciato!</string>
|
||||
<string name="watch_quality_pref_data">Qualità di visualizzazione preferita (Dati mobili)</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="quality_profile_help">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!</string>
|
||||
<string name="profile_number">Profilo %d</string>
|
||||
<string name="wifi">Wi-Fi</string>
|
||||
<string name="set_default">Imposta predefinito</string>
|
||||
|
|
@ -560,11 +547,7 @@
|
|||
<string name="favorite_removed">%s rimosso dai preferiti</string>
|
||||
<string name="favorites_list_name">Preferiti</string>
|
||||
<string name="favorite_added">%s aggiunto ai preferiti</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">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?</string>
|
||||
<string name="backup_frequency">Frequenza di backup</string>
|
||||
<string name="duplicate_title">Trovato Possibile Duplicato</string>
|
||||
<string name="action_add_to_favorites">Aggiungi ai preferiti</string>
|
||||
|
|
@ -577,9 +560,7 @@
|
|||
<string name="action_subscribe">Iscriviti</string>
|
||||
<string name="action_remove_from_favorites">Rimuovi dai preferiti</string>
|
||||
<string name="select_an_account">Seleziona un account</string>
|
||||
<string name="duplicate_message_single">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?</string>
|
||||
<string name="duplicate_message_single">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?</string>
|
||||
<string name="enter_pin">Inserisci PIN</string>
|
||||
<string name="pin">PIN</string>
|
||||
<string name="enter_current_pin">Inserisci PIN corrente</string>
|
||||
|
|
@ -609,8 +590,7 @@
|
|||
<string name="biometric_prompt_description">Dopo alcuni tentativi falliti, il prompt si chiuderà. Riavvia semplicemente l\'app per riprovare.</string>
|
||||
<string name="biometric_warning">È 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.</string>
|
||||
<string name="unfavorite">Non preferito</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\nresiduo</string>
|
||||
<string name="resume_remaining" formatted="true">%s \nresiduo</string>
|
||||
<string name="favorite">Preferito</string>
|
||||
<string name="repo_copy_label">Nome e URL del repository</string>
|
||||
<string name="toast_copied">copiato!</string>
|
||||
|
|
@ -652,20 +632,12 @@
|
|||
<string name="select_all">Seleziona tutto</string>
|
||||
<string name="deselect_all">Deseleziona tutto</string>
|
||||
<string name="delete_format" formatted="true">Elimina (%1$d | %2$s)</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Eliminerai definitivamente anche tutti gli episodi delle seguenti serie:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">Sei sicuro di voler eliminare definitivamente i seguenti episodi in %1$s? \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">Eliminerai definitivamente anche tutti gli episodi delle seguenti serie: \n \n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">Sei sicuro di voler eliminare definitivamente tutti gli episodi delle seguenti serie? \n \n%s</string>
|
||||
<string name="sort_release_date_old">Data di rilascio (dal più vecchio)</string>
|
||||
<string name="delete_files">Elimina file</string>
|
||||
<string name="delete_message_multiple" formatted="true">Sei sicuro di voler eliminare definitivamente i seguenti elementi?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">Sei sicuro di voler eliminare definitivamente i seguenti elementi? \n \n%s</string>
|
||||
<string name="preview_seekbar">Anteprima barra di avanzamento</string>
|
||||
<string name="preview_seekbar_desc">Abilita miniatura di anteprima sulla barra di avanzamento</string>
|
||||
<string name="no_subtitles_loaded">Nessun sottotitolo caricato</string>
|
||||
|
|
@ -784,4 +756,7 @@
|
|||
<string name="source_priority">Priorità sorgente</string>
|
||||
<string name="source_priority_help">Decidi come le sorgenti video devono essere ordinate nel lettore</string>
|
||||
<string name="show_player_metadata_overlay">Mostra sovrapposizione metadati lettore</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Anteprima</string>
|
||||
<string name="player_is_live">Live</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
<string name="home_change_provider_img_des">לשנות ספק</string>
|
||||
<string name="player_speed_text_format" formatted="true">מהירות (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">דירוג: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">נמצא עדכון חדש!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">נמצא עדכון חדש! \n%1$s -> %2$s</string>
|
||||
<string name="filler" formatted="true">סינון</string>
|
||||
<string name="duration_format" formatted="true">%d דקות</string>
|
||||
<string name="app_name">קלאודסטרים</string>
|
||||
|
|
@ -146,10 +145,8 @@
|
|||
<string name="resume">המשך</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="resume_time_left" formatted="true">%dדקות
|
||||
\nנותרו</string>
|
||||
<string name="delete_message" formatted="true">פעולה זאת תמחק לצמיתות את %s
|
||||
\nהאם אתם בטוחים?</string>
|
||||
<string name="resume_time_left" formatted="true">%dדקות \nנותרו</string>
|
||||
<string name="delete_message" formatted="true">פעולה זאת תמחק לצמיתות את %s \nהאם אתם בטוחים?</string>
|
||||
<string name="status_ongoing">מתמשך</string>
|
||||
<string name="duration">משך זמן</string>
|
||||
<string name="rating">דירוג</string>
|
||||
|
|
@ -425,10 +422,8 @@
|
|||
<string name="skip_type_credits">קרדיטים</string>
|
||||
<string name="sort">מיין</string>
|
||||
<string name="select_library">בחר ספרייה</string>
|
||||
<string name="empty_library_no_accounts_message">נראה שהספרייה שלכם ריקה :(
|
||||
\nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם.</string>
|
||||
<string name="safe_mode_file">קובץ מצב בטוח נמצא!
|
||||
\nלא טוען שום תוספות בהפעלה עד להסרת הקובץ.</string>
|
||||
<string name="empty_library_no_accounts_message">נראה שהספרייה שלכם ריקה :( \nהתחברו לחשבון ספריה או הוסיפו סדרות לספרייה המקומית שלכם.</string>
|
||||
<string name="safe_mode_file">קובץ מצב בטוח נמצא! \nלא טוען שום תוספות בהפעלה עד להסרת הקובץ.</string>
|
||||
<string name="update_notification_failed">לא ניתן להתקין את הגרסה החדשה של האפליקציה</string>
|
||||
<string name="batch_download">הורדת אצווה</string>
|
||||
<string name="plugin_singular">תוסף</string>
|
||||
|
|
@ -444,11 +439,7 @@
|
|||
<string name="plugins_downloaded" formatted="true">הורד: %d</string>
|
||||
<string name="plugins_disabled" formatted="true">מוגבל: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">לא מורד: %d</string>
|
||||
<string name="blank_repo_message">לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים.
|
||||
\n
|
||||
\nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה.
|
||||
\n
|
||||
\nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט.</string>
|
||||
<string name="blank_repo_message">לקלאודסטרים אין אתרים מותקנים כברירת מחדל. עליכם להתקין את האתרים ממאגרים. \n \nבגלל הסרת DMCA על ידי Sky UK LImited 🤮 אנחנו לא יכולים לקשר את אתר המאגרים באפליקציה. \n \nתצטרפו לדיסקורד שלנו או תחפשו באינטרנט.</string>
|
||||
<string name="view_public_repositories_button">הצג מאגרים קהילתיים</string>
|
||||
<string name="view_public_repositories_button_short">רשימה ציבורית</string>
|
||||
<string name="uppercase_all_subtitles">לשים את הכתוביות באותיות רישיות</string>
|
||||
|
|
@ -530,13 +521,7 @@
|
|||
<string name="set_default">קביעה כברירת מחדל</string>
|
||||
<string name="test_passed">עבר</string>
|
||||
<string name="pref_category_bypass">מעקף ספק אינטרנט</string>
|
||||
<string name="quality_profile_help">כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו.
|
||||
\n
|
||||
\nמקור א: 3
|
||||
\nאיכות ב: 7
|
||||
\nיגרמו לעדיפות הסרטון להיות 10.
|
||||
\n
|
||||
\nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען!</string>
|
||||
<string name="quality_profile_help">כאן ניתן לשנות את סדר המקורות. אם לוידיאו יש עדיפות גבוהה יותר, הוא יופיע גבוה יותר בבחירת המקור. סכום עדיפות המקור ועדיפות האיכות היא עדיפות הוידיאו. \n \nמקור א: 3 \nאיכות ב: 7 \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען!</string>
|
||||
<string name="next_season_episode_format" formatted="true">עונה %1$d פרק %2$d תשודר ב:</string>
|
||||
<string name="download_time_left_hour_min_sec_format" formatted="true">%1$d שעות %2$d דקות %3$d שניות</string>
|
||||
<string name="download_time_left_min_sec_format" formatted="true">%1$d דקות %2$d שניות</string>
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@
|
|||
<string name="loading">ローディング…</string>
|
||||
<string name="result_open_in_browser">ブラウザで開く</string>
|
||||
<string name="season_short">シーズン</string>
|
||||
<string name="resume_time_left" formatted="true">残り
|
||||
\n%d分</string>
|
||||
<string name="resume_time_left" formatted="true">残り \n%d分</string>
|
||||
<string name="play_episode">再生エピソード</string>
|
||||
<string name="downloaded">ダウンロード済</string>
|
||||
<string name="pref_category_backup">バックアップ</string>
|
||||
|
|
@ -82,8 +81,7 @@
|
|||
<string name="home_next_random_img_des">次のランダム</string>
|
||||
<string name="go_back_img_des">戻り</string>
|
||||
<string name="rated_format" formatted="true">評価: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">新しいアップデートを発見!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">新しいアップデートを発見! \n%1$s -> %2$s</string>
|
||||
<string name="duration_format" formatted="true">%d分</string>
|
||||
<string name="search_hint_site" formatted="true">%sを検索…</string>
|
||||
<string name="pick_source">ソース</string>
|
||||
|
|
|
|||
|
|
@ -83,8 +83,7 @@
|
|||
<string name="result_share">ಶೇರ್</string>
|
||||
<string name="popup_delete_file">ಫೈಲ್ ಅಳಿಸಿ</string>
|
||||
<string name="home_more_info">ಹೆಚ್ಚಿನ ಮಾಹಿತಿ</string>
|
||||
<string name="new_update_format" formatted="true">ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ
|
||||
\n%1$s-%2$s</string>
|
||||
<string name="new_update_format" formatted="true">ಹೊಸ ಅಪ್ಡೇಟ್ ಬಂದಿದೆ \n%1$s-%2$s</string>
|
||||
<string name="loading">ಲೋಡಿಂಗ್…</string>
|
||||
<string name="subs_download_languages">ಡೌನ್ಲೋಡ್ ಭಾಷೆಗಳನ್ನು ಮಾಡಿ</string>
|
||||
<string name="play_livestream_button">ಲೈವ್ಸ್ಟ್ರೀಮ್ ಪ್ಲೇ ಮಾಡಿ</string>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@
|
|||
<string name="preview_background_img_des">배경 미리보기</string>
|
||||
<string name="player_speed_text_format" formatted="true">속도 (%.2fx)</string>
|
||||
<string name="rated_format" formatted="true">평점: %.1f</string>
|
||||
<string name="new_update_format" formatted="true">새로운 업데이트!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">새로운 업데이트! \n%1$s -> %2$s</string>
|
||||
<string name="duration_format" formatted="true">%d분</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="play_with_app_name">CloudStream으로 재생</string>
|
||||
|
|
@ -28,7 +27,7 @@
|
|||
<string name="result_share">공유</string>
|
||||
<string name="result_open_in_browser">브라우저로 열기</string>
|
||||
<string name="browser">브라우저</string>
|
||||
<string name="skip_loading">로딩 건너뛰기</string>
|
||||
<string name="skip_loading">로딩 스킵</string>
|
||||
<string name="loading">로딩중…</string>
|
||||
<string name="type_watching">시청</string>
|
||||
<string name="type_on_hold">보류</string>
|
||||
|
|
@ -124,7 +123,7 @@
|
|||
<string name="use_system_brightness_settings_des">어두운 오버레이 대신 앱 플레이어의 시스템 밝기를 사용합니다</string>
|
||||
<string name="episode_sync_settings">시청 진행 상황 업데이트</string>
|
||||
<string name="episode_sync_settings_des">현재 에피소드 진행 상황을 자동으로 동기화합니다</string>
|
||||
<string name="restore_settings">백업에서 데이터 복원</string>
|
||||
<string name="restore_settings">데이터 복원</string>
|
||||
<string name="backup_settings">데이터 백업</string>
|
||||
<string name="restore_failed_format" formatted="true">파일에서 데이터를 복원하지 못했습니다 %s</string>
|
||||
<string name="backup_success">저장된 데이터</string>
|
||||
|
|
@ -161,10 +160,8 @@
|
|||
<string name="year">년</string>
|
||||
<string name="rating">평점</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="delete_message" formatted="true">%s가 영구 삭제됩니다
|
||||
\n정말 삭제하시겠습니까?</string>
|
||||
<string name="resume_time_left" formatted="true">%d분
|
||||
\n남음</string>
|
||||
<string name="delete_message" formatted="true">%s이(가) 영구적으로 삭제됩니다 \n정말 삭제하시겠습니까?</string>
|
||||
<string name="resume_time_left" formatted="true">%d분 \n남음</string>
|
||||
<string name="site">사이트</string>
|
||||
<string name="duration">시간</string>
|
||||
<string name="synopsis">개요</string>
|
||||
|
|
@ -184,7 +181,7 @@
|
|||
<string name="episode_action_play_in_format">%s로 재생</string>
|
||||
<string name="episode_action_auto_download">자동 다운로드</string>
|
||||
<string name="episode_action_download_mirror">다운로드 소스 목록</string>
|
||||
<string name="episode_action_reload_links">링크 새로고침</string>
|
||||
<string name="episode_action_reload_links">링크 초기화</string>
|
||||
<string name="episode_action_download_subtitle">자막 다운로드</string>
|
||||
<string name="show_hd">화질 라벨</string>
|
||||
<string name="show_dub">더빙 라벨</string>
|
||||
|
|
@ -195,7 +192,7 @@
|
|||
<string name="video_aspect_ratio_resize">크기 조정</string>
|
||||
<string name="video_source">소스</string>
|
||||
<string name="video_skip_op">오프닝 스킵</string>
|
||||
<string name="skip_update">이 업데이트 건너뛰기</string>
|
||||
<string name="skip_update">다음에 업데이트</string>
|
||||
<string name="watch_quality_pref">선호하는 화질 (WiFi)</string>
|
||||
<string name="watch_quality_pref_data">선호하는 화질 (모바일 데이터)</string>
|
||||
<string name="limit_title_rez">플레이어 내 표시 정보</string>
|
||||
|
|
@ -297,11 +294,7 @@
|
|||
<string name="delete_repository">저장소 삭제</string>
|
||||
<string name="setup_extensions_subtext">사용하려는 사이트 목록 다운로드</string>
|
||||
<string name="plugins_downloaded" formatted="true">다운로드됨: %d</string>
|
||||
<string name="blank_repo_message">CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다.
|
||||
\n
|
||||
\nSky UK Limited의 무분별한 DMCA 게시 중단으로 인해 앱에서 저장소 사이트를 연결 할 수 없습니다.
|
||||
\n
|
||||
\nDiscord에 가입하거나 온라인으로 검색해 보세요.</string>
|
||||
<string name="blank_repo_message">CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다. \n \nDiscord에 가입하거나 온라인으로 검색해 보세요.</string>
|
||||
<string name="view_public_repositories_button">커뮤니티 저장소 보기</string>
|
||||
<string name="view_public_repositories_button_short">공개 목록</string>
|
||||
<string name="uppercase_all_subtitles">자막 대문자화 표시</string>
|
||||
|
|
@ -322,7 +315,7 @@
|
|||
<string name="safe_mode_crash_info">충돌 정보 보기</string>
|
||||
<string name="extension_language">언어</string>
|
||||
<string name="subscription_episode_released">에피소드 %d 공개!</string>
|
||||
<string name="picture_in_picture">PIP 모드</string>
|
||||
<string name="picture_in_picture">Picture-in-picture 모드</string>
|
||||
<string name="player_size_settings">플레이어 크기 조정 버튼</string>
|
||||
<string name="picture_in_picture_des">미니플레이어를 통해 다른 앱 상단에서 계속 재생합니다</string>
|
||||
<string name="player_size_settings_des">레터박스 제거</string>
|
||||
|
|
@ -331,7 +324,7 @@
|
|||
<string name="restore_success">백업 파일을 성공적으로 로드하였습니다</string>
|
||||
<string name="settings_info">정보</string>
|
||||
<string name="advanced_search">고급 검색</string>
|
||||
<string name="redo_setup_process">설정 프로세스 다시 실행</string>
|
||||
<string name="redo_setup_process">설정 프로세스 재실행</string>
|
||||
<string name="apk_installer_settings">APK 인스톨러</string>
|
||||
<string name="github">Github</string>
|
||||
<string name="source_error">소스 오류</string>
|
||||
|
|
@ -438,16 +431,16 @@
|
|||
<string name="extension_install_first">먼저 확장 프로그램을 설치하세요</string>
|
||||
<string name="app_not_found_error">앱을 찾을 수 없음</string>
|
||||
<string name="all_languages_preference">모든 언어</string>
|
||||
<string name="skip_type_format" formatted="true">건너뛰기 %s</string>
|
||||
<string name="skip_type_format" formatted="true">%s 스킵</string>
|
||||
<string name="skip_type_op">오프닝</string>
|
||||
<string name="skip_type_ed">엔딩</string>
|
||||
<string name="skip_type_mixed_ed">혼합 엔딩</string>
|
||||
<string name="skip_type_mixed_op">혼합 오프닝</string>
|
||||
<string name="skip_type_credits">크레딧</string>
|
||||
<string name="skip_type_intro">소개</string>
|
||||
<string name="skip_type_intro">인트로</string>
|
||||
<string name="clear_history">기록 삭제</string>
|
||||
<string name="history">기록</string>
|
||||
<string name="enable_skip_op_from_database_des">오프닝/엔딩 시 건너뛰기 팝업 표시</string>
|
||||
<string name="enable_skip_op_from_database_des">오프닝/엔딩 시 스킵 팝업 표시</string>
|
||||
<string name="clipboard_too_large">텍스트가 너무 많습니다. 클립보드에 저장할 수 없습니다.</string>
|
||||
<string name="action_remove_from_watched">시청에서 삭제</string>
|
||||
<string name="confirm_exit_dialog">정말 종료하시겠습니까?</string>
|
||||
|
|
@ -466,10 +459,8 @@
|
|||
<string name="sort_alphabetical_a">알파벳순 (A에서 Z)</string>
|
||||
<string name="sort_alphabetical_z">알파벳순 (Z에서 A)</string>
|
||||
<string name="open_with">다음으로 열기</string>
|
||||
<string name="empty_library_no_accounts_message">라이브러리가 비어 있습니다 :(
|
||||
\n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요.</string>
|
||||
<string name="safe_mode_file">안전 모드 파일을 찾았습니다!
|
||||
\n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다.</string>
|
||||
<string name="empty_library_no_accounts_message">라이브러리가 비어 있습니다 :( \n라이브러리 계정으로 로그인하거나 로컬 라이브러리에 프로그램을 추가하세요.</string>
|
||||
<string name="safe_mode_file">안전 모드 파일을 찾았습니다! \n파일이 제거될 때까지 시작 시 확장 프로그램을 로드하지 않습니다.</string>
|
||||
<string name="hls_playlist">HLS 재생목록</string>
|
||||
<string name="player_settings_play_in_app">내부 플레이어</string>
|
||||
<string name="player_pref">선호하는 동영상 플레이어</string>
|
||||
|
|
@ -485,7 +476,7 @@
|
|||
<string name="action_open_play">@string/home_play</string>
|
||||
<string name="normal_no_plot">플롯을 찾을 수 없음</string>
|
||||
<string name="torrent_no_plot">설명을 찾을 수 없음</string>
|
||||
<string name="show_log_cat">Logcat 🐈 표시</string>
|
||||
<string name="show_log_cat">로그캣 🐈 보기</string>
|
||||
<string name="show_fillers_settings">애니메이션용 필러 에피소드 표시</string>
|
||||
<string name="test_passed">통과</string>
|
||||
<string name="resume">계속</string>
|
||||
|
|
@ -517,11 +508,11 @@
|
|||
<string name="pref_category_security">보안</string>
|
||||
<string name="pref_category_accounts">계정</string>
|
||||
<string name="no_plugins_found_error">리포지토리에서 플러그인을 찾을 수 없습니다</string>
|
||||
<string name="toast_copied">복사됨!</string>
|
||||
<string name="toast_copied">복사 완료!</string>
|
||||
<string name="repo_copy_label">레포지토리 이름 및 URL</string>
|
||||
<string name="test_extensions_summary">본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다.</string>
|
||||
<string name="cs3wiki">CloudStream 위키</string>
|
||||
<string name="links_reloaded_toast">링크 새로고침 완료</string>
|
||||
<string name="links_reloaded_toast">링크 초기화 완료</string>
|
||||
<string name="backup_frequency">백업 빈도</string>
|
||||
<string name="favorites_list_name">즐겨찾기</string>
|
||||
<string name="qr_image">QR 이미지</string>
|
||||
|
|
@ -572,17 +563,11 @@
|
|||
<string name="set_default">기본값 설정</string>
|
||||
<string name="action_subscribe">구독</string>
|
||||
<string name="use">사용</string>
|
||||
<string name="duplicate_message_single" formatted="true">당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'.
|
||||
\n
|
||||
\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까?</string>
|
||||
<string name="duplicate_message_single" formatted="true">당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까?</string>
|
||||
<string name="duplicate_replace_all">전부 대체</string>
|
||||
<string name="duplicate_add">추가</string>
|
||||
<string name="favorite_removed">즐겨찾기에서 %s 제거</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다:
|
||||
\n
|
||||
\n%s
|
||||
\n
|
||||
\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까?</string>
|
||||
<string name="duplicate_message_multiple" formatted="true">당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: \n \n%s \n \n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까?</string>
|
||||
<string name="select_an_account">계정 선택</string>
|
||||
<string name="use_default_account">기본 계정 사용</string>
|
||||
<string name="rotate_video">회전</string>
|
||||
|
|
@ -599,7 +584,7 @@
|
|||
<string name="reset_btn">재설정</string>
|
||||
<string name="automatic_plugin_download_mode_title">플러그인 다운로드를 필터링할 모드 선택</string>
|
||||
<string name="biometric_warning">CloudStream 데이터 백업이 완료되었습니다. 드문 경우지만, 기기에 따라 앱 접속이 안 되는 오류가 발생할 수 있습니다. 만약 앱이 열리지 않는다면, 앱 데이터를 완전히 삭제(초기화)한 후 이 백업 파일로 복구해 주시기 바랍니다. 이용에 불편을 드려 대단히 죄송합니다.</string>
|
||||
<string name="device_pin_url_message">스마트폰이나 컴퓨터에서 <b>%s</b>를 방문하여 위의 코드를 입력하세요</string>
|
||||
<string name="device_pin_url_message">스마트폰이나 컴퓨터에서 <b>%s</b> 위의 코드를 입력하세요</string>
|
||||
<string name="battery_dialog_message">구독 중인 TV 쇼의 알림을 받고 다운로드를 끊김 없이 완료하려면, CloudStream의 백그라운드 실행 권한이 필요합니다. \'확인\'을 누른 후 나타나는 요청 창에서 \'허용\'을 선택해 주세요.\n\n참고로, 이 권한을 허용한다고 해서 배터리가 계속 소모되는 것은 아닙니다. 알림을 받거나 공식 확장 프로그램에서 영상을 다운로드할 때처럼 꼭 필요한 상황에서만 백그라운드 작업을 수행합니다.</string>
|
||||
<string name="quality_profile_help">여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택 화면에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. \n \n참고 A: 3 \n품질 B: 7 \n총 비디오 우선 순위는 10입니다. \n \n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다!</string>
|
||||
<string name="next_season_episode_format" formatted="true">시즌 %1$d 에피소드 %2$d 공개 예정</string>
|
||||
|
|
@ -608,8 +593,7 @@
|
|||
<string name="recommendations_tooltip">추천목록 보기</string>
|
||||
<string name="speed_setting_summary">플레이어에 속도 옵션을 추가합니다</string>
|
||||
<string name="episode_upcoming_format" formatted="true">%s 후 공개 예정</string>
|
||||
<string name="resume_remaining" formatted="true">%s
|
||||
\n남음</string>
|
||||
<string name="resume_remaining" formatted="true">%s \n남음</string>
|
||||
<string name="duplicate_title">잠재적 중복 발견</string>
|
||||
<string name="enter_pin_with_name" formatted="true">%s의 PIN 입력</string>
|
||||
<string name="action_remove_from_favorites">즐겨찾기에서 제거</string>
|
||||
|
|
@ -627,18 +611,10 @@
|
|||
<string name="open_local_video">로컬 비디오 열기</string>
|
||||
<string name="delete_files">파일 삭제</string>
|
||||
<string name="delete_format" formatted="true">삭제 (%1$d | %2$s)</string>
|
||||
<string name="delete_message_multiple" formatted="true">다음 항목을 영구적으로 삭제 하시겠습니까??
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s?
|
||||
\n
|
||||
\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다:
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까??
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="delete_message_multiple" formatted="true">다음 항목을 영구적으로 삭제 하시겠습니까? \n \n%s</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">다음 에피소드를 영구적으로 삭제 하시겠습니까? %1$s? \n \n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">또한 다음 시리즈의 모든 에피소드를 영구적으로 삭제합니다: \n \n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까? \n \n%s</string>
|
||||
<string name="sort_release_date_new">공개일 (최신순)</string>
|
||||
<string name="sort_release_date_old">공개일 (오래된순)</string>
|
||||
<string name="hide_player_control_names">플레이어 내 버튼명 숨기기</string>
|
||||
|
|
@ -754,4 +730,7 @@
|
|||
<string name="action_reload">새로고침</string>
|
||||
<string name="extra_brightness_key">최대 밝기 확장 활성화</string>
|
||||
<string name="show_player_metadata_overlay">플레이어에 메타데이터 오버레이 표시</string>
|
||||
<string name="video_singular">비디오</string>
|
||||
<string name="skip_type_preview">프리뷰</string>
|
||||
<string name="player_is_live">라이브</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@
|
|||
<string name="go_forward_30">+30</string>
|
||||
<string name="download_done">Atsiuntimas baigtas</string>
|
||||
<string name="continue_watching">Tęsti žiūrėjimą</string>
|
||||
<string name="new_update_format" formatted="true">Rastas atnaujinimas!
|
||||
\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Rastas atnaujinimas! \n%1$s -> %2$s</string>
|
||||
<string name="subs_download_languages">Atsisiųsti kalbas</string>
|
||||
<string name="search_provider_text_providers">Ieškoti naudojant tiekėjus</string>
|
||||
<string name="go_back_img_des">Grįžti atgal</string>
|
||||
|
|
@ -88,8 +87,7 @@
|
|||
<string name="popup_resume_download">Pratęsti siuntimą</string>
|
||||
<string name="asian_drama">Azijietiškos dramos</string>
|
||||
<string name="episode">Serija</string>
|
||||
<string name="empty_library_no_accounts_message">Jūsų biblioteka tuščia :(
|
||||
\nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos.</string>
|
||||
<string name="empty_library_no_accounts_message">Jūsų biblioteka tuščia :( \nPrisijunkite prie bibliotekos paskyros arba pridėkite laidas prie vietinės bibliotekos.</string>
|
||||
<string name="autoplay_next_settings_des">Pradėti sekančia seriją, kai dabartinė baigsis</string>
|
||||
<string name="subs_text_color">Teksto spalva</string>
|
||||
<string name="type_completed">Užbaigta</string>
|
||||
|
|
@ -181,11 +179,7 @@
|
|||
<string name="example_ip">127.0.0.1</string>
|
||||
<string name="batch_download_finish_format" formatted="true">Atsiųsta %1$d %2$s</string>
|
||||
<string name="skip_type_format" formatted="true">Praleisti %s</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="blank_repo_message">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.</string>
|
||||
<string name="mobile_data">Mobilūs duomenys</string>
|
||||
<string name="example_username">šaunusPrisijungimoVardas</string>
|
||||
<string name="extension_authors">Autoriai</string>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue