Compare commits

..

1 commit

Author SHA1 Message Date
Osten
cb5d517da0
fixed observe, aka #2567 2026-04-29 22:28:32 +02:00
192 changed files with 3567 additions and 5268 deletions

View file

@ -71,10 +71,7 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
MAL_KEY: ${{ secrets.MAL_KEY }}
ANILIST_KEY: ${{ secrets.ANILIST_KEY }}
- uses: actions/checkout@v6
with:

98
.github/workflows/issue_action.yml vendored Normal file
View file

@ -0,0 +1,98 @@
name: Issue automatic actions
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
jobs:
issue-moderator:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
- name: Similarity analysis
id: similarity
uses: actions-cool/issues-similarity-analysis@v1
with:
token: ${{ steps.generate_token.outputs.token }}
filter-threshold: 0.60
title-excludes: ''
comment-title: |
### Your issue looks similar to these issues:
Please close if duplicate.
comment-body: '${index}. ${similarity} #${number}'
- name: Label if possible duplicate
if: steps.similarity.outputs.similar-issues-found =='true'
uses: actions/github-script@v9
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible duplicate"]
})
- uses: actions/checkout@v6
- name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2
with:
github-token: ${{ steps.generate_token.outputs.token }}
issue-close-message: |
@${issue.user.login}: hello! :wave:
This issue is being automatically closed because it does not follow the issue template."
closed-issues-label: "invalid"
- name: Check if issue mentions a provider
id: provider_check
env:
GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
run: |
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
pip3 install httpx
RES="$(python3 ./check_issue.py)"
echo "name=${RES}" >> $GITHUB_OUTPUT
- name: Comment if issue mentions a provider
if: steps.provider_check.outputs.name != 'none'
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ steps.generate_token.outputs.token }}
body: |
Hello ${{ github.event.issue.user.login }}.
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none'
uses: actions/github-script@v9
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible provider issue"]
})
- name: Add eyes reaction to all issues
uses: actions-cool/emoji-helper@v1.0.0
with:
type: 'issue'
token: ${{ steps.generate_token.outputs.token }}
emoji: 'eyes'

View file

@ -62,10 +62,7 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
MAL_KEY: ${{ secrets.MAL_KEY }}
ANILIST_KEY: ${{ secrets.ANILIST_KEY }}
- name: Create pre-release
uses: marvinpinto/action-automatic-releases@latest

View file

@ -27,7 +27,7 @@ jobs:
cache-read-only: false
- name: Run Gradle
run: ./gradlew assemblePrereleaseDebug lint check
run: ./gradlew assemblePrereleaseDebug lint
- name: Upload Artifact
uses: actions/upload-artifact@v7

View file

@ -8,7 +8,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.serialization)
}
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
@ -104,8 +103,8 @@ android {
applicationId = "com.lagradost.cloudstream3"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
versionCode = 68
versionName = "4.7.0"
manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
@ -127,16 +126,6 @@ android {
"SIMKL_CLIENT_SECRET",
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
)
buildConfigField(
"String",
"MAL_KEY",
"\"" + (System.getenv("MAL_KEY") ?: localProperties["mal.key"]) + "\""
)
buildConfigField(
"String",
"ANILIST_KEY",
"\"" + (System.getenv("ANILIST_KEY") ?: localProperties["anilist.key"]) + "\""
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -217,22 +206,17 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.json)
androidTestImplementation(libs.core)
androidTestImplementation(libs.espresso.core)
implementation(libs.junit.ktx)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.instancio.core)
androidTestImplementation(libs.junit.ktx)
androidTestImplementation(libs.kotlin.test)
androidTestImplementation(libs.espresso.core)
// Android Core & Lifecycle
implementation(libs.core.ktx)
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)
@ -269,15 +253,13 @@ dependencies {
// Extensions & Other Libs
implementation(libs.jsoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript
implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance
implementation(libs.safefile) // To Prevent the URI File Fu*kery
coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor
implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9
implementation(libs.jackson.module.kotlin) // JSON Parser
implementation(libs.zipline)
// Deprecated; will be removed once extensions have time to migrate from using it
implementation("me.xdrop:fuzzywuzzy:1.4.0")
// Torrent Support
implementation(libs.torrentserver)
@ -326,7 +308,6 @@ tasks.withType<KotlinJvmCompile> {
optIn.addAll(
"com.lagradost.cloudstream3.InternalAPI",
"com.lagradost.cloudstream3.Prerelease",
"kotlin.uuid.ExperimentalUuidApi",
)
}
}

View file

@ -1,134 +0,0 @@
package com.lagradost.cloudstream3
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import dalvik.system.DexFile
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import kotlinx.serialization.serializerOrNull
import org.instancio.Instancio
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@RunWith(AndroidJUnit4::class)
class SerializationClassTester {
// Same as app, or using app reference
val jacksonMapper = mapper
val kotlinxMapper = json
@Test
fun isIdenticalSerialization() {
val serializableClasses = findSerializableClasses("com.lagradost")
println("Number of serializable classes: ${serializableClasses.size}")
serializableClasses.forEach { kClass ->
val instance = Instancio.create(kClass.java)
val jacksonJson = jacksonMapper.writeValueAsString(instance)
val kotlinxJson = serializeWithKotlinx(kClass, instance)
assertEquals(
jacksonJson,
kotlinxJson,
"""
Serialization mismatch for:
${kClass.qualifiedName}
Jackson:
$jacksonJson
Kotlinx:
$kotlinxJson
""".trimIndent()
)
println("Identical serialization for: ${kClass.jvmName}")
}
}
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
@Test
fun isIdenticalDeserialization() {
val serializableClasses = findSerializableClasses("com.lagradost")
println("Number of serializable classes: ${serializableClasses.size}")
serializableClasses.forEach { kClass ->
val instance = Instancio.create(kClass.java)
// Convert to JSON to get example JSON object
// We prefer jackson here because the app may have many jackson JSON strings in local storage
val originalJson = jacksonMapper.writeValueAsString(instance)
// Create an object from the JSON using kotlinx
val serializer =
kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass)
assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!")
val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson)
// Create an object from the JSON using jackson
val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java)
// Deep inspect both object using the mapper toJson function.
// This deep equality check can be performed using other methods, but this just works.
val jacksonJson = mapperDecoded.toJson()
val kotlinxJson = kotlinxDecoded.toJson()
assertEquals(
jacksonJson,
kotlinxJson,
"""
Serialization mismatch for:
${kClass.qualifiedName}
Jackson:
$jacksonJson
Kotlinx:
$kotlinxJson
""".trimIndent()
)
println("Identical deserialization for: ${kClass.jvmName}")
}
}
// DEX files are the best solution to read all our classes dynamically.
// classgraph could be used instead, but it only gives results on the JVM, not Android.
@Suppress("DEPRECATION")
private fun findSerializableClasses(packageName: String): List<KClass<*>> {
val context = InstrumentationRegistry
.getInstrumentation()
.targetContext
val dexFile = DexFile(context.packageCodePath)
return dexFile.entries()
.toList()
.filter { it.startsWith(packageName) }
.mapNotNull {
runCatching { Class.forName(it).kotlin }.getOrNull()
}.filter { kClass ->
// Not possible to use .hasAnnotation() on newer Android versions.
kClass.java.annotations.any {
it is Serializable
}
}
}
@OptIn(InternalSerializationApi::class)
@Suppress("UNCHECKED_CAST")
private fun serializeWithKotlinx(
kClass: KClass<*>,
value: Any
): String {
val serializer = kClass.serializer() as KSerializer<Any>
return kotlinxMapper.encodeToString(serializer, value)
}
}

View file

@ -1,157 +0,0 @@
package com.lagradost.cloudstream3.utils.serializers
import android.net.Uri
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KeepGeneratedSerializer
import kotlinx.serialization.Serializable
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = NonEmptyData.Serializer::class)
data class NonEmptyData(
val title: String = "",
val tags: List<String> = emptyList(),
val meta: Map<String, String> = emptyMap(),
val name: String = "hello",
) {
object Serializer : NonEmptySerializer<NonEmptyData>(NonEmptyData.generatedSerializer())
}
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = WriteOnlyData.Serializer::class)
data class WriteOnlyData(
val fieldA: String = "",
val fieldB: String = "",
) {
object Serializer : WriteOnlySerializer<WriteOnlyData>(
WriteOnlyData.generatedSerializer(),
setOf("fieldB"),
)
}
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = MultiWriteOnly.Serializer::class)
data class MultiWriteOnly(
val fieldA: String = "",
val fieldB: String = "",
val fieldC: String = "",
) {
object Serializer : WriteOnlySerializer<MultiWriteOnly>(
MultiWriteOnly.generatedSerializer(),
setOf("fieldB", "fieldC"),
)
}
@Serializable
data class UriData(
@Serializable(with = UriSerializer::class)
val uri: Uri = Uri.EMPTY,
)
class SerializerTest {
@Test
fun nonEmptySerializerOmitsEmptyStrings() {
val data = NonEmptyData(title = "", name = "hello")
val result = data.toJson()
assertFalse(result.contains("title"))
assertTrue(result.contains("name"))
}
@Test
fun nonEmptySerializerOmitsEmptyLists() {
val data = NonEmptyData(tags = emptyList(), name = "hello")
val result = data.toJson()
assertFalse(result.contains("tags"))
}
@Test
fun nonEmptySerializerOmitsEmptyMaps() {
val data = NonEmptyData(meta = emptyMap(), name = "hello")
val result = data.toJson()
assertFalse(result.contains("meta"))
}
@Test
fun nonEmptySerializerKeepsNonEmptyFields() {
val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v"))
val result = data.toJson()
assertTrue(result.contains("title"))
assertTrue(result.contains("tags"))
assertTrue(result.contains("meta"))
}
@Test
fun nonEmptySerializerDoesNotAffectDeserialization() {
val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}"""
val result = parseJson<NonEmptyData>(input)
assertEquals("hello", result.title)
assertEquals(listOf("a"), result.tags)
assertEquals(mapOf("k" to "v"), result.meta)
assertEquals("world", result.name)
}
@Test
fun writeOnlySerializerOmitsFieldOnSerialize() {
val data = WriteOnlyData(fieldA = "hello", fieldB = "secret")
val result = data.toJson()
assertTrue(result.contains("fieldA"))
assertFalse(result.contains("fieldB"))
}
@Test
fun writeOnlySerializerDeserializesNormally() {
val input = """{"fieldA":"hello","fieldB":"secret"}"""
val result = parseJson<WriteOnlyData>(input)
assertEquals("hello", result.fieldA)
assertEquals("secret", result.fieldB)
}
@Test
fun writeOnlySerializerDeserializesMissingAsDefault() {
val input = """{"fieldA":"hello"}"""
val result = parseJson<WriteOnlyData>(input)
assertEquals("hello", result.fieldA)
assertEquals("", result.fieldB)
}
@Test
fun writeOnlySerializerHandlesMultipleKeys() {
val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2")
val result = data.toJson()
assertTrue(result.contains("fieldA"))
assertFalse(result.contains("fieldB"))
assertFalse(result.contains("fieldC"))
}
@Test
fun uriSerializerSerializesUriToString() {
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
val result = data.toJson()
assertTrue(result.contains("https://example.com/path?query=1"))
}
@Test
fun uriSerializerDeserializesStringToUri() {
val input = """{"uri":"https://example.com/path?query=1"}"""
val result = parseJson<UriData>(input)
assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
}
@Test
fun uriSerializerRoundtripsCorrectly() {
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
val encoded = data.toJson()
val decoded = parseJson<UriData>(encoded)
assertEquals(data.uri, decoded.uri)
}
}

View file

@ -22,47 +22,6 @@
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"

View file

@ -41,6 +41,7 @@ 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
@ -116,6 +117,7 @@ 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
@ -532,7 +534,87 @@ 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 */
@ -579,10 +661,8 @@ object CommonActivity {
// TODO: Figure out why removing the check for SearchAutoComplete seems
// to break focus on TV as it shouldn't need to be used.
// Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote)
// send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button.
@SuppressLint("RestrictedApi")
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) &&
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) {
showInputMethod(act.currentFocus?.findFocus())
@ -603,4 +683,4 @@ object CommonActivity {
}
return null
}
}
}

View file

@ -362,8 +362,7 @@ 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) {
@ -408,10 +407,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
return true
}
val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull()
if (matchedApi != null) {
loadResult(str, matchedApi.name, "")
return true
synchronized(apis) {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name, "")
return true
}
}
}
}
}
@ -557,11 +559,9 @@ 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,11 +570,7 @@ 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
}
@ -806,11 +802,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
private val pluginsLock = Mutex()
private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe {
pluginsLock.withLock {
allProviders.withLock {
synchronized(allProviders) {
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
@ -1653,7 +1650,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
ioSafe {
initAll()
// No duplicates (which can happen by registerMainAPI)
apis = allProviders.distinctBy { it }
apis = synchronized(allProviders) {
allProviders.distinctBy { it }
}
}
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
@ -1961,7 +1960,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n"
allProviders.withLock {
synchronized(allProviders) {
for (api in allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix(

View file

@ -20,10 +20,8 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage
import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage
import com.lagradost.cloudstream3.actions.temp.OnlyPlayer
import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
@ -34,8 +32,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
@ -45,7 +43,7 @@ import java.util.concurrent.FutureTask
import kotlin.reflect.jvm.jvmName
object VideoClickActionHolder {
val allVideoClickActions = atomicListOf(
val allVideoClickActions = threadSafeListOf(
// Default
PlayInBrowserAction(),
CopyClipboardAction(),
@ -66,8 +64,6 @@ object VideoClickActionHolder {
MpvYTDLPackage(),
MpvKtPackage(),
MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option
AlwaysAskAction(),
// added by plugins

View file

@ -1,75 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.lagradost.api.Log
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/Riteshp2001/mpvRx
*
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L132
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L56
* */
class MpvRxPackage : OpenInAppAction(
appName = txt("mpvRx"),
packageName = "app.gyrolet.mpvrx",
intentClass = "app.gyrolet.mpvrx.ui.player.PlayerActivity"
) {
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.apply {
putExtra("title", video.name)
val link = result.links[index!!]
val headers = link.headers
setData(link.url.toUri())
if (headers.isNotEmpty()) {
// PlayerActivity expects a flat array: [key1, value1, key2, value2, ...]
val flat = headers.entries.flatMap { listOf(it.key, it.value) }.toTypedArray()
intent.putExtra("headers", flat)
}
/*val subs = result.subs // disabled due to https://github.com/Riteshp2001/mpvRx/issues/146
intent.putExtra("subs", subs.map { it.url.toUri() }.toTypedArray())
intent.putExtra(
"subs.titles",
subs.map { it.name }.toTypedArray(),
)
intent.putExtra(
"subs.langs",
subs.map { it.languageCode }.toTypedArray(),
)
val selected = subs.firstOrNull { it.matchesLanguageCode("en") }?.url?.toUri()
intent.putExtra("subs.enable", selected?.let { arrayOf(it) } ?: arrayOf<Uri>() )*/
if (video.tvType.isEpisodeBased()) {
video.season?.let { intent.putExtra("introdb_season", it) }
video.episode.let { intent.putExtra("introdb_episode", it) }
}
val position = getViewPos(video.id)?.position
if (position != null)
putExtra("position", position.toInt())
}
}
override fun onResult(activity: Activity, intent: Intent?) {
val position = intent?.getIntExtra("position", -1) ?: -1
val duration = intent?.getIntExtra("duration", -1) ?: -1
Log.d("MPV", "Position: $position, Duration: $duration")
updateDurationAndPosition(position.toLong(), duration.toLong())
}
}

View file

@ -1,44 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/Kindness-Kismet/only_player/tree/main
* https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */
class OnlyPlayer : OpenInAppAction(
txt("Only Player"),
"one.only.player",
intentClass = "one.only.player.feature.player.PlayerActivity"
) {
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
/** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */
intent.apply {
val link = result.links[index!!]
setData(link.url.toUri())
putExtra("headers", Bundle().apply {
for ((key, value) in link.headers) {
putExtra(key, value)
}
})
}
}
override fun onResult(activity: Activity, intent: Intent?) {
/* onResult does not get called */
}
}

View file

@ -35,11 +35,9 @@ 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,
@ -49,7 +47,7 @@ class PlayMirrorAction : VideoClickAction() {
offset: Int,
isCasting: Boolean
): Boolean {
index?.let { callback(link to null) }
index?.let { callback(result.links[it] to null) }
result.subs.forEach { subtitle -> subtitleCallback(subtitle) }
return true
}
@ -58,7 +56,7 @@ class PlayMirrorAction : VideoClickAction() {
activity.navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
generatorMirror, 0, result.syncData
generatorMirror, result.syncData
)
)
}

View file

@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import kotlin.Throws
abstract class Plugin : BasePlugin() {
/**
* Called when your Plugin is loaded
@ -25,7 +26,9 @@ abstract class Plugin : BasePlugin() {
fun registerVideoClickAction(element: VideoClickAction) {
Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction")
element.sourcePlugin = this.filename
VideoClickActionHolder.allVideoClickActions.add(element)
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.add(element)
}
}
/**
@ -37,4 +40,4 @@ abstract class Plugin : BasePlugin() {
* This will add a button in the settings allowing you to add custom settings
*/
var openSettings: ((context: Context) -> Unit)? = null
}
}

View file

@ -610,7 +610,7 @@ object PluginManager {
return false
}
InputStreamReader(stream).use { reader ->
manifest = parseJson<BasePlugin.Manifest>(reader.readText())
manifest = parseJson(reader, BasePlugin.Manifest::class.java)
}
}
@ -651,15 +651,9 @@ object PluginManager {
context.resources.configuration
)
}
synchronized(plugins) {
plugins[filePath] = pluginInstance
}
synchronized(classLoaders) {
classLoaders[loader] = pluginInstance
}
synchronized(urlPlugins) {
urlPlugins[data.url ?: filePath] = pluginInstance
}
plugins[filePath] = pluginInstance
classLoaders[loader] = pluginInstance
urlPlugins[data.url ?: filePath] = pluginInstance
if (pluginInstance is Plugin) {
pluginInstance.load(context)
} else {
@ -695,20 +689,21 @@ object PluginManager {
}
// remove all registered apis
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it)
synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it)
}
}
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
}
APIHolder.allProviders.withLock {
APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename }
synchronized(extractorApis) {
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
}
extractorApis.withLock {
extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename }
}
VideoClickActionHolder.allVideoClickActions.withLock {
VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename }
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename }
}
synchronized(classLoaders) {

View file

@ -36,7 +36,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
@ -187,16 +186,6 @@ class DownloadQueueService : Service() {
debugAssert({ timeTaken == null }, { "Downloader startup should not time out" })
totalDownloadFlow
.debounce { (instances, queue) ->
// Filter away incorrect transient queue states.
// For example when we pop the queue and add a download instance there exists a transient state where
// there is no queue and no download instances (leading to an early exit)
if (instances.isEmpty() && queue.isEmpty()) {
500.milliseconds
} else {
0.milliseconds
}
}
.takeWhile { (instances, queue) ->
// Stop if destroyed
isRunning

View file

@ -1,14 +1,50 @@
package com.lagradost.cloudstream3.syncproviders
import android.util.Base64
import androidx.annotation.WorkerThread
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.NextAiring
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID
import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.LocalList
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi
import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
import java.net.URI
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UiText
import com.lagradost.cloudstream3.utils.txt
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.net.URL
import java.security.SecureRandom
import java.util.Date
import java.util.concurrent.TimeUnit
data class AuthLoginPage(
/** The website to open to authenticate */
@ -45,10 +81,10 @@ data class AuthToken(
val payload: String? = null,
) {
fun isAccessTokenExpired(marginSec: Long = 10L) =
accessTokenLifetime != null && unixTime + marginSec >= accessTokenLifetime
accessTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= accessTokenLifetime
fun isRefreshTokenExpired(marginSec: Long = 10L) =
refreshTokenLifetime != null && unixTime + marginSec >= refreshTokenLifetime
refreshTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= refreshTokenLifetime
}
data class AuthUser(
@ -143,33 +179,16 @@ abstract class AuthAPI {
open val inAppLoginRequirement: AuthLoginRequirement? = null
companion object {
@Deprecated(
message = "Use APIHolder.unixTime instead",
replaceWith = ReplaceWith(
expression = "APIHolder.unixTime",
imports = ["com.lagradost.cloudstream3.APIHolder"]
),
level = DeprecationLevel.WARNING,
)
val unixTime: Long
get() = APIHolder.unixTime
@Deprecated(
message = "Use APIHolder.unixTimeMS instead",
replaceWith = ReplaceWith(
expression = "unixTimeMS",
imports = ["com.lagradost.cloudstream3.APIHolder.unixTimeMS"]
),
level = DeprecationLevel.WARNING,
)
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long
get() = unixTimeMS
get() = System.currentTimeMillis()
fun splitRedirectUrl(redirectUrl: String): Map<String, String> {
return splitQuery(
URI(
URL(
redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
).toURL()
)
)
}
@ -179,8 +198,9 @@ abstract class AuthAPI {
val secureRandom = SecureRandom()
val codeVerifierBytes = ByteArray(96) // base64 has 6bit per char; (8/6)*96 = 128
secureRandom.nextBytes(codeVerifierBytes)
return base64Encode(codeVerifierBytes).trimEnd('=')
.replace("+", "-").replace("/", "_").replace("\n", "")
return Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=')
.replace("+", "-")
.replace("/", "_").replace("\n", "")
}
}

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
import com.lagradost.cloudstream3.subtitles.SubtitleResource
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
/** Stateless safe abstraction of SubtitleAPI */
class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
@ -24,30 +24,26 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
)
// maybe make this a generic struct? right now there is a lot of boilerplate
private val searchCache = atomicListOf<SavedSearchResponse>()
private val searchCache = threadSafeListOf<SavedSearchResponse>()
private var searchCacheIndex: Int = 0
private val resourceCache = atomicListOf<SavedResourceResponse>()
private val resourceCache = threadSafeListOf<SavedResourceResponse>()
private var resourceCacheIndex: Int = 0
const val CACHE_SIZE = 20
}
@WorkerThread
suspend fun resource(data: SubtitleEntity): Result<SubtitleResource> = runCatching {
val cached = resourceCache.withLock {
var found: SubtitleResource? = null
synchronized(resourceCache) {
for (item in resourceCache) {
// 20 min save
if (item.query == data && (unixTime - item.unixTime) < 60 * 20) {
found = item.response
break
return@runCatching item.response
}
}
found
}
if (cached != null) return@runCatching cached
val returnValue = api.resource(freshAuth(), data)
resourceCache.withLock {
synchronized(resourceCache) {
val add = SavedResourceResponse(unixTime, returnValue, data)
if (resourceCache.size > CACHE_SIZE) {
resourceCache[resourceCacheIndex] = add // rolling cache
@ -62,25 +58,22 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
@WorkerThread
suspend fun search(query: SubtitleSearch): Result<List<SubtitleEntity>> {
return runCatching {
val cached = searchCache.withLock {
var found: List<SubtitleEntity>? = null
synchronized(searchCache) {
for (item in searchCache) {
// 120 min save
if (item.query == query && (unixTime - item.unixTime) < 60 * 120) {
found = item.response
break
return@runCatching item.response
}
}
found
}
if (cached != null) return@runCatching cached
val returnValue = api.search(freshAuth(), query) ?: emptyList()
val returnValue =
api.search(freshAuth(), query) ?: emptyList()
// only cache valid return values
if (returnValue.isNotEmpty()) {
val add = SavedSearchResponse(unixTime, returnValue, query)
searchCache.withLock {
synchronized(searchCache) {
if (searchCache.size > CACHE_SIZE) {
searchCache[searchCacheIndex] = add // rolling cache
searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE
@ -93,3 +86,4 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
}
}
}

View file

@ -10,8 +10,8 @@ import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.Levenshtein
import com.lagradost.cloudstream3.utils.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.util.Date
/**
@ -138,7 +138,7 @@ abstract class SyncAPI : AuthAPI() {
ListSorting.Query ->
if (query != null) {
items.sortedBy {
-Levenshtein.partialRatio(
-FuzzySearch.partialRatio(
query.lowercase(), it.name.lowercase()
)
}
@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() {
override var score: Score? = null,
val tags: List<String>? = null
) : SearchResponse
}
}

View file

@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.Actor
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.ActorRole
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.ErrorLoadingException
@ -37,7 +35,7 @@ class AniListApi : SyncAPI() {
override var name = "AniList"
override val idPrefix = "anilist"
private val key = BuildConfig.ANILIST_KEY
val key = "6871"
override val redirectUrlIdentifier = "anilistlogin"
override var requireLibraryRefresh = true
override val hasOAuth2 = true
@ -52,10 +50,9 @@ class AniListApi : SyncAPI() {
override suspend fun login(redirectUrl: String, payload: String?): AuthToken? {
val sanitizer = splitRedirectUrl(redirectUrl)
val token = AuthToken(
accessToken = sanitizer["access_token"]
?: throw ErrorLoadingException("No access token"),
accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"),
//refreshToken = sanitizer["refresh_token"],
accessTokenLifetime = APIHolder.unixTime + sanitizer["expires_in"]!!.toLong(),
accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(),
)
return token
}
@ -86,8 +83,8 @@ class AniListApi : SyncAPI() {
return "$mainUrl/anime/$id"
}
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(query) ?: return null
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(name) ?: return null
return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult(
it.title.romaji ?: return null,
@ -99,7 +96,7 @@ class AniListApi : SyncAPI() {
}
}
override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? {
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
val season = getSeason(internalId).data.media
@ -109,7 +106,7 @@ class AniListApi : SyncAPI() {
nextAiring = season.nextAiringEpisode?.let {
NextAiring(
it.episode ?: return@let null,
(it.timeUntilAiring ?: return@let null) + APIHolder.unixTime
(it.timeUntilAiring ?: return@let null) + unixTime
)
},
title = season.title?.userPreferred,
@ -161,7 +158,7 @@ class AniListApi : SyncAPI() {
)
}
override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
val internalId = id.toIntOrNull() ?: return null
val data = getDataAboutId(auth ?: return null, internalId) ?: return null
@ -462,7 +459,7 @@ class AniListApi : SyncAPI() {
}
}
private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? {
private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? {
val q =
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
@ -509,7 +506,7 @@ class AniListApi : SyncAPI() {
}
private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? {
private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? {
return app.post(
"https://graphql.anilist.co/",
headers = mapOf(
@ -641,7 +638,7 @@ class AniListApi : SyncAPI() {
}
}
override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? {
override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? {
val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group ->
@ -669,7 +666,7 @@ class AniListApi : SyncAPI() {
)
}
private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? {
private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? {
val userID = auth.user.id
val mediaType = "ANIME"
@ -717,7 +714,7 @@ class AniListApi : SyncAPI() {
return text?.toKotlinObject()
}
suspend fun toggleLike(auth: AuthData, id: Int): Boolean {
suspend fun toggleLike(auth : AuthData, id: Int): Boolean {
val q = """mutation (${'$'}animeId: Int = $id) {
ToggleFavourite (animeId: ${'$'}animeId) {
anime {
@ -740,7 +737,7 @@ class AniListApi : SyncAPI() {
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId(
auth: AuthData,
auth : AuthData,
id: Int,
type: AniListStatusType,
score: Score?,
@ -789,7 +786,7 @@ class AniListApi : SyncAPI() {
return data != ""
}
private suspend fun getUser(token: AuthToken): AniListUser? {
private suspend fun getUser(token : AuthToken): AniListUser? {
val q = """
{
Viewer {

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R
@ -28,8 +27,9 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.Locale
@ -107,7 +107,7 @@ class KitsuApi: SyncAPI() {
).parsed<ResponseToken>()
return AuthToken(
accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(),
accessTokenLifetime = unixTime + token.expiresIn.toLong(),
refreshToken = token.refreshToken,
accessToken = token.accessToken,
)
@ -126,7 +126,7 @@ class KitsuApi: SyncAPI() {
return AuthToken(
accessToken = res.accessToken,
refreshToken = res.refreshToken,
accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong()
accessTokenLifetime = unixTime + res.expiresIn.toLong()
)
}
@ -202,7 +202,7 @@ class KitsuApi: SyncAPI() {
id = id,
totalEpisodes = anime.episodeCount,
title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(),
publicScore = Score.from(anime.ratingTwenty, 20),
publicScore = Score.from(anime.ratingTwenty.toString(), 20),
duration = anime.episodeLength,
synopsis = anime.synopsis,
airStatus = when(anime.status) {
@ -250,7 +250,7 @@ class KitsuApi: SyncAPI() {
}
return SyncStatus(
score = Score.from(anime.ratingTwenty, 20),
score = Score.from(anime.ratingTwenty.toString(), 20),
status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)),
isFavorite = null,
watchedEpisodes = anime.progress,
@ -454,8 +454,8 @@ class KitsuApi: SyncAPI() {
private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array<KitsuNode> {
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount")
val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","updatedAt", "status")
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount")
val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status")
val limit = 500
var url = "$apiUrl/library-entries?filter[userId]=$userId&filter[kind]=anime&include=anime&page[limit]=$limit&page[offset]=0&fields[anime]=${animeSelectedFields.joinToString(",")}&fields[libraryEntries]=${libraryEntriesSelectedFields.joinToString(",")}"
@ -526,7 +526,7 @@ class KitsuApi: SyncAPI() {
this.id,
this.attributes.progress,
numEpisodes,
Score.from(this.attributes.ratingTwenty, 20),
Score.from(this.attributes.ratingTwenty.toString(), 20),
parseDateLong(this.attributes.updatedAt),
"Kitsu",
TvType.Anime,
@ -535,9 +535,12 @@ class KitsuApi: SyncAPI() {
null,
plot = synopsis,
releaseDate = if (startDate == null) null else try {
Date.from(LocalDate.parse(startDate).atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant())
Date.from(
Instant.from(
DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd")
.parse(startDate)
)
)
} catch (_: RuntimeException) {
null
}
@ -580,7 +583,7 @@ class KitsuApi: SyncAPI() {
@JsonProperty("avatar") val avatar: KitsuUserAvatar?,
/* User list anime attributes */
@JsonProperty("progress") val progress: Int?,
@JsonProperty("ratingTwenty") val ratingTwenty: Int?,
@JsonProperty("ratingTwenty") val ratingTwenty: Float?,
@JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("status") val status: String?,
)
@ -629,7 +632,7 @@ class KitsuApi: SyncAPI() {
const val KITSU_CACHED_LIST: String = "kitsu_cached_list"
private fun parseDateLong(string: String?): Long? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse(
string ?: return null
)?.time?.div(1000)
} catch (e: Exception) {

View file

@ -2,8 +2,6 @@ package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R
@ -36,7 +34,7 @@ class MALApi : SyncAPI() {
override var name = "MAL"
override val idPrefix = "mal"
private val key = BuildConfig.MAL_KEY
val key = "1714d6f2f4f7cc19644384f8c4629910"
private val apiUrl = "https://api.myanimelist.net"
override val hasOAuth2 = true
override val redirectUrlIdentifier: String? = "mallogin"
@ -80,7 +78,7 @@ class MALApi : SyncAPI() {
)
).parsed<ResponseToken>()
return AuthToken(
accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(),
accessTokenLifetime = unixTime + token.expiresIn.toLong(),
refreshToken = token.refreshToken,
accessToken = token.accessToken
)
@ -100,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=$query&limit=$MAL_MAX_SEARCH_LIMIT"
val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $auth",
@ -124,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 {
@ -227,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 =
@ -273,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
@ -368,7 +366,7 @@ class MALApi : SyncAPI() {
return AuthToken(
accessToken = res.accessToken,
refreshToken = res.refreshToken,
accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong()
accessTokenLifetime = unixTime + res.expiresIn.toLong()
)
}
@ -479,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 ->
@ -507,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)

View file

@ -2,11 +2,9 @@ package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement
@ -45,17 +43,17 @@ class OpenSubtitlesApi : SubtitleAPI() {
}
private fun canDoRequest(): Boolean {
return unixTimeMS > currentCoolDown
return unixTimeMs > currentCoolDown
}
private fun throwIfCantDoRequest() {
if (!canDoRequest()) {
throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMS) / 1000L}s")
throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s")
}
}
private fun throwGotTooManyRequests() {
currentCoolDown = unixTimeMS + COOLDOWN_DURATION
currentCoolDown = unixTimeMs + COOLDOWN_DURATION
throw ErrorLoadingException("Too many requests")
}
@ -91,7 +89,7 @@ class OpenSubtitlesApi : SubtitleAPI() {
accessToken = response.token
?: throw ErrorLoadingException("Invalid password or username"),
/// JWT token is valid 24 hours after successfully authentication of user
accessTokenLifetime = APIHolder.unixTime + 60 * 60 * 24,
accessTokenLifetime = unixTime + 60 * 60 * 24,
payload = form.toJson()
)
}

View file

@ -4,7 +4,6 @@ import androidx.annotation.StringRes
import androidx.core.net.toUri
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CloudStreamApp
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
@ -17,6 +16,7 @@ import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.SimklSyncServices
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
@ -30,7 +30,6 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
import com.lagradost.cloudstream3.utils.txt
import java.math.BigInteger
@ -78,15 +77,15 @@ class SimklApi : SyncAPI() {
private class SimklCacheWrapper<T>(
@JsonProperty("obj") val obj: T?,
@JsonProperty("validUntil") val validUntil: Long,
@JsonProperty("cacheTime") val cacheTime: Long = APIHolder.unixTime,
@JsonProperty("cacheTime") val cacheTime: Long = unixTime,
) {
/** Returns true if cache is newer than cacheDays */
fun isFresh(): Boolean {
return validUntil > APIHolder.unixTime
return validUntil > unixTime
}
fun remainingTime(): Duration {
val unixTime = APIHolder.unixTime
val unixTime = unixTime
return if (validUntil > unixTime) {
(validUntil - unixTime).toDuration(DurationUnit.SECONDS)
} else {
@ -110,7 +109,7 @@ class SimklApi : SyncAPI() {
SIMKL_CACHE_KEY,
path,
// Storing as plain sting is required to make generics work.
SimklCacheWrapper(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson()
SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson()
)
}
@ -118,8 +117,13 @@ 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 {
tryParseJson<SimklCacheWrapper<T>>(it)
mapper.readValue<SimklCacheWrapper<T>>(it, type)
}
return if (cache?.isFresh() == true) {
@ -419,7 +423,7 @@ class SimklApi : SyncAPI() {
}
suspend fun execute(): Boolean {
val time = getDateTime(APIHolder.unixTime)
val time = getDateTime(unixTime)
val headers = this.headers ?: emptyMap()
return if (this.status == SimklListStatusType.None.value) {
app.post(
@ -569,7 +573,7 @@ class SimklApi : SyncAPI() {
@JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?,
@JsonProperty("rating") val rating: Int,
@JsonProperty("rated_at") val ratedAt: String? = getDateTime(APIHolder.unixTime)
@JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime)
) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@ -578,7 +582,7 @@ class SimklApi : SyncAPI() {
@JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?,
@JsonProperty("to") val to: String,
@JsonProperty("watched_at") val watchedAt: String? = getDateTime(APIHolder.unixTime)
@JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime)
) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@ -863,7 +867,7 @@ class SimklApi : SyncAPI() {
newStatus: AbstractSyncStatus
): Boolean {
val parsedId = readIdFromString(id)
lastScoreTime = APIHolder.unixTime
lastScoreTime = unixTime
val simklStatus = newStatus as? SimklSyncStatus
val builder = SimklScoreBuilder.Builder()
@ -912,7 +916,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 query)
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name)
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
}

View file

@ -12,8 +12,6 @@ import com.lagradost.cloudstream3.syncproviders.AuthToken
import com.lagradost.cloudstream3.syncproviders.AuthUser
import com.lagradost.cloudstream3.syncproviders.SubtitleAPI
import com.lagradost.cloudstream3.TvType
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
class SubDlApi : SubtitleAPI() {
override val name = "SubDL"
@ -26,7 +24,7 @@ class SubDlApi : SubtitleAPI() {
override val createAccountUrl = "https://subdl.com/panel/register"
companion object {
const val APIURL = "https://api.subdl.com"
const val APIURL = "https://apiold.subdl.com"
const val APIENDPOINT = "$APIURL/api/v1/subtitles"
const val DOWNLOADENDPOINT = "https://dl.subdl.com"
}
@ -124,80 +122,72 @@ class SubDlApi : SubtitleAPI() {
}
}
@Serializable
data class SubtitleOAuthEntity(
@JsonProperty("userEmail") @SerialName("userEmail") var userEmail: String,
@JsonProperty("pass") @SerialName("pass") var pass: String,
@JsonProperty("name") @SerialName("name") var name: String? = null,
@JsonProperty("accessToken") @SerialName("accessToken") var accessToken: String? = null,
@JsonProperty("apiKey") @SerialName("apiKey") var apiKey: String? = null,
@JsonProperty("userEmail") var userEmail: String,
@JsonProperty("pass") var pass: String,
@JsonProperty("name") var name: String? = null,
@JsonProperty("accessToken") var accessToken: String? = null,
@JsonProperty("apiKey") var apiKey: String? = null,
)
@Serializable
data class OAuthTokenResponse(
@JsonProperty("token") @SerialName("token") val token: String,
@JsonProperty("userData") @SerialName("userData") val userData: UserData? = null,
@JsonProperty("status") @SerialName("status") val status: Boolean? = null,
@JsonProperty("message") @SerialName("message") val message: String? = null,
@JsonProperty("token") val token: String,
@JsonProperty("userData") val userData: UserData? = null,
@JsonProperty("status") val status: Boolean? = null,
@JsonProperty("message") val message: String? = null,
)
@Serializable
data class UserData(
@JsonProperty("email") @SerialName("email") val email: String,
@JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("country") @SerialName("country") val country: String,
@JsonProperty("scStepCode") @SerialName("scStepCode") val scStepCode: String,
@JsonProperty("scVerified") @SerialName("scVerified") val scVerified: Boolean,
@JsonProperty("username") @SerialName("username") val username: String? = null,
@JsonProperty("scUsername") @SerialName("scUsername") val scUsername: String,
@JsonProperty("email") val email: String,
@JsonProperty("name") val name: String,
@JsonProperty("country") val country: String,
@JsonProperty("scStepCode") val scStepCode: String,
@JsonProperty("scVerified") val scVerified: Boolean,
@JsonProperty("username") val username: String? = null,
@JsonProperty("scUsername") val scUsername: String,
)
@Serializable
data class ApiKeyResponse(
@JsonProperty("ok") @SerialName("ok") val ok: Boolean? = false,
@JsonProperty("api_key") @SerialName("api_key") val apiKey: String,
@JsonProperty("usage") @SerialName("usage") val usage: Usage? = null,
@JsonProperty("ok") val ok: Boolean? = false,
@JsonProperty("api_key") val apiKey: String,
@JsonProperty("usage") val usage: Usage? = null,
)
@Serializable
data class Usage(
@JsonProperty("total") @SerialName("total") val total: Long? = 0,
@JsonProperty("today") @SerialName("today") val today: Long? = 0,
@JsonProperty("total") val total: Long? = 0,
@JsonProperty("today") val today: Long? = 0,
)
@Serializable
data class ApiResponse(
@JsonProperty("status") @SerialName("status") val status: Boolean? = null,
@JsonProperty("results") @SerialName("results") val results: List<Result>? = null,
@JsonProperty("subtitles") @SerialName("subtitles") val subtitles: List<Subtitle>? = null,
@JsonProperty("status") val status: Boolean? = null,
@JsonProperty("results") val results: List<Result>? = null,
@JsonProperty("subtitles") val subtitles: List<Subtitle>? = null,
)
@Serializable
data class Result(
@JsonProperty("sd_id") @SerialName("sd_id") val sdId: Int? = null,
@JsonProperty("type") @SerialName("type") val type: String? = null,
@JsonProperty("name") @SerialName("name") val name: String? = null,
@JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null,
@JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long? = null,
@JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null,
@JsonProperty("year") @SerialName("year") val year: Int? = null,
@JsonProperty("sd_id") val sdId: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("imdb_id") val imdbId: String? = null,
@JsonProperty("tmdb_id") val tmdbId: Long? = null,
@JsonProperty("first_air_date") val firstAirDate: String? = null,
@JsonProperty("year") val year: Int? = null,
)
@Serializable
data class Subtitle(
@JsonProperty("release_name") @SerialName("release_name") val releaseName: String,
@JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("lang") @SerialName("lang") val lang: String, // subdl language code
@JsonProperty("author") @SerialName("author") val author: String? = null,
@JsonProperty("url") @SerialName("url") val url: String? = null,
@JsonProperty("subtitlePage") @SerialName("subtitlePage") val subtitlePage: String? = null,
@JsonProperty("season") @SerialName("season") val season: Int? = null,
@JsonProperty("episode") @SerialName("episode") val episode: Int? = null,
@JsonProperty("language") @SerialName("language") val language: String? = null, // full language name
@JsonProperty("hi") @SerialName("hi") val hearingImpaired: Boolean? = null,
@JsonProperty("release_name") val releaseName: String,
@JsonProperty("name") val name: String,
@JsonProperty("lang") val lang: String, // subdl language code
@JsonProperty("author") val author: String? = null,
@JsonProperty("url") val url: String? = null,
@JsonProperty("subtitlePage") val subtitlePage: String? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null,
@JsonProperty("language") val language: String? = null, // full language name
@JsonProperty("hi") val hearingImpaired: Boolean? = null,
)
// https://subdl.com/api-files/language_list.json
// https://subdl.com/api-files/language_list.json
// most of it is IETF BPC 47 conformant tag
// but there are some exceptions
private val langTagIETF2subdl = mapOf(
@ -207,63 +197,63 @@ class SubDlApi : SubtitleAPI() {
"en-nl" to "NL_EN", // "Dutch_English"
"pt-br" to "BR_PT", // "Brazillian Portuguese"
"zh-hant" to "ZH_BG", // "Big 5 code" -> traditional Chinese (?_?)
// "ar" to "AR", // "Arabic"
// "az" to "AZ", // "Azerbaijani"
// "be" to "BE", // "Belarusian"
// "bg" to "BG", // "Bulgarian"
// "bn" to "BN", // "Bengali"
// "bs" to "BS", // "Bosnian"
// "ca" to "CA", // "Catalan"
// "cs" to "CS", // "Czech"
// "da" to "DA", // "Danish"
// "de" to "DE", // "German"
// "el" to "EL", // "Greek"
// "en" to "EN", // "English"
// "eo" to "EO", // "Esperanto"
// "es" to "ES", // "Spanish"
// "et" to "ET", // "Estonian"
// "fa" to "FA", // "Farsi_Persian"
// "fi" to "FI", // "Finnish"
// "fr" to "FR", // "French"
// "he" to "HE", // "Hebrew"
// "hi" to "HI", // "Hindi"
// "hr" to "HR", // "Croatian"
// "hu" to "HU", // "Hungarian"
// "id" to "ID", // "Indonesian"
// "is" to "IS", // "Icelandic"
// "it" to "IT", // "Italian"
// "ja" to "JA", // "Japanese"
// "ka" to "KA", // "Georgian"
// "kl" to "KL", // "Greenlandic"
// "ko" to "KO", // "Korean"
// "ku" to "KU", // "Kurdish"
// "lt" to "LT", // "Lithuanian"
// "lv" to "LV", // "Latvian"
// "mk" to "MK", // "Macedonian"
// "ml" to "ML", // "Malayalam"
// "mni" to "MNI", // "Manipuri"
// "ms" to "MS", // "Malay"
// "my" to "MY", // "Burmese"
// "nl" to "NL", // "Dutch"
// "no" to "NO", // "Norwegian"
// "pl" to "PL", // "Polish"
// "pt" to "PT", // "Portuguese"
// "ro" to "RO", // "Romanian"
// "ru" to "RU", // "Russian"
// "si" to "SI", // "Sinhala"
// "sk" to "SK", // "Slovak"
// "sl" to "SL", // "Slovenian"
// "sq" to "SQ", // "Albanian"
// "sr" to "SR", // "Serbian"
// "sv" to "SV", // "Swedish"
// "ta" to "TA", // "Tamil"
// "te" to "TE", // "Telugu"
// "th" to "TH", // "Thai"
// "tl" to "TL", // "Tagalog"
// "tr" to "TR", // "Turkish"
// "uk" to "UK", // "Ukranian"
// "ur" to "UR", // "Urdu"
// "vi" to "VI", // "Vietnamese"
// "zh" to "ZH", // "Chinese BG code"
// "ar" to "AR", // "Arabic"
// "az" to "AZ", // "Azerbaijani"
// "be" to "BE", // "Belarusian"
// "bg" to "BG", // "Bulgarian"
// "bn" to "BN", // "Bengali"
// "bs" to "BS", // "Bosnian"
// "ca" to "CA", // "Catalan"
// "cs" to "CS", // "Czech"
// "da" to "DA", // "Danish"
// "de" to "DE", // "German"
// "el" to "EL", // "Greek"
// "en" to "EN", // "English"
// "eo" to "EO", // "Esperanto"
// "es" to "ES", // "Spanish"
// "et" to "ET", // "Estonian"
// "fa" to "FA", // "Farsi_Persian"
// "fi" to "FI", // "Finnish"
// "fr" to "FR", // "French"
// "he" to "HE", // "Hebrew"
// "hi" to "HI", // "Hindi"
// "hr" to "HR", // "Croatian"
// "hu" to "HU", // "Hungarian"
// "id" to "ID", // "Indonesian"
// "is" to "IS", // "Icelandic"
// "it" to "IT", // "Italian"
// "ja" to "JA", // "Japanese"
// "ka" to "KA", // "Georgian"
// "kl" to "KL", // "Greenlandic"
// "ko" to "KO", // "Korean"
// "ku" to "KU", // "Kurdish"
// "lt" to "LT", // "Lithuanian"
// "lv" to "LV", // "Latvian"
// "mk" to "MK", // "Macedonian"
// "ml" to "ML", // "Malayalam"
// "mni" to "MNI", // "Manipuri"
// "ms" to "MS", // "Malay"
// "my" to "MY", // "Burmese"
// "nl" to "NL", // "Dutch"
// "no" to "NO", // "Norwegian"
// "pl" to "PL", // "Polish"
// "pt" to "PT", // "Portuguese"
// "ro" to "RO", // "Romanian"
// "ru" to "RU", // "Russian"
// "si" to "SI", // "Sinhala"
// "sk" to "SK", // "Slovak"
// "sl" to "SL", // "Slovenian"
// "sq" to "SQ", // "Albanian"
// "sr" to "SR", // "Serbian"
// "sv" to "SV", // "Swedish"
// "ta" to "TA", // "Tamil"
// "te" to "TE", // "Telugu"
// "th" to "TH", // "Thai"
// "tl" to "TL", // "Tagalog"
// "tr" to "TR", // "Turkish"
// "uk" to "UK", // "Ukranian"
// "ur" to "UR", // "Urdu"
// "vi" to "VI", // "Vietnamese"
// "zh" to "ZH", // "Chinese BG code"
)
}

View file

@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.newSearchResponseList
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) {
val hash: Pair<String, String>
)
private val cache = atomicListOf<SavedLoadResponse>()
private val cache = threadSafeListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0
const val CACHE_SIZE = 20
@ -66,7 +66,9 @@ class APIRepository(val api: MainAPI) {
private fun afterPluginsLoaded(forceReload: Boolean) {
if (forceReload) {
cache.clear()
synchronized(cache) {
cache.clear()
}
}
}
@ -89,25 +91,21 @@ class APIRepository(val api: MainAPI) {
val fixedUrl = api.fixUrl(url)
val lookingForHash = Pair(api.name, fixedUrl)
val cached = cache.withLock {
var found: LoadResponse? = null
synchronized(cache) {
for (item in cache) {
// 10 min save
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
found = item.response
break
return@withTimeout item.response
}
}
found
}
if (cached != null) return@withTimeout cached
api.load(fixedUrl)?.also { response ->
// Remove all blank tags as early as possible
response.tags = response.tags?.filter { it.isNotBlank() }
val add = SavedLoadResponse(unixTime, response, lookingForHash)
cache.withLock {
synchronized(cache) {
if (cache.size > CACHE_SIZE) {
cache[cacheIndex] = add // rolling cache
cacheIndex = (cacheIndex + 1) % CACHE_SIZE
@ -217,4 +215,4 @@ class APIRepository(val api: MainAPI) {
return false
}
}
}
}

View file

@ -12,6 +12,9 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListView
import androidx.appcompat.app.AlertDialog
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.MediaSeekOptions
@ -102,6 +105,9 @@ data class MetadataHolder(
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) :
UIController() {
private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
init {
view.setImageResource(R.drawable.ic_baseline_playlist_play_24)
view.setOnClickListener {
@ -328,7 +334,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
}, subtitleCallback = {
currentSubs.add(it)
},
offset = 0,
isCasting = true
)
}
@ -443,4 +448,4 @@ class ControllerActivity : ExpandedControllerActivity() {
SkipNextEpisodeController(skipOpButton)
)
}
}
}

View file

@ -162,8 +162,7 @@ object DownloadButtonSetup {
}
act.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
DownloadFileGenerator(items),
items.indexOfFirst { it.id == click.data.id }
DownloadFileGenerator(items).apply { goto(items.indexOfFirst { it.id == click.data.id }) }
)
)
}

View file

@ -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,8 +349,7 @@ class DownloadFragment : BaseFragment<FragmentDownloadsBinding>(
listOf(BasicLink(url)),
extract = true,
refererUrl = referer,
id = url.hashCode()
), 0
)
)
)
dialog.dismissSafe(activity)

View file

@ -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(appContext, id)
val savedData = VideoDownloadManager.getDownloadFileInfo(context, 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()
}
}

View file

@ -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.getMainLooper().isCurrentThread) {
// Runs on the main thread, but also instant if it already is
if (Looper.myLooper() == Looper.getMainLooper()) {
try {
setStatusInternal(status)
} catch (t: Throwable) {

View file

@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() {
private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository {
return APIRepository(apis.withLock { apis.first { it.hasMainPage } })
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } })
}
private val _availableWatchStatusTypes =

View file

@ -210,13 +210,14 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(
syncId: SyncIdName,
apiName: String? = null,
) {
val availableProviders = allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList())
val availableProviders = synchronized(allProviders) {
allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList())
}
val baseOptions = listOf(
LibraryOpenerType.Default,
LibraryOpenerType.None,

View file

@ -12,7 +12,6 @@ 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
@ -96,7 +95,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.CLEARKEY_DRM_UUID
import com.lagradost.cloudstream3.utils.CLEARKEY_UUID
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
@ -104,9 +103,9 @@ import com.lagradost.cloudstream3.utils.DrmExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.PLAYREADY_DRM_UUID
import com.lagradost.cloudstream3.utils.PLAYREADY_UUID
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName
import com.lagradost.cloudstream3.utils.WIDEVINE_DRM_UUID
import com.lagradost.cloudstream3.utils.WIDEVINE_UUID
import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp
import kotlinx.coroutines.delay
import okhttp3.Interceptor
@ -118,7 +117,6 @@ import java.util.concurrent.Executors
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession
import kotlin.uuid.toJavaUuid
const val TAG = "CS3ExoPlayer"
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language"
@ -208,14 +206,16 @@ 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 thread.
if (Looper.getMainLooper().isCurrentThread) {
eventHandler?.invoke(event)
} else runOnMainThread {
// Ensure that all work is done on the main looper, aka main thread
if (Looper.myLooper() == mainHandler.looper) {
eventHandler?.invoke(event)
} else {
mainHandler.post {
eventHandler?.invoke(event)
}
}
}
@ -235,9 +235,8 @@ class CS3IPlayer : IPlayer {
}
}
@AnyThread
override fun initCallbacks(
@MainThread eventHandler: ((PlayerEvent) -> Unit),
eventHandler: ((PlayerEvent) -> Unit),
requestedListeningPercentages: List<Int>?,
) {
this.requestedListeningPercentages = requestedListeningPercentages
@ -1279,7 +1278,7 @@ class CS3IPlayer : IPlayer {
item.drm?.let { drm ->
when (drm.uuid) {
CLEARKEY_DRM_UUID.toJavaUuid() -> {
CLEARKEY_UUID -> {
// Use headers from DrmMetadata for media requests
val client = dataSourceFactory
?: throw IllegalArgumentException("Must supply onlineSource")
@ -1300,8 +1299,8 @@ class CS3IPlayer : IPlayer {
.createMediaSource(item.mediaItem)
}
WIDEVINE_DRM_UUID.toJavaUuid(),
PLAYREADY_DRM_UUID.toJavaUuid() -> {
WIDEVINE_UUID,
PLAYREADY_UUID -> {
// Use headers from DrmMetadata for media requests
val client = dataSourceFactory
?: throw IllegalArgumentException("Must supply onlineSource")
@ -1771,6 +1770,7 @@ 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.toJavaUuid(),
uuid = link.uuid,
kty = link.kty,
licenseUrl = link.licenseUrl,
keyRequestParameters = link.keyRequestParameters,

View file

@ -14,13 +14,12 @@ import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getFol
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo
class DownloadFileGenerator(
episodes: List<ExtractorUri>
) : VideoGenerator<ExtractorUri>(episodes) {
episodes: List<ExtractorUri>,
currentIndex: Int = 0
) : VideoGenerator<ExtractorUri>(episodes, currentIndex) {
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>,
@ -29,7 +28,7 @@ class DownloadFileGenerator(
offset: Int,
isCasting: Boolean
): Boolean {
val meta = videos.getOrNull(offset) ?: return false
val meta = getCurrent(offset) ?: return false
if (meta.uri == Uri.EMPTY) {
// We do this here so that we only load it when

View file

@ -14,9 +14,7 @@ import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPres
import com.lagradost.cloudstream3.utils.UIHelper.enableEdgeToEdgeCompat
class DownloadedPlayerActivity : AppCompatActivity() {
companion object {
const val TAG = "DownloadedPlayerActivity"
}
private val dTAG = "DownloadedPlayerAct"
override fun dispatchKeyEvent(event: KeyEvent): Boolean =
CommonActivity.dispatchKeyEvent(this, event) ?: super.dispatchKeyEvent(event)
@ -29,83 +27,53 @@ 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(TAG, "onCreate")
handleIntent(intent)
Log.i(dTAG, "onCreate")
/**
* 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 { 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 { // I dont trust android
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()
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()
// 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
}
} else if (data?.scheme == "content") {
playUri(this, data)
} else finishAndRemoveTask()
} else {
finish()
return
}
attachBackPressedCallback("DownloadedPlayerActivity") { finish() }
}
override fun onResume() {
super.onResume()
CommonActivity.setActivityInstance(this)
}
}
}

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType
class ExtractorLinkGenerator(
private val links: List<ExtractorLink>,
private val subtitles: List<SubtitleData>,
) : NoVideoGenerator(null) {
) : NoVideoGenerator() {
override suspend fun generateLinks(
clearCache: Boolean,
sourceTypes: Set<ExtractorLinkType>,

View file

@ -39,6 +39,7 @@ 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
@ -434,8 +435,7 @@ 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 = { (event, hasNavigated) ->
keyEventListener = { eventNav ->
val (event, hasNavigated) = eventNav
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,23 +763,24 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
}
}
playerBinding?.apply {
playerLockHolder.isGone = isGone
playerVideoBar.isGone = isGone
playerPausePlayHolderHolder.isGone =
isGone || currentPlayerStatus == CSPlayerLoading.IsBuffering
playerPausePlay.isGone = isGone
// player_buffering?.isGone = isGone
playerTopHolder.isGone = isGone
val showPlayerEpisodes = !isGone && isThereEpisodes()
playerEpisodesButtonRoot.isVisible = showPlayerEpisodes
playerEpisodesButton.isVisible = showPlayerEpisodes
playerVideoTitleHolder.isGone = togglePlayerTitleGone
playerVideoTitleRez.isGone = isGone || playerVideoTitleRez.text.isBlank()
playerVideoTitleRez.isGone = isGone
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
}
}
@ -879,145 +880,6 @@ 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()
@ -1026,9 +888,53 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
val keyCode = event.keyCode
if (event.action == KeyEvent.ACTION_DOWN) {
val value = handleKeyDownEvent(keyCode)
if (value != null) {
return value
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
}
}
}
@ -1094,8 +1000,7 @@ 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)
@ -1113,6 +1018,81 @@ 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()
@ -1157,9 +1137,8 @@ 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
@ -1200,6 +1179,15 @@ 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)
}

View file

@ -131,9 +131,6 @@ 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() {
@ -142,18 +139,11 @@ class GeneratorPlayer : FullScreenPlayer() {
const val CHANNEL_ID = 7340
const val STOP_ACTION = "stopcs3"
private val generators = ConcurrentHashMap<String, VideoGenerator<*>>()
fun newInstance(
generator: VideoGenerator<*>,
index: Int,
syncData: HashMap<String, String>? = null
): Bundle {
private var lastUsedGenerator: IGenerator? = null
fun newInstance(generator: IGenerator, syncData: HashMap<String, String>? = null): Bundle {
Log.i(TAG, "newInstance = $syncData")
val uuid = UUID.randomUUID().toString()
generators[uuid] = generator
lastUsedGenerator = generator
return Bundle().apply {
putString("uuid", uuid)
putInt("index", index)
if (syncData != null) putSerializable("syncData", syncData)
}
}
@ -172,24 +162,26 @@ 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 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 currentMeta: Any? = null
private var nextMeta: Any? = null
private var isActive: Boolean = 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 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 var allMeta: List<ResultEpisode>? = null
private fun startLoading() {
player.release()
currentSelectedSubtitles = null
isActive = false
binding?.overlayLoadingSkipButton?.isVisible = false
binding?.playerLoadingOverlay?.isVisible = true
}
private fun setSubtitles(subtitle: SubtitleData?, userInitiated: Boolean): Boolean {
// If subtitle is changed and user initiated -> Save the language
@ -221,7 +213,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)
}
@ -240,7 +232,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
private fun getPos(): Long {
val durPos = getViewPos(viewModel.state.generatorState?.id) ?: return 0L
val durPos = getViewPos(viewModel.getId()) ?: return 0L
if (durPos.duration == 0L) return 0L
if (durPos.position * 100L / durPos.duration > 95L) {
return 0L
@ -391,7 +383,9 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onCustomAction(player: Player, action: String, intent: Intent) {
when (action) {
STOP_ACTION -> {
exitPlayer()
playerHostView?.exitFullscreen()
this@GeneratorPlayer.player.release()
activity?.popCurrentPage()
}
}
}
@ -491,9 +485,9 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
private fun loadLink(link: VideoLink?, sameEpisode: Boolean) {
private fun loadLink(link: Pair<ExtractorLink?, ExtractorUri?>?, sameEpisode: Boolean) {
if (link == null) return
isPlayerActive.set(true)
// manage UI
binding?.playerLoadingOverlay?.isVisible = false
val isTorrent =
@ -509,7 +503,16 @@ 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)
@ -519,7 +522,6 @@ class GeneratorPlayer : FullScreenPlayer() {
// load player
context?.let { ctx ->
val (url, uri) = link
val subtitles = viewModel.state.subtitles
player.loadPlayer(
ctx,
sameEpisode,
@ -528,9 +530,9 @@ class GeneratorPlayer : FullScreenPlayer() {
startPosition = if (sameEpisode) null else {
if (isNextEpisode) 0L else getPos()
},
subtitles,
currentSubs,
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
subtitles, settings = true, downloads = true
currentSubs, settings = true, downloads = true
),
preview = true
)
@ -543,6 +545,13 @@ 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,
@ -868,18 +877,20 @@ class GeneratorPlayer : FullScreenPlayer() {
vararg subtitleData: SubtitleData
) {
if (subtitleData.isEmpty()) return
val ctx = context ?: return
val selectedSubtitle = subtitleData.first()
viewModel.addSubtitles(subtitleData.toSet())
val ctx = context ?: return
val subs = currentSubs + subtitleData
// this is used instead of observe(viewModel._currentSubs), because observe is too slow
player.setActiveSubtitles(viewModel.state.subtitles)
player.setActiveSubtitles(subs)
// 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
@ -978,7 +989,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
// checks for both a race condition and if any of the subs generated is new
if (this.isActive && !viewModel.state.subtitles.containsAll(subtitles) && !hasSelectASubtitle) {
if (this.isActive && !currentSubs.containsAll(subtitles) && !hasSelectASubtitle) {
hasSelectASubtitle = true
runOnMainThread {
addAndSelectSubtitles(*subtitles.toTypedArray())
@ -1001,7 +1012,7 @@ class GeneratorPlayer : FullScreenPlayer() {
context?.let { ctx ->
val isPlaying = player.getIsPlaying()
player.handleEvent(CSPlayerEvent.Pause, PlayerEventSource.UI)
val currentSubtitles = sortSubs(viewModel.state.subtitles)
val currentSubtitles = sortSubs(currentSubs)
val sourceDialog = Dialog(ctx, R.style.DialogFullscreenPlayer)
val binding =
@ -1043,7 +1054,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
if (subsProvidersIsActive) {
val currentLoadResponse = viewModel.state.generatorState?.response
val currentLoadResponse = viewModel.getLoadResponse()
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
R.layout.sort_bottom_footer_add_choice, null
@ -1101,7 +1112,7 @@ class GeneratorPlayer : FullScreenPlayer() {
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
fun refreshLinks(qualityProfile: Int) {
sortedUrls = viewModel.state.sortLinks(qualityProfile)
sortedUrls = sortLinks(qualityProfile)
if (sortedUrls.isEmpty()) {
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
true
@ -1266,28 +1277,16 @@ class GeneratorPlayer : FullScreenPlayer() {
binding.profilesClickSettings.setOnClickListener {
val activity = activity ?: return@setOnClickListener
val dialog = QualityProfileDialog(
QualityProfileDialog(
activity,
R.style.DialogFullscreenPlayer,
viewModel.state.links.mapNotNull {
it.first?.let { extractorLink ->
LinkSource(
extractorLink
)
}
},
currentLinks.mapNotNull { it.first?.let { extractorLink -> LinkSource(extractorLink) } },
currentQualityProfile
) { profile ->
currentQualityProfile = profile.id
setProfileName(profile.id)
}
dialog.setOnDismissListener {
viewModel.state.clearSortedLinksCache()
refreshLinks(currentQualityProfile)
}
dialog.show()
refreshLinks(profile.id)
}.show()
}
binding.subtitlesEncodingFormat.apply {
@ -1431,12 +1430,11 @@ 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 ->
@ -1444,9 +1442,7 @@ 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
@ -1468,8 +1464,7 @@ 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("")
@ -1497,7 +1492,7 @@ class GeneratorPlayer : FullScreenPlayer() {
binding.applyBtt.setOnClickListener {
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
player.setPreferredAudioTrack(
currentTrack?.language,
currentTrack?.language,
currentTrack?.id,
currentTrack?.formatIndex,
)
@ -1546,20 +1541,13 @@ class GeneratorPlayer : FullScreenPlayer() {
}
private fun startPlayer() {
// We don't want double load when you skip loading
if (isPlayerActive.get()) {
return
}
if (isActive) return // we don't want double load when you skip loading
val links = viewModel.state.sortLinks(currentQualityProfile)
val links = sortLinks(currentQualityProfile)
if (links.isEmpty()) {
noLinksFound()
return
}
// Atomic operation to prevent double loading
if (!isPlayerActive.compareAndSet(false, true)) {
return
}
loadLink(links.first(), false)
showPlayerMetadata()
}
@ -1572,7 +1560,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.state.generatorState?.response ?: return
val load = viewModel.getLoadResponse() ?: return
val episode = currentMeta as? ResultEpisode
titleView.text = load.name
@ -1614,7 +1602,7 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun nextEpisode() {
if (viewModel.hasNextEpisode() == true) {
isNextEpisode = true
releasePlayer()
player.release()
viewModel.loadLinksNext()
}
}
@ -1622,18 +1610,18 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun prevEpisode() {
if (viewModel.hasPrevEpisode() == true) {
isNextEpisode = true
releasePlayer()
player.release()
viewModel.loadLinksPrev()
}
}
override fun hasNextMirror(): Boolean {
val links = viewModel.state.sortLinks(currentQualityProfile)
val links = sortLinks(currentQualityProfile)
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
}
override fun nextMirror() {
val links = viewModel.state.sortLinks(currentQualityProfile)
val links = sortLinks(currentQualityProfile)
if (links.isEmpty()) {
noLinksFound()
return
@ -1680,7 +1668,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val percentage = position * 100L / duration
DataStoreHelper.setViewPosAndResume(
viewModel.state.generatorState?.id,
viewModel.getId(),
position,
duration,
currentMeta,
@ -1732,18 +1720,14 @@ class GeneratorPlayer : FullScreenPlayer() {
): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null
if (downloads) {
sortSubs(subtitles).firstOrNull {
it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode(
langCode
)
}?.let { return it }
return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode(langCode) }
}
if (!settings) return null
return sortSubs(subtitles).firstOrNull { it.matchesLanguageCode(langCode) }
}
private fun autoSelectFromSettings(): Boolean {
// auto select subtitle based on settings
val langCode = preferredAutoSelectSubtitles
@ -1760,7 +1744,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
} else if (!langCode.isNullOrEmpty()) {
getAutoSelectSubtitle(
viewModel.state.subtitles, settings = true, downloads = false
currentSubs, settings = true, downloads = false
)?.let { sub ->
if (setSubtitles(sub, false)) {
player.saveData()
@ -1774,20 +1758,20 @@ class GeneratorPlayer : FullScreenPlayer() {
return false
}
private fun autoSelectFromDownloads() {
if (player.getCurrentPreferredSubtitle() != null) {
return
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
}
}
}
}
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)
return false
}
private fun autoSelectSubtitles() {
@ -1871,7 +1855,7 @@ class GeneratorPlayer : FullScreenPlayer() {
playerBinding?.playerEpisodeFillerHolder?.isVisible = isFiller ?: false
playerBinding?.playerVideoTitle?.text = playerVideoTitle
playerBinding?.offlinePin?.isVisible = viewModel.generator is DownloadFileGenerator
playerBinding?.offlinePin?.isVisible = lastUsedGenerator is DownloadFileGenerator
}
fun setPlayerDimen(widthHeight: Pair<Int, Int>?) {
@ -2012,12 +1996,6 @@ 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
@ -2026,7 +2004,12 @@ class GeneratorPlayer : FullScreenPlayer() {
from, to
).apply {
addListener(onEnd = {
if (!show) {
if (show) {
if (!isShowing) {
// Automatically request focus if the menu is not opened
playerBinding?.skipChapterButton?.requestFocus()
}
} else {
playerBinding?.skipChapterButton?.isVisible = false
if (!isShowing) {
// Automatically return focus to play pause
@ -2065,9 +2048,8 @@ class GeneratorPlayer : FullScreenPlayer() {
}
override fun isThereEpisodes(): Boolean {
// 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
val meta = allMeta
return !meta.isNullOrEmpty() && meta.size > 1
}
override fun showEpisodesOverlay() {
@ -2079,7 +2061,7 @@ class GeneratorPlayer : FullScreenPlayer() {
{ episodeClick ->
if (episodeClick.action == ACTION_CLICK_DEFAULT) {
isNextEpisode = false
releasePlayer()
player.release()
playerEpisodeOverlay.isGone = true
episodeClick.position?.let { viewModel.loadThisEpisode(it) }
}
@ -2098,7 +2080,7 @@ class GeneratorPlayer : FullScreenPlayer() {
(playerEpisodeList.adapter as? EpisodeAdapter)?.submitList(episodes)
// Scroll to current episode
viewModel.state.generatorState?.index?.let { index ->
viewModel.getCurrentIndex()?.let { index ->
playerEpisodeList.scrollToPosition(index)
// Ensure focus on tv
if (isLayout(TV)) {
@ -2142,64 +2124,32 @@ 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]
val uuid = savedInstanceState?.getString("uuid") ?: arguments?.getString("uuid")
val index = savedInstanceState?.getInt("index") ?: arguments?.getInt("index")
val generator = generators[uuid]
viewModel.attachGenerator(lastUsedGenerator)
unwrapBundle(savedInstanceState)
unwrapBundle(arguments)
super.onBindingCreated(binding, savedInstanceState)
// Avoid showing no links found
if (generator == null || index == null) {
exitPlayer()
return
}
viewModel.attachGenerator(generator, index)
var langFilterList = listOf<String>()
var filterSubByLang = false
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)
viewModel.filterSubByLang =
filterSubByLang =
settingsManager.getBoolean(getString(R.string.filter_sub_lang_key), false)
if (viewModel.filterSubByLang) {
if (filterSubByLang) {
val langFromPrefMedia = settingsManager.getStringSet(
this.getString(R.string.provider_lang_key), mutableSetOf("en")
)
viewModel.langFilterList = langFromPrefMedia?.mapNotNull {
langFilterList = langFromPrefMedia?.mapNotNull {
fromTagToEnglishLanguageName(it)?.lowercase() ?: return@mapNotNull null
} ?: listOf()
}
@ -2212,23 +2162,18 @@ class GeneratorPlayer : FullScreenPlayer() {
preferredAutoSelectSubtitles = getAutoSelectLanguageTagIETF()
val selectedLink = currentSelectedLink
if (selectedLink == null) {
if (currentSelectedLink == null) {
viewModel.loadLinks()
} else {
// Recreated view, so we need to recreate the
loadLink(selectedLink, true)
}
binding.overlayLoadingSkipButton.setOnClickListener {
// Mark as "success" early
viewModel.modifyState {
copy(loading = Resource.Success(Unit))
}
startPlayer()
}
binding.playerLoadingGoBack.setOnClickListener {
exitPlayer()
playerHostView?.exitFullscreen()
player.release()
activity?.popCurrentPage()
}
playerBinding?.downloadHeader?.setOnClickListener {
@ -2241,29 +2186,14 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
observe(viewModel.currentStamps) { (stamps, instance) ->
if (instance != viewModel.state.instance) return@observe // Outdated observe
observe(viewModel.currentStamps) { stamps ->
player.addTimeStamps(stamps)
}
observe(viewModel.currentSubtitles) { (subtitles, instance) ->
if (instance != viewModel.state.instance) return@observe // Outdated observe
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) {
observe(viewModel.loadingLinks) {
when (it) {
is Resource.Loading -> {
releasePlayer()
startLoading()
}
is Resource.Success -> {
@ -2275,30 +2205,30 @@ class GeneratorPlayer : FullScreenPlayer() {
}
is Resource.Failure -> {
showToast(loading.errorString, Toast.LENGTH_LONG)
showToast(it.errorString, Toast.LENGTH_LONG)
startPlayer()
}
}
}
observe(viewModel.currentLinks) { (links, instance) ->
if (instance != viewModel.state.instance) return@observe // Outdated observe
val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true
observe(viewModel.currentLinks) {
currentLinks = it
val turnVisible = it.isNotEmpty() && lastUsedGenerator?.canSkipLoading == true
val wasGone = binding.overlayLoadingSkipButton.isGone
binding.overlayLoadingSkipButton.apply {
isVisible = turnVisible
if (links.isEmpty()) {
val value = viewModel.currentLinks.value
if (value.isNullOrEmpty()) {
setText(R.string.skip_loading)
} else {
@SuppressLint("SetTextI18n")
text = "${context.getString(R.string.skip_loading)} (${links.size})"
text = "${context.getString(R.string.skip_loading)} (${value.size})"
}
}
safe {
if (!isPlayerActive.get() && viewModel.state.links.any { link ->
if (currentLinks.any { link ->
getLinkPriority(currentQualityProfile, link.first) >=
QualityDataHelper.AUTO_SKIP_PRIORITY
}
@ -2311,7 +2241,34 @@ 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")

View file

@ -1,7 +1,10 @@
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,
@ -25,27 +28,71 @@ val LOADTYPE_CHROMECAST = setOf(
val LOADTYPE_ALL = ExtractorLinkType.entries.toSet()
abstract class NoVideoGenerator(val id : Int?) : VideoGenerator<Nothing>(emptyList()) {
abstract class NoVideoGenerator : VideoGenerator<Nothing>(emptyList(), 0) {
override val hasCache = false
override val canSkipLoading = false
override fun getId(index: Int): Int? = id
}
abstract class VideoGenerator<T : Any>(val videos: List<T>) {
abstract val hasCache: Boolean
abstract val canSkipLoading: Boolean
abstract fun getId(index : Int) : Int?
abstract class VideoGenerator<T : Any>(val videos: List<T>, var videoIndex: Int = 0) :
IGenerator {
fun hasNext(videoIndex : Int): Boolean = videoIndex < videos.lastIndex
fun hasPrev(videoIndex : Int): Boolean = videoIndex > 0
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
}
}
@Throws
abstract suspend fun generateLinks(
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(
clearCache: Boolean,
sourceTypes: Set<ExtractorLinkType>,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit,
offset: Int,
isCasting: Boolean
offset: Int = 0,
isCasting: Boolean = false
): Boolean
}

View file

@ -3,12 +3,31 @@ 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),
@ -201,6 +220,8 @@ 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"
@ -222,9 +243,8 @@ interface IPlayer {
fun getSubtitleOffset(): Long // in ms
fun setSubtitleOffset(offset: Long) // in ms
@AnyThread
fun initCallbacks(
@MainThread eventHandler: ((PlayerEvent) -> Unit),
eventHandler: ((PlayerEvent) -> Unit),
/** this is used to request when the player should report back view percentage */
requestedListeningPercentages: List<Int>? = null,
)
@ -291,4 +311,4 @@ interface IPlayer {
/** Get the current subtitle cues, for use with syncing */
fun getSubtitleCues(): List<SubtitleCue>
}
}

View file

@ -40,8 +40,7 @@ class LinkGenerator(
private val links: List<BasicLink>,
private val extract: Boolean = true,
private val refererUrl: String? = null,
id: Int?
) : NoVideoGenerator(id) {
) : NoVideoGenerator() {
override suspend fun generateLinks(
clearCache: Boolean,
sourceTypes: Set<ExtractorLinkType>,
@ -79,8 +78,10 @@ class LinkGenerator(
class MinimalLinkGenerator(
private val links: List<CloudStreamPackage.MinimalVideoLink>,
private val subs: List<CloudStreamPackage.MinimalSubtitleLink>,
id: Int?
) : NoVideoGenerator(id) {
private val id: Int? = null
) : NoVideoGenerator() {
override fun getCurrentId(): Int? = id
override suspend fun generateLinks(
clearCache: Boolean,
sourceTypes: Set<ExtractorLinkType>,

View file

@ -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,25 +13,15 @@ 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
)
)
)
)
}
@ -62,9 +52,8 @@ object OfflinePlaybackHelper {
links,
subs,
if (id != -1) id else null,
), 0
),
replacePlayerNavOptions
)
)
)
return true
}
@ -84,12 +73,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 = uri.lastPathSegment?.toLongOrNull()?.hashCode() ?: uri.lastPathSegment?.hashCode()
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()
?.hashCode()
)
)
), 0
),
replacePlayerNavOptions
)
)
)
}
}

View file

@ -9,188 +9,35 @@ 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"
}
@Volatile
var generator: VideoGenerator<*>? = null
private var generator: IGenerator? = null
@Volatile
var episodeIndex: Int = 0
private val _currentLinks = MutableLiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>>(setOf())
val currentLinks: LiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>> = _currentLinks
/**
* 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 _currentSubs = MutableLiveData<Set<SubtitleData>>(setOf())
val currentSubs: LiveData<Set<SubtitleData>> = _currentSubs
private val _currentLinks =
MutableLiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>>(null)
val currentLinks: LiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>> = _currentLinks
private val _loadingLinks = MutableLiveData<Resource<Boolean?>>()
val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks
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 _currentStamps = MutableLiveData<List<VideoSkipStamp>>(emptyList())
val currentStamps: LiveData<List<VideoSkipStamp>> = _currentStamps
private val _currentSubtitleYear = MutableLiveData<Int?>(null)
val currentSubtitleYear: LiveData<Int?> = _currentSubtitleYear
@ -206,32 +53,41 @@ 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(episodeIndex) == true) {
episodeIndex += 1
if (generator?.hasPrev() == true) {
generator?.prev()
loadLinks()
}
}
fun loadLinksNext() {
Log.i(TAG, "loadLinksNext")
if (generator?.hasNext(episodeIndex) == true) {
episodeIndex += 1
if (generator?.hasNext() == true) {
generator?.next()
loadLinks()
}
}
fun hasNextEpisode(): Boolean? {
return generator?.hasNext(episodeIndex)
return generator?.hasNext()
}
fun hasPrevEpisode(): Boolean? {
return generator?.hasPrev(episodeIndex)
return generator?.hasPrev()
}
fun preLoadNextLinks() {
val id = generator?.getId(episodeIndex)
val id = getId()
// Do not preload if already loading
if (id == currentLoadingEpisodeId) return
@ -241,15 +97,14 @@ class PlayerGeneratorViewModel : ViewModel() {
currentJob = viewModelScope.launch {
try {
if (generator?.hasCache == true && generator?.hasNext(episodeIndex) == true) {
if (generator?.hasCache == true && generator?.hasNext() == true) {
safeApiCall {
generator?.generateLinks(
sourceTypes = LOADTYPE_INAPP,
clearCache = false,
isCasting = false,
callback = {},
subtitleCallback = {},
offset = episodeIndex + 1
offset = 1
)
}
}
@ -263,137 +118,129 @@ class PlayerGeneratorViewModel : ViewModel() {
}
}
fun loadThisEpisode(index: Int) {
episodeIndex = index
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)
loadLinks()
}
fun attachGenerator(newGenerator: VideoGenerator<*>, index: Int) {
Log.i(TAG, "attachGenerator with generator=$newGenerator and index=$index")
generator = newGenerator
episodeIndex = index
fun getCurrentIndex():Int?{
val repoGen = generator as? RepoLinkGenerator ?: return null
return repoGen.videoIndex
}
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>) {
val validFile = file.filter(::isValidSubtitle)
if (validFile.isNotEmpty())
modifyState {
add(validFile)
}
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)
}
}
private var currentJob: Job? = null
private var currentStampJob: Job? = null
fun loadStamps(duration: Long) {
//currentStampJob?.cancel()
currentStampJob = ioSafe {
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 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 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 with generator=$generator and index=$episodeIndex")
Log.i(TAG, "loadLinks")
currentJob?.cancel()
val index = episodeIndex
// Clear old data and reset the state
modifyState {
VideoState(
loading = Resource.Loading(),
generatorState = generator?.let { gen ->
GeneratorState(
meta = gen.videos.getOrNull(index),
nextMeta = gen.videos.getOrNull(index + 1),
id = gen.getId(index),
response = (gen as? RepoLinkGenerator)?.page,
index = index,
allMeta = gen.videos
)
},
instance = instance + 1
)
}
currentJob = viewModelScope.launchSafe {
// Load more data
// 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())
val loadingState = safeApiCall {
generator?.generateLinks(
sourceTypes = sourceTypes,
clearCache = forceClearCache,
callback = { link ->
if (isActive)
modifyState {
add(link)
callback = {
synchronized(currentLinks) {
currentLinks.add(it)
// Clone to prevent ConcurrentModificationException
safe {
// Extra safe since .toSet() iterates.
_currentLinks.postValue(currentLinks.toSet())
}
}
},
isCasting = false,
offset = index,
subtitleCallback = { link ->
if (isActive && isValidSubtitle(link))
modifyState {
add(link)
subtitleCallback = {
synchronized(extraSubtitles) {
currentSubs.add(it)
safe {
_currentSubs.postValue(currentSubs + extraSubtitles)
}
}
})
Unit
}
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
}
}
_loadingLinks.postValue(loadingState)
_currentLinks.postValue(currentLinks)
synchronized(extraSubtitles) {
_currentSubs.postValue(currentSubs + extraSubtitles)
}
}
}
}

View file

@ -1055,7 +1055,7 @@ class PlayerGestureHelper(private val playerView: PlayerView) {
return validHeight && validWidth
}
return rawY > context.getStatusBarHeight() && rawX < screenWidthWithOrientation
return rawY > (context.getStatusBarHeight() ?: 0) && rawX < screenWidthWithOrientation
}
private fun handleGesture(view: View, event: MotionEvent): Boolean {

View file

@ -25,7 +25,6 @@ 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
@ -45,6 +44,7 @@ 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,13 +287,7 @@ class PlayerView @JvmOverloads constructor(
val previewFrameLayout: FrameLayout? =
exoPlayerView?.findViewById(R.id.previewFrameLayout)
/** 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)) {
if (progressBar != null && previewImageView != null && previewFrameLayout != null) {
var resume = false
progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener {
override fun onScrubStart(previewBar: PreviewBar?) {
@ -375,8 +369,7 @@ class PlayerView @JvmOverloads constructor(
exoFfwdText?.text = context.getString(R.string.ffw_text_regular_format).format(seekSecs)
playerPausePlay?.setOnClickListener {
scheduleAutoHide()
if (currentPlayerStatus == CSPlayerLoading.IsEnded && isLayout(PHONE)) {
if (currentPlayerStatus == CSPlayerLoading.IsEnded) {
player.handleEvent(CSPlayerEvent.Restart, PlayerEventSource.UI)
} else {
player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI)
@ -467,6 +460,7 @@ 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().
@ -620,10 +614,9 @@ class PlayerView @JvmOverloads constructor(
/** Error handling */
@MainThread
fun playerError(exception: Throwable) {
fun showErrorToast(message: String) {
if (callbacks?.hasNextMirror() == true) {
fun showErrorToast(message: String, gotoNext: Boolean = false) {
if (gotoNext && callbacks?.hasNextMirror() == true) {
showToast(message, Toast.LENGTH_SHORT)
callbacks?.nextMirror()
} else {
@ -645,7 +638,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")
showErrorToast("${context.getString(R.string.source_error)}\n$errorName ($code)\n$msg", gotoNext = true)
PlaybackException.ERROR_CODE_REMOTE_ERROR,
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
@ -653,7 +646,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")
showErrorToast("${context.getString(R.string.remote_error)}\n$errorName ($code)\n$msg", gotoNext = true)
PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED,
PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER,
@ -661,31 +654,43 @@ 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")
showErrorToast("${context.getString(R.string.render_error)}\n$errorName ($code)\n$msg", gotoNext = true)
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")
showErrorToast("${context.getString(R.string.unsupported_error)}\n$errorName ($code)\n$msg", gotoNext = true)
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED ->
showErrorToast("${context.getString(R.string.encoding_error)}\n$errorName ($code)\n$msg")
showErrorToast("${context.getString(R.string.encoding_error)}\n$errorName ($code)\n$msg", gotoNext = true)
else ->
showErrorToast("${context.getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg")
showErrorToast("${context.getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg", gotoNext = false)
}
}
is SocketTimeoutException ->
showErrorToast("${context.getString(R.string.remote_error)}\n${exception.message}")
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 ErrorLoadingException ->
exception.message?.let { showErrorToast(it) }
?: showErrorToast(exception.toString())
exception.message?.let { showErrorToast(it, gotoNext = true) }
?: showErrorToast(exception.toString(), gotoNext = true)
else ->
exception.message?.let { showErrorToast(it) }
?: showErrorToast(exception.toString())
exception.message?.let { showErrorToast(it, gotoNext = false) }
?: showErrorToast(exception.toString(), gotoNext = false)
}
}
@ -724,7 +729,8 @@ 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 */
@ -735,7 +741,6 @@ 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")
@ -756,7 +761,11 @@ class PlayerView @JvmOverloads constructor(
is TimestampInvokedEvent -> callbacks?.onTimestamp(event.timestamp)
is TracksChangedEvent -> callbacks?.onTracksInfoChanged()
is EmbeddedSubtitlesFetchedEvent -> callbacks?.embeddedSubtitlesFetched(event.tracks)
is ErrorEvent -> callbacks?.playerError(event.error) ?: playerError(event.error)
is ErrorEvent -> {
val cb = callbacks
if (cb != null) cb.playerError(event.error)
else playerError(event.error)
}
is RequestAudioFocusEvent -> requestAudioFocus()
is EpisodeSeekEvent -> when (event.offset) {
-1 -> callbacks?.prevEpisode()

View file

@ -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 java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.max
import kotlin.math.min
data class Cache(
val linkCache: MutableSet<ExtractorLink>,
@ -23,8 +23,9 @@ data class Cache(
class RepoLinkGenerator(
episodes: List<ResultEpisode>,
currentIndex: Int = 0,
val page: LoadResponse? = null,
) : VideoGenerator<ResultEpisode>(episodes) {
) : VideoGenerator<ResultEpisode>(episodes, currentIndex) {
companion object {
const val TAG = "RepoLink"
val cache: HashMap<Pair<String, Int>, Cache> =
@ -33,7 +34,6 @@ 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 = videos.getOrNull(offset) ?: return false
val current = getCurrent(offset) ?: return false
val currentCache = synchronized(cache) {
cache[current.apiName to current.id] ?: Cache(
@ -61,12 +61,10 @@ class RepoLinkGenerator(
}
}
// 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>()
// 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>()
synchronized(currentCache) {
val outdatedCache =
@ -77,10 +75,7 @@ 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
@ -93,7 +88,8 @@ class RepoLinkGenerator(
currentCache.subtitleCache.forEach { sub ->
currentSubsUrls.add(sub.url)
lastCountedSuffix.getOrPut(sub.originalName) { AtomicInteger(0) }.incrementAndGet()
val suffixCount = lastCountedSuffix.getOrDefault(sub.originalName, 0u) + 1u
lastCountedSuffix[sub.originalName] = suffixCount
subtitleCallback(sub)
}
@ -112,15 +108,17 @@ class RepoLinkGenerator(
subtitleCallback = { file ->
Log.d(TAG, "Loaded SubtitleFile: $file")
val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
if (correctFile.url.isBlank() || !currentSubsUrls.add(correctFile.url)) {
if (correctFile.url.isBlank() || currentSubsUrls.contains(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.getOrPut(nameDecoded) { AtomicInteger(0) }.incrementAndGet()
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 updatedFile =
correctFile.copy(originalName = nameDecoded, nameSuffix = "$suffixCount")
@ -134,9 +132,10 @@ class RepoLinkGenerator(
},
callback = { link ->
Log.d(TAG, "Loaded ExtractorLink: $link")
if (link.url.isBlank() || !currentLinksUrls.add(link.url)) {
if (link.url.isBlank() || currentLinksUrls.contains(link.url)) {
return@loadLinks
}
currentLinksUrls.add(link.url)
synchronized(currentCache) {
if (currentCache.linkCache.add(link)) {

View file

@ -559,7 +559,7 @@ class ResultFragmentTv : BaseFragment<FragmentResultTvBinding>(
ExtractorLinkGenerator(
extractedTrailerLinks,
emptyList()
), 0
)
)
)
}
@ -925,12 +925,8 @@ 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())

View file

@ -38,8 +38,9 @@ class ResultTrailerPlayer : ResultFragmentPhone() {
// Single-tap on empty player area: toggle controls.
override fun onSingleTap() {
if (introVisible) return
if (isShowing) uiReset() else showControls()
if (!introVisible) {
if (isShowing) uiReset() else showControls()
}
}
private fun showControls() {
@ -57,19 +58,6 @@ 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) {}

View file

@ -1,8 +1,7 @@
package com.lagradost.cloudstream3.ui.result
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.*
import android.util.Log
import android.widget.Toast
import androidx.annotation.MainThread
@ -11,50 +10,24 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.actions.AlwaysAskAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
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
@ -71,7 +44,9 @@ 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
@ -83,7 +58,6 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
@ -131,20 +105,22 @@ 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 */
@ -449,7 +425,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(),
)
@ -480,7 +456,7 @@ class ResultViewModel2 : ViewModel() {
var currentRepo: APIRepository? = null
private var currentId: Int? = null
private var fillers: HashSet<Int> = hashSetOf()
private var generator: RepoLinkGenerator? = null
private var generator: IGenerator? = null
private var preferDubStatus: DubStatus? = null
private var preferStartEpisode: Int? = null
private var preferStartSeason: Int? = null
@ -1293,10 +1269,9 @@ class ResultViewModel2 : ViewModel() {
subs += sub
updatePage()
},
isCasting = isCasting,
offset = 0
isCasting = isCasting
)
} catch (_: CancellationException) {
} catch (e: CancellationException) {
// Do nothing
} catch (e: Exception) {
logError(e)
@ -1325,7 +1300,7 @@ class ResultViewModel2 : ViewModel() {
episodeIds: Array<String>,
watchState: VideoWatchState
) {
val watchStateString = watchState.toJson()
val watchStateString = DataStore.mapper.writeValueAsString(watchState)
episodeIds.forEach {
if (getVideoWatchState(it.toInt()) != watchState) {
editor.setKeyRaw(
@ -1545,24 +1520,26 @@ 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(
offset = index,
generator?.generateLinks(
clearCache = true,
isCasting = false,
sourceTypes = LOADTYPE_ALL,
LOADTYPE_ALL,
callback = {},
subtitleCallback = {})
} else {
activity?.navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
generator, index,list
generator ?: return, list
)
)
}
@ -1686,13 +1663,14 @@ class ResultViewModel2 : ViewModel() {
}
val realRecommendations = ArrayList<SearchResponse>()
val apiNames = apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
val apiNames = synchronized(apis) {
apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
}
}
meta.recommendations?.forEach { rec ->
apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name))
@ -1831,10 +1809,11 @@ class ResultViewModel2 : ViewModel() {
}
private suspend fun updateFillers(data: LoadResponse) {
fillers = ioWorkSafe {
FillerEpisodeCheck.getFillerEpisodes(data)
} ?: hashSetOf()
private suspend fun updateFillers(data : LoadResponse) {
fillers =
withContext(Dispatchers.IO) {
safe { FillerEpisodeCheck.getFillerEpisodes(data) }
} ?: hashSetOf()
}
fun changeDubStatus(status: DubStatus) {
@ -2453,34 +2432,26 @@ 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
@ -2706,4 +2677,4 @@ class ResultViewModel2 : ViewModel() {
}
}
}
}
}

View file

@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() {
private var suggestionJob: Job? = null
private var repos = apis.withLock { apis.map { APIRepository(it) } }
private var repos = synchronized(apis) { apis.map { APIRepository(it) } }
fun clearSearch() {
_searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false)))
@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() {
private var onGoingSearch: Job? = null
fun reloadRepos() {
repos = apis.withLock { apis.map { APIRepository(it) } }
repos = synchronized(apis) { apis.map { APIRepository(it) } }
}
fun searchAndCancel(

View file

@ -219,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() {
}
fun showAdd() {
val providers = allProviders.distinctBy { it::class }.sortedBy { it.name }
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" },
-1,

View file

@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() {
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
activity?.getApiProviderLangSettings()?.let { currentLangTags ->
val languagesTagName = APIHolder.apis.withLock {
listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) +
APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) }
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() }
val languagesTagName = synchronized(APIHolder.apis) {
listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) +
APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) }
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
}
val currentIndexList = currentLangTags.map { langTag ->

View file

@ -119,14 +119,13 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
}, { repo ->
// Prompt user before deleting repo
main {
val uiContext = context ?: binding.root.context
val builder = AlertDialog.Builder(uiContext)
val builder = AlertDialog.Builder(context ?: binding.root.context)
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
ioSafe {
RepositoryManager.removeRepository(uiContext.applicationContext, repo)
RepositoryManager.removeRepository(binding.root.context, repo)
extensionViewModel.loadStats()
extensionViewModel.loadRepositories()
}
@ -137,7 +136,9 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
}
builder.setTitle(R.string.delete_repository)
.setMessage(uiContext.getString(R.string.delete_repository_plugins))
.setMessage(
context?.getString(R.string.delete_repository_plugins)
)
.setPositiveButton(R.string.delete, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener)
.show().setDefaultFocus()
@ -209,9 +210,9 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
binding.applyBtt.setOnClickListener secondListener@{
val name = binding.repoNameInput.text?.toString()
val urlInput = binding.repoUrlInput.text?.toString()
ioSafe {
val url = urlInput?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
val url = binding.repoUrlInput.text?.toString()
?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
if (url.isNullOrBlank()) {
main {
showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT)

View file

@ -23,7 +23,7 @@ import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.Levenshtein
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.io.File
// String => repository url
@ -246,7 +246,7 @@ class PluginsViewModel : ViewModel() {
this.sortedBy { it.plugin.second.name }
} else {
this.sortedBy {
-Levenshtein.partialRatio(
-FuzzySearch.partialRatio(
it.plugin.second.name.lowercase(),
query.lowercase()
)

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -40,7 +40,7 @@ class TestViewModel : ViewModel() {
get() = scope != null
private var filter = ProviderFilter.All
private val providers = atomicListOf<Pair<MainAPI, TestingUtils.TestResultProvider>>()
private val providers = threadSafeListOf<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() {
providers.withLock {
synchronized(providers) {
val filtered = when (filter) {
ProviderFilter.All -> providers.toList()
ProviderFilter.All -> providers
ProviderFilter.Passed -> providers.filter { it.second.success }
ProviderFilter.Failed -> providers.filter { !it.second.success }
}
@ -68,7 +68,7 @@ class TestViewModel : ViewModel() {
}
private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) {
providers.withLock {
synchronized(providers) {
val index = providers.indexOfFirst { it.first == api }
if (index == -1) {
providers.add(api to results)
@ -81,14 +81,14 @@ class TestViewModel : ViewModel() {
}
fun init() {
total = APIHolder.allProviders.withLock { APIHolder.allProviders.size }
total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
updateProgress()
}
fun startTest() {
scope = CoroutineScope(Dispatchers.Default)
val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() }
val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
total = apis.size
failed = 0
passed = 0

View file

@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
if (isSetup)
if (
// If any available languages
apis.distinctBy { it.lang }.size > 1
synchronized(apis) { apis.distinctBy { it.lang }.size > 1 }
) {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else {

View file

@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment<FragmentSetupProviderLanguage
val currentLangTags = ctx.getApiProviderLangSettings()
val languagesTagName = APIHolder.apis.withLock {
listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) +
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
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
}
val currentIndexList = currentLangTags.map { langTag ->

View file

@ -369,10 +369,28 @@ 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(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name })
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
.map { it.name })
/*val set = settingsManager.getStringSet(
this.getString(R.string.search_providers_list_key),
hashSet
)?.toHashSet() ?: hashSet
val list = HashSet<String>()
for (name in set) {
val api = getApiFromNameNull(name) ?: continue
if (activeLangs.contains(api.lang)) {
list.add(name)
}
}*/
//if (list.isEmpty()) return hashSet
//return list
return hashSet
}
@ -463,7 +481,9 @@ object AppContextUtils {
} ?: default
val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName)
val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
val allApis = synchronized(apis) {
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
}
return if (currentPrefMedia.isEmpty()) {
allApis
} else {

View file

@ -10,6 +10,7 @@ import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
@ -20,12 +21,11 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST
import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi.Companion.KITSU_CACHED_LIST
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.mapper
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream
@ -133,7 +133,9 @@ object BackupUtils {
)
@Suppress("UNCHECKED_CAST")
private fun getBackup(context: Context): BackupFile {
private fun getBackup(context: Context?): BackupFile? {
if (context == null) return null
val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() }
val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
@ -212,7 +214,7 @@ object BackupUtils {
fileStream = stream.openNew()
printStream = PrintWriter(fileStream)
printStream.print(backupFile.toJson())
printStream.print(mapper.writeValueAsString(backupFile))
showToast(
R.string.backup_success,
@ -257,8 +259,8 @@ object BackupUtils {
val input = activity.contentResolver.openInputStream(uri)
?: return@ioSafe
val text = input.bufferedReader().readText()
val restoredValue = parseJson<BackupFile>(text)
val restoredValue =
mapper.readValue<BackupFile>(input)
restore(
activity,

View file

@ -2,16 +2,17 @@ package com.lagradost.cloudstream3.utils
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass
import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import androidx.core.content.edit
/** Used to display metadata about downloads and resume watching */
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
@ -87,18 +88,8 @@ data class Editor(
}
object DataStore {
// Extensions shouldn't have really been using this version of it, but it seems
// some have. Since there has always been a very easy alternative, we won't
// need to deprecate it that long, and should be able to fully remove it
// once extensions at least use the other version.
@Deprecated(
"Please do not use the mapper version from DataStore. Preferably use methods from AppUtils " +
"to parse JSON. However, you can use the stable-API version of the mapper at " +
"com.lagradost.cloudstream3.mapper to access the mapper directly if necessary.",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("com.lagradost.cloudstream3.mapper"),
)
val mapper = com.lagradost.cloudstream3.mapper
val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
private fun getPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
@ -108,6 +99,7 @@ object DataStore {
return getPreferences(this)
}
fun getFolderName(folder: String, path: String): String {
return "${folder}/${path}"
}
@ -173,17 +165,17 @@ object DataStore {
fun <T> Context.setKey(path: String, value: T) {
try {
getSharedPrefs().edit {
putString(path, value?.toJsonLiteral())
putString(path, mapper.writeValueAsString(value))
}
} catch (e: Exception) {
logError(e)
}
}
fun <T : Any> Context.getKey(path: String, valueType: Class<T>): T? {
fun <T> Context.getKey(path: String, valueType: Class<T>): T? {
try {
val json: String = getSharedPrefs().getString(path, null) ?: return null
return parseJson(json, valueType.kotlin)
return json.toKotlinObject(valueType)
} catch (e: Exception) {
return null
}
@ -194,11 +186,11 @@ object DataStore {
}
inline fun <reified T : Any> String.toKotlinObject(): T {
return parseJson(this)
return mapper.readValue(this, T::class.java)
}
fun <T : Any> String.toKotlinObject(valueType: Class<T>): T {
return parseJson(this, valueType.kotlin)
fun <T> String.toKotlinObject(valueType: Class<T>): T {
return mapper.readValue(this, valueType)
}
// GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR
@ -222,4 +214,4 @@ object DataStore {
inline fun <reified T : Any> Context.getKey(folder: String, path: String, defVal: T?): T? {
return getKey(getFolderName(folder, path), defVal) ?: defVal
}
}
}

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import android.widget.ImageView
@ -12,7 +11,6 @@ import coil3.EventListener
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import coil3.decode.BitmapFactoryDecoder
import coil3.disk.DiskCache
import coil3.dispose
import coil3.load
@ -24,86 +22,82 @@ import coil3.request.CachePolicy
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.allowHardware
import coil3.request.bitmapConfig
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.buildDefaultClient
import okhttp3.HttpUrl
import okio.Path.Companion.toOkioPath
import java.io.File
import java.nio.ByteBuffer
object ImageLoader {
private const val TAG = "CoilImgLoader"
internal fun buildImageLoader(context: PlatformContext): ImageLoader {
val isBrokenHardware = hasPotentialBrokenHardware()
return ImageLoader.Builder(context)
internal fun buildImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context)
.crossfade(200)
.allowHardware(SDK_INT >= 28 && !isBrokenHardware)
.allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder
.diskCachePolicy(CachePolicy.ENABLED)
.networkCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache
.strongReferencesEnabled(false)
MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath())
.maxSizeBytes(512L * 1024 * 1024) // 512 MB
.maxSizePercent(0.04) // max 4% of storage for disk caching
.maxSizePercent(0.04) // Use 4 % of the device's storage space for disk caching
.build()
}
/** Pass interceptors with care, unnecessary passing tokens to servers
or image hosting services causes unauthorized exceptions **/
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) }))
if (isBrokenHardware) {
add(BitmapFactoryDecoder.Factory())
} // sw decoder
}
.apply {
if (isBrokenHardware) { // coil will auto choose optimal config on modern device
bitmapConfig(Bitmap.Config.ARGB_8888)
}
setupCoilLogger()
.components { add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) }
.also {
it.setupCoilLogger()
Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.")
}
.build()
}
/** DebugLogger on debug builds which won't slow down release builds & use EventListener for
/** Use DebugLogger on debug builds which won't slow down release builds & use EventListener for
Errors on release builds. **/
internal fun ImageLoader.Builder.setupCoilLogger() {
if (BuildConfig.DEBUG) {
logger(DebugLogger())
Log.d(TAG, "setupCoilLogger: Activated DEBUG_LOGGER FOR COIL")
} else {
eventListener(object : EventListener() {
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
Log.e(TAG, "Image load error: ${result.throwable.message ?: result.throwable}")
Log.e(TAG, " URL: ${request.data}")
Log.e(TAG, " allowHardware: ${request.allowHardware}")
Log.e(TAG, " hardware: ${Build.HARDWARE}, board: ${Build.BOARD}")
Log.e(TAG, "Error loading image: ${result.throwable}")
}
})
Log.d(TAG, "setupCoilLogger: Activated EVENT_LISTENER FOR COIL")
}
}
/** coil's built in loader attached w/ global synchronized instance **/
/** we use coil's built in loader with our global synchronized instance, this way we achieve
latest and complete functionality as well as stability **/
private fun ImageView.loadImageInternal(
imageData: Any?,
headers: Map<String, String>? = null,
builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations
) {
// clear image to avoid loading & flickering issue at fast scrolling (~recycler view/lazy column)
// clear image to avoid loading & flickering issue at fast scrolling (e.g, an image recycler)
this.dispose()
if (imageData == null) return
if(imageData == null) return // Just in case
// setImageResource is better than coil3 on resources due to attr
if (imageData is Int) {
this.setImageResource(imageData); return
if(imageData is Int) {
this.setImageResource(imageData)
return
}
// headers can be overridden by extensions.
// Use Coil's built-in load method but with our custom module & a decent USER-AGENT always
// which can be overridden by extensions.
this.load(imageData, SingletonImageLoader.get(context)) {
this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder ->
headerBuilder["User-Agent"] = USER_AGENT
@ -111,22 +105,11 @@ object ImageLoader {
headerBuilder[key] = value
}
}.build())
builder() // if passed
}
}
private fun hasPotentialBrokenHardware(): Boolean {
val hardware = Build.HARDWARE?.lowercase() ?: ""
val board = Build.BOARD?.lowercase() ?: ""
val model = Build.MODEL?.lowercase() ?: ""
val manufacturer = Build.MANUFACTURER?.lowercase() ?: ""
val allwinnerPatterns = listOf("sun50iw9", "h713", "allwinner", "sunxi")
val problematicModels =
listOf("hy320", "hy300", "a10plus", "magcubic", "sinoy", "android tv box")
return allwinnerPatterns.any { it in hardware || it in board || it in manufacturer } ||
problematicModels.any { it in model }
}
/** TYPE_SAFE_LOADERS **/
fun ImageView.loadImage(
imageData: UiImage?,
@ -155,6 +138,12 @@ object ImageLoader {
builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder)
fun ImageView.loadImage(
imageData: HttpUrl?,
headers: Map<String, String>? = null,
builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder)
fun ImageView.loadImage(
imageData: File?,
builder: ImageRequest.Builder.() -> Unit = {}
@ -184,4 +173,4 @@ object ImageLoader {
imageData: ByteBuffer?,
builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, builder = builder)
}
}

View file

@ -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<Array<GithubRelease>>(
val response = parseJson<List<GithubRelease>>(
app.get(url, headers = headers).text
).toList()
)
val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""")
val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""")
@ -103,7 +103,9 @@ object InAppUpdater {
!rel.prerelease
}.sortedWith(compareBy { release ->
release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 ->
versionRegex.find(it1)?.groupValues?.let {
versionRegex.find(
it1
)?.groupValues?.let {
it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt()
}
}
@ -148,9 +150,9 @@ object InAppUpdater {
"https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/git/ref/tags/pre-release"
val releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases"
val headers = mapOf("Accept" to "application/vnd.github.v3+json")
val response = parseJson<Array<GithubRelease>>(
val response = parseJson<List<GithubRelease>>(
app.get(releaseUrl, headers = headers).text
).toList()
)
val found = response.lastOrNull { rel ->
rel.prerelease || rel.tagName == "pre-release"

View file

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
//import com.lagradost.cloudstream3.animeproviders.AniflixProvider
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import java.util.concurrent.TimeUnit
object SyncUtil {
@ -71,7 +71,7 @@ object SyncUtil {
val url =
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json"
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text
val mapped = tryParseJson<MalSyncPage?>(response)
val mapped = parseJson<MalSyncPage?>(response)
val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId
val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id
@ -96,8 +96,10 @@ object SyncUtil {
.mapNotNull { it.url }.toMutableList()
if (type == "anilist") { // TODO MAKE BETTER
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id")
synchronized(apis) {
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id")
}
}
}
return current
@ -167,4 +169,4 @@ object SyncUtil {
@JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("deletedAt") val deletedAt: String?
)
}
}

View file

@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.*
import org.junit.Assert
import kotlin.random.Random
object TestingUtils {
open class TestResult(val success: Boolean) {
companion object {
val Pass = TestResult(true)
@ -48,10 +48,6 @@ object TestingUtils {
messageLog.add(Message(LogLevel.Error, message))
}
}
private fun fail(message: String): Nothing = throw AssertionError(message)
private fun assertTrue(message: String, condition: Boolean) { if (!condition) fail(message) }
private fun assertNotNull(message: String, value: Any?) { if (value == null) fail(message) }
class TestResultList(val results: List<SearchResponse>) : TestResult(true)
class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true)
@ -91,7 +87,7 @@ object TestingUtils {
} catch (e: Throwable) {
when (e) {
is NotImplementedError -> {
fail("Provider marked as hasMainPage, while in reality is has not been implemented")
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
}
is CancellationException -> {
@ -119,7 +115,7 @@ object TestingUtils {
api.search(query, 1)?.items?.takeIf { it.isNotEmpty() }
} catch (e: Throwable) {
if (e is NotImplementedError) {
fail("Provider has not implemented search()")
Assert.fail("Provider has not implemented search()")
} else if (e is CancellationException) {
throw e
}
@ -129,7 +125,7 @@ object TestingUtils {
}
return if (searchResults.isNullOrEmpty()) {
fail("Api ${api.name} did not return any search responses")
Assert.fail("Api ${api.name} did not return any search responses")
TestResult.Fail // Should not be reached
} else {
TestResultList(searchResults)
@ -220,7 +216,7 @@ object TestingUtils {
// return TestResult(validResults)
} catch (e: Throwable) {
if (e is NotImplementedError) {
fail("Provider has not implemented load()")
Assert.fail("Provider has not implemented load()")
}
throw e
}
@ -232,14 +228,14 @@ object TestingUtils {
url: String?,
logger: Logger
): TestResult {
assertNotNull("Api ${api.name} has invalid url on episode", url)
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
if (url == null) return TestResult.Fail // Should never trigger
var linksLoaded = 0
try {
val success = api.loadLinks(url, false, {}) { link ->
logger.log("Video loaded: ${link.name}")
assertTrue(
Assert.assertTrue(
"Api ${api.name} returns link with invalid url ${link.url}",
link.url.length > 4
)
@ -249,12 +245,12 @@ object TestingUtils {
logger.log("Links loaded: $linksLoaded")
return TestResult(linksLoaded > 0)
} else {
fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded")
Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded")
}
} catch (e: Throwable) {
when (e) {
is NotImplementedError -> {
fail("Provider has not implemented loadLinks()")
Assert.fail("Provider has not implemented loadLinks()")
}
else -> {
@ -280,7 +276,7 @@ object TestingUtils {
// Test Homepage
val homepage = testHomepage(api, logger)
assertTrue("Homepage failed to load", homepage.success)
Assert.assertTrue("Homepage failed to load", homepage.success)
val homePageList = (homepage as? TestResultList)?.results ?: emptyList()
// Test Search Results
@ -291,7 +287,7 @@ object TestingUtils {
listOf("over", "iron", "guy")).take(3)
val searchResults = testSearch(api, searchQueries, logger)
assertTrue("Failed to get search results", searchResults.success)
Assert.assertTrue("Failed to get search results", searchResults.success)
searchResults as TestResultList
// Test Load and LoadLinks
@ -325,4 +321,4 @@ object TestingUtils {
}
}
}
}
}

View file

@ -804,7 +804,6 @@ object VideoDownloadManager {
private suspend fun resolve(
startByte: Long,
endByte: Long?,
buffer: ByteArray,
callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit)
): Long = withContext(Dispatchers.IO) {
var currentByte: Long = startByte
@ -823,6 +822,7 @@ object VideoDownloadManager {
)
val requestStream = request.body.byteStream()
val buffer = ByteArray(bufferSize)
var read: Int
try {
@ -853,7 +853,6 @@ object VideoDownloadManager {
suspend fun resolveSafe(
index: Int,
retries: Int = 3,
buffer: ByteArray,
callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit)
): Boolean {
var start = chuckStartByte.getOrNull(index) ?: return false
@ -862,7 +861,7 @@ object VideoDownloadManager {
for (i in 0 until retries) {
try {
// in case
start = resolve(start, end, buffer, callback)
start = resolve(start, end, callback)
// no end defined, so we don't care exactly where it ended
if (end == null) return true
// we have download more or exactly what we needed
@ -1159,29 +1158,13 @@ object VideoDownloadManager {
}
}
// Reuse a download buffer to decrease unnecessary alloc
val buffer = ByteArray(items.bufferSize)
// This will take up the first available job and resolve
// this will take up the first available job and resolve
while (true) {
if (!isActive) return@launch
var isTooFarAhead = false
fileMutex.withLock {
if (metadata.type == DownloadType.IsStopped
|| metadata.type == DownloadType.IsFailed
) return@launch
// Limit RAM usage by throttling if too much data is downloaded but not yet written to disk
// 50MB limit
if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) {
isTooFarAhead = true
}
}
if (isTooFarAhead) {
delay(500)
continue
}
// mutex just in case, we never want this to fail due to multithreading
@ -1192,7 +1175,7 @@ object VideoDownloadManager {
// in case something has gone wrong set to failed if the fail is not caused by
// user cancellation
if (!items.resolveSafe(index, buffer = buffer, callback = callback)) {
if (!items.resolveSafe(index, callback = callback)) {
fileMutex.withLock {
if (metadata.type != DownloadType.IsStopped) {
metadata.type = DownloadType.IsFailed
@ -1350,23 +1333,10 @@ object VideoDownloadManager {
launch(Dispatchers.IO) {
while (true) {
if (!isActive) return@launch
var isTooFarAhead = false
fileMutex.withLock {
if (metadata.type == DownloadType.IsStopped
|| metadata.type == DownloadType.IsFailed
) return@launch
// Limit RAM usage by throttling if too much data is downloaded but not yet written to disk
// 50MB limit
if (metadata.bytesDownloaded - metadata.bytesWritten > 50_000_000) {
isTooFarAhead = true
}
}
if (isTooFarAhead) {
delay(500)
continue
}
// mutex just in case, we never want this to fail due to multithreading
@ -2030,8 +2000,6 @@ object VideoDownloadManager {
linkLoadingJob = ioSafe {
generator.generateLinks(
offset = 0,
isCasting = false,
clearCache = false,
sourceTypes = LOADTYPE_INAPP_DOWNLOAD,
callback = {

View file

@ -1,40 +0,0 @@
package com.lagradost.cloudstream3.utils.serializers
import android.net.Uri
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* Custom KSerializer for Android's [Uri] type.
*
* Uri is an Android platform type and cannot be annotated with @Serializable directly.
* Registering it in a SerializersModule globally would require a custom module passed to
* every Json instance, which adds hidden coupling. This serializer is also used sparingly
* across the codebase, so the overhead of a global registration isn't justified.
* Instead, we keep it explicit so that each usage site opts in intentionally and the
* serialization behavior remains visible.
*
* Usage:
*
* @Serializable
* data class MyData(
* @Serializable(with = UriSerializer::class)
* val uri: Uri,
* )
*/
object UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uri) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Uri {
return Uri.parse(decoder.decodeString())
}
}

View file

@ -11,7 +11,6 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -12,18 +12,17 @@
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_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:id="@+id/player_metadata_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="vertical"
android:layout_gravity="center_vertical"
android:paddingStart="64dp"
android:paddingEnd="32dp"
android:paddingBottom="32dp">
@ -40,23 +39,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:ellipsize="end"
android:maxLines="2"
android:textColor="@android:color/white"
android:textSize="30sp"
android:textStyle="bold"
android:shadowColor="@android:color/black"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="4"
android:textColor="@android:color/white"
android:textSize="30sp"
android:textStyle="bold"
tools:text="Zootopia 2" />
android:maxLines="2"
android:ellipsize="end"
tools:text="Zootopia 2"/>
</FrameLayout>
<!-- GENRES / YEAR / RATING -->
@ -65,10 +64,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:maxLines="2"
android:textColor="#B3FFFFFF"
android:textSize="14sp"
tools:text="Animation • Action • Adventure • 2025 • ⭐ 7.6" />
android:maxLines="2"
tools:text="Animation • Action • Adventure • 2025 • ⭐ 7.6"/>
<!-- SYNOPSIS -->
<TextView
@ -76,24 +75,23 @@
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:textColor="#E6FFFFFF"
android:textSize="16sp"
tools:text="Brave rabbit cop Judy Hopps..." />
android:maxLines="5"
tools:text="Brave rabbit cop Judy Hopps..."/>
</LinearLayout>
<ImageView
android:id="@+id/video_outline"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:src="@drawable/video_outline"
android:visibility="gone" />
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<FrameLayout
@ -174,7 +172,6 @@
<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"
@ -186,11 +183,11 @@
android:progressDrawable="@drawable/progress_drawable_vertical"
android:progressTint="@color/white"
android:progressTintMode="src_in"
tools:progress="30" />
tools:progress="30"
style="@android:style/Widget.Material.ProgressBar.Horizontal" />
<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"
@ -202,7 +199,8 @@
android:progressDrawable="@drawable/progress_drawable_vertical"
android:progressTint="@color/colorPrimaryOrange"
android:progressTintMode="src_in"
tools:progress="0" />
tools:progress="0"
style="@android:style/Widget.Material.ProgressBar.Horizontal" />
</RelativeLayout>
<RelativeLayout
@ -324,18 +322,23 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/skip_chapter_button"
style="@style/VideoButtonTV"
style="@style/NiceButton"
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:textSize="15sp"
android:padding="10dp"
android:textColor="@color/white"
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" />
@ -357,30 +360,28 @@
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" />
tools:visibility="visible"
android:layout_gravity="start"/>
</LinearLayout>
<TextView
@ -399,9 +400,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"
@ -580,7 +581,6 @@
tools:visibility="visible" />
</LinearLayout>
<LinearLayout
android:id="@+id/player_episodes_button_root"
android:layout_width="60dp"
@ -964,18 +964,19 @@
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"
@ -1128,11 +1129,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" />
@ -1140,34 +1141,34 @@
<LinearLayout
android:id="@+id/player_episode_overlay"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:visibility="gone"
android:padding="5dp"
android:background="?attr/primaryBlackBackground"
android:orientation="vertical"
android:padding="5dp"
android:visibility="gone">
android:layout_gravity="end"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/player_episode_overlay_title"
style="@style/WatchHeaderText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="0dp"
android:padding="10dp"
style="@style/WatchHeaderText"
android:textSize="15sp"
android:layout_marginEnd="0dp"
android:text="@string/episodes"
android:textSize="15sp" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<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:nextFocusLeft="@id/player_episodes_button"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/player_episodes">
android:descendantFocusability="afterDescendants">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>

View file

@ -11,7 +11,6 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"

View file

@ -1,3 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
</resources>

View file

@ -24,7 +24,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Nuwe opdatering gevind!
\n%1$s -&gt; %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>

View file

@ -8,7 +8,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">አዲስ ማሻሻያ ተገኝቷል!
\n%1$s -&gt; %2$s</string>
<string name="go_back_img_des">ተመለስ</string>
<string name="episode_more_options_des">ተጨማሪ አማራጮች</string>
<string name="type_watching">በማየት ላይ</string>

View file

@ -29,7 +29,8 @@
<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>
@ -95,7 +96,8 @@
<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>
@ -164,7 +166,8 @@
<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>
@ -191,7 +194,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>
@ -358,7 +361,11 @@
<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>
@ -366,7 +373,9 @@
<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>
@ -379,7 +388,8 @@
<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>
@ -409,7 +419,9 @@
<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>
@ -465,7 +477,8 @@
<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>
@ -492,7 +505,8 @@
<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>
@ -517,7 +531,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">تحزير: \"كلاود ستريم\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا!</string>
<string name="download_all_plugins_from_repo">تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، و م بتدعمن أبدًا!</string>
<string name="extension_status">الحالة</string>
<string name="delete_repository">محي الريپو</string>
<string name="category_player">مشغل الڤيديو</string>
@ -526,7 +540,10 @@
<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>
@ -546,7 +563,13 @@
<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>
@ -566,14 +589,16 @@
<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>
@ -609,13 +634,21 @@
<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>
@ -706,33 +739,4 @@
<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>

View file

@ -12,7 +12,8 @@
<!-- 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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">يوجد تحديث جديد!
\n%1$s -&gt; %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>
@ -175,8 +176,10 @@
<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>
@ -398,7 +401,9 @@
<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>
@ -488,13 +493,15 @@
<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>
@ -526,7 +533,13 @@
<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>
@ -539,7 +552,11 @@
<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>
@ -552,7 +569,9 @@
<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>
@ -580,7 +599,8 @@
<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>
@ -622,13 +642,21 @@
<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>
@ -721,7 +749,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>
@ -753,7 +781,4 @@
<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>

View file

@ -35,7 +35,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">!تم العثور على تحديث جديد
\n%1$s -&gt; %2$s</string>
<string name="rated_format" formatted="true">%.1f:قدر</string>
<string name="duration_format" formatted="true">%dاقل</string>
<string name="app_name">كلاودستريم</string>
@ -156,15 +157,23 @@
<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>
@ -218,7 +227,8 @@
<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>
@ -245,7 +255,8 @@
<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>

View file

@ -252,9 +252,12 @@
<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>
@ -453,7 +456,9 @@
<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>
@ -518,9 +523,11 @@
<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>
@ -532,7 +539,13 @@
<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>
@ -543,8 +556,14 @@
<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>
@ -569,7 +588,8 @@
<string name="download">ডাউনলোড</string>
<string name="updates_settings_des">এপ্‌ আৰম্ভণিৰ পিছত নতুন আপডেটৰ সন্ধান কৰক।</string>
<string name="anim">একেই ডেভেলপাৰৰ দ্বাৰা এনিম এপ্‌</string>
<string name="new_update_format" formatted="true">নতুন আপডেট পোৱা গ’ল! \n%1$s -&gt; %2$s</string>
<string name="new_update_format" formatted="true">নতুন আপডেট পোৱা গ’ল!
\n%1$s -&gt; %2$s</string>
<string name="filler" formatted="true">ফিলাৰ</string>
<string name="play_with_app_name">CloudStreamৰে প্লে কৰক</string>
<string name="title_search">সন্ধান</string>
@ -589,10 +609,18 @@
<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>

View file

@ -16,7 +16,8 @@
<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 -&gt; %2$s</string>
<string formatted="true" name="new_update_format">Намерена е нова актуализация!
\n%1$s -&gt; %2$s</string>
<string formatted="true" name="filler">Шаблон</string>
<string formatted="true" name="duration_format">%d мин</string>
<string name="app_name">CloudStream</string>
@ -182,8 +183,10 @@
<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>
@ -402,7 +405,9 @@
<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>
@ -514,10 +519,12 @@
<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>
@ -544,11 +551,23 @@
<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>

View file

@ -15,7 +15,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">নতুন আপডেট এসেছে!
\n%1$s -&gt; %2$s</string>
<string name="filler" formatted="true">ফিলার</string>
<string name="duration_format" formatted="true">%d মিনিট</string>
<string name="app_name">ক্লাউডস্ট্রিম</string>
@ -158,7 +159,8 @@
<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>
@ -182,7 +184,8 @@
<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>
@ -305,7 +308,8 @@
<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>

View file

@ -16,7 +16,8 @@
<!-- 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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Nalezena nová aktualizace!
\n%1$s -&gt; %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>
@ -171,8 +172,10 @@
<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>
@ -413,7 +416,9 @@
<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>
@ -441,7 +446,8 @@
<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>
@ -449,7 +455,8 @@
<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>
@ -530,7 +537,13 @@
<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>
@ -540,7 +553,11 @@
<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>
@ -554,7 +571,9 @@
<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>
@ -583,7 +602,8 @@
<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>
@ -624,12 +644,20 @@
<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>
@ -751,7 +779,4 @@
<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>

View file

@ -27,7 +27,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Neues Update gefunden!
\n%1$s -&gt; %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>
@ -187,8 +188,10 @@
<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>
@ -252,7 +255,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">Zeige Playerinformationen</string>
<string name="limit_title_rez">Playerinformationen anzeigen</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>
@ -398,7 +401,9 @@
<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>
@ -479,9 +484,11 @@
<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>
@ -515,7 +522,13 @@
<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>
@ -547,8 +560,14 @@
<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>
@ -568,7 +587,8 @@
<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>
@ -587,7 +607,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>
@ -610,10 +630,18 @@
<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>
@ -712,8 +740,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">Zeige Cast-Panel</string>
<string name="video_info">Mediainfo</string>
<string name="show_cast_in_details">Cast-Panel zeigen</string>
<string name="video_info">Medieninfo</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>
@ -731,8 +759,4 @@
<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>

View file

@ -110,7 +110,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Νέα διαθέσιμη ενημέρωση!
\n%1$s -&gt; %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>
@ -148,8 +149,10 @@
<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>
@ -320,7 +323,9 @@
<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>
@ -482,15 +487,23 @@
<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>
@ -536,7 +549,9 @@
<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>
@ -563,7 +578,11 @@
<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>
@ -576,7 +595,8 @@
<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>

View file

@ -83,7 +83,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Nova ĝisdatigo trovita!
\n%1$s -&gt; %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>

View file

@ -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,7 +80,8 @@
<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>
@ -103,7 +104,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>
@ -248,7 +249,8 @@
<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>
@ -480,8 +482,10 @@
<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>
@ -506,7 +510,13 @@
<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>
@ -526,7 +536,11 @@
<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>
@ -569,7 +583,8 @@
<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>
@ -594,7 +609,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 equipo e introduce el código anterior</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_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>
@ -606,16 +621,24 @@
<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>
@ -672,9 +695,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>
@ -733,8 +756,4 @@
<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>

View file

@ -113,7 +113,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">بروزرسانی جدید پیدا شد!
\n%1$s -&gt; %2$s</string>
<string name="play_movie_button">پخش فیلم</string>
<string name="browser">مرورگر</string>
<string name="play_episode">پخش قسمت</string>
@ -129,7 +130,8 @@
<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>
@ -178,11 +180,13 @@
<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>

View file

@ -79,7 +79,8 @@
<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>
@ -121,7 +122,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Nouvelle mise à jour trouvée!
\n%1$s -&gt; %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>
@ -135,7 +137,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>
@ -143,7 +145,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>
@ -289,8 +291,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>
@ -304,9 +306,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>
@ -319,7 +321,8 @@
<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>
@ -360,7 +363,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>
@ -406,14 +409,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>
@ -429,10 +432,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 lutilisation dextensions 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 lutilisation dextensions 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>
@ -445,7 +448,9 @@
<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>
@ -474,7 +479,8 @@
<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>
@ -483,7 +489,8 @@
<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>
@ -508,7 +515,13 @@
<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>
@ -541,14 +554,20 @@
<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>
@ -561,7 +580,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>
@ -600,10 +619,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>
@ -619,9 +638,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 na encore été chargé</string>
<string name="backup_path_title">Emplacement du dossier de sauvegarde</string>
@ -641,7 +660,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>
@ -652,7 +671,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>
@ -722,8 +741,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>
@ -735,7 +754,4 @@
<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>

View file

@ -13,7 +13,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Nova actualización atopada!
\n%1$s -&gt; %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>

View file

@ -2,7 +2,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">नया अपडेट आया है!
\n%1$s -&gt; %2$s</string>
<string name="title_home">होम</string>
<string name="title_search">खोजें</string>
<string name="title_downloads">डाउनलोडस</string>
@ -86,7 +87,8 @@
<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>
@ -151,7 +153,11 @@
<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>
@ -171,7 +177,9 @@
<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>

View file

@ -19,7 +19,8 @@
<!-- 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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Pronađeno je novo ažuriranje!
\n%1$s -&gt; %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>
@ -185,8 +186,10 @@
<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>
@ -408,7 +411,9 @@
<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>
@ -493,9 +498,11 @@
<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>
@ -534,13 +541,23 @@
<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>
@ -553,7 +570,9 @@
<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>
@ -577,7 +596,8 @@
<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>
@ -596,7 +616,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>
@ -622,14 +642,22 @@
<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>
@ -721,7 +749,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>
@ -747,8 +775,4 @@
<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>

View file

@ -10,7 +10,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Új frissítés található!
\n%1$s -&gt; %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>
@ -148,7 +149,8 @@
<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>
@ -204,7 +206,8 @@
<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>
@ -320,7 +323,8 @@
<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>
@ -412,7 +416,9 @@
<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>
@ -466,7 +472,8 @@
<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>
@ -529,8 +536,18 @@
<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>
@ -539,7 +556,9 @@
<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>

View file

@ -169,8 +169,10 @@
<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>
@ -388,7 +390,9 @@
<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>
@ -483,7 +487,8 @@
<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>
@ -495,7 +500,8 @@
<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>
@ -521,7 +527,13 @@
<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>
@ -581,7 +593,8 @@
<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>
@ -625,10 +638,18 @@
<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>
@ -744,7 +765,4 @@
<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>

View file

@ -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,7 +19,8 @@
<!-- 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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Nuovo aggiornamento trovato!
\n%1$s -&gt; %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> -->
@ -185,8 +186,10 @@
<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>
@ -409,7 +412,9 @@
<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>
@ -497,13 +502,15 @@
<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>
@ -527,7 +534,13 @@
<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>
@ -547,7 +560,11 @@
<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>
@ -560,7 +577,9 @@
<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>
@ -590,7 +609,8 @@
<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>
@ -632,12 +652,20 @@
<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>
@ -756,7 +784,4 @@
<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>

View file

@ -6,7 +6,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">נמצא עדכון חדש!
\n%1$s -&gt; %2$s</string>
<string name="filler" formatted="true">סינון</string>
<string name="duration_format" formatted="true">%d דקות</string>
<string name="app_name">קלאודסטרים</string>
@ -145,8 +146,10 @@
<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>
@ -422,8 +425,10 @@
<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>
@ -439,7 +444,11 @@
<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>
@ -521,7 +530,13 @@
<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>

View file

@ -62,7 +62,8 @@
<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>
@ -81,7 +82,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">新しいアップデートを発見!
\n%1$s -&gt; %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>

View file

@ -83,7 +83,8 @@
<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>

View file

@ -11,7 +11,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">새로운 업데이트!
\n%1$s -&gt; %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>
@ -27,7 +28,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>
@ -123,7 +124,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>
@ -160,8 +161,10 @@
<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>
@ -181,7 +184,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>
@ -192,7 +195,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>
@ -294,7 +297,11 @@
<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 \nDiscord에 가입하거나 온라인으로 검색해 보세요.</string>
<string name="blank_repo_message">CloudStream에는 기본적으로 설치된 사이트가 없습니다. 저장소에서 사이트를 설치해야 합니다.
\n
\nSky UK Limited의 무분별한 DMCA 게시 중단으로 인해 앱에서 저장소 사이트를 연결 할 수 없습니다.
\n
\nDiscord에 가입하거나 온라인으로 검색해 보세요.</string>
<string name="view_public_repositories_button">커뮤니티 저장소 보기</string>
<string name="view_public_repositories_button_short">공개 목록</string>
<string name="uppercase_all_subtitles">자막 대문자화 표시</string>
@ -315,7 +322,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">Picture-in-picture 모드</string>
<string name="picture_in_picture">PIP 모드</string>
<string name="player_size_settings">플레이어 크기 조정 버튼</string>
<string name="picture_in_picture_des">미니플레이어를 통해 다른 앱 상단에서 계속 재생합니다</string>
<string name="player_size_settings_des">레터박스 제거</string>
@ -324,7 +331,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>
@ -431,16 +438,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>
@ -459,8 +466,10 @@
<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>
@ -476,7 +485,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">로그캣 🐈 보기</string>
<string name="show_log_cat">Logcat 🐈 표시</string>
<string name="show_fillers_settings">애니메이션용 필러 에피소드 표시</string>
<string name="test_passed">통과</string>
<string name="resume">계속</string>
@ -508,11 +517,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>
@ -563,11 +572,17 @@
<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>
@ -584,7 +599,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>
@ -593,7 +608,8 @@
<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>
@ -611,10 +627,18 @@
<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>
@ -730,7 +754,4 @@
<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>

View file

@ -31,7 +31,8 @@
<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 -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Rastas atnaujinimas!
\n%1$s -&gt; %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>
@ -87,7 +88,8 @@
<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>
@ -179,7 +181,11 @@
<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>

View file

@ -12,7 +12,8 @@
<string name="preview_background_img_des">Apskatīt background</string>
<string name="player_speed_text_format" formatted="true">Ātrums (%.2fx)</string>
<string name="rated_format" formatted="true">Lidzīgi: %.1f</string>
<string name="new_update_format" formatted="true">Jauns atjauninājums atrasts! \n%1$s -&gt; %2$s</string>
<string name="new_update_format" formatted="true">Jauns atjauninājums atrasts!
\n%1$s -&gt; %2$s</string>
<string name="duration_format" formatted="true">%d galvenais</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">Atskaņo ar cloudstream</string>
@ -192,8 +193,10 @@
<string name="resume">Atsākt</string>
<string name="go_back_30">-30</string>
<string name="go_forward_30">+30</string>
<string name="delete_message" formatted="true">Šis pilnibā dzesīs %s \nEsat parliecināts?</string>
<string name="resume_time_left" formatted="true">%dm \natlikušas</string>
<string name="delete_message" formatted="true">Šis pilnibā dzesīs %s
\nEsat parliecināts?</string>
<string name="resume_time_left" formatted="true">%dm
\natlikušas</string>
<string name="status_completed">Pabeigts</string>
<string name="status">Statuss</string>
<string name="year">Gads</string>
@ -455,7 +458,8 @@
<string name="sort_alphabetical_z">Alfabētiskā secībā (Z līdz A)</string>
<string name="select_library">Atlasiet Bibliotēka</string>
<string name="open_with">Atvērt ar</string>
<string name="empty_library_no_accounts_message">Šķiet, ka jūsu bibliotēka ir tukša :( \nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai.</string>
<string name="empty_library_no_accounts_message">Šķiet, ka jūsu bibliotēka ir tukša :(
\nPiesakieties bibliotēkas kontā vai pievienojiet pārraides savai vietējai bibliotēkai.</string>
<string name="revert">Atgriest</string>
<string name="subscription_deleted">Anulēts %s abonements</string>
<string name="subscription_episode_released">%d sērija izlaista!</string>

View file

@ -126,7 +126,8 @@
<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">Изгледанo</string>
<string name="status">Статус</string>
@ -244,7 +245,7 @@
<string name="quality_tc">TC</string>
<string name="subscription_new">Претплатен на %s</string>
<string name="pref_category_subtitles">Преводи</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="backup_failed">Недостасуваат дозволи за складирање. Обиди се повторно.</string>
<string name="sort_save">Зачувај</string>
<string name="player_load_subtitles">Вчитај од датотека</string>
@ -416,7 +417,8 @@
<string name="batch_download_start_format" formatted="true">Почна да презема %1$d %2$s…</string>
<string name="automatic_plugin_updates">Автоматски ажурирања на приклучоци</string>
<string name="go_back_30">-30</string>
<string name="resume_time_left" formatted="true">%dm \nпреостанува</string>
<string name="resume_time_left" formatted="true">%dm
\nпреостанува</string>
<string name="video_buffer_disk_settings">Видео кеш на дискот</string>
<string name="network_adress_example">https://example.com/example.mp4</string>
<string name="setup_done">Готово</string>
@ -445,7 +447,7 @@
<string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string>
<string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string>
<string name="apk_installer_settings_des">Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат.</string>
<string name="limit_title_rez">Прикажи информации за плеерот</string>
<string name="limit_title_rez">Резолуција на видео плеер</string>
<string name="video_buffer_size_settings">Големина на видео баферот</string>
<string name="pref_category_player_layout">Распоред</string>
<string name="pref_category_defaults">Стандардно</string>
@ -585,7 +587,8 @@
<string name="biometric_prompt_description">По неколку неуспешни обиди, известувањето ќе се затвори. Едноставно вклучи ја апликацијата повторно за да се обидеш повторно.</string>
<string name="custom_media_singular">Медиуми</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="battery_dialog_message">За да се обезбедат непрекинати преземања и известувања за претплатените ТВ серии, CloudStream треба дозвола за работа во позадина. Со притискање на „ОК“, ќе ви биде прикажан дијалог за барање дозвола. Ве молиме, притиснете „Дозволи“.\n\nИмајте предвид дека оваа дозвола не значи дека CS3 ќе ја троши вашата батерија. Ќе работи во позадина само кога е потребно, како на пример при примање известувања или преземање видеа од официјални екстензии.</string>
<string name="clipboard_permission_error">Грешка при пристапот до таблата со исечоци, обиди се повторно.</string>
<string name="clipboard_unknown_error">Грешка при копирање, молам копирај го логот и контактирај ја поддршката на апликацијата.</string>
@ -705,37 +708,4 @@
<string name="top_center">Горе во центар</string>
<string name="top_right">Горе на десно</string>
<string name="play_full_series_button">Пушти ја целата серија</string>
<string name="download_queue">Редица за преземање</string>
<string name="queue_empty_message">Моментално нема преземања во редицата.</string>
<string name="extra_brightness_settings">Дополнителна осветленост</string>
<string name="extra_brightness_settings_des">Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот</string>
<string name="extra_brightness_key">овозможенаополнителна_осветленост</string>
<string name="search_suggestions">Предлози за пребарување</string>
<string name="search_suggestions_des">Прикажувај предлози за пребарување додека пишуваш</string>
<string name="clear_suggestions">Исчисти предлози</string>
<string name="show_player_metadata_overlay">Прикажи преклоп со метаподатоци на плеерот</string>
<string name="show_cast_in_details">Прикажи панел за емитување</string>
<string name="install_prerelease">Инсталирај предиздавачка верзија</string>
<string name="prerelease_already_installed">Предиздавачката верзија е веќе инсталирана.</string>
<string name="prerelease_install_failed">Неуспешна инсталација на предиздавачката верзија.</string>
<string name="video_singular">Видео</string>
<string name="show_episode_text">Текст на епизода</string>
<string name="video_info">Информации за медиумот</string>
<string name="skip_type_preview">Преглед</string>
<string name="source_priority">Приоритет на извор</string>
<string name="source_priority_help">Одреди како ќе се подредуваат видео изворите во плеерот</string>
<string name="source_name">Име на изворот</string>
<string name="download_all">Преземи сѐ</string>
<string name="cancel_all">Откажи сѐ</string>
<string name="download_episode_range">Дали сакате да ја преземете епизодата %s?</string>
<string name="cancel_queue_message">Дали сакате да ги откажете сите преземања во редицата?</string>
<plurals name="downloads_active">
<item quantity="one">%d активно преземање</item>
<item quantity="other">%d активни преземања</item>
</plurals>
<plurals name="downloads_queued">
<item quantity="one">%d преземање во редицата</item>
<item quantity="other">%d преземања во редицата</item>
</plurals>
<string name="player_is_live">Во живо</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more