mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge remote-tracking branch 'origin/master'
# Conflicts: # app/src/main/AndroidManifest.xml
This commit is contained in:
commit
fb94cc8a86
51 changed files with 866 additions and 1151 deletions
|
@ -32,11 +32,12 @@ android {
|
||||||
enable = true
|
enable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
// disable this for now
|
||||||
cmake {
|
//externalNativeBuild {
|
||||||
path("CMakeLists.txt")
|
// cmake {
|
||||||
}
|
// path("CMakeLists.txt")
|
||||||
}
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("prerelease") {
|
create("prerelease") {
|
||||||
|
@ -50,7 +51,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdk = 33
|
compileSdk = 33
|
||||||
buildToolsVersion = "30.0.3"
|
buildToolsVersion = "34.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.lagradost.cloudstream3"
|
applicationId = "com.lagradost.cloudstream3"
|
||||||
|
@ -58,7 +59,7 @@ android {
|
||||||
targetSdk = 29
|
targetSdk = 29
|
||||||
|
|
||||||
versionCode = 59
|
versionCode = 59
|
||||||
versionName = "4.1.7"
|
versionName = "4.1.8"
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
|
@ -232,7 +233,7 @@ dependencies {
|
||||||
// To fix SSL fuckery on android 9
|
// To fix SSL fuckery on android 9
|
||||||
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
||||||
// Util to skip the URI file fuckery 🙏
|
// Util to skip the URI file fuckery 🙏
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
implementation("com.github.LagradOst:SafeFile:0.0.5")
|
||||||
|
|
||||||
// API because cba maintaining it myself
|
// API because cba maintaining it myself
|
||||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||||
|
|
||||||
<permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
|
||||||
<!-- Fixes android tv fuckery -->
|
<!-- Fixes android tv fuckery -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
|
|
|
@ -107,7 +107,7 @@ class AcraApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
NativeCrashHandler.initCrashHandler()
|
//NativeCrashHandler.initCrashHandler()
|
||||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
|
|
|
@ -144,6 +144,7 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "MAINACT"
|
const val TAG = "MAINACT"
|
||||||
var lastError: String? = null
|
var lastError: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setting this will automatically enter the query in the search
|
* Setting this will automatically enter the query in the search
|
||||||
* next time the search fragment is opened.
|
* next time the search fragment is opened.
|
||||||
|
@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
snackbar.show()
|
snackbar.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ioSafe { SafeFile.check(this@MainActivity) }
|
||||||
|
|
||||||
if (PluginManager.checkSafeModeFile()) {
|
if (PluginManager.checkSafeModeFile()) {
|
||||||
normalSafeApiCall {
|
normalSafeApiCall {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
object NativeCrashHandler {
|
object NativeCrashHandler {
|
||||||
// external fun triggerNativeCrash()
|
// external fun triggerNativeCrash()
|
||||||
private external fun initNativeCrashHandler()
|
/*private external fun initNativeCrashHandler()
|
||||||
private external fun getSignalStatus(): Int
|
private external fun getSignalStatus(): Int
|
||||||
|
|
||||||
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
@ -49,5 +49,5 @@ object NativeCrashHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
initSignalPolling()
|
initSignalPolling()
|
||||||
}
|
}*/
|
||||||
}
|
}
|
|
@ -2,15 +2,12 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.SecretKeyFactory
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.PBEKeySpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class Moviesapi : Chillx() {
|
class Moviesapi : Chillx() {
|
||||||
override val name = "Moviesapi"
|
override val name = "Moviesapi"
|
||||||
|
@ -32,7 +29,7 @@ open class Chillx : ExtractorApi() {
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY = "11x&W5UBrcqn\$9Yl"
|
private const val KEY = "m4H6D9%0\$N&F6rQ&"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
|
@ -47,8 +44,7 @@ open class Chillx : ExtractorApi() {
|
||||||
referer = referer
|
referer = referer
|
||||||
).text
|
).text
|
||||||
)?.groupValues?.get(1)
|
)?.groupValues?.get(1)
|
||||||
val encData = AppUtils.tryParseJson<AESData>(base64Decode(master ?: return))
|
val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||||
val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
|
|
||||||
|
|
||||||
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||||
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
|
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
|
||||||
|
@ -86,52 +82,6 @@ open class Chillx : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cryptoAESHandler(
|
|
||||||
data: AESData,
|
|
||||||
pass: String,
|
|
||||||
encrypt: Boolean = true
|
|
||||||
): String {
|
|
||||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
|
|
||||||
val spec = PBEKeySpec(
|
|
||||||
pass.toCharArray(),
|
|
||||||
data.salt?.hexToByteArray(),
|
|
||||||
data.iterations?.toIntOrNull() ?: 1,
|
|
||||||
256
|
|
||||||
)
|
|
||||||
val key = factory.generateSecret(spec)
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
return if (!encrypt) {
|
|
||||||
cipher.init(
|
|
||||||
Cipher.DECRYPT_MODE,
|
|
||||||
SecretKeySpec(key.encoded, "AES"),
|
|
||||||
IvParameterSpec(data.iv?.hexToByteArray())
|
|
||||||
)
|
|
||||||
String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString())))
|
|
||||||
} else {
|
|
||||||
cipher.init(
|
|
||||||
Cipher.ENCRYPT_MODE,
|
|
||||||
SecretKeySpec(key.encoded, "AES"),
|
|
||||||
IvParameterSpec(data.iv?.hexToByteArray())
|
|
||||||
)
|
|
||||||
base64Encode(cipher.doFinal(data.ciphertext?.toByteArray()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.hexToByteArray(): ByteArray {
|
|
||||||
check(length % 2 == 0) { "Must have an even length" }
|
|
||||||
return chunked(2)
|
|
||||||
.map { it.toInt(16).toByte() }
|
|
||||||
|
|
||||||
.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class AESData(
|
|
||||||
@JsonProperty("ciphertext") val ciphertext: String? = null,
|
|
||||||
@JsonProperty("iv") val iv: String? = null,
|
|
||||||
@JsonProperty("salt") val salt: String? = null,
|
|
||||||
@JsonProperty("iterations") val iterations: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Tracks(
|
data class Tracks(
|
||||||
@JsonProperty("file") val file: String? = null,
|
@JsonProperty("file") val file: String? = null,
|
||||||
@JsonProperty("label") val label: String? = null,
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
|
|
@ -2,14 +2,10 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.security.DigestException
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class DatabaseGdrive2 : Gdriveplayer() {
|
class DatabaseGdrive2 : Gdriveplayer() {
|
||||||
override var mainUrl = "https://databasegdriveplayer.co"
|
override var mainUrl = "https://databasegdriveplayer.co"
|
||||||
|
@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
?.data()?.let { getAndUnpack(it) }
|
?.data()?.let { getAndUnpack(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.decodeHex(): ByteArray {
|
|
||||||
check(length % 2 == 0) { "Must have an even length" }
|
|
||||||
return chunked(2)
|
|
||||||
.map { it.toInt(16).toByte() }
|
|
||||||
.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/41434590/8166854
|
|
||||||
private fun GenerateKeyAndIv(
|
|
||||||
password: ByteArray,
|
|
||||||
salt: ByteArray,
|
|
||||||
hashAlgorithm: String = "MD5",
|
|
||||||
keyLength: Int = 32,
|
|
||||||
ivLength: Int = 16,
|
|
||||||
iterations: Int = 1
|
|
||||||
): List<ByteArray>? {
|
|
||||||
|
|
||||||
val md = MessageDigest.getInstance(hashAlgorithm)
|
|
||||||
val digestLength = md.digestLength
|
|
||||||
val targetKeySize = keyLength + ivLength
|
|
||||||
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
|
||||||
val generatedData = ByteArray(requiredLength)
|
|
||||||
var generatedLength = 0
|
|
||||||
|
|
||||||
try {
|
|
||||||
md.reset()
|
|
||||||
|
|
||||||
while (generatedLength < targetKeySize) {
|
|
||||||
if (generatedLength > 0)
|
|
||||||
md.update(
|
|
||||||
generatedData,
|
|
||||||
generatedLength - digestLength,
|
|
||||||
digestLength
|
|
||||||
)
|
|
||||||
|
|
||||||
md.update(password)
|
|
||||||
md.update(salt, 0, 8)
|
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
|
||||||
|
|
||||||
for (i in 1 until iterations) {
|
|
||||||
md.update(generatedData, generatedLength, digestLength)
|
|
||||||
md.digest(generatedData, generatedLength, digestLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedLength += digestLength
|
|
||||||
}
|
|
||||||
return listOf(
|
|
||||||
generatedData.copyOfRange(0, keyLength),
|
|
||||||
generatedData.copyOfRange(keyLength, targetKeySize)
|
|
||||||
)
|
|
||||||
} catch (e: DigestException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cryptoAESHandler(
|
|
||||||
data: AesData,
|
|
||||||
pass: ByteArray,
|
|
||||||
encrypt: Boolean = true
|
|
||||||
): String? {
|
|
||||||
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
|
|
||||||
return if (!encrypt) {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
String(cipher.doFinal(base64DecodeArray(data.ct)))
|
|
||||||
} else {
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
|
||||||
base64Encode(cipher.doFinal(data.ct.toByteArray()))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Regex.first(str: String): String? {
|
private fun Regex.first(str: String): String? {
|
||||||
return find(str)?.groupValues?.getOrNull(1)
|
return find(str)?.groupValues?.getOrNull(1)
|
||||||
}
|
}
|
||||||
|
@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
val document = app.get(url).document
|
val document = app.get(url).document
|
||||||
|
|
||||||
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
||||||
val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
|
val data = Regex("data='(\\S+?)'").first(eval) ?: return
|
||||||
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||||
?.split(Regex("\\D+"))
|
?.split(Regex("\\D+"))
|
||||||
?.joinToString("") {
|
?.joinToString("") {
|
||||||
Char(it.toInt()).toString()
|
Char(it.toInt()).toString()
|
||||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||||
?: throw ErrorLoadingException("can't find password")
|
?: throw ErrorLoadingException("can't find password")
|
||||||
val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
|
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||||
|
|
||||||
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
||||||
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
||||||
|
@ -194,12 +118,6 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AesData(
|
|
||||||
@JsonProperty("ct") val ct: String,
|
|
||||||
@JsonProperty("iv") val iv: String,
|
|
||||||
@JsonProperty("s") val s: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Tracks(
|
data class Tracks(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
@JsonProperty("kind") val kind: String,
|
@JsonProperty("kind") val kind: String,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
element.attr("href").contains(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,6 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
open val embed = "ajax/embed-4"
|
open val embed = "ajax/embed-4"
|
||||||
open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"
|
open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"
|
||||||
private var rawKey: String? = null
|
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -82,9 +81,10 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getRawKey(): String = rawKey ?: app.get(key).text.also { rawKey = it }
|
private suspend fun getRawKey(): String = app.get(key).text
|
||||||
|
|
||||||
private fun extractRealKey(originalString: String?, stops: String): Pair<String, String> {
|
private fun extractRealKey(originalString: String?, stops: String): Pair<String, String> {
|
||||||
val table = parseJson<List<List<Int>>>(stops)
|
val table = parseJson<List<List<Int>>>(stops)
|
||||||
|
|
|
@ -7,15 +7,22 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
|
class SpeedoStream2 : SpeedoStream() {
|
||||||
|
override val mainUrl = "https://speedostream.mom"
|
||||||
|
}
|
||||||
|
|
||||||
class SpeedoStream1 : SpeedoStream() {
|
class SpeedoStream1 : SpeedoStream() {
|
||||||
override val mainUrl = "https://speedostream.pm"
|
override val mainUrl = "https://speedostream.pm"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class SpeedoStream : ExtractorApi() {
|
open class SpeedoStream : ExtractorApi() {
|
||||||
override val name = "SpeedoStream"
|
override val name = "SpeedoStream"
|
||||||
override val mainUrl = "https://speedostream.mom"
|
override val mainUrl = "https://speedostream.bond"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
// .bond, .pm, .mom redirect to .bond
|
||||||
|
private val hostUrl = "https://speedostream.bond"
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
app.get(url, referer = referer).document.select("script").map { script ->
|
app.get(url, referer = referer).document.select("script").map { script ->
|
||||||
|
@ -26,7 +33,7 @@ open class SpeedoStream : ExtractorApi() {
|
||||||
M3u8Helper.generateM3u8(
|
M3u8Helper.generateM3u8(
|
||||||
name,
|
name,
|
||||||
it.file,
|
it.file,
|
||||||
"$mainUrl/",
|
"$hostUrl/",
|
||||||
).forEach { m3uData -> sources.add(m3uData) }
|
).forEach { m3uData -> sources.add(m3uData) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +44,4 @@ open class SpeedoStream : ExtractorApi() {
|
||||||
private data class File(
|
private data class File(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.argamap
|
import com.lagradost.cloudstream3.argamap
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
element.attr("href").contains(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
class Vidstreamz : WcoStream() {
|
class Vidstreamz : WcoStream() {
|
||||||
|
@ -126,8 +127,7 @@ open class WcoStream : ExtractorApi() {
|
||||||
|
|
||||||
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
||||||
return response.parsed<Response>().data.media.sources.map {
|
return response.parsed<Response>().data.media.sources.map {
|
||||||
ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8"))
|
ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
open class Wibufile : ExtractorApi() {
|
open class Wibufile : ExtractorApi() {
|
||||||
override val name: String = "Wibufile"
|
override val name: String = "Wibufile"
|
||||||
|
@ -28,10 +28,8 @@ open class Wibufile : ExtractorApi() {
|
||||||
video ?: return,
|
video ?: return,
|
||||||
"$mainUrl/",
|
"$mainUrl/",
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
URI(url).path.endsWith(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import java.security.DigestException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object AesHelper {
|
||||||
|
|
||||||
|
private const val HASH = "AES/CBC/PKCS5PADDING"
|
||||||
|
private const val KDF = "MD5"
|
||||||
|
|
||||||
|
fun cryptoAESHandler(
|
||||||
|
data: String,
|
||||||
|
pass: ByteArray,
|
||||||
|
encrypt: Boolean = true,
|
||||||
|
padding: String = HASH,
|
||||||
|
): String? {
|
||||||
|
val parse = AppUtils.tryParseJson<AesData>(data) ?: return null
|
||||||
|
val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: return null
|
||||||
|
val cipher = Cipher.getInstance(padding)
|
||||||
|
return if (!encrypt) {
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
String(cipher.doFinal(base64DecodeArray(parse.ct)))
|
||||||
|
} else {
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
base64Encode(cipher.doFinal(parse.ct.toByteArray()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/41434590/8166854
|
||||||
|
fun generateKeyAndIv(
|
||||||
|
password: ByteArray,
|
||||||
|
salt: ByteArray,
|
||||||
|
hashAlgorithm: String = KDF,
|
||||||
|
keyLength: Int = 32,
|
||||||
|
ivLength: Int = 16,
|
||||||
|
iterations: Int = 1
|
||||||
|
): Pair<ByteArray,ByteArray>? {
|
||||||
|
|
||||||
|
val md = MessageDigest.getInstance(hashAlgorithm)
|
||||||
|
val digestLength = md.digestLength
|
||||||
|
val targetKeySize = keyLength + ivLength
|
||||||
|
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
||||||
|
val generatedData = ByteArray(requiredLength)
|
||||||
|
var generatedLength = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
md.reset()
|
||||||
|
|
||||||
|
while (generatedLength < targetKeySize) {
|
||||||
|
if (generatedLength > 0)
|
||||||
|
md.update(
|
||||||
|
generatedData,
|
||||||
|
generatedLength - digestLength,
|
||||||
|
digestLength
|
||||||
|
)
|
||||||
|
|
||||||
|
md.update(password)
|
||||||
|
md.update(salt, 0, 8)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
|
||||||
|
for (i in 1 until iterations) {
|
||||||
|
md.update(generatedData, generatedLength, digestLength)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedLength += digestLength
|
||||||
|
}
|
||||||
|
return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize)
|
||||||
|
} catch (e: DigestException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.hexToByteArray(): ByteArray {
|
||||||
|
check(length % 2 == 0) { "Must have an even length" }
|
||||||
|
return chunked(2)
|
||||||
|
.map { it.toInt(16).toByte() }
|
||||||
|
.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AesData(
|
||||||
|
@JsonProperty("ct") val ct: String,
|
||||||
|
@JsonProperty("iv") val iv: String,
|
||||||
|
@JsonProperty("s") val s: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -1,16 +1,24 @@
|
||||||
package com.lagradost.cloudstream3.ui
|
package com.lagradost.cloudstream3.ui
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||||
|
import com.lagradost.cloudstream3.DubStatus
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.HomePageResponse
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||||
|
import com.lagradost.cloudstream3.MainPageRequest
|
||||||
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
import com.lagradost.cloudstream3.fixUrl
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope.coroutineContext
|
import kotlinx.coroutines.GlobalScope.coroutineContext
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -174,7 +182,7 @@ class APIRepository(val api: MainAPI) {
|
||||||
data: String,
|
data: String,
|
||||||
isCasting: Boolean,
|
isCasting: Boolean,
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (isInvalidData(data)) return false // this makes providers cleaner
|
if (isInvalidData(data)) return false // this makes providers cleaner
|
||||||
return try {
|
return try {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.sortSubs
|
import com.lagradost.cloudstream3.sortSubs
|
||||||
import com.lagradost.cloudstream3.sortUrls
|
import com.lagradost.cloudstream3.sortUrls
|
||||||
|
import com.lagradost.cloudstream3.ui.player.LoadType
|
||||||
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
|
@ -294,7 +295,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
val generator = RepoLinkGenerator(listOf(epData))
|
val generator = RepoLinkGenerator(listOf(epData))
|
||||||
|
|
||||||
val isSuccessful = safeApiCall {
|
val isSuccessful = safeApiCall {
|
||||||
generator.generateLinks(clearCache = false, isCasting = true,
|
generator.generateLinks(
|
||||||
|
clearCache = false, type = LoadType.Chromecast,
|
||||||
callback = {
|
callback = {
|
||||||
it.first?.let { link ->
|
it.first?.let { link ->
|
||||||
currentLinks.add(link)
|
currentLinks.add(link)
|
||||||
|
|
|
@ -658,12 +658,14 @@ class HomeFragment : Fragment() {
|
||||||
return@observeNullable
|
return@observeNullable
|
||||||
}
|
}
|
||||||
|
|
||||||
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = {
|
val (items, delete) = item
|
||||||
|
|
||||||
|
bottomSheetDialog = activity?.loadHomepageList(items, expandCallback = {
|
||||||
homeViewModel.expandAndReturn(it)
|
homeViewModel.expandAndReturn(it)
|
||||||
}, dismissCallback = {
|
}, dismissCallback = {
|
||||||
homeViewModel.popup(null)
|
homeViewModel.popup(null)
|
||||||
bottomSheetDialog = null
|
bottomSheetDialog = null
|
||||||
})
|
}, deleteCallback = delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
homeViewModel.reloadStored()
|
homeViewModel.reloadStored()
|
||||||
|
|
|
@ -283,7 +283,11 @@ class HomeParentItemAdapterPreview(
|
||||||
item.plot ?: ""
|
item.plot ?: ""
|
||||||
|
|
||||||
homePreviewText.text = item.name
|
homePreviewText.text = item.name
|
||||||
populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent)
|
populateChips(
|
||||||
|
homePreviewTags,
|
||||||
|
item.tags ?: emptyList(),
|
||||||
|
R.style.ChipFilledSemiTransparent
|
||||||
|
)
|
||||||
|
|
||||||
homePreviewTags.isGone =
|
homePreviewTags.isGone =
|
||||||
item.tags.isNullOrEmpty()
|
item.tags.isNullOrEmpty()
|
||||||
|
@ -422,8 +426,14 @@ class HomeParentItemAdapterPreview(
|
||||||
resumeRecyclerView.adapter = resumeAdapter
|
resumeRecyclerView.adapter = resumeAdapter
|
||||||
bookmarkRecyclerView.adapter = bookmarkAdapter
|
bookmarkRecyclerView.adapter = bookmarkAdapter
|
||||||
|
|
||||||
resumeRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF)
|
resumeRecyclerView.setLinearListLayout(
|
||||||
bookmarkRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF)
|
nextLeft = R.id.nav_rail_view,
|
||||||
|
nextRight = FOCUS_SELF
|
||||||
|
)
|
||||||
|
bookmarkRecyclerView.setLinearListLayout(
|
||||||
|
nextLeft = R.id.nav_rail_view,
|
||||||
|
nextRight = FOCUS_SELF
|
||||||
|
)
|
||||||
|
|
||||||
fixPaddingStatusbarMargin(topPadding)
|
fixPaddingStatusbarMargin(topPadding)
|
||||||
|
|
||||||
|
@ -547,7 +557,10 @@ class HomeParentItemAdapterPreview(
|
||||||
resumeWatching,
|
resumeWatching,
|
||||||
false
|
false
|
||||||
), 1, false
|
), 1, false
|
||||||
)
|
),
|
||||||
|
deleteCallback = {
|
||||||
|
viewModel.deleteResumeWatching()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -572,7 +585,9 @@ class HomeParentItemAdapterPreview(
|
||||||
list,
|
list,
|
||||||
false
|
false
|
||||||
), 1, false
|
), 1, false
|
||||||
)
|
), deleteCallback = {
|
||||||
|
viewModel.deleteBookmarks(list)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||||
|
@ -92,6 +94,21 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteResumeWatching() {
|
||||||
|
deleteAllResumeStateIds()
|
||||||
|
loadResumeWatching()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteBookmarks(list: List<SearchResponse>) {
|
||||||
|
list.forEach { DataStoreHelper.deleteBookmarkedData(it.id) }
|
||||||
|
loadStoredData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteBookmarks() {
|
||||||
|
deleteAllBookmarkedData()
|
||||||
|
loadStoredData()
|
||||||
|
}
|
||||||
|
|
||||||
var repo: APIRepository? = null
|
var repo: APIRepository? = null
|
||||||
|
|
||||||
private val _apiName = MutableLiveData<String>()
|
private val _apiName = MutableLiveData<String>()
|
||||||
|
@ -394,11 +411,14 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val _popup = MutableLiveData<ExpandableHomepageList?>(null)
|
private val _popup = MutableLiveData<Pair<ExpandableHomepageList, (() -> Unit)?>?>(null)
|
||||||
val popup: LiveData<ExpandableHomepageList?> = _popup
|
val popup: LiveData<Pair<ExpandableHomepageList, (() -> Unit)?>?> = _popup
|
||||||
|
|
||||||
fun popup(list: ExpandableHomepageList?) {
|
fun popup(list: ExpandableHomepageList?, deleteCallback: (() -> Unit)? = null) {
|
||||||
_popup.postValue(list)
|
if (list == null)
|
||||||
|
_popup.postValue(null)
|
||||||
|
else
|
||||||
|
_popup.postValue(list to deleteCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bookmarksUpdated(unused: Boolean) {
|
private fun bookmarksUpdated(unused: Boolean) {
|
||||||
|
@ -436,8 +456,7 @@ class HomeViewModel : ViewModel() {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadStored() {
|
fun loadStoredData() {
|
||||||
loadResumeWatching()
|
|
||||||
val list = EnumSet.noneOf(WatchType::class.java)
|
val list = EnumSet.noneOf(WatchType::class.java)
|
||||||
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
|
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
|
||||||
list.addAll(it)
|
list.addAll(it)
|
||||||
|
@ -445,6 +464,11 @@ class HomeViewModel : ViewModel() {
|
||||||
loadStoredData(list)
|
loadStoredData(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reloadStored() {
|
||||||
|
loadResumeWatching()
|
||||||
|
loadStoredData()
|
||||||
|
}
|
||||||
|
|
||||||
fun click(load: LoadClickCallback) {
|
fun click(load: LoadClickCallback) {
|
||||||
loadResult(load.response.url, load.response.apiName, load.action)
|
loadResult(load.response.url, load.response.apiName, load.action)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,9 +53,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.SSLSession
|
import javax.net.ssl.SSLSession
|
||||||
|
@ -1257,10 +1259,12 @@ class CS3IPlayer : IPlayer {
|
||||||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
val mime = when {
|
val mime = when(link.type) {
|
||||||
link.isM3u8 -> MimeTypes.APPLICATION_M3U8
|
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||||
link.isDash -> MimeTypes.APPLICATION_MPD
|
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
|
||||||
else -> MimeTypes.VIDEO_MP4
|
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
|
||||||
|
ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support")
|
||||||
|
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support")
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaItems = if (link is ExtractorLinkPlayList) {
|
val mediaItems = if (link is ExtractorLinkPlayList) {
|
||||||
|
|
|
@ -50,47 +50,60 @@ class DownloadFileGenerator(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cleanDisplayName(name: String): String {
|
||||||
|
return name.substringBeforeLast('.').trim()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun generateLinks(
|
override suspend fun generateLinks(
|
||||||
clearCache: Boolean,
|
clearCache: Boolean,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||||
subtitleCallback: (SubtitleData) -> Unit,
|
subtitleCallback: (SubtitleData) -> Unit,
|
||||||
offset: Int,
|
offset: Int
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val meta = episodes[currentIndex + offset]
|
val meta = episodes[currentIndex + offset]
|
||||||
callback(Pair(null, meta))
|
callback(null to meta)
|
||||||
|
|
||||||
context?.let { ctx ->
|
val ctx = context ?: return true
|
||||||
val relative = meta.relativePath
|
val relative = meta.relativePath ?: return true
|
||||||
val display = meta.displayName
|
val display = meta.displayName ?: return true
|
||||||
|
|
||||||
|
val cleanDisplay = cleanDisplayName(display)
|
||||||
|
|
||||||
if (display == null || relative == null) {
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
|
VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
|
||||||
?.forEach { file ->
|
?.forEach { (name, uri) ->
|
||||||
val name = display.removeSuffix(".mp4")
|
// only these files are allowed, so no videos as subtitles
|
||||||
if (file.first != meta.displayName && file.first.startsWith(name)) {
|
if (listOf(
|
||||||
val realName = file.first.removePrefix(name)
|
".vtt",
|
||||||
.removeSuffix(".vtt")
|
".srt",
|
||||||
.removeSuffix(".srt")
|
".txt",
|
||||||
.removeSuffix(".txt")
|
".ass",
|
||||||
.trim()
|
".ttml",
|
||||||
.removePrefix("(")
|
".sbv",
|
||||||
.removeSuffix(")")
|
".dfxp"
|
||||||
|
).none { name.contains(it, true) }
|
||||||
|
) return@forEach
|
||||||
|
|
||||||
|
// cant have the exact same file as a subtitle
|
||||||
|
if (name.equals(display, true)) return@forEach
|
||||||
|
|
||||||
|
val cleanName = cleanDisplayName(name)
|
||||||
|
|
||||||
|
// we only want files with the approx same name
|
||||||
|
if (!cleanName.startsWith(cleanDisplay, true)) return@forEach
|
||||||
|
|
||||||
|
val realName = cleanName.removePrefix(cleanDisplay)
|
||||||
|
|
||||||
subtitleCallback(
|
subtitleCallback(
|
||||||
SubtitleData(
|
SubtitleData(
|
||||||
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
|
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
|
||||||
file.second.toString(),
|
uri.toString(),
|
||||||
SubtitleOrigin.DOWNLOADED_FILE,
|
SubtitleOrigin.DOWNLOADED_FILE,
|
||||||
name.toSubtitleMimeType(),
|
name.toSubtitleMimeType(),
|
||||||
emptyMap()
|
emptyMap()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
|
|
||||||
const val DTAG = "PlayerActivity"
|
const val DTAG = "PlayerActivity"
|
||||||
|
|
||||||
|
@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
listOf(
|
listOf(
|
||||||
ExtractorUri(
|
ExtractorUri(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
name = name ?: getString(R.string.downloaded_file)
|
name = name ?: getString(R.string.downloaded_file),
|
||||||
|
// well not the same as a normal id, but we take it as users may want to
|
||||||
|
// play downloaded files and save the location
|
||||||
|
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,15 +37,18 @@ class ExtractorLinkGenerator(
|
||||||
|
|
||||||
override suspend fun generateLinks(
|
override suspend fun generateLinks(
|
||||||
clearCache: Boolean,
|
clearCache: Boolean,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||||
subtitleCallback: (SubtitleData) -> Unit,
|
subtitleCallback: (SubtitleData) -> Unit,
|
||||||
offset: Int
|
offset: Int
|
||||||
): Boolean {
|
): Boolean {
|
||||||
subtitles.forEach(subtitleCallback)
|
subtitles.forEach(subtitleCallback)
|
||||||
|
val allowedTypes = type.toSet()
|
||||||
links.forEach {
|
links.forEach {
|
||||||
|
if(allowedTypes.contains(it.type)) {
|
||||||
callback.invoke(it to null)
|
callback.invoke(it to null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.media3.common.Format.NO_VALUE
|
import androidx.media3.common.Format.NO_VALUE
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.*
|
import com.lagradost.cloudstream3.mvvm.*
|
||||||
|
@ -52,7 +50,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
return durPos.position
|
return durPos.position
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentVerifyLink: Job? = null
|
private var currentVerifyLink: Job? = null
|
||||||
|
|
||||||
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
|
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
|
||||||
currentVerifyLink?.cancel()
|
currentVerifyLink?.cancel()
|
||||||
|
|
|
@ -1,8 +1,43 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
|
|
||||||
|
enum class LoadType {
|
||||||
|
Unknown,
|
||||||
|
InApp,
|
||||||
|
InAppDownload,
|
||||||
|
ExternalApp,
|
||||||
|
Browser,
|
||||||
|
Chromecast
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadType.toSet() : Set<ExtractorLinkType> {
|
||||||
|
return when(this) {
|
||||||
|
LoadType.InApp -> setOf(
|
||||||
|
ExtractorLinkType.VIDEO,
|
||||||
|
ExtractorLinkType.DASH,
|
||||||
|
ExtractorLinkType.M3U8
|
||||||
|
)
|
||||||
|
LoadType.Browser -> setOf(
|
||||||
|
ExtractorLinkType.VIDEO,
|
||||||
|
ExtractorLinkType.DASH,
|
||||||
|
ExtractorLinkType.M3U8
|
||||||
|
)
|
||||||
|
LoadType.InAppDownload -> setOf(
|
||||||
|
ExtractorLinkType.VIDEO,
|
||||||
|
ExtractorLinkType.M3U8
|
||||||
|
)
|
||||||
|
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet()
|
||||||
|
LoadType.Chromecast -> setOf(
|
||||||
|
ExtractorLinkType.VIDEO,
|
||||||
|
ExtractorLinkType.DASH,
|
||||||
|
ExtractorLinkType.M3U8
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface IGenerator {
|
interface IGenerator {
|
||||||
val hasCache: Boolean
|
val hasCache: Boolean
|
||||||
|
|
||||||
|
@ -19,7 +54,7 @@ interface IGenerator {
|
||||||
/* not safe, must use try catch */
|
/* not safe, must use try catch */
|
||||||
suspend fun generateLinks(
|
suspend fun generateLinks(
|
||||||
clearCache: Boolean,
|
clearCache: Boolean,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||||
subtitleCallback: (SubtitleData) -> Unit,
|
subtitleCallback: (SubtitleData) -> Unit,
|
||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
|
|
|
@ -48,7 +48,7 @@ class LinkGenerator(
|
||||||
|
|
||||||
override suspend fun generateLinks(
|
override suspend fun generateLinks(
|
||||||
clearCache: Boolean,
|
clearCache: Boolean,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||||
subtitleCallback: (SubtitleData) -> Unit,
|
subtitleCallback: (SubtitleData) -> Unit,
|
||||||
offset: Int
|
offset: Int
|
||||||
|
|
|
@ -78,10 +78,10 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
generator?.generateLinks(
|
generator?.generateLinks(
|
||||||
|
type = LoadType.InApp,
|
||||||
clearCache = false,
|
clearCache = false,
|
||||||
isCasting = false,
|
callback = {},
|
||||||
{},
|
subtitleCallback = {},
|
||||||
{},
|
|
||||||
offset = 1
|
offset = 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) {
|
fun loadLinks(clearCache: Boolean = false, type: LoadType = LoadType.InApp) {
|
||||||
Log.i(TAG, "loadLinks")
|
Log.i(TAG, "loadLinks")
|
||||||
currentJob?.cancel()
|
currentJob?.cancel()
|
||||||
|
|
||||||
|
@ -162,14 +162,14 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
// load more data
|
// load more data
|
||||||
_loadingLinks.postValue(Resource.Loading())
|
_loadingLinks.postValue(Resource.Loading())
|
||||||
val loadingState = safeApiCall {
|
val loadingState = safeApiCall {
|
||||||
generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, {
|
generator?.generateLinks(type = type,clearCache = clearCache, callback = {
|
||||||
currentLinks.add(it)
|
currentLinks.add(it)
|
||||||
// Clone to prevent ConcurrentModificationException
|
// Clone to prevent ConcurrentModificationException
|
||||||
normalSafeApiCall {
|
normalSafeApiCall {
|
||||||
// Extra normalSafeApiCall since .toSet() iterates.
|
// Extra normalSafeApiCall since .toSet() iterates.
|
||||||
_currentLinks.postValue(currentLinks.toSet())
|
_currentLinks.postValue(currentLinks.toSet())
|
||||||
}
|
}
|
||||||
}, {
|
}, subtitleCallback = {
|
||||||
currentSubs.add(it)
|
currentSubs.add(it)
|
||||||
normalSafeApiCall {
|
normalSafeApiCall {
|
||||||
_currentSubs.postValue(currentSubs.toSet())
|
_currentSubs.postValue(currentSubs.toSet())
|
||||||
|
|
|
@ -67,18 +67,19 @@ class RepoLinkGenerator(
|
||||||
|
|
||||||
override suspend fun generateLinks(
|
override suspend fun generateLinks(
|
||||||
clearCache: Boolean,
|
clearCache: Boolean,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||||
subtitleCallback: (SubtitleData) -> Unit,
|
subtitleCallback: (SubtitleData) -> Unit,
|
||||||
offset: Int,
|
offset: Int
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
val allowedTypes = type.toSet()
|
||||||
val index = currentIndex
|
val index = currentIndex
|
||||||
val current = episodes.getOrNull(index + offset) ?: return false
|
val current = episodes.getOrNull(index + offset) ?: return false
|
||||||
|
|
||||||
val (currentLinkCache, currentSubsCache) = if (clearCache) {
|
val (currentLinkCache, currentSubsCache) = if (clearCache) {
|
||||||
Pair(mutableSetOf(), mutableSetOf())
|
Pair(mutableSetOf(), mutableSetOf())
|
||||||
} else {
|
} else {
|
||||||
cache[Pair(current.apiName, current.id)] ?: Pair(mutableSetOf(), mutableSetOf())
|
cache[current.apiName to current.id] ?: Pair(mutableSetOf(), mutableSetOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
//val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
|
//val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
|
||||||
|
@ -88,9 +89,9 @@ class RepoLinkGenerator(
|
||||||
val currentSubsUrls = mutableSetOf<String>() // makes all subs urls unique
|
val currentSubsUrls = mutableSetOf<String>() // makes all subs urls unique
|
||||||
val currentSubsNames = mutableSetOf<String>() // makes all subs names unique
|
val currentSubsNames = mutableSetOf<String>() // makes all subs names unique
|
||||||
|
|
||||||
currentLinkCache.forEach { link ->
|
currentLinkCache.filter { allowedTypes.contains(it.type) }.forEach { link ->
|
||||||
currentLinks.add(link.url)
|
currentLinks.add(link.url)
|
||||||
callback(Pair(link, null))
|
callback(link to null)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSubsCache.forEach { sub ->
|
currentSubsCache.forEach { sub ->
|
||||||
|
@ -108,8 +109,8 @@ class RepoLinkGenerator(
|
||||||
val result = APIRepository(
|
val result = APIRepository(
|
||||||
getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist")
|
getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist")
|
||||||
).loadLinks(current.data,
|
).loadLinks(current.data,
|
||||||
isCasting,
|
isCasting = LoadType.Chromecast == type,
|
||||||
{ file ->
|
subtitleCallback = { file ->
|
||||||
val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
|
val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
|
||||||
if (!currentSubsUrls.contains(correctFile.url)) {
|
if (!currentSubsUrls.contains(correctFile.url)) {
|
||||||
currentSubsUrls.add(correctFile.url)
|
currentSubsUrls.add(correctFile.url)
|
||||||
|
@ -132,12 +133,14 @@ class RepoLinkGenerator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ link ->
|
callback = { link ->
|
||||||
Log.d(TAG, "Loaded ExtractorLink: $link")
|
Log.d(TAG, "Loaded ExtractorLink: $link")
|
||||||
if (!currentLinks.contains(link.url)) {
|
if (!currentLinks.contains(link.url)) {
|
||||||
if (!currentLinkCache.contains(link)) {
|
if (!currentLinkCache.contains(link)) {
|
||||||
currentLinks.add(link.url)
|
currentLinks.add(link.url)
|
||||||
|
if (allowedTypes.contains(link.type)) {
|
||||||
callback(Pair(link, null))
|
callback(Pair(link, null))
|
||||||
|
}
|
||||||
currentLinkCache.add(link)
|
currentLinkCache.add(link)
|
||||||
//linkCache[index] = currentLinkCache
|
//linkCache[index] = currentLinkCache
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||||
|
import com.lagradost.cloudstream3.ui.player.LoadType
|
||||||
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
|
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
|
||||||
|
@ -591,7 +592,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
link,
|
link,
|
||||||
"$fileName ${link.name}",
|
"$fileName ${link.name}",
|
||||||
folder,
|
folder,
|
||||||
if (link.url.contains(".srt")) ".srt" else "vtt",
|
if (link.url.contains(".srt")) "srt" else "vtt",
|
||||||
false,
|
false,
|
||||||
null, createNotificationCallback = {}
|
null, createNotificationCallback = {}
|
||||||
)
|
)
|
||||||
|
@ -719,7 +720,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.map { ExtractorSubtitleLink(it.name, it.url, "") }
|
.map { ExtractorSubtitleLink(it.name, it.url, "") }.take(3)
|
||||||
.forEach { link ->
|
.forEach { link ->
|
||||||
val fileName = VideoDownloadManager.getFileName(context, meta)
|
val fileName = VideoDownloadManager.getFileName(context, meta)
|
||||||
downloadSubtitle(context, link, fileName, folder)
|
downloadSubtitle(context, link, fileName, folder)
|
||||||
|
@ -745,7 +746,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
val generator = RepoLinkGenerator(listOf(episode))
|
val generator = RepoLinkGenerator(listOf(episode))
|
||||||
val currentLinks = mutableSetOf<ExtractorLink>()
|
val currentLinks = mutableSetOf<ExtractorLink>()
|
||||||
val currentSubs = mutableSetOf<SubtitleData>()
|
val currentSubs = mutableSetOf<SubtitleData>()
|
||||||
generator.generateLinks(clearCache = false, isCasting = false, callback = {
|
generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = {
|
||||||
it.first?.let { link ->
|
it.first?.let { link ->
|
||||||
currentLinks.add(link)
|
currentLinks.add(link)
|
||||||
}
|
}
|
||||||
|
@ -825,7 +826,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
isVisible: Boolean = true
|
isVisible: Boolean = true
|
||||||
) {
|
) {
|
||||||
if (activity == null) return
|
if (activity == null) return
|
||||||
loadLinks(result, isVisible = isVisible, isCasting = true) { data ->
|
loadLinks(result, isVisible = isVisible, LoadType.Chromecast) { data ->
|
||||||
startChromecast(activity, result, data.links, data.subs, 0)
|
startChromecast(activity, result, data.links, data.subs, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -936,7 +937,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
private fun loadLinks(
|
private fun loadLinks(
|
||||||
result: ResultEpisode,
|
result: ResultEpisode,
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
clearCache: Boolean = false,
|
clearCache: Boolean = false,
|
||||||
work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit)
|
work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit)
|
||||||
) {
|
) {
|
||||||
|
@ -945,7 +946,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
val links = loadLinks(
|
val links = loadLinks(
|
||||||
result,
|
result,
|
||||||
isVisible = isVisible,
|
isVisible = isVisible,
|
||||||
isCasting = isCasting,
|
type = type,
|
||||||
clearCache = clearCache
|
clearCache = clearCache
|
||||||
)
|
)
|
||||||
if (!this.isActive) return@ioSafe
|
if (!this.isActive) return@ioSafe
|
||||||
|
@ -956,11 +957,11 @@ class ResultViewModel2 : ViewModel() {
|
||||||
private var currentLoadLinkJob: Job? = null
|
private var currentLoadLinkJob: Job? = null
|
||||||
private fun acquireSingleLink(
|
private fun acquireSingleLink(
|
||||||
result: ResultEpisode,
|
result: ResultEpisode,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
text: UiText,
|
text: UiText,
|
||||||
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
|
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
|
||||||
) {
|
) {
|
||||||
loadLinks(result, isVisible = true, isCasting = isCasting) { links ->
|
loadLinks(result, isVisible = true, type) { links ->
|
||||||
postPopup(
|
postPopup(
|
||||||
text,
|
text,
|
||||||
links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) {
|
links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) {
|
||||||
|
@ -971,11 +972,10 @@ class ResultViewModel2 : ViewModel() {
|
||||||
|
|
||||||
private fun acquireSingleSubtitle(
|
private fun acquireSingleSubtitle(
|
||||||
result: ResultEpisode,
|
result: ResultEpisode,
|
||||||
isCasting: Boolean,
|
|
||||||
text: UiText,
|
text: UiText,
|
||||||
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
|
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
|
||||||
) {
|
) {
|
||||||
loadLinks(result, isVisible = true, isCasting = isCasting) { links ->
|
loadLinks(result, isVisible = true, type = LoadType.Unknown) { links ->
|
||||||
postPopup(
|
postPopup(
|
||||||
text,
|
text,
|
||||||
links.subs.map { txt(it.name) })
|
links.subs.map { txt(it.name) })
|
||||||
|
@ -988,7 +988,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
private suspend fun CoroutineScope.loadLinks(
|
private suspend fun CoroutineScope.loadLinks(
|
||||||
result: ResultEpisode,
|
result: ResultEpisode,
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
isCasting: Boolean,
|
type: LoadType,
|
||||||
clearCache: Boolean = false,
|
clearCache: Boolean = false,
|
||||||
): LinkLoadingResult {
|
): LinkLoadingResult {
|
||||||
val tempGenerator = RepoLinkGenerator(listOf(result))
|
val tempGenerator = RepoLinkGenerator(listOf(result))
|
||||||
|
@ -1002,7 +1002,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
updatePage()
|
updatePage()
|
||||||
tempGenerator.generateLinks(clearCache, isCasting, { (link, _) ->
|
tempGenerator.generateLinks(clearCache, type, { (link, _) ->
|
||||||
if (link != null) {
|
if (link != null) {
|
||||||
links += link
|
links += link
|
||||||
updatePage()
|
updatePage()
|
||||||
|
@ -1272,7 +1272,6 @@ class ResultViewModel2 : ViewModel() {
|
||||||
|
|
||||||
acquireSingleSubtitle(
|
acquireSingleSubtitle(
|
||||||
click.data,
|
click.data,
|
||||||
false,
|
|
||||||
txt(R.string.episode_action_download_subtitle)
|
txt(R.string.episode_action_download_subtitle)
|
||||||
) { (links, index) ->
|
) { (links, index) ->
|
||||||
downloadSubtitle(
|
downloadSubtitle(
|
||||||
|
@ -1317,7 +1316,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
val response = currentResponse ?: return
|
val response = currentResponse ?: return
|
||||||
acquireSingleLink(
|
acquireSingleLink(
|
||||||
click.data,
|
click.data,
|
||||||
false,
|
LoadType.InAppDownload,
|
||||||
txt(R.string.episode_action_download_mirror)
|
txt(R.string.episode_action_download_mirror)
|
||||||
) { (result, index) ->
|
) { (result, index) ->
|
||||||
ioSafe {
|
ioSafe {
|
||||||
|
@ -1347,7 +1346,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
loadLinks(
|
loadLinks(
|
||||||
click.data,
|
click.data,
|
||||||
isVisible = false,
|
isVisible = false,
|
||||||
isCasting = false,
|
type = LoadType.InApp,
|
||||||
clearCache = true
|
clearCache = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1356,7 +1355,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
ACTION_CHROME_CAST_MIRROR -> {
|
ACTION_CHROME_CAST_MIRROR -> {
|
||||||
acquireSingleLink(
|
acquireSingleLink(
|
||||||
click.data,
|
click.data,
|
||||||
isCasting = true,
|
LoadType.Chromecast,
|
||||||
txt(R.string.episode_action_chromecast_mirror)
|
txt(R.string.episode_action_chromecast_mirror)
|
||||||
) { (result, index) ->
|
) { (result, index) ->
|
||||||
startChromecast(activity, click.data, result.links, result.subs, index)
|
startChromecast(activity, click.data, result.links, result.subs, index)
|
||||||
|
@ -1365,7 +1364,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
|
|
||||||
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
||||||
click.data,
|
click.data,
|
||||||
isCasting = true,
|
LoadType.Browser,
|
||||||
txt(R.string.episode_action_play_in_browser)
|
txt(R.string.episode_action_play_in_browser)
|
||||||
) { (result, index) ->
|
) { (result, index) ->
|
||||||
try {
|
try {
|
||||||
|
@ -1380,7 +1379,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
ACTION_COPY_LINK -> {
|
ACTION_COPY_LINK -> {
|
||||||
acquireSingleLink(
|
acquireSingleLink(
|
||||||
click.data,
|
click.data,
|
||||||
isCasting = true,
|
LoadType.ExternalApp,
|
||||||
txt(R.string.episode_action_copy_link)
|
txt(R.string.episode_action_copy_link)
|
||||||
) { (result, index) ->
|
) { (result, index) ->
|
||||||
val act = activity ?: return@acquireSingleLink
|
val act = activity ?: return@acquireSingleLink
|
||||||
|
@ -1399,7 +1398,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
|
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
|
||||||
loadLinks(click.data, isVisible = true, isCasting = true) { links ->
|
loadLinks(click.data, isVisible = true, LoadType.ExternalApp) { links ->
|
||||||
if (links.links.isEmpty()) {
|
if (links.links.isEmpty()) {
|
||||||
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||||
return@loadLinks
|
return@loadLinks
|
||||||
|
@ -1415,7 +1414,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
|
|
||||||
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
|
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
|
||||||
click.data,
|
click.data,
|
||||||
isCasting = true,
|
LoadType.Chromecast,
|
||||||
txt(
|
txt(
|
||||||
R.string.episode_action_play_in_format,
|
R.string.episode_action_play_in_format,
|
||||||
txt(R.string.player_settings_play_in_web)
|
txt(R.string.player_settings_play_in_web)
|
||||||
|
@ -1432,7 +1431,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
|
|
||||||
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
|
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
|
||||||
click.data,
|
click.data,
|
||||||
isCasting = true,
|
LoadType.Chromecast,
|
||||||
txt(
|
txt(
|
||||||
R.string.episode_action_play_in_format,
|
R.string.episode_action_play_in_format,
|
||||||
txt(R.string.player_settings_play_in_mpv)
|
txt(R.string.player_settings_play_in_mpv)
|
||||||
|
@ -1461,7 +1460,6 @@ class ResultViewModel2 : ViewModel() {
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
it.goto(index)
|
it.goto(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
} ?: return, list
|
} ?: return, list
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -2173,7 +2171,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
trailerData.extractorUrl,
|
trailerData.extractorUrl,
|
||||||
trailerData.referer ?: "",
|
trailerData.referer ?: "",
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
trailerData.extractorUrl.contains(".m3u8")
|
type = INFER_TYPE
|
||||||
)
|
)
|
||||||
) to arrayListOf()
|
) to arrayListOf()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -38,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
|
|
||||||
fun getCurrentLocale(context: Context): String {
|
fun getCurrentLocale(context: Context): String {
|
||||||
val res = context.resources
|
val res = context.resources
|
||||||
|
@ -54,6 +54,8 @@ fun getCurrentLocale(context: Context): String {
|
||||||
// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes leave blank for auto
|
// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes leave blank for auto
|
||||||
val appLanguages = arrayListOf(
|
val appLanguages = arrayListOf(
|
||||||
/* begin language list */
|
/* begin language list */
|
||||||
|
Triple("", "ajp", "ajp"),
|
||||||
|
Triple("", "አማርኛ", "am"),
|
||||||
Triple("", "العربية", "ar"),
|
Triple("", "العربية", "ar"),
|
||||||
Triple("", "ars", "ars"),
|
Triple("", "ars", "ars"),
|
||||||
Triple("", "български", "bg"),
|
Triple("", "български", "bg"),
|
||||||
|
@ -96,6 +98,7 @@ val appLanguages = arrayListOf(
|
||||||
Triple("", "Soomaaliga", "so"),
|
Triple("", "Soomaaliga", "so"),
|
||||||
Triple("", "svenska", "sv"),
|
Triple("", "svenska", "sv"),
|
||||||
Triple("", "தமிழ்", "ta"),
|
Triple("", "தமிழ்", "ta"),
|
||||||
|
Triple("", "ትግርኛ", "ti"),
|
||||||
Triple("", "Tagalog", "tl"),
|
Triple("", "Tagalog", "tl"),
|
||||||
Triple("", "Türkçe", "tr"),
|
Triple("", "Türkçe", "tr"),
|
||||||
Triple("", "українська", "uk"),
|
Triple("", "українська", "uk"),
|
||||||
|
@ -335,7 +338,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
val currentDir =
|
val currentDir =
|
||||||
settingsManager.getString(getString(R.string.download_path_pref), null)
|
settingsManager.getString(getString(R.string.download_path_pref), null)
|
||||||
?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() }
|
?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx)?.filePath() }
|
||||||
|
|
||||||
activity?.showBottomDialog(
|
activity?.showBottomDialog(
|
||||||
dirs + listOf("Custom"),
|
dirs + listOf("Custom"),
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
|
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY
|
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY
|
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY
|
||||||
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
|
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
|
||||||
|
@ -143,65 +144,26 @@ object BackupUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
fun FragmentActivity.backup() {
|
fun FragmentActivity.backup() = ioSafe {
|
||||||
var fileStream: OutputStream? = null
|
var fileStream: OutputStream? = null
|
||||||
var printStream: PrintWriter? = null
|
var printStream: PrintWriter? = null
|
||||||
try {
|
try {
|
||||||
if (!checkWrite()) {
|
if (!checkWrite()) {
|
||||||
showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG)
|
showToast(R.string.backup_failed, Toast.LENGTH_LONG)
|
||||||
requestRW()
|
requestRW()
|
||||||
return
|
return@ioSafe
|
||||||
}
|
}
|
||||||
|
|
||||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||||
val ext = "json"
|
val ext = "txt"
|
||||||
val displayName = "CS3_Backup_${date}"
|
val displayName = "CS3_Backup_${date}"
|
||||||
val backupFile = getBackup()
|
val backupFile = getBackup()
|
||||||
val stream = setupStream(this, displayName, null, ext, false)
|
val stream = setupStream(this@backup, displayName, null, ext, false)
|
||||||
|
|
||||||
fileStream = stream.openNew()
|
fileStream = stream.openNew()
|
||||||
printStream = PrintWriter(fileStream)
|
printStream = PrintWriter(fileStream)
|
||||||
printStream.print(mapper.writeValueAsString(backupFile))
|
printStream.print(mapper.writeValueAsString(backupFile))
|
||||||
|
|
||||||
/*val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
|
||||||
&& subDir?.isDownloadDir() == true
|
|
||||||
) {
|
|
||||||
val cr = this.contentResolver
|
|
||||||
val contentUri =
|
|
||||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
|
||||||
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
|
||||||
|
|
||||||
val newFile = ContentValues().apply {
|
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
|
||||||
put(MediaStore.MediaColumns.TITLE, displayName)
|
|
||||||
// While it a json file we store as txt because not
|
|
||||||
// all file managers support mimetype json
|
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
|
||||||
//put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newFileUri = cr.insert(
|
|
||||||
contentUri,
|
|
||||||
newFile
|
|
||||||
) ?: throw IOException("Error creating file uri")
|
|
||||||
cr.openOutputStream(newFileUri, "w")
|
|
||||||
?: throw IOException("Error opening stream")
|
|
||||||
} else {
|
|
||||||
val fileName = "$displayName.$ext"
|
|
||||||
val rFile = subDir?.findFile(fileName)
|
|
||||||
if (rFile?.exists() == true) {
|
|
||||||
rFile.delete()
|
|
||||||
}
|
|
||||||
val file =
|
|
||||||
subDir?.createFile(fileName)
|
|
||||||
?: throw IOException("Error creating file")
|
|
||||||
if (!file.exists()) throw IOException("File does not exist")
|
|
||||||
file.openOutputStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
val printStream = PrintWriter(steam)
|
|
||||||
printStream.print(mapper.writeValueAsString(backupFile))
|
|
||||||
printStream.close()*/
|
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
R.string.backup_success,
|
R.string.backup_success,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
|
@ -210,7 +172,7 @@ object BackupUtils {
|
||||||
logError(e)
|
logError(e)
|
||||||
try {
|
try {
|
||||||
showToast(
|
showToast(
|
||||||
getString(R.string.backup_failed_error_format).format(e.toString()),
|
txt(R.string.backup_failed_error_format, e.toString()),
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -55,7 +55,11 @@ object CastHelper {
|
||||||
|
|
||||||
val builder = MediaInfo.Builder(link.url)
|
val builder = MediaInfo.Builder(link.url)
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||||
.setContentType(if (link.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.VIDEO_MP4)
|
.setContentType(when(link.type) {
|
||||||
|
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||||
|
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
|
||||||
|
else -> MimeTypes.VIDEO_MP4
|
||||||
|
})
|
||||||
.setMetadata(movieMetadata)
|
.setMetadata(movieMetadata)
|
||||||
.setMediaTracks(tracks)
|
.setMediaTracks(tracks)
|
||||||
data?.let {
|
data?.let {
|
||||||
|
|
|
@ -353,6 +353,12 @@ object DataStoreHelper {
|
||||||
removeKeys(folder2)
|
removeKeys(folder2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteBookmarkedData(id : Int?) {
|
||||||
|
if (id == null) return
|
||||||
|
removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString())
|
||||||
|
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
||||||
|
}
|
||||||
|
|
||||||
fun getAllResumeStateIds(): List<Int>? {
|
fun getAllResumeStateIds(): List<Int>? {
|
||||||
val folder = "$currentAccount/$RESULT_RESUME_WATCHING"
|
val folder = "$currentAccount/$RESULT_RESUME_WATCHING"
|
||||||
return getKeys(folder)?.mapNotNull {
|
return getKeys(folder)?.mapNotNull {
|
||||||
|
@ -519,12 +525,10 @@ object DataStoreHelper {
|
||||||
|
|
||||||
fun setResultWatchState(id: Int?, status: Int) {
|
fun setResultWatchState(id: Int?, status: Int) {
|
||||||
if (id == null) return
|
if (id == null) return
|
||||||
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
|
||||||
if (status == WatchType.NONE.internalId) {
|
if (status == WatchType.NONE.internalId) {
|
||||||
removeKey(folder, id.toString())
|
deleteBookmarkedData(id)
|
||||||
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
|
||||||
} else {
|
} else {
|
||||||
setKey(folder, id.toString(), status)
|
setKey("$currentAccount/$RESULT_WATCH_STATE", id.toString(), status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,10 @@ import android.net.Uri
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.extractors.*
|
import com.lagradost.cloudstream3.extractors.*
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
import java.net.URL
|
||||||
import kotlin.collections.MutableList
|
import kotlin.collections.MutableList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,35 +37,101 @@ data class ExtractorLinkPlayList(
|
||||||
val playlist: List<PlayListItem>,
|
val playlist: List<PlayListItem>,
|
||||||
override val referer: String,
|
override val referer: String,
|
||||||
override val quality: Int,
|
override val quality: Int,
|
||||||
override val isM3u8: Boolean = false,
|
val isM3u8: Boolean = false,
|
||||||
override val headers: Map<String, String> = mapOf(),
|
override val headers: Map<String, String> = mapOf(),
|
||||||
/** Used for getExtractorVerifierJob() */
|
/** Used for getExtractorVerifierJob() */
|
||||||
override val extractorData: String? = null,
|
override val extractorData: String? = null,
|
||||||
|
override val type: ExtractorLinkType,
|
||||||
) : ExtractorLink(
|
) : ExtractorLink(
|
||||||
source,
|
source = source,
|
||||||
name,
|
name = name,
|
||||||
// Blank as un-used
|
url = "",
|
||||||
"",
|
referer = referer,
|
||||||
referer,
|
quality = quality,
|
||||||
quality,
|
headers = headers,
|
||||||
isM3u8,
|
extractorData = extractorData,
|
||||||
headers,
|
type = type
|
||||||
extractorData
|
) {
|
||||||
|
constructor(
|
||||||
|
source: String,
|
||||||
|
name: String,
|
||||||
|
playlist: List<PlayListItem>,
|
||||||
|
referer: String,
|
||||||
|
quality: Int,
|
||||||
|
isM3u8: Boolean = false,
|
||||||
|
headers: Map<String, String> = mapOf(),
|
||||||
|
extractorData: String? = null,
|
||||||
|
) : this(
|
||||||
|
source = source,
|
||||||
|
name = name,
|
||||||
|
playlist = playlist,
|
||||||
|
referer = referer,
|
||||||
|
quality = quality,
|
||||||
|
type = if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO,
|
||||||
|
headers = headers,
|
||||||
|
extractorData = extractorData,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Metadata about the file type used for downloads and exoplayer hint,
|
||||||
|
* if you respond with the wrong one the file will fail to download or be played */
|
||||||
|
enum class ExtractorLinkType {
|
||||||
|
/** Single stream of bytes no matter the actual file type */
|
||||||
|
VIDEO,
|
||||||
|
/** Split into several .ts files, has support for encrypted m3u8s */
|
||||||
|
M3U8,
|
||||||
|
/** Like m3u8 but uses xml, currently no download support */
|
||||||
|
DASH,
|
||||||
|
/** No support at the moment */
|
||||||
|
TORRENT,
|
||||||
|
/** No support at the moment */
|
||||||
|
MAGNET,
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
|
||||||
|
val path = normalSafeApiCall { URL(url).path }
|
||||||
|
return when {
|
||||||
|
path?.endsWith(".m3u8") == true -> ExtractorLinkType.M3U8
|
||||||
|
path?.endsWith(".mpd") == true -> ExtractorLinkType.DASH
|
||||||
|
path?.endsWith(".torrent") == true -> ExtractorLinkType.TORRENT
|
||||||
|
url.startsWith("magnet:") -> ExtractorLinkType.MAGNET
|
||||||
|
else -> ExtractorLinkType.VIDEO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val INFER_TYPE : ExtractorLinkType? = null
|
||||||
open class ExtractorLink constructor(
|
open class ExtractorLink constructor(
|
||||||
open val source: String,
|
open val source: String,
|
||||||
open val name: String,
|
open val name: String,
|
||||||
override val url: String,
|
override val url: String,
|
||||||
override val referer: String,
|
override val referer: String,
|
||||||
open val quality: Int,
|
open val quality: Int,
|
||||||
open val isM3u8: Boolean = false,
|
|
||||||
override val headers: Map<String, String> = mapOf(),
|
override val headers: Map<String, String> = mapOf(),
|
||||||
/** Used for getExtractorVerifierJob() */
|
/** Used for getExtractorVerifierJob() */
|
||||||
open val extractorData: String? = null,
|
open val extractorData: String? = null,
|
||||||
open val isDash: Boolean = false,
|
open val type: ExtractorLinkType,
|
||||||
) : VideoDownloadManager.IDownloadableMinimum {
|
) : VideoDownloadManager.IDownloadableMinimum {
|
||||||
|
constructor(
|
||||||
|
source: String,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
referer: String,
|
||||||
|
quality: Int,
|
||||||
|
/** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */
|
||||||
|
type: ExtractorLinkType?,
|
||||||
|
headers: Map<String, String> = mapOf(),
|
||||||
|
/** Used for getExtractorVerifierJob() */
|
||||||
|
extractorData: String? = null,
|
||||||
|
) : this(
|
||||||
|
source = source,
|
||||||
|
name = name,
|
||||||
|
url = url,
|
||||||
|
referer = referer,
|
||||||
|
quality = quality,
|
||||||
|
headers = headers,
|
||||||
|
extractorData = extractorData,
|
||||||
|
type = type ?: inferTypeFromUrl(url)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Old constructor without isDash, allows for backwards compatibility with extensions.
|
* Old constructor without isDash, allows for backwards compatibility with extensions.
|
||||||
* Should be removed after all extensions have updated their cloudstream.jar
|
* Should be removed after all extensions have updated their cloudstream.jar
|
||||||
|
@ -80,8 +148,30 @@ open class ExtractorLink constructor(
|
||||||
extractorData: String? = null
|
extractorData: String? = null
|
||||||
) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false)
|
) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
source: String,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
referer: String,
|
||||||
|
quality: Int,
|
||||||
|
isM3u8: Boolean = false,
|
||||||
|
headers: Map<String, String> = mapOf(),
|
||||||
|
/** Used for getExtractorVerifierJob() */
|
||||||
|
extractorData: String? = null,
|
||||||
|
isDash: Boolean,
|
||||||
|
) : this(
|
||||||
|
source = source,
|
||||||
|
name = name,
|
||||||
|
url = url,
|
||||||
|
referer = referer,
|
||||||
|
quality = quality,
|
||||||
|
headers = headers,
|
||||||
|
extractorData = extractorData,
|
||||||
|
type = if (isDash) ExtractorLinkType.DASH else if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO
|
||||||
|
)
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "ExtractorLink(name=$name, url=$url, referer=$referer, isM3u8=$isM3u8)"
|
return "ExtractorLink(name=$name, url=$url, referer=$referer, type=$type)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,6 +225,7 @@ enum class Qualities(var value: Int, val defaultPriority: Int) {
|
||||||
else -> "${qual}p"
|
else -> "${qual}p"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStringByIntFull(quality: Int): String {
|
fun getStringByIntFull(quality: Int): String {
|
||||||
return when (quality) {
|
return when (quality) {
|
||||||
0 -> "Auto"
|
0 -> "Auto"
|
||||||
|
@ -389,6 +480,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
||||||
Acefile(),
|
Acefile(),
|
||||||
SpeedoStream(),
|
SpeedoStream(),
|
||||||
SpeedoStream1(),
|
SpeedoStream1(),
|
||||||
|
SpeedoStream2(),
|
||||||
Zorofile(),
|
Zorofile(),
|
||||||
Embedgram(),
|
Embedgram(),
|
||||||
Mvidoo(),
|
Mvidoo(),
|
||||||
|
|
|
@ -35,8 +35,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
import com.lagradost.cloudstream3.utils.storage.MediaFileContentType
|
import com.lagradost.safefile.MediaFileContentType
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -53,7 +53,7 @@ import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.net.URL
|
import java.lang.IllegalArgumentException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
|
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
|
||||||
|
@ -62,6 +62,7 @@ const val DOWNLOAD_CHANNEL_DESCRIPT = "The download notification channel"
|
||||||
|
|
||||||
object VideoDownloadManager {
|
object VideoDownloadManager {
|
||||||
var maxConcurrentDownloads = 3
|
var maxConcurrentDownloads = 3
|
||||||
|
var maxConcurrentConnections = 3
|
||||||
private var currentDownloads = mutableListOf<Int>()
|
private var currentDownloads = mutableListOf<Int>()
|
||||||
|
|
||||||
private const val USER_AGENT =
|
private const val USER_AGENT =
|
||||||
|
@ -504,7 +505,7 @@ object VideoDownloadManager {
|
||||||
): List<Pair<String, Uri>>? {
|
): List<Pair<String, Uri>>? {
|
||||||
val base = basePathToFile(context, basePath)
|
val base = basePathToFile(context, basePath)
|
||||||
val folder = base?.gotoDirectory(relativePath, false) ?: return null
|
val folder = base?.gotoDirectory(relativePath, false) ?: return null
|
||||||
if (folder.isDirectory() != false) return null
|
//if (folder.isDirectory() != false) return null
|
||||||
|
|
||||||
return folder.listFiles()
|
return folder.listFiles()
|
||||||
?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) }
|
?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) }
|
||||||
|
@ -553,9 +554,8 @@ object VideoDownloadManager {
|
||||||
extension: String,
|
extension: String,
|
||||||
tryResume: Boolean,
|
tryResume: Boolean,
|
||||||
): StreamData {
|
): StreamData {
|
||||||
val (base, _) = context.getBasePath()
|
|
||||||
return setupStream(
|
return setupStream(
|
||||||
base ?: throw IOException("Bad config"),
|
context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"),
|
||||||
name,
|
name,
|
||||||
folder,
|
folder,
|
||||||
extension,
|
extension,
|
||||||
|
@ -951,7 +951,10 @@ object VideoDownloadManager {
|
||||||
/** how many bytes every connection should be, by default it is 10 MiB */
|
/** how many bytes every connection should be, by default it is 10 MiB */
|
||||||
chuckSize: Long = (1 shl 20) * 10,
|
chuckSize: Long = (1 shl 20) * 10,
|
||||||
/** maximum bytes in the buffer that responds */
|
/** maximum bytes in the buffer that responds */
|
||||||
bufferSize: Int = DEFAULT_BUFFER_SIZE
|
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||||
|
/** how many bytes bytes it should require to use the parallel downloader instead,
|
||||||
|
* if we download a very small file we don't want it parallel */
|
||||||
|
maximumSmallSize : Long = chuckSize * 2
|
||||||
): LazyStreamDownloadData {
|
): LazyStreamDownloadData {
|
||||||
// we don't want to make a separate connection for every 1kb
|
// we don't want to make a separate connection for every 1kb
|
||||||
require(chuckSize > 1000)
|
require(chuckSize > 1000)
|
||||||
|
@ -963,7 +966,7 @@ object VideoDownloadManager {
|
||||||
var downloadLength: Long? = null
|
var downloadLength: Long? = null
|
||||||
var totalLength: Long? = null
|
var totalLength: Long? = null
|
||||||
|
|
||||||
val ranges = if (contentLength == null) {
|
val ranges = if (contentLength == null || contentLength < maximumSmallSize) {
|
||||||
// is the equivalent of [startByte..EOF] as we don't know the size we can only do one
|
// is the equivalent of [startByte..EOF] as we don't know the size we can only do one
|
||||||
// connection
|
// connection
|
||||||
LongArray(1) { startByte }
|
LongArray(1) { startByte }
|
||||||
|
@ -1024,6 +1027,7 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** download a file that consist of a single stream of data*/
|
||||||
suspend fun downloadThing(
|
suspend fun downloadThing(
|
||||||
context: Context,
|
context: Context,
|
||||||
link: IDownloadableMinimum,
|
link: IDownloadableMinimum,
|
||||||
|
@ -1035,8 +1039,7 @@ object VideoDownloadManager {
|
||||||
createNotificationCallback: (CreateNotificationMetadata) -> Unit,
|
createNotificationCallback: (CreateNotificationMetadata) -> Unit,
|
||||||
parallelConnections: Int = 3
|
parallelConnections: Int = 3
|
||||||
): DownloadStatus = withContext(Dispatchers.IO) {
|
): DownloadStatus = withContext(Dispatchers.IO) {
|
||||||
// we cant download torrents with this implementation, aria2c might be used in the future
|
if (parallelConnections < 1) {
|
||||||
if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) {
|
|
||||||
return@withContext DOWNLOAD_INVALID_INPUT
|
return@withContext DOWNLOAD_INVALID_INPUT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1400,7 +1403,12 @@ object VideoDownloadManager {
|
||||||
metadata.type = DownloadType.IsFailed
|
metadata.type = DownloadType.IsFailed
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
try {
|
||||||
|
// may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling
|
||||||
fileMutex.unlock()
|
fileMutex.unlock()
|
||||||
|
} catch (t : Throwable) {
|
||||||
|
logError(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1524,6 +1532,11 @@ object VideoDownloadManager {
|
||||||
notificationCallback: (Int, Notification) -> Unit,
|
notificationCallback: (Int, Notification) -> Unit,
|
||||||
tryResume: Boolean = false,
|
tryResume: Boolean = false,
|
||||||
): DownloadStatus {
|
): DownloadStatus {
|
||||||
|
// no support for these file formats
|
||||||
|
if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) {
|
||||||
|
return DOWNLOAD_INVALID_INPUT
|
||||||
|
}
|
||||||
|
|
||||||
val name = getFileName(context, ep)
|
val name = getFileName(context, ep)
|
||||||
|
|
||||||
// Make sure this is cancelled when download is done or cancelled.
|
// Make sure this is cancelled when download is done or cancelled.
|
||||||
|
@ -1552,7 +1565,8 @@ object VideoDownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) {
|
when(link.type) {
|
||||||
|
ExtractorLinkType.M3U8 -> {
|
||||||
val startIndex = if (tryResume) {
|
val startIndex = if (tryResume) {
|
||||||
context.getKey<DownloadedFileInfo>(
|
context.getKey<DownloadedFileInfo>(
|
||||||
KEY_DOWNLOAD_INFO,
|
KEY_DOWNLOAD_INFO,
|
||||||
|
@ -1568,9 +1582,10 @@ object VideoDownloadManager {
|
||||||
folder ?: "",
|
folder ?: "",
|
||||||
ep.id,
|
ep.id,
|
||||||
startIndex,
|
startIndex,
|
||||||
callback
|
callback, parallelConnections = maxConcurrentConnections
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
|
ExtractorLinkType.VIDEO -> {
|
||||||
return downloadThing(
|
return downloadThing(
|
||||||
context,
|
context,
|
||||||
link,
|
link,
|
||||||
|
@ -1579,9 +1594,11 @@ object VideoDownloadManager {
|
||||||
"mp4",
|
"mp4",
|
||||||
tryResume,
|
tryResume,
|
||||||
ep.id,
|
ep.id,
|
||||||
callback
|
callback, parallelConnections = maxConcurrentConnections
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else -> throw IllegalArgumentException("unsuported download type")
|
||||||
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
return DOWNLOAD_FAILED
|
return DOWNLOAD_FAILED
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -1682,7 +1699,7 @@ object VideoDownloadManager {
|
||||||
|
|
||||||
// only delete the key if the file is not found
|
// only delete the key if the file is not found
|
||||||
if (file == null || !file.existsOrThrow()) {
|
if (file == null || !file.existsOrThrow()) {
|
||||||
if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString())
|
//if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) // TODO READD
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,369 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.utils.storage
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.hippo.unifile.UniRandomAccessFile
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
|
|
||||||
enum class MediaFileContentType {
|
|
||||||
Downloads,
|
|
||||||
Audio,
|
|
||||||
Video,
|
|
||||||
Images,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.android.com/training/data-storage/shared/media
|
|
||||||
fun MediaFileContentType.toPath(): String {
|
|
||||||
return when (this) {
|
|
||||||
MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS
|
|
||||||
MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC
|
|
||||||
MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES
|
|
||||||
MediaFileContentType.Images -> Environment.DIRECTORY_DCIM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MediaFileContentType.defaultPrefix(): String {
|
|
||||||
return Environment.getExternalStorageDirectory().absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MediaFileContentType.toAbsolutePath(): String {
|
|
||||||
return defaultPrefix() + File.separator +
|
|
||||||
this.toPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun replaceDuplicateFileSeparators(path: String): String {
|
|
||||||
return path.replace(Regex("${File.separator}+"), File.separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
fun MediaFileContentType.toUri(external: Boolean): Uri {
|
|
||||||
val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL
|
|
||||||
return when (this) {
|
|
||||||
MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume)
|
|
||||||
MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume)
|
|
||||||
MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume)
|
|
||||||
MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
class MediaFile(
|
|
||||||
private val context: Context,
|
|
||||||
private val folderType: MediaFileContentType,
|
|
||||||
private val external: Boolean = true,
|
|
||||||
absolutePath: String,
|
|
||||||
) : SafeFile {
|
|
||||||
// this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt"
|
|
||||||
private val sanitizedAbsolutePath: String =
|
|
||||||
replaceDuplicateFileSeparators(absolutePath)
|
|
||||||
|
|
||||||
// this is only a directory if the filepath ends with a /
|
|
||||||
private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator)
|
|
||||||
private val isFile: Boolean = !isDir
|
|
||||||
|
|
||||||
// this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello"
|
|
||||||
private val relativePath: String =
|
|
||||||
replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast(
|
|
||||||
File.separator
|
|
||||||
)
|
|
||||||
|
|
||||||
// "/hello/text.txt" => "text.txt"
|
|
||||||
private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator)
|
|
||||||
private val baseUri = folderType.toUri(external)
|
|
||||||
private val contentResolver: ContentResolver = context.contentResolver
|
|
||||||
|
|
||||||
init {
|
|
||||||
// some standard asserts that should always be hold or else this class wont work
|
|
||||||
assert(!relativePath.endsWith(File.separator))
|
|
||||||
assert(!(isDir && isFile))
|
|
||||||
assert(!relativePath.contains(File.separator + File.separator))
|
|
||||||
assert(!namePath.contains(File.separator))
|
|
||||||
|
|
||||||
if (isDir) {
|
|
||||||
assert(namePath.isBlank())
|
|
||||||
} else {
|
|
||||||
assert(namePath.isNotBlank())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private fun splitFilenameExt(name: String): Pair<String, String?> {
|
|
||||||
val split = name.indexOfLast { it == '.' }
|
|
||||||
if (split <= 0) return name to null
|
|
||||||
val ext = name.substring(split + 1 until name.length)
|
|
||||||
if (ext.isBlank()) return name to null
|
|
||||||
|
|
||||||
return name.substring(0 until split) to ext
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun splitFilenameMime(name: String): Pair<String, String?> {
|
|
||||||
val (display, ext) = splitFilenameExt(name)
|
|
||||||
val mimeType = when (ext) {
|
|
||||||
|
|
||||||
// Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents
|
|
||||||
// downloading to /Downloads yet it works with null
|
|
||||||
|
|
||||||
"vtt" -> null // "text/vtt"
|
|
||||||
"mp4" -> "video/mp4"
|
|
||||||
"srt" -> null // "application/x-subrip"//"text/plain"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
return display to mimeType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun appendRelativePath(path: String, folder: Boolean): MediaFile? {
|
|
||||||
if (isFile) return null
|
|
||||||
|
|
||||||
// VideoDownloadManager.sanitizeFilename(path.replace(File.separator, ""))
|
|
||||||
|
|
||||||
val newPath =
|
|
||||||
sanitizedAbsolutePath + path + if (folder) File.separator else ""
|
|
||||||
|
|
||||||
return MediaFile(
|
|
||||||
context = context,
|
|
||||||
folderType = folderType,
|
|
||||||
external = external,
|
|
||||||
absolutePath = newPath
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createUri(displayName: String? = namePath): Uri? {
|
|
||||||
if (displayName == null) return null
|
|
||||||
if (isFile) return null
|
|
||||||
val (name, mime) = splitFilenameMime(displayName)
|
|
||||||
|
|
||||||
val newFile = ContentValues().apply {
|
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
|
||||||
put(MediaStore.MediaColumns.TITLE, name)
|
|
||||||
if (mime != null)
|
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, mime)
|
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
|
||||||
}
|
|
||||||
return contentResolver.insert(baseUri, newFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFile(displayName: String?): SafeFile? {
|
|
||||||
if (isFile || displayName == null) return null
|
|
||||||
query(displayName)?.uri ?: createUri(displayName) ?: return null
|
|
||||||
return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createDirectory(directoryName: String?): SafeFile? {
|
|
||||||
if (directoryName == null) return null
|
|
||||||
// we don't create a dir here tbh, just fake create it
|
|
||||||
return appendRelativePath(directoryName, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class QueryResult(
|
|
||||||
val uri: Uri,
|
|
||||||
val lastModified: Long,
|
|
||||||
val length: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
private fun query(displayName: String = namePath): QueryResult? {
|
|
||||||
try {
|
|
||||||
//val (name, mime) = splitFilenameMime(fullName)
|
|
||||||
|
|
||||||
val projection = arrayOf(
|
|
||||||
MediaStore.MediaColumns._ID,
|
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
|
||||||
MediaStore.MediaColumns.SIZE,
|
|
||||||
)
|
|
||||||
|
|
||||||
val selection =
|
|
||||||
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'"
|
|
||||||
|
|
||||||
contentResolver.query(
|
|
||||||
baseUri,
|
|
||||||
projection, selection, null, null
|
|
||||||
)?.use { cursor ->
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val id =
|
|
||||||
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
|
||||||
|
|
||||||
return QueryResult(
|
|
||||||
uri = ContentUris.withAppendedId(
|
|
||||||
baseUri, id
|
|
||||||
),
|
|
||||||
lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)),
|
|
||||||
length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
logError(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun uri(): Uri? {
|
|
||||||
return query()?.uri
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun name(): String? {
|
|
||||||
if (isDir) return null
|
|
||||||
return namePath
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun type(): String? {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filePath(): String {
|
|
||||||
return replaceDuplicateFileSeparators(relativePath + File.separator + namePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDirectory(): Boolean {
|
|
||||||
return isDir
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isFile(): Boolean {
|
|
||||||
return isFile
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastModified(): Long? {
|
|
||||||
if (isDir) return null
|
|
||||||
return query()?.lastModified
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun length(): Long? {
|
|
||||||
if (isDir) return null
|
|
||||||
val length = query()?.length ?: return null
|
|
||||||
if(length <= 0) {
|
|
||||||
val inputStream : InputStream = openInputStream() ?: return null
|
|
||||||
return try {
|
|
||||||
inputStream.available().toLong()
|
|
||||||
} catch (t : Throwable) {
|
|
||||||
null
|
|
||||||
} finally {
|
|
||||||
inputStream.closeQuietly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canRead(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canWrite(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun delete(uri: Uri): Boolean {
|
|
||||||
return contentResolver.delete(uri, null, null) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(): Boolean {
|
|
||||||
return if (isDir) {
|
|
||||||
(listFiles() ?: return false).all {
|
|
||||||
it.delete()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete(uri() ?: return false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exists(): Boolean {
|
|
||||||
if (isDir) return true
|
|
||||||
return query() != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun listFiles(): List<SafeFile>? {
|
|
||||||
if (isFile) return null
|
|
||||||
try {
|
|
||||||
val projection = arrayOf(
|
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME
|
|
||||||
)
|
|
||||||
|
|
||||||
val selection =
|
|
||||||
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'"
|
|
||||||
contentResolver.query(
|
|
||||||
baseUri,
|
|
||||||
projection, selection, null, null
|
|
||||||
)?.use { cursor ->
|
|
||||||
val out = ArrayList<SafeFile>(cursor.count)
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
|
|
||||||
if (nameIdx == -1) continue
|
|
||||||
val name = cursor.getString(nameIdx)
|
|
||||||
|
|
||||||
appendRelativePath(name, false)?.let { new ->
|
|
||||||
out.add(new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
logError(t)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? {
|
|
||||||
if (isFile || displayName == null) return null
|
|
||||||
|
|
||||||
val new = appendRelativePath(displayName, false) ?: return null
|
|
||||||
if (new.exists()) {
|
|
||||||
return new
|
|
||||||
}
|
|
||||||
|
|
||||||
return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun renameTo(name: String?): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openOutputStream(append: Boolean): OutputStream? {
|
|
||||||
try {
|
|
||||||
// use current file
|
|
||||||
uri()?.let {
|
|
||||||
return contentResolver.openOutputStream(
|
|
||||||
it,
|
|
||||||
if (append) "wa" else "wt"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new file if current is not found,
|
|
||||||
// as we know it is new only write access is needed
|
|
||||||
createUri()?.let {
|
|
||||||
return contentResolver.openOutputStream(
|
|
||||||
it,
|
|
||||||
"w"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openInputStream(): InputStream? {
|
|
||||||
try {
|
|
||||||
return contentResolver.openInputStream(uri() ?: return null)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,244 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.utils.storage
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import com.hippo.unifile.UniRandomAccessFile
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
interface SafeFile {
|
|
||||||
companion object {
|
|
||||||
fun fromUri(context: Context, uri: Uri): SafeFile? {
|
|
||||||
return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromFile(context: Context, file: File?): SafeFile? {
|
|
||||||
if (file == null) return null
|
|
||||||
// because UniFile sucks balls on Media we have to do this
|
|
||||||
val absPath = file.absolutePath.removePrefix(File.separator)
|
|
||||||
for (value in MediaFileContentType.values()) {
|
|
||||||
val prefixes = listOf(value.toAbsolutePath(), value.toPath())
|
|
||||||
for (prefix in prefixes) {
|
|
||||||
if (!absPath.startsWith(prefix)) continue
|
|
||||||
return fromMedia(
|
|
||||||
context,
|
|
||||||
value,
|
|
||||||
absPath.removePrefix(prefix).ifBlank { File.separator }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return UniFileWrapper(UniFile.fromFile(file) ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromAsset(
|
|
||||||
context: Context,
|
|
||||||
filename: String?
|
|
||||||
): SafeFile? {
|
|
||||||
return UniFileWrapper(
|
|
||||||
UniFile.fromAsset(context.assets, filename ?: return null) ?: return null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromResource(
|
|
||||||
context: Context,
|
|
||||||
id: Int
|
|
||||||
): SafeFile? {
|
|
||||||
return UniFileWrapper(
|
|
||||||
UniFile.fromResource(context, id) ?: return null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromMedia(
|
|
||||||
context: Context,
|
|
||||||
folderType: MediaFileContentType,
|
|
||||||
path: String = File.separator,
|
|
||||||
external: Boolean = true,
|
|
||||||
): SafeFile? {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
//fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path)
|
|
||||||
|
|
||||||
return MediaFile(
|
|
||||||
context = context,
|
|
||||||
folderType = folderType,
|
|
||||||
external = external,
|
|
||||||
absolutePath = path
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
fromFile(
|
|
||||||
context,
|
|
||||||
File(
|
|
||||||
(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
|
||||||
folderType.toPath() + File.separator + folderType).replace(
|
|
||||||
File.separator + File.separator,
|
|
||||||
File.separator
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*val uri: Uri? get() = getUri()
|
|
||||||
val name: String? get() = getName()
|
|
||||||
val type: String? get() = getType()
|
|
||||||
val filePath: String? get() = getFilePath()
|
|
||||||
val isFile: Boolean? get() = isFile()
|
|
||||||
val isDirectory: Boolean? get() = isDirectory()
|
|
||||||
val length: Long? get() = length()
|
|
||||||
val canRead: Boolean get() = canRead()
|
|
||||||
val canWrite: Boolean get() = canWrite()
|
|
||||||
val lastModified: Long? get() = lastModified()*/
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun isFileOrThrow(): Boolean {
|
|
||||||
return isFile() ?: throw IOException("Unable to get if file is a file or directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun lengthOrThrow(): Long {
|
|
||||||
return length() ?: throw IOException("Unable to get file length")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun isDirectoryOrThrow(): Boolean {
|
|
||||||
return isDirectory() ?: throw IOException("Unable to get if file is a directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun filePathOrThrow(): String {
|
|
||||||
return filePath() ?: throw IOException("Unable to get file path")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun uriOrThrow(): Uri {
|
|
||||||
return uri() ?: throw IOException("Unable to get uri")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun renameOrThrow(name: String?) {
|
|
||||||
if (!renameTo(name)) {
|
|
||||||
throw IOException("Unable to rename to $name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun openOutputStreamOrThrow(append: Boolean = false): OutputStream {
|
|
||||||
return openOutputStream(append) ?: throw IOException("Unable to open output stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun openInputStreamOrThrow(): InputStream {
|
|
||||||
return openInputStream() ?: throw IOException("Unable to open input stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun existsOrThrow(): Boolean {
|
|
||||||
return exists() ?: throw IOException("Unable get if file exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile {
|
|
||||||
return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun gotoDirectoryOrThrow(
|
|
||||||
directoryName: String?,
|
|
||||||
createMissingDirectories: Boolean = true
|
|
||||||
): SafeFile {
|
|
||||||
return gotoDirectory(directoryName, createMissingDirectories)
|
|
||||||
?: throw IOException("Unable to go to directory $directoryName")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun listFilesOrThrow(): List<SafeFile> {
|
|
||||||
return listFiles() ?: throw IOException("Unable to get files")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun createFileOrThrow(displayName: String?): SafeFile {
|
|
||||||
return createFile(displayName) ?: throw IOException("Unable to create file $displayName")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun createDirectoryOrThrow(directoryName: String?): SafeFile {
|
|
||||||
return createDirectory(
|
|
||||||
directoryName ?: throw IOException("Unable to create file with invalid name")
|
|
||||||
)
|
|
||||||
?: throw IOException("Unable to create directory $directoryName")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun deleteOrThrow() {
|
|
||||||
if (!delete()) {
|
|
||||||
throw IOException("Unable to delete file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName
|
|
||||||
* returns itself. createMissingDirectories specifies if the dirs should be created
|
|
||||||
* when travelling or break at a dir not found */
|
|
||||||
fun gotoDirectory(
|
|
||||||
directoryName: String?,
|
|
||||||
createMissingDirectories: Boolean = true
|
|
||||||
): SafeFile? {
|
|
||||||
if (directoryName == null) return this
|
|
||||||
|
|
||||||
return directoryName.split(File.separatorChar).filter { it.isNotBlank() }
|
|
||||||
.fold(this) { file: SafeFile?, directory ->
|
|
||||||
// as MediaFile does not actually create a directory we can do this
|
|
||||||
if (createMissingDirectories || this is MediaFile) {
|
|
||||||
file?.createDirectory(directory)
|
|
||||||
} else {
|
|
||||||
val next = file?.findFile(directory)
|
|
||||||
|
|
||||||
// we require the file to be a directory
|
|
||||||
if (next?.isDirectory() != true) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createFile(displayName: String?): SafeFile?
|
|
||||||
fun createDirectory(directoryName: String?): SafeFile?
|
|
||||||
fun uri(): Uri?
|
|
||||||
fun name(): String?
|
|
||||||
fun type(): String?
|
|
||||||
fun filePath(): String?
|
|
||||||
fun isDirectory(): Boolean?
|
|
||||||
fun isFile(): Boolean?
|
|
||||||
fun lastModified(): Long?
|
|
||||||
fun length(): Long?
|
|
||||||
fun canRead(): Boolean
|
|
||||||
fun canWrite(): Boolean
|
|
||||||
fun delete(): Boolean
|
|
||||||
fun exists(): Boolean?
|
|
||||||
fun listFiles(): List<SafeFile>?
|
|
||||||
|
|
||||||
// fun listFiles(filter: FilenameFilter?): Array<File>?
|
|
||||||
fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile?
|
|
||||||
|
|
||||||
fun renameTo(name: String?): Boolean
|
|
||||||
|
|
||||||
/** Open a stream on to the content associated with the file */
|
|
||||||
fun openOutputStream(append: Boolean = false): OutputStream?
|
|
||||||
|
|
||||||
/** Open a stream on to the content associated with the file */
|
|
||||||
fun openInputStream(): InputStream?
|
|
||||||
|
|
||||||
/** Get a random access stuff of the UniFile, "r" or "rw" */
|
|
||||||
fun createRandomAccessFile(mode: String?): UniRandomAccessFile?
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.utils.storage
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import com.hippo.unifile.UniRandomAccessFile
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
private fun UniFile.toFile(): SafeFile {
|
|
||||||
return UniFileWrapper(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> safe(apiCall: () -> T): T? {
|
|
||||||
return try {
|
|
||||||
apiCall.invoke()
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
logError(throwable)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UniFileWrapper(val file: UniFile) : SafeFile {
|
|
||||||
override fun createFile(displayName: String?): SafeFile? {
|
|
||||||
return file.createFile(displayName)?.toFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createDirectory(directoryName: String?): SafeFile? {
|
|
||||||
return file.createDirectory(directoryName)?.toFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun uri(): Uri? {
|
|
||||||
return safe { file.uri }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun name(): String? {
|
|
||||||
return safe { file.name }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun type(): String? {
|
|
||||||
return safe { file.type }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filePath(): String? {
|
|
||||||
return safe { file.filePath }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDirectory(): Boolean? {
|
|
||||||
return safe { file.isDirectory }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isFile(): Boolean? {
|
|
||||||
return safe { file.isFile }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastModified(): Long? {
|
|
||||||
return safe { file.lastModified() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun length(): Long? {
|
|
||||||
return safe {
|
|
||||||
val len = file.length()
|
|
||||||
if (len <= 1) {
|
|
||||||
val inputStream = this.openInputStream() ?: return@safe null
|
|
||||||
try {
|
|
||||||
inputStream.available().toLong()
|
|
||||||
} finally {
|
|
||||||
inputStream.closeQuietly()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canRead(): Boolean {
|
|
||||||
return safe { file.canRead() } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canWrite(): Boolean {
|
|
||||||
return safe { file.canWrite() } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(): Boolean {
|
|
||||||
return safe { file.delete() } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exists(): Boolean? {
|
|
||||||
return safe { file.exists() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun listFiles(): List<SafeFile>? {
|
|
||||||
return safe { file.listFiles()?.mapNotNull { it?.toFile() } }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? {
|
|
||||||
return safe { file.findFile(displayName, ignoreCase)?.toFile() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun renameTo(name: String?): Boolean {
|
|
||||||
return safe { file.renameTo(name) } ?: return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openOutputStream(append: Boolean): OutputStream? {
|
|
||||||
return safe { file.openOutputStream(append) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openInputStream(): InputStream? {
|
|
||||||
return safe { file.openInputStream() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? {
|
|
||||||
return safe { file.createRandomAccessFile(mode) }
|
|
||||||
}
|
|
||||||
}
|
|
2
app/src/main/res/values-ajp/strings.xml
Normal file
2
app/src/main/res/values-ajp/strings.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources/>
|
5
app/src/main/res/values-am/strings.xml
Normal file
5
app/src/main/res/values-am/strings.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_dub_sub_episode_text_format" formatted="true">%s ክፍል %d</string>
|
||||||
|
<string name="cast_format" formatted="true">ተዋናዮች: %s</string>
|
||||||
|
</resources>
|
|
@ -1,2 +1,203 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources/>
|
<resources>
|
||||||
|
<string name="search_poster_img_des">لافتة</string>
|
||||||
|
<string name="home_change_provider_img_des">تغيير مزود</string>
|
||||||
|
<string name="downloading">جارى التحميل</string>
|
||||||
|
<string name="cast_format" formatted="true">بث%s</string>
|
||||||
|
<string name="filler" formatted="true">ملء</string>
|
||||||
|
<string name="skip_loading">تخطي التحميل</string>
|
||||||
|
<string name="loading">تحميل…</string>
|
||||||
|
<string name="pick_subtitle">ترجمات</string>
|
||||||
|
<string name="reload_error">إعادة محاولة الاتصال …</string>
|
||||||
|
<string name="app_dub_sub_episode_text_format" formatted="true">%sييبي%d</string>
|
||||||
|
<string name="next_episode_format" formatted="true">الحلقة%dسيتم نشرها في</string>
|
||||||
|
<string name="next_episode_time_day_format" formatted="true">%dي%dس%dد</string>
|
||||||
|
<string name="next_episode_time_hour_format" formatted="true">%dس%dد</string>
|
||||||
|
<string name="next_episode_time_min_format" formatted="true">%dد</string>
|
||||||
|
<string name="episode_poster_img_des">لافتة الحلقة</string>
|
||||||
|
<string name="home_main_poster_img_des">اللافتة الاساسية</string>
|
||||||
|
<string name="go_back_img_des">اذهب للخالف</string>
|
||||||
|
<string name="preview_background_img_des">معاينة الخلفية</string>
|
||||||
|
<string name="player_speed_text_format" formatted="true">سرعة(%.2fx)</string>
|
||||||
|
<string name="play_with_app_name">فتح مع كلاودستريم</string>
|
||||||
|
<string name="title_home">الصفحة الاساسية</string>
|
||||||
|
<string name="search_hint_site" formatted="true">...%sابحث</string>
|
||||||
|
<string name="no_data">لايوجد بيانات</string>
|
||||||
|
<string name="episode_more_options_des">المزيد من الخيارات</string>
|
||||||
|
<string name="result_open_in_browser">فتح في المتصفح</string>
|
||||||
|
<string name="browser">المتصفح</string>
|
||||||
|
<string name="play_movie_button">شاهد الفلم</string>
|
||||||
|
<string name="play_torrent_button">دفق التورنت</string>
|
||||||
|
<string name="download_started">بدأ التنزيل</string>
|
||||||
|
<string name="home_next_random_img_des">عشوائي قادم</string>
|
||||||
|
<string name="play_trailer_button">تشغيل المقطع الدعائي</string>
|
||||||
|
<string name="result_tags">الأنواع</string>
|
||||||
|
<string name="download_paused">توقف التنزيل</string>
|
||||||
|
<string name="type_plan_to_watch">خطط للمشاهدة</string>
|
||||||
|
<string name="type_none">لا يوجد</string>
|
||||||
|
<string name="type_re_watching">إعادة المشاهدة</string>
|
||||||
|
<string name="new_update_format" formatted="true">!تم العثور على تحديث جديد
|
||||||
|
\n%s->%s</string>
|
||||||
|
<string name="rated_format" formatted="true">%.1f:قدر</string>
|
||||||
|
<string name="duration_format" formatted="true">%dاقل</string>
|
||||||
|
<string name="app_name">كلاودستريم</string>
|
||||||
|
<string name="title_search">بحث</string>
|
||||||
|
<string name="title_downloads">التحميلات</string>
|
||||||
|
<string name="title_settings">اعدادات</string>
|
||||||
|
<string name="search_hint">...بحث</string>
|
||||||
|
<string name="next_episode">الحلقة القادمة</string>
|
||||||
|
<string name="result_share">شارك</string>
|
||||||
|
<string name="type_watching">مشاهدة</string>
|
||||||
|
<string name="type_on_hold">في التوقف</string>
|
||||||
|
<string name="type_completed">مكتمل</string>
|
||||||
|
<string name="type_dropped">توقف</string>
|
||||||
|
<string name="play_livestream_button">تشغيل البث المباشر</string>
|
||||||
|
<string name="pick_source">مصادر</string>
|
||||||
|
<string name="play_episode">تشغيل الحلقة</string>
|
||||||
|
<string name="download_canceled">تم إلغاء التنزيل</string>
|
||||||
|
<string name="download_done">تم التنزيل</string>
|
||||||
|
<string name="downloaded">تنززل</string>
|
||||||
|
<string name="download">تحميل</string>
|
||||||
|
<string name="go_back">عُد</string>
|
||||||
|
<string name="download_failed">التحميل فشل</string>
|
||||||
|
<string name="use_system_brightness_settings_des">استخدم سطوع النظام في مشغل التطبيق بدلاً من التراكب الداكن</string>
|
||||||
|
<string name="restore_success">تم تحميل ملف النسخ الاحتياطي</string>
|
||||||
|
<string name="advanced_search">البحث المتقدم</string>
|
||||||
|
<string name="player_size_settings_des">إزالة الحدود السوداء</string>
|
||||||
|
<string name="player_subtitles_settings">ترجمات</string>
|
||||||
|
<string name="eigengraumode_settings_des">يضيف خيار السرعة في المشغل</string>
|
||||||
|
<string name="double_tap_to_seek_settings">انقر نقرا مزدوجا للبحث</string>
|
||||||
|
<string name="double_tap_to_pause_settings">انقر نقرًا مزدوجًا للإيقاف المؤقت</string>
|
||||||
|
<string name="double_tap_to_seek_amount_settings">اللاعب يبحث عن المبلغ (بالثواني)</string>
|
||||||
|
<string name="swipe_to_seek_settings_des">اسحب من جانب إلى آخر للتحكم بموقعك في الفيديو</string>
|
||||||
|
<string name="autoplay_next_settings_des">ابدأ الحلقة التالية عندما تنتهي الحلقة الحالية</string>
|
||||||
|
<string name="use_system_brightness_settings">استخدام سطوع النظام</string>
|
||||||
|
<string name="episode_sync_settings">تحديث مراقبة التقدم</string>
|
||||||
|
<string name="episode_sync_settings_des">قم بمزامنة تقدم الحلقة الحالية تلقائيًا</string>
|
||||||
|
<string name="swipe_to_change_settings">اسحب لتغيير الإعدادات</string>
|
||||||
|
<string name="restore_settings">استعادة البيانات من النسخة الاحتياطية</string>
|
||||||
|
<string name="restore_failed_format" formatted="true">فشل في استعادة البيانات من الملف %s</string>
|
||||||
|
<string name="double_tap_to_seek_settings_des">انقر مرتين على الجانب الأيمن أو الأيسر للبحث للأمام أو للخلف</string>
|
||||||
|
<string name="backup_success">البيانات المخزنة</string>
|
||||||
|
<string name="double_tap_to_pause_settings_des">اضغط مرتين في المنتصف للتوقف مؤقتًا</string>
|
||||||
|
<string name="backup_failed">أذونات التخزين مفقودة. حاول مرة اخرى.</string>
|
||||||
|
<string name="backup_failed_error_format">حدث خطأ أثناء النسخ الاحتياطي %s</string>
|
||||||
|
<string name="search">بحث</string>
|
||||||
|
<string name="library">مكتبة</string>
|
||||||
|
<string name="settings_info">معلومات</string>
|
||||||
|
<string name="category_updates">التحديثات والنسخ الاحتياطي</string>
|
||||||
|
<string name="advanced_search_des">يعطيك نتائج البحث مفصولة حسب المزود</string>
|
||||||
|
<string name="bug_report_settings_off">يرسل فقط البيانات عن الأعطال</string>
|
||||||
|
<string name="show_trailers_settings">عرض المقطورات</string>
|
||||||
|
<string name="kitsu_settings">عرض الملصقات من كيتسو</string>
|
||||||
|
<string name="category_account">حسابات</string>
|
||||||
|
<string name="bug_report_settings_on">لا يرسل أي بيانات</string>
|
||||||
|
<string name="show_fillers_settings">عرض حلقة حشو للأنمي</string>
|
||||||
|
<string name="pref_filter_search_quality">إخفاء جودة الفيديو المحددة في نتائج البحث</string>
|
||||||
|
<string name="automatic_plugin_updates">تحديثات البرنامج المساعد التلقائي</string>
|
||||||
|
<string name="updates_settings_des">البحث تلقائيًا عن التحديثات الجديدة بعد بدء تشغيل التطبيق.</string>
|
||||||
|
<string name="uprereleases_settings">التحديث إلى الإصداراالمسبق</string>
|
||||||
|
<string name="automatic_plugin_download">تنزيل المكونات الإضافية تلقائيًا</string>
|
||||||
|
<string name="redo_setup_process">إعادة عملية الإعداد</string>
|
||||||
|
<string name="uprereleases_settings_des">ابحث عن تحديثات الإصدار التجريبي بدلاً من الإصدارات الكاملة فقط</string>
|
||||||
|
<string name="automatic_plugin_download_mode_title">حدد الوضع لتصفية تنزيل المكونات الإضافية</string>
|
||||||
|
<string name="automatic_plugin_download_summary">قم تلقائيًا بتثبيت جميع المكونات الإضافية التي لم يتم تثبيتها بعد من المستودعات المضافة.</string>
|
||||||
|
<string name="chromecast_subtitles_settings_des">إعدادات ترجمات كرومكاست</string>
|
||||||
|
<string name="eigengraumode_settings">وضع إيجينجرافي</string>
|
||||||
|
<string name="swipe_to_seek_settings">انتقد للبحث</string>
|
||||||
|
<string name="backup_settings">نسخ إحتياطي للبيانات</string>
|
||||||
|
<string name="updates_settings">إظهار تحديثات التطبيق</string>
|
||||||
|
<string name="player_subtitles_settings_des">إعدادات ترجمات المشغل</string>
|
||||||
|
<string name="chromecast_subtitles_settings">ترجمات كرومكاست</string>
|
||||||
|
<string name="swipe_to_change_settings_des">قم بالتمرير لأعلى أو لأسفل على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت</string>
|
||||||
|
<string name="autoplay_next_settings">التشغيل التلقائي للحلقة القادمة</string>
|
||||||
|
<string name="lightnovel">تطبيق رواية خفيفة من نفس المطورين</string>
|
||||||
|
<string name="benene">أعط بينيني للمطورين</string>
|
||||||
|
<string name="github">جيتهب</string>
|
||||||
|
<string name="anim">تطبيق انيمي من نفس المطورين</string>
|
||||||
|
<string name="app_language">لغة التطبيق</string>
|
||||||
|
<string name="discord">انضم إلى الديسكورد</string>
|
||||||
|
<string name="benene_des">بنيني معطا</string>
|
||||||
|
<string name="apk_installer_settings_des">بعض الهواتف لا تدعم مثبت الحزمة الجديد. جرب الخيار القديم إذا لم يتم تثبيت التحديثات.</string>
|
||||||
|
<string name="apk_installer_settings">مثبت تتبيق</string>
|
||||||
|
<string name="test_passed">اجتاز</string>
|
||||||
|
<string name="episodes">الحلقات</string>
|
||||||
|
<string name="season">موسم</string>
|
||||||
|
<string name="copy_link_toast">تم نسخ الرابط إلى الحافظة</string>
|
||||||
|
<string name="delete">مسح</string>
|
||||||
|
<string name="pause">وقف</string>
|
||||||
|
<string name="update_notification_downloading">جارٍ تنزيل تحديث التطبيق…</string>
|
||||||
|
<string name="subs_default_reset_toast">إعادة التعيين إلى القيمة العادية</string>
|
||||||
|
<string name="season_short">س</string>
|
||||||
|
<string name="episode_format" formatted="true">%d%s</string>
|
||||||
|
<string name="no_chromecast_support_toast">لا يتمتع هذا المزود بدعم كرومكاست</string>
|
||||||
|
<string name="no_links_found_toast">لم يتم العثور على أي روابط</string>
|
||||||
|
<string name="play_episode_toast">تشغيل الحلقة</string>
|
||||||
|
<string name="acra_report_toast">عذرًا، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين</string>
|
||||||
|
<string name="season_format">%s%d%s</string>
|
||||||
|
<string name="no_season">لا يوجد موسم</string>
|
||||||
|
<string name="episode">حلقة</string>
|
||||||
|
<string name="episodes_range">%d-%d</string>
|
||||||
|
<string name="episode_short">يي</string>
|
||||||
|
<string name="clear_history">امسح التاريخ</string>
|
||||||
|
<string name="update_notification_installing">جارٍ تثبيت تحديث التطبيق…</string>
|
||||||
|
<string name="start">بدأ</string>
|
||||||
|
<string name="no_episodes_found">لم يتم العثور على أي حلقات</string>
|
||||||
|
<string name="enable_skip_op_from_database_des">إظهار تخطي النوافذ المنبثقة للفتح/الإنهاء</string>
|
||||||
|
<string name="clipboard_too_large">الكثير من النص. غير قادر على الحفظ في الحافظة.</string>
|
||||||
|
<string name="action_mark_as_watched">وضع علامة كما شاهدت</string>
|
||||||
|
<string name="action_remove_from_watched">إزالة من شاهد</string>
|
||||||
|
<string name="delete_file">حذف ملف</string>
|
||||||
|
<string name="test_failed">فشل</string>
|
||||||
|
<string name="resume">اكتمل</string>
|
||||||
|
<string name="go_back_30">-30</string>
|
||||||
|
<string name="go_forward_30">+30</string>
|
||||||
|
<string name="history">تاريخ</string>
|
||||||
|
<string name="confirm_exit_dialog">هل أنت متأكد أنك تريد الخروج؟</string>
|
||||||
|
<string name="yes">نعم</string>
|
||||||
|
<string name="no">لا</string>
|
||||||
|
<string name="update_notification_failed">تعذر تثبيت الإصدار الجديد من التطبيق</string>
|
||||||
|
<string name="apk_installer_legacy">إرث</string>
|
||||||
|
<string name="apk_installer_package_installer">منزل المجموعة</string>
|
||||||
|
<string name="sort_rating_asc">التقييم (من الأقل إلى الأعلى)</string>
|
||||||
|
<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="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="already_voted">لقد صوت بالفعل</string>
|
||||||
|
<string name="sort_alphabetical_z">أبجديًا (ياء إلى ألف)</string>
|
||||||
|
<string name="sort_by">ترتيب حسب</string>
|
||||||
|
<string name="subscription_list_name">مشترك</string>
|
||||||
|
<string name="delayed_update_notice">سيتم تحديث التطبيق عند الخروج</string>
|
||||||
|
<string name="sort">رتب</string>
|
||||||
|
<string name="sort_rating_desc">التقييم (من الأعلى إلى الأقل)</string>
|
||||||
|
<string name="select_library">حدد المكتبة</string>
|
||||||
|
<string name="open_with">افتع مع</string>
|
||||||
|
<string name="empty_library_logged_in_message">.هذه القائمة فارغة. حاول التبديل إلى واحد آخر</string>
|
||||||
|
<string name="subscription_new">%sتم الاشتراك في</string>
|
||||||
|
<string name="subscription_deleted">%sتم إلغاء الاشتراك من</string>
|
||||||
|
<string name="subscription_episode_released">!%dتم إصدار الحلقة</string>
|
||||||
|
<string name="profile_background_des">خلفية الملف الشخصي</string>
|
||||||
|
<string name="profile_number">%dملف التعريف</string>
|
||||||
|
<string name="wifi">واي فاي</string>
|
||||||
|
<string name="mobile_data">بيانات الجوال</string>
|
||||||
|
<string name="use">استخدم</string>
|
||||||
|
<string name="unable_to_inflate">%sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور</string>
|
||||||
|
<string name="qualities">الصفات</string>
|
||||||
|
</resources>
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
<string name="bug_report_settings_on">Não enviar nenhum dado</string>
|
<string name="bug_report_settings_on">Não enviar nenhum dado</string>
|
||||||
<string name="show_fillers_settings">Mostrar episódios de Filler em anime</string>
|
<string name="show_fillers_settings">Mostrar episódios de Filler em anime</string>
|
||||||
<string name="show_trailers_settings">Mostrar trailers</string>
|
<string name="show_trailers_settings">Mostrar trailers</string>
|
||||||
<string name="kitsu_settings">Mostrar posters do kitsu</string>
|
<string name="kitsu_settings">Mostrar posters do Kitsu</string>
|
||||||
<string name="pref_filter_search_quality">Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa</string>
|
<string name="pref_filter_search_quality">Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa</string>
|
||||||
<string name="automatic_plugin_updates">Atualizações de plugin automáticas</string>
|
<string name="automatic_plugin_updates">Atualizações de plugin automáticas</string>
|
||||||
<string name="updates_settings">Mostrar atualizações do app</string>
|
<string name="updates_settings">Mostrar atualizações do app</string>
|
||||||
|
@ -183,7 +183,7 @@
|
||||||
<string name="season_short">S</string>
|
<string name="season_short">S</string>
|
||||||
<string name="episode_short">E</string>
|
<string name="episode_short">E</string>
|
||||||
<string name="no_episodes_found">Nenhum Episódio encontrado</string>
|
<string name="no_episodes_found">Nenhum Episódio encontrado</string>
|
||||||
<string name="delete_file">Deletar Arquivo</string>
|
<string name="delete_file">Apagar Arquivo</string>
|
||||||
<string name="delete">Deletar</string>
|
<string name="delete">Deletar</string>
|
||||||
<string name="pause">Pausar</string>
|
<string name="pause">Pausar</string>
|
||||||
<string name="resume">Retomar</string>
|
<string name="resume">Retomar</string>
|
||||||
|
@ -410,15 +410,19 @@
|
||||||
<string name="batch_download_finish_format" formatted="true">Transferido %d %s com sucesso</string>
|
<string name="batch_download_finish_format" formatted="true">Transferido %d %s com sucesso</string>
|
||||||
<string name="batch_download_nothing_to_download_format" formatted="true">Tudo %s já transferido</string>
|
<string name="batch_download_nothing_to_download_format" formatted="true">Tudo %s já transferido</string>
|
||||||
<string name="batch_download">Transferência em batch</string>
|
<string name="batch_download">Transferência em batch</string>
|
||||||
<string name="plugin_singular">plugin</string>
|
<string name="plugin_singular">Plugin</string>
|
||||||
<string name="plugin">plugins</string>
|
<string name="plugin">Plugins</string>
|
||||||
<string name="delete_repository_plugins">Isto irá apagar todos os repositórios de plugins</string>
|
<string name="delete_repository_plugins">Isto irá apagar todos os repositórios de plugins</string>
|
||||||
<string name="delete_repository">Apagar repositório</string>
|
<string name="delete_repository">Apagar repositório</string>
|
||||||
<string name="setup_extensions_subtext">Transferir lista de sites a usar</string>
|
<string name="setup_extensions_subtext">Transferir lista de sites a usar</string>
|
||||||
<string name="plugins_downloaded" formatted="true">Transferido: %d</string>
|
<string name="plugins_downloaded" formatted="true">Transferido: %d</string>
|
||||||
<string name="plugins_disabled" formatted="true">Desativado: %d</string>
|
<string name="plugins_disabled" formatted="true">Desativado: %d</string>
|
||||||
<string name="plugins_not_downloaded" formatted="true">Não transferido: %d</string>
|
<string name="plugins_not_downloaded" formatted="true">Não transferido: %d</string>
|
||||||
<string name="blank_repo_message">Adicionar um repositório para instalar extensões de sites</string>
|
<string name="blank_repo_message">CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios.
|
||||||
|
\n
|
||||||
|
\nPor causa das limitações do DMCA (Digital Millennium Copyright Act ) feito em nome de Sky UK Limited 🤮nós não podemos adicionar site de repositórios no app.
|
||||||
|
\n
|
||||||
|
\nEntre no nosso Discord ou pesquise online.</string>
|
||||||
<string name="view_public_repositories_button">Ver repositórios da comunidade</string>
|
<string name="view_public_repositories_button">Ver repositórios da comunidade</string>
|
||||||
<string name="view_public_repositories_button_short">Lista pública</string>
|
<string name="view_public_repositories_button_short">Lista pública</string>
|
||||||
<string name="uppercase_all_subtitles">Todas as legendas em maiúsculas</string>
|
<string name="uppercase_all_subtitles">Todas as legendas em maiúsculas</string>
|
||||||
|
@ -455,7 +459,7 @@
|
||||||
<string name="edit">Editar</string>
|
<string name="edit">Editar</string>
|
||||||
<string name="profiles">Perfis</string>
|
<string name="profiles">Perfis</string>
|
||||||
<string name="android_tv_interface_on_seek_settings">Exibindo Player - procure na Barra de Progresso</string>
|
<string name="android_tv_interface_on_seek_settings">Exibindo Player - procure na Barra de Progresso</string>
|
||||||
<string name="action_remove_from_watched">remover dos assitidos</string>
|
<string name="action_remove_from_watched">Remover dos assistidos</string>
|
||||||
<string name="pref_category_extensions">Extensões</string>
|
<string name="pref_category_extensions">Extensões</string>
|
||||||
<string name="sort_alphabetical_a">Alfabética(A => Z)</string>
|
<string name="sort_alphabetical_a">Alfabética(A => Z)</string>
|
||||||
<string name="open_with">Abrir com</string>
|
<string name="open_with">Abrir com</string>
|
||||||
|
@ -499,4 +503,51 @@
|
||||||
<string name="subscription_in_progress_notification">Atualizando shows inscritos</string>
|
<string name="subscription_in_progress_notification">Atualizando shows inscritos</string>
|
||||||
<string name="android_tv_interface_off_seek_settings">Player oculto - Procure na barra de progresso</string>
|
<string name="android_tv_interface_off_seek_settings">Player oculto - Procure na barra de progresso</string>
|
||||||
<string name="nsfw">Conteúdo +18</string>
|
<string name="nsfw">Conteúdo +18</string>
|
||||||
|
<string name="restart">Reiniciar</string>
|
||||||
|
<string name="stop">Parar</string>
|
||||||
|
<string name="action_mark_as_watched">Marcar como assistido</string>
|
||||||
|
<string name="delayed_update_notice">Aplicativo precisa ser fechado para atualizar</string>
|
||||||
|
<string name="enable_skip_op_from_database_des">Mostrar popups pulados para abertura e finalização</string>
|
||||||
|
<string name="episodes_range">%d-%d</string>
|
||||||
|
<string name="player_settings_play_in_app">Player interno</string>
|
||||||
|
<string name="extension_size">Tamanho</string>
|
||||||
|
<string name="skip_type_op">Abrindo</string>
|
||||||
|
<string name="season_format">%s %d%s</string>
|
||||||
|
<string name="plugins_updated" formatted="true">%d plugins atualizados</string>
|
||||||
|
<string name="safe_mode_description">Todos as extensões serão desligadas para ajuda se talvez estejam causando algum bug.</string>
|
||||||
|
<string name="app_not_found_error">Aplicativo não encontrado</string>
|
||||||
|
<string name="skip_type_recap">Recapitular</string>
|
||||||
|
<string name="all_languages_preference">Todas as linguagens</string>
|
||||||
|
<string name="skip_type_format" formatted="true">Pula %s</string>
|
||||||
|
<string name="skip_type_mixed_ed">Mistura terminada</string>
|
||||||
|
<string name="safe_mode_title">Modo seguro ligado</string>
|
||||||
|
<string name="extension_rating" formatted="true">Ranquear: %s</string>
|
||||||
|
<string name="extension_language">Linguagem</string>
|
||||||
|
<string name="hls_playlist">Lista de reprodução HLS</string>
|
||||||
|
<string name="skip_type_ed">Terminando</string>
|
||||||
|
<string name="episode_format" formatted="true">%d %s</string>
|
||||||
|
<string name="sort_updated_old">Adicionado em (antigo para novo)</string>
|
||||||
|
<string name="skip_type_intro">Introdução</string>
|
||||||
|
<string name="no_plugins_found_error">plug-ins não foram encontrados no repositório</string>
|
||||||
|
<string name="no_repository_found_error">Repositório não encontrado, verifique o URL e tente usa uma VPN</string>
|
||||||
|
<string name="extension_description">Descrição</string>
|
||||||
|
<string name="extension_version">Versão</string>
|
||||||
|
<string name="extension_authors">Autores</string>
|
||||||
|
<string name="extension_install_first">Instale a extensão primeiro</string>
|
||||||
|
<string name="skip_type_creddits">Créditos</string>
|
||||||
|
<string name="history">Historico</string>
|
||||||
|
<string name="clear_history">Limpar historico</string>
|
||||||
|
<string name="clipboard_too_large">Tem Muito texto. Não é possível salvar no clipboard.</string>
|
||||||
|
<string name="player_pref">Player de vídeo preferido</string>
|
||||||
|
<string name="start">Começar</string>
|
||||||
|
<string name="extension_types">Suportado</string>
|
||||||
|
<string name="extension_status">Status</string>
|
||||||
|
<string name="player_settings_play_in_mpv">MPV</string>
|
||||||
|
<string name="skip_type_mixed_op">Abrindo mistura</string>
|
||||||
|
<string name="player_settings_play_in_vlc">VLC</string>
|
||||||
|
<string name="apply_on_restart">Aplicar quando reiniciar</string>
|
||||||
|
<string name="safe_mode_crash_info">Visualização info de crash</string>
|
||||||
|
<string name="audio_tracks">Faixas de áudio</string>
|
||||||
|
<string name="sort_updated_new">Adicionado em (novo para antigo)</string>
|
||||||
|
<string name="video_tracks">Faixas de video</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
<string name="sort_cancel">Abbrechen</string>
|
<string name="sort_cancel">Abbrechen</string>
|
||||||
<string name="sort_copy">Kopieren</string>
|
<string name="sort_copy">Kopieren</string>
|
||||||
<string name="sort_close">Schließen</string>
|
<string name="sort_close">Schließen</string>
|
||||||
<string name="sort_clear">Löschen</string>
|
<string name="sort_clear">Leeren</string>
|
||||||
<string name="sort_save">Speichern</string>
|
<string name="sort_save">Speichern</string>
|
||||||
<string name="player_speed">Player-Geschwindigkeit</string>
|
<string name="player_speed">Player-Geschwindigkeit</string>
|
||||||
<string name="subtitles_settings">Untertiteleinstellungen</string>
|
<string name="subtitles_settings">Untertiteleinstellungen</string>
|
||||||
|
@ -390,7 +390,7 @@
|
||||||
<string name="skip_setup">Einrichtung überspringen</string>
|
<string name="skip_setup">Einrichtung überspringen</string>
|
||||||
<string name="app_layout_subtext">Aussehen der App passend zu dem des Geräts ändern</string>
|
<string name="app_layout_subtext">Aussehen der App passend zu dem des Geräts ändern</string>
|
||||||
<string name="crash_reporting_title">Absturzmeldung</string>
|
<string name="crash_reporting_title">Absturzmeldung</string>
|
||||||
<string name="preferred_media_subtext">Was möchtest du anschauen\?</string>
|
<string name="preferred_media_subtext">Was möchten Sie sehen\?</string>
|
||||||
<string name="setup_done">Fertig</string>
|
<string name="setup_done">Fertig</string>
|
||||||
<string name="extensions">Erweiterungen</string>
|
<string name="extensions">Erweiterungen</string>
|
||||||
<string name="add_repository">Repository hinzufügen</string>
|
<string name="add_repository">Repository hinzufügen</string>
|
||||||
|
@ -546,4 +546,10 @@
|
||||||
\nWerden eine kombinierte Videopriorität von 10 haben.
|
\nWerden eine kombinierte Videopriorität von 10 haben.
|
||||||
\n
|
\n
|
||||||
\nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird!</string>
|
\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>
|
||||||
|
<string name="no_repository_found_error">Repository nicht gefunden, überprüfe die URL und probiere eine VPN</string>
|
||||||
|
<string name="unable_to_inflate">Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s</string>
|
||||||
|
<string name="disable">Deaktivieren</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -190,7 +190,7 @@
|
||||||
<string name="backup_success">Adatok eltárolva</string>
|
<string name="backup_success">Adatok eltárolva</string>
|
||||||
<string name="backup_failed_error_format">Hiba a biztonsági mentés során %s</string>
|
<string name="backup_failed_error_format">Hiba a biztonsági mentés során %s</string>
|
||||||
<string name="category_account">Fiókok</string>
|
<string name="category_account">Fiókok</string>
|
||||||
<string name="advanced_search_des">Szolgáltatás szerinti keresés eredmények</string>
|
<string name="advanced_search_des">Szolgáltató szerint elkülönítve adja meg a keresési eredményeket</string>
|
||||||
<string name="bug_report_settings_on">Nem küld adatokat</string>
|
<string name="bug_report_settings_on">Nem küld adatokat</string>
|
||||||
<string name="kitsu_settings">Poszterek megjelenítése Kitsu-ról</string>
|
<string name="kitsu_settings">Poszterek megjelenítése Kitsu-ról</string>
|
||||||
<string name="pref_filter_search_quality">Kiválasztott videóminőségek elrejtése keresési eredményekbe</string>
|
<string name="pref_filter_search_quality">Kiválasztott videóminőségek elrejtése keresési eredményekbe</string>
|
||||||
|
@ -198,7 +198,7 @@
|
||||||
<string name="automatic_plugin_download">Bővítmények automatikus letöltése</string>
|
<string name="automatic_plugin_download">Bővítmények automatikus letöltése</string>
|
||||||
<string name="automatic_plugin_download_summary">Automatikusan telepíti az összes még nem telepített bővítményt a hozzáadott tárolókból.</string>
|
<string name="automatic_plugin_download_summary">Automatikusan telepíti az összes még nem telepített bővítményt a hozzáadott tárolókból.</string>
|
||||||
<string name="updates_settings">Alkalmazás frissítések megjelenítése</string>
|
<string name="updates_settings">Alkalmazás frissítések megjelenítése</string>
|
||||||
<string name="updates_settings_des">Automatikusan keressen új frissítéseket indításkor</string>
|
<string name="updates_settings_des">Automatikusan keressen új frissítéseket indításkor.</string>
|
||||||
<string name="uprereleases_settings">Frissítés az előzetes kiadásokhoz (prerelease)</string>
|
<string name="uprereleases_settings">Frissítés az előzetes kiadásokhoz (prerelease)</string>
|
||||||
<string name="uprereleases_settings_des">Csak előzetesen kiadott frissítések (prerelease) keresése a teljes kiadások helyett</string>
|
<string name="uprereleases_settings_des">Csak előzetesen kiadott frissítések (prerelease) keresése a teljes kiadások helyett</string>
|
||||||
<string name="github">Github</string>
|
<string name="github">Github</string>
|
||||||
|
@ -232,30 +232,30 @@
|
||||||
<string name="episode_action_play_in_browser">Lejátszás böngészőben</string>
|
<string name="episode_action_play_in_browser">Lejátszás böngészőben</string>
|
||||||
<string name="episode_action_download_subtitle">Feliratok letöltése</string>
|
<string name="episode_action_download_subtitle">Feliratok letöltése</string>
|
||||||
<string name="reload_error">Újracsatlakozás…</string>
|
<string name="reload_error">Újracsatlakozás…</string>
|
||||||
<string name="swipe_to_seek_settings_des">Swipe balra vagy jobbra a videólejátszóban az idő vezérléséhez</string>
|
<string name="swipe_to_seek_settings_des">Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez</string>
|
||||||
<string name="swipe_to_change_settings">Csúsztassa ujját a beállítások módosításához</string>
|
<string name="swipe_to_change_settings">Csúsztassa ujját a beállítások módosításához</string>
|
||||||
<string name="swipe_to_change_settings_des">Csúsztassa az újját bal vagy jobb oldalon a fényerő vagy hangerő megváltoztatásához</string>
|
<string name="swipe_to_change_settings_des">Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához</string>
|
||||||
<string name="backup_settings">Biztonsági mentés</string>
|
<string name="backup_settings">Biztonsági mentés</string>
|
||||||
<string name="benene_count_text_none">0 Banán a fejlesztőknek</string>
|
<string name="benene_count_text_none">0 Banán a fejlesztőknek</string>
|
||||||
<string name="swipe_to_seek_settings">Swipe to seek</string>
|
<string name="swipe_to_seek_settings">Húzás a kereséshez</string>
|
||||||
<string name="autoplay_next_settings">Következő epizód automatikus lejátszása</string>
|
<string name="autoplay_next_settings">Következő epizód automatikus lejátszása</string>
|
||||||
<string name="autoplay_next_settings_des">Következő epizód lejátszása amikor az aktuális epizód véget ér</string>
|
<string name="autoplay_next_settings_des">Következő epizód lejátszása amikor az aktuális epizód véget ér</string>
|
||||||
<string name="double_tap_to_seek_settings">Dupla koppintás to seek</string>
|
<string name="double_tap_to_seek_settings">Dupla koppintás a kereséshez</string>
|
||||||
<string name="double_tap_to_pause_settings">Dupla koppintás a szüneteltetéshez</string>
|
<string name="double_tap_to_pause_settings">Dupla koppintás a szüneteltetéshez</string>
|
||||||
<string name="double_tap_to_seek_amount_settings">Player seek amount</string>
|
<string name="double_tap_to_seek_amount_settings">Lejátszó keresési értéke (Másodpercben)</string>
|
||||||
<string name="double_tap_to_seek_settings_des">Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz</string>
|
<string name="double_tap_to_seek_settings_des">Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz</string>
|
||||||
<string name="double_tap_to_pause_settings_des">Koppintson középre a szüneteltetéshez</string>
|
<string name="double_tap_to_pause_settings_des">Koppintson kétszer középen a szüneteltetéshez</string>
|
||||||
<string name="use_system_brightness_settings">Rendszer fényerejének használata</string>
|
<string name="use_system_brightness_settings">Rendszer fényerejének használata</string>
|
||||||
<string name="use_system_brightness_settings_des">Rendszer fényerejének használata az appban a sötét átfedés helyett</string>
|
<string name="use_system_brightness_settings_des">Rendszer fényerejének használata az appban a sötét átfedés helyett</string>
|
||||||
<string name="episode_sync_settings">Előrehaladás frissítése</string>
|
<string name="episode_sync_settings">Előrehaladás frissítése</string>
|
||||||
<string name="episode_sync_settings_des">Automatikusan szinkronizálja az aktuális epizód előrehaladását</string>
|
<string name="episode_sync_settings_des">Automatikusan szinkronizálja az aktuális epizód előrehaladását</string>
|
||||||
<string name="restore_settings">Adatok visszaállítása a biztonsági mentésből</string>
|
<string name="restore_settings">Adatok visszaállítása biztonsági mentésből</string>
|
||||||
<string name="restore_success">Biztonsági mentés betöltve</string>
|
<string name="restore_success">Biztonsági mentés betöltve</string>
|
||||||
<string name="settings_info">Információ</string>
|
<string name="settings_info">Információ</string>
|
||||||
<string name="resume">Folytatás</string>
|
<string name="resume">Folytatás</string>
|
||||||
<string name="go_back_30">-30</string>
|
<string name="go_back_30">-30</string>
|
||||||
<string name="update_started">Frissítés elkezdődött</string>
|
<string name="update_started">Frissítés elkezdődött</string>
|
||||||
<string name="restore_failed_format" formatted="true">Nem sikerült visszaállítani az adatok a fájlból %s</string>
|
<string name="restore_failed_format" formatted="true">Nem sikerült visszaállítani az adatokat a %s fájlból</string>
|
||||||
<string name="backup_failed">Tárolási engedélyek hiányoznak. Kérjük próbálja újra.</string>
|
<string name="backup_failed">Tárolási engedélyek hiányoznak. Kérjük próbálja újra.</string>
|
||||||
<string name="bug_report_settings_off">Csak összeomlásokról küld adatokat</string>
|
<string name="bug_report_settings_off">Csak összeomlásokról küld adatokat</string>
|
||||||
<string name="apk_installer_settings">APK Telepítő</string>
|
<string name="apk_installer_settings">APK Telepítő</string>
|
||||||
|
@ -280,7 +280,7 @@
|
||||||
<string name="dns_pref">DNS HTTPS-en keresztül</string>
|
<string name="dns_pref">DNS HTTPS-en keresztül</string>
|
||||||
<string name="browser">Böngésző</string>
|
<string name="browser">Böngésző</string>
|
||||||
<string name="pref_category_android_tv">Android TV</string>
|
<string name="pref_category_android_tv">Android TV</string>
|
||||||
<string name="pref_category_gestures">kézmozdulatok</string>
|
<string name="pref_category_gestures">Kézmozdulatok</string>
|
||||||
<string name="skip_update">frissítés kihagyása</string>
|
<string name="skip_update">frissítés kihagyása</string>
|
||||||
<string name="pref_category_app_updates">Alkalmazásfrissítések</string>
|
<string name="pref_category_app_updates">Alkalmazásfrissítések</string>
|
||||||
<string name="category_providers">Szolgáltatók</string>
|
<string name="category_providers">Szolgáltatók</string>
|
||||||
|
@ -496,4 +496,18 @@
|
||||||
<string name="quality_hq">HQ</string>
|
<string name="quality_hq">HQ</string>
|
||||||
<string name="plugins_downloaded" formatted="true">%d letöltve</string>
|
<string name="plugins_downloaded" formatted="true">%d letöltve</string>
|
||||||
<string name="start">Start</string>
|
<string name="start">Start</string>
|
||||||
|
<string name="emulator_layout">Emulátor elrendezés</string>
|
||||||
|
<string name="add_sync">Nyomkövetés hozzáadása</string>
|
||||||
|
<string name="phone_layout">Telefon elrendezés</string>
|
||||||
|
<string name="bottom_title_settings">Poszter cím helye</string>
|
||||||
|
<string name="bottom_title_settings_des">Tegye a címet a poszter alá</string>
|
||||||
|
<string name="android_tv_interface_off_seek_settings_summary">Az átugrás mértéke, amikor a lejátszó el van rejtve</string>
|
||||||
|
<string name="legal_notice">Jogi nyilatkozat</string>
|
||||||
|
<string name="android_tv_interface_on_seek_settings">Lejátszó megjelenítve - Ugrási Érték</string>
|
||||||
|
<string name="android_tv_interface_off_seek_settings">Lejátszó elrejtve - Ugrási Érték</string>
|
||||||
|
<string name="add_site_pref">Klónozott oldal</string>
|
||||||
|
<string name="add_site_summary">Egy meglévő webhely klónjának hozzáadása, más URL-címmel</string>
|
||||||
|
<string name="tv_layout">TV elrendezés</string>
|
||||||
|
<string name="automatic">Automatikus</string>
|
||||||
|
<string name="android_tv_interface_on_seek_settings_summary">Az átugrás mértéke, amikor a lejátszó látható</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
6
app/src/main/res/values-ti/strings.xml
Normal file
6
app/src/main/res/values-ti/strings.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_dub_sub_episode_text_format" formatted="true">%s ክፋል %d</string>
|
||||||
|
<string name="next_episode_format" formatted="true">ክፋል %d በ ላይ ይወጣል</string>
|
||||||
|
<string name="cast_format" formatted="true">ተዋሳእቲ፡ %s</string>
|
||||||
|
</resources>
|
|
@ -418,7 +418,7 @@
|
||||||
<string name="batch_download_start_format" formatted="true">Почалося завантаження %d %s…</string>
|
<string name="batch_download_start_format" formatted="true">Почалося завантаження %d %s…</string>
|
||||||
<string name="batch_download_finish_format" formatted="true">Завантажено %d %s</string>
|
<string name="batch_download_finish_format" formatted="true">Завантажено %d %s</string>
|
||||||
<string name="batch_download_nothing_to_download_format" formatted="true">Всі %s вже завантажено</string>
|
<string name="batch_download_nothing_to_download_format" formatted="true">Всі %s вже завантажено</string>
|
||||||
<string name="batch_download">Пакетне завантаження</string>
|
<string name="batch_download">Завантажити пакети</string>
|
||||||
<string name="plugin_singular">плагін</string>
|
<string name="plugin_singular">плагін</string>
|
||||||
<string name="plugin">плагіни</string>
|
<string name="plugin">плагіни</string>
|
||||||
<string name="delete_repository">Видалити репозиторій</string>
|
<string name="delete_repository">Видалити репозиторій</string>
|
||||||
|
|
1
fastlane/metadata/android/ar-SA/changelogs/2.txt
Normal file
1
fastlane/metadata/android/ar-SA/changelogs/2.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
تمت إضافة سجل التغيير!
|
|
@ -1,14 +1,10 @@
|
||||||
يتيح لك كلاود ستريم -3 بث وتنزيل الأفلام والمسلسلات التلفزيونية والأنيمي. يأتي التطبيق بدون أي إعلانات وتحليلات. و يدعم العديد من مواقع البث الاولي(التريلر) والأفلام والمزيد. وتشمل الميزات:
|
يسمح لك كلاود ستريم -3 ببث وتنزيل الأفلام, المسلسلات التلفزيونية, والأنيمي.
|
||||||
|
|
||||||
|
|
||||||
|
يأتي التطبيق بدون أي إعلانات وتحليلات و
|
||||||
|
يدعم العديد من مواقع البث الاولي(التريلر) ,والأفلام, والمزيد.
|
||||||
|
|
||||||
إشارات مرجعية
|
إشارات مرجعية
|
||||||
|
|
||||||
|
|
||||||
قم بتنزيل ودفق الأفلام والبرامج التلفزيونية والأنيمي
|
|
||||||
|
|
||||||
|
|
||||||
تنزيلات الترجمة
|
تنزيلات الترجمة
|
||||||
|
|
||||||
|
|
||||||
دعم كروم كاست
|
دعم كروم كاست
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
بث وتحميل الأفلام والأنمي والمسلسلات التلفزيونية.
|
بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية.
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen.
|
Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen.
|
||||||
|
|
||||||
Die App kommt ganz ohne Werbung und Analytik aus.
|
Die App kommt ganz ohne Werbung und Analytik aus.
|
||||||
Sie unterstützt mehrere Trailer-, Filmseiten und vieles mehr. Integrierte Features:
|
Sie unterstützt zahlreiche Trailer, Filmseiten und vieles mehr, unter anderem:
|
||||||
|
|
||||||
Lesezeichen
|
Lesezeichen
|
||||||
|
|
||||||
Herunterladen und Streamen von Filmen, Fernsehsendungen und Animes
|
Herunterladen und Streaming von Filmen, Fernsehsendungen und Animes
|
||||||
|
|
||||||
Downloads von Untertiteln
|
Downloads von Untertiteln
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue