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
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
// disable this for now
|
||||
//externalNativeBuild {
|
||||
// cmake {
|
||||
// path("CMakeLists.txt")
|
||||
// }
|
||||
//}
|
||||
|
||||
signingConfigs {
|
||||
create("prerelease") {
|
||||
|
@ -50,7 +51,7 @@ android {
|
|||
}
|
||||
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "30.0.3"
|
||||
buildToolsVersion = "34.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.lagradost.cloudstream3"
|
||||
|
@ -58,7 +59,7 @@ android {
|
|||
targetSdk = 29
|
||||
|
||||
versionCode = 59
|
||||
versionName = "4.1.7"
|
||||
versionName = "4.1.8"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
|
@ -232,7 +233,7 @@ dependencies {
|
|||
// To fix SSL fuckery on android 9
|
||||
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
||||
// 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
|
||||
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.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 -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
|
|
|
@ -107,7 +107,7 @@ class AcraApplication : Application() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
NativeCrashHandler.initCrashHandler()
|
||||
//NativeCrashHandler.initCrashHandler()
|
||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||
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.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.safefile.SafeFile
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
|
@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
companion object {
|
||||
const val TAG = "MAINACT"
|
||||
var lastError: String? = null
|
||||
|
||||
/**
|
||||
* Setting this will automatically enter the query in the search
|
||||
* next time the search fragment is opened.
|
||||
|
@ -366,7 +368,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
nextSearchQuery =
|
||||
try {
|
||||
URLDecoder.decode(query, "UTF-8")
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
query
|
||||
}
|
||||
|
@ -859,7 +861,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
RecyclerView::class.java.declaredMethods.firstOrNull {
|
||||
it.name == "scrollStep"
|
||||
}?.also { it.isAccessible = true }
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -906,11 +908,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (dx > 0) dx else 0
|
||||
}
|
||||
|
||||
if(!NO_MOVE_LIST) {
|
||||
if (!NO_MOVE_LIST) {
|
||||
parent.smoothScrollBy(rdx, 0)
|
||||
}else {
|
||||
} else {
|
||||
val smoothScroll = reflectedScroll
|
||||
if(smoothScroll == null) {
|
||||
if (smoothScroll == null) {
|
||||
parent.smoothScrollBy(rdx, 0)
|
||||
} else {
|
||||
try {
|
||||
|
@ -920,12 +922,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
val out = IntArray(2)
|
||||
smoothScroll.invoke(parent, rdx, 0, out)
|
||||
val scrolledX = out[0]
|
||||
if(abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
|
||||
if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
|
||||
smoothScroll.invoke(parent, -rdx, 0, out)
|
||||
parent.smoothScrollBy(scrolledX, 0)
|
||||
if (NO_MOVE_LIST) targetDx = scrolledX
|
||||
}
|
||||
} catch (t : Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
parent.smoothScrollBy(rdx, 0)
|
||||
}
|
||||
}
|
||||
|
@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
snackbar.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ioSafe { SafeFile.check(this@MainActivity) }
|
||||
|
||||
if (PluginManager.checkSafeModeFile()) {
|
||||
normalSafeApiCall {
|
||||
|
|
|
@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
object NativeCrashHandler {
|
||||
// external fun triggerNativeCrash()
|
||||
private external fun initNativeCrashHandler()
|
||||
/*private external fun initNativeCrashHandler()
|
||||
private external fun getSignalStatus(): Int
|
||||
|
||||
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
||||
|
@ -49,5 +49,5 @@ object NativeCrashHandler {
|
|||
}
|
||||
|
||||
initSignalPolling()
|
||||
}
|
||||
}*/
|
||||
}
|
|
@ -2,15 +2,12 @@ package com.lagradost.cloudstream3.extractors
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
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.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
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() {
|
||||
override val name = "Moviesapi"
|
||||
|
@ -32,7 +29,7 @@ open class Chillx : ExtractorApi() {
|
|||
override val requiresReferer = true
|
||||
|
||||
companion object {
|
||||
private const val KEY = "11x&W5UBrcqn\$9Yl"
|
||||
private const val KEY = "m4H6D9%0\$N&F6rQ&"
|
||||
}
|
||||
|
||||
override suspend fun getUrl(
|
||||
|
@ -47,8 +44,7 @@ open class Chillx : ExtractorApi() {
|
|||
referer = referer
|
||||
).text
|
||||
)?.groupValues?.get(1)
|
||||
val encData = AppUtils.tryParseJson<AESData>(base64Decode(master ?: return))
|
||||
val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
|
||||
val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
|
||||
val source = Regex(""""?file"?:\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(
|
||||
@JsonProperty("file") val file: 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.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
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() {
|
||||
override var mainUrl = "https://databasegdriveplayer.co"
|
||||
|
@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
?.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? {
|
||||
return find(str)?.groupValues?.getOrNull(1)
|
||||
}
|
||||
|
@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
val document = app.get(url).document
|
||||
|
||||
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)
|
||||
?.split(Regex("\\D+"))
|
||||
?.joinToString("") {
|
||||
Char(it.toInt()).toString()
|
||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||
?: throw ErrorLoadingException("can't find password")
|
||||
val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||
|
||||
val sourceData = decryptedData?.substringAfter("sources:[")?.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(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("kind") val kind: String,
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
|
|||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.extractorApis
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) {
|
|||
href,
|
||||
page.url,
|
||||
getQualityFromName(qual),
|
||||
element.attr("href").contains(".m3u8")
|
||||
type = INFER_TYPE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ open class Rabbitstream : ExtractorApi() {
|
|||
override val requiresReferer = false
|
||||
open val embed = "ajax/embed-4"
|
||||
open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"
|
||||
private var rawKey: String? = null
|
||||
|
||||
override suspend fun getUrl(
|
||||
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> {
|
||||
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.M3u8Helper
|
||||
|
||||
class SpeedoStream2 : SpeedoStream() {
|
||||
override val mainUrl = "https://speedostream.mom"
|
||||
}
|
||||
|
||||
class SpeedoStream1 : SpeedoStream() {
|
||||
override val mainUrl = "https://speedostream.pm"
|
||||
}
|
||||
|
||||
open class SpeedoStream : ExtractorApi() {
|
||||
override val name = "SpeedoStream"
|
||||
override val mainUrl = "https://speedostream.mom"
|
||||
override val mainUrl = "https://speedostream.bond"
|
||||
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> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
app.get(url, referer = referer).document.select("script").map { script ->
|
||||
|
@ -26,7 +33,7 @@ open class SpeedoStream : ExtractorApi() {
|
|||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
it.file,
|
||||
"$mainUrl/",
|
||||
"$hostUrl/",
|
||||
).forEach { m3uData -> sources.add(m3uData) }
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +44,4 @@ open class SpeedoStream : ExtractorApi() {
|
|||
private data class File(
|
||||
@JsonProperty("file") val file: String,
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
|
|||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.argamap
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.extractorApis
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) {
|
|||
href,
|
||||
page.url,
|
||||
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.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
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")
|
||||
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.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import java.net.URI
|
||||
|
||||
open class Wibufile : ExtractorApi() {
|
||||
override val name: String = "Wibufile"
|
||||
|
@ -28,10 +28,8 @@ open class Wibufile : ExtractorApi() {
|
|||
video ?: return,
|
||||
"$mainUrl/",
|
||||
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
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
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.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.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope.coroutineContext
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -174,7 +182,7 @@ class APIRepository(val api: MainAPI) {
|
|||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
): Boolean {
|
||||
if (isInvalidData(data)) return false // this makes providers cleaner
|
||||
return try {
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.sortSubs
|
||||
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.SubtitleData
|
||||
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 isSuccessful = safeApiCall {
|
||||
generator.generateLinks(clearCache = false, isCasting = true,
|
||||
generator.generateLinks(
|
||||
clearCache = false, type = LoadType.Chromecast,
|
||||
callback = {
|
||||
it.first?.let { link ->
|
||||
currentLinks.add(link)
|
||||
|
|
|
@ -658,12 +658,14 @@ class HomeFragment : Fragment() {
|
|||
return@observeNullable
|
||||
}
|
||||
|
||||
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = {
|
||||
val (items, delete) = item
|
||||
|
||||
bottomSheetDialog = activity?.loadHomepageList(items, expandCallback = {
|
||||
homeViewModel.expandAndReturn(it)
|
||||
}, dismissCallback = {
|
||||
homeViewModel.popup(null)
|
||||
bottomSheetDialog = null
|
||||
})
|
||||
}, deleteCallback = delete)
|
||||
}
|
||||
|
||||
homeViewModel.reloadStored()
|
||||
|
|
|
@ -246,7 +246,7 @@ class HomeParentItemAdapterPreview(
|
|||
private val previewViewpagerText: ViewGroup =
|
||||
itemView.findViewById(R.id.home_preview_viewpager_text)
|
||||
|
||||
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
|
||||
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
|
||||
private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
|
||||
private var resumeRecyclerView: RecyclerView =
|
||||
itemView.findViewById(R.id.home_watch_child_recyclerview)
|
||||
|
@ -257,7 +257,7 @@ class HomeParentItemAdapterPreview(
|
|||
private var homeAccount: View? =
|
||||
itemView.findViewById(R.id.home_preview_switch_account)
|
||||
|
||||
private var topPadding : View? = itemView.findViewById(R.id.home_padding)
|
||||
private var topPadding: View? = itemView.findViewById(R.id.home_padding)
|
||||
|
||||
private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding)
|
||||
|
||||
|
@ -283,7 +283,11 @@ class HomeParentItemAdapterPreview(
|
|||
item.plot ?: ""
|
||||
|
||||
homePreviewText.text = item.name
|
||||
populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent)
|
||||
populateChips(
|
||||
homePreviewTags,
|
||||
item.tags ?: emptyList(),
|
||||
R.style.ChipFilledSemiTransparent
|
||||
)
|
||||
|
||||
homePreviewTags.isGone =
|
||||
item.tags.isNullOrEmpty()
|
||||
|
@ -413,7 +417,7 @@ class HomeParentItemAdapterPreview(
|
|||
Pair(itemView.findViewById(R.id.home_plan_to_watch_btt), WatchType.PLANTOWATCH),
|
||||
)
|
||||
|
||||
private val toggleListHolder : ChipGroup? = itemView.findViewById(R.id.home_type_holder)
|
||||
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
|
||||
|
||||
init {
|
||||
previewViewpager.setPageTransformer(HomeScrollTransformer())
|
||||
|
@ -422,8 +426,14 @@ class HomeParentItemAdapterPreview(
|
|||
resumeRecyclerView.adapter = resumeAdapter
|
||||
bookmarkRecyclerView.adapter = bookmarkAdapter
|
||||
|
||||
resumeRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF)
|
||||
bookmarkRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF)
|
||||
resumeRecyclerView.setLinearListLayout(
|
||||
nextLeft = R.id.nav_rail_view,
|
||||
nextRight = FOCUS_SELF
|
||||
)
|
||||
bookmarkRecyclerView.setLinearListLayout(
|
||||
nextLeft = R.id.nav_rail_view,
|
||||
nextRight = FOCUS_SELF
|
||||
)
|
||||
|
||||
fixPaddingStatusbarMargin(topPadding)
|
||||
|
||||
|
@ -547,7 +557,10 @@ class HomeParentItemAdapterPreview(
|
|||
resumeWatching,
|
||||
false
|
||||
), 1, false
|
||||
)
|
||||
),
|
||||
deleteCallback = {
|
||||
viewModel.deleteResumeWatching()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -572,7 +585,9 @@ class HomeParentItemAdapterPreview(
|
|||
list,
|
||||
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.DOWNLOAD_HEADER_CACHE
|
||||
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.getAllWatchStateIds
|
||||
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
|
||||
|
||||
private val _apiName = MutableLiveData<String>()
|
||||
|
@ -394,11 +411,14 @@ class HomeViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
|
||||
private val _popup = MutableLiveData<ExpandableHomepageList?>(null)
|
||||
val popup: LiveData<ExpandableHomepageList?> = _popup
|
||||
private val _popup = MutableLiveData<Pair<ExpandableHomepageList, (() -> Unit)?>?>(null)
|
||||
val popup: LiveData<Pair<ExpandableHomepageList, (() -> Unit)?>?> = _popup
|
||||
|
||||
fun popup(list: ExpandableHomepageList?) {
|
||||
_popup.postValue(list)
|
||||
fun popup(list: ExpandableHomepageList?, deleteCallback: (() -> Unit)? = null) {
|
||||
if (list == null)
|
||||
_popup.postValue(null)
|
||||
else
|
||||
_popup.postValue(list to deleteCallback)
|
||||
}
|
||||
|
||||
private fun bookmarksUpdated(unused: Boolean) {
|
||||
|
@ -436,8 +456,7 @@ class HomeViewModel : ViewModel() {
|
|||
// do nothing
|
||||
}
|
||||
|
||||
fun reloadStored() {
|
||||
loadResumeWatching()
|
||||
fun loadStoredData() {
|
||||
val list = EnumSet.noneOf(WatchType::class.java)
|
||||
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
|
||||
list.addAll(it)
|
||||
|
@ -445,6 +464,11 @@ class HomeViewModel : ViewModel() {
|
|||
loadStoredData(list)
|
||||
}
|
||||
|
||||
fun reloadStored() {
|
||||
loadResumeWatching()
|
||||
loadStoredData()
|
||||
}
|
||||
|
||||
fun click(load: LoadClickCallback) {
|
||||
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.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||
import java.io.File
|
||||
import java.lang.IllegalArgumentException
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSession
|
||||
|
@ -1257,10 +1259,12 @@ class CS3IPlayer : IPlayer {
|
|||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
||||
}
|
||||
|
||||
val mime = when {
|
||||
link.isM3u8 -> MimeTypes.APPLICATION_M3U8
|
||||
link.isDash -> MimeTypes.APPLICATION_MPD
|
||||
else -> MimeTypes.VIDEO_MP4
|
||||
val mime = when(link.type) {
|
||||
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
|
||||
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) {
|
||||
|
|
|
@ -50,47 +50,60 @@ class DownloadFileGenerator(
|
|||
return null
|
||||
}
|
||||
|
||||
fun cleanDisplayName(name: String): String {
|
||||
return name.substringBeforeLast('.').trim()
|
||||
}
|
||||
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||
subtitleCallback: (SubtitleData) -> Unit,
|
||||
offset: Int,
|
||||
offset: Int
|
||||
): Boolean {
|
||||
val meta = episodes[currentIndex + offset]
|
||||
callback(Pair(null, meta))
|
||||
callback(null to meta)
|
||||
|
||||
context?.let { ctx ->
|
||||
val relative = meta.relativePath
|
||||
val display = meta.displayName
|
||||
val ctx = context ?: return true
|
||||
val relative = meta.relativePath ?: return true
|
||||
val display = meta.displayName ?: return true
|
||||
|
||||
if (display == null || relative == null) {
|
||||
return@let
|
||||
val cleanDisplay = cleanDisplayName(display)
|
||||
|
||||
VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
|
||||
?.forEach { (name, uri) ->
|
||||
// only these files are allowed, so no videos as subtitles
|
||||
if (listOf(
|
||||
".vtt",
|
||||
".srt",
|
||||
".txt",
|
||||
".ass",
|
||||
".ttml",
|
||||
".sbv",
|
||||
".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(
|
||||
SubtitleData(
|
||||
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
|
||||
uri.toString(),
|
||||
SubtitleOrigin.DOWNLOADED_FILE,
|
||||
name.toSubtitleMimeType(),
|
||||
emptyMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
|
||||
?.forEach { file ->
|
||||
val name = display.removeSuffix(".mp4")
|
||||
if (file.first != meta.displayName && file.first.startsWith(name)) {
|
||||
val realName = file.first.removePrefix(name)
|
||||
.removeSuffix(".vtt")
|
||||
.removeSuffix(".srt")
|
||||
.removeSuffix(".txt")
|
||||
.trim()
|
||||
.removePrefix("(")
|
||||
.removeSuffix(")")
|
||||
|
||||
subtitleCallback(
|
||||
SubtitleData(
|
||||
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
|
||||
file.second.toString(),
|
||||
SubtitleOrigin.DOWNLOADED_FILE,
|
||||
name.toSubtitleMimeType(),
|
||||
emptyMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
|
@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity
|
|||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.SafeFile
|
||||
|
||||
const val DTAG = "PlayerActivity"
|
||||
|
||||
|
@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
|||
listOf(
|
||||
ExtractorUri(
|
||||
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,14 +37,17 @@ class ExtractorLinkGenerator(
|
|||
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||
subtitleCallback: (SubtitleData) -> Unit,
|
||||
offset: Int
|
||||
): Boolean {
|
||||
subtitles.forEach(subtitleCallback)
|
||||
val allowedTypes = type.toSet()
|
||||
links.forEach {
|
||||
callback.invoke(it to null)
|
||||
if(allowedTypes.contains(it.type)) {
|
||||
callback.invoke(it to null)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.preference.PreferenceManager
|
||||
import androidx.media3.common.Format.NO_VALUE
|
||||
import androidx.media3.common.MimeTypes
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
||||
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.popCurrentPage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.SafeFile
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
return durPos.position
|
||||
}
|
||||
|
||||
var currentVerifyLink: Job? = null
|
||||
private var currentVerifyLink: Job? = null
|
||||
|
||||
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
|
||||
currentVerifyLink?.cancel()
|
||||
|
|
|
@ -1,8 +1,43 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
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 {
|
||||
val hasCache: Boolean
|
||||
|
||||
|
@ -13,15 +48,15 @@ interface IGenerator {
|
|||
fun goto(index: Int)
|
||||
|
||||
fun getCurrentId(): Int? // this is used to save data or read data about this id
|
||||
fun getCurrent(offset : Int = 0): Any? // this is used to get metadata about the current playing, can return null
|
||||
fun getAll() : List<Any>? // this us used to get the metadata about all entries, not needed
|
||||
fun getCurrent(offset: Int = 0): Any? // this is used to get metadata about the current playing, can return null
|
||||
fun getAll(): List<Any>? // this us used to get the metadata about all entries, not needed
|
||||
|
||||
/* not safe, must use try catch */
|
||||
suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||
subtitleCallback: (SubtitleData) -> Unit,
|
||||
offset : Int = 0,
|
||||
offset: Int = 0,
|
||||
): Boolean
|
||||
}
|
|
@ -48,7 +48,7 @@ class LinkGenerator(
|
|||
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||
subtitleCallback: (SubtitleData) -> Unit,
|
||||
offset: Int
|
||||
|
|
|
@ -78,10 +78,10 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
||||
safeApiCall {
|
||||
generator?.generateLinks(
|
||||
type = LoadType.InApp,
|
||||
clearCache = false,
|
||||
isCasting = false,
|
||||
{},
|
||||
{},
|
||||
callback = {},
|
||||
subtitleCallback = {},
|
||||
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")
|
||||
currentJob?.cancel()
|
||||
|
||||
|
@ -162,14 +162,14 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
// load more data
|
||||
_loadingLinks.postValue(Resource.Loading())
|
||||
val loadingState = safeApiCall {
|
||||
generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, {
|
||||
generator?.generateLinks(type = type,clearCache = clearCache, callback = {
|
||||
currentLinks.add(it)
|
||||
// Clone to prevent ConcurrentModificationException
|
||||
normalSafeApiCall {
|
||||
// Extra normalSafeApiCall since .toSet() iterates.
|
||||
_currentLinks.postValue(currentLinks.toSet())
|
||||
}
|
||||
}, {
|
||||
}, subtitleCallback = {
|
||||
currentSubs.add(it)
|
||||
normalSafeApiCall {
|
||||
_currentSubs.postValue(currentSubs.toSet())
|
||||
|
|
|
@ -67,18 +67,19 @@ class RepoLinkGenerator(
|
|||
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||
subtitleCallback: (SubtitleData) -> Unit,
|
||||
offset: Int,
|
||||
offset: Int
|
||||
): Boolean {
|
||||
val allowedTypes = type.toSet()
|
||||
val index = currentIndex
|
||||
val current = episodes.getOrNull(index + offset) ?: return false
|
||||
|
||||
val (currentLinkCache, currentSubsCache) = if (clearCache) {
|
||||
Pair(mutableSetOf(), mutableSetOf())
|
||||
} 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()
|
||||
|
@ -88,9 +89,9 @@ class RepoLinkGenerator(
|
|||
val currentSubsUrls = mutableSetOf<String>() // makes all subs urls 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)
|
||||
callback(Pair(link, null))
|
||||
callback(link to null)
|
||||
}
|
||||
|
||||
currentSubsCache.forEach { sub ->
|
||||
|
@ -108,8 +109,8 @@ class RepoLinkGenerator(
|
|||
val result = APIRepository(
|
||||
getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist")
|
||||
).loadLinks(current.data,
|
||||
isCasting,
|
||||
{ file ->
|
||||
isCasting = LoadType.Chromecast == type,
|
||||
subtitleCallback = { file ->
|
||||
val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
|
||||
if (!currentSubsUrls.contains(correctFile.url)) {
|
||||
currentSubsUrls.add(correctFile.url)
|
||||
|
@ -132,12 +133,14 @@ class RepoLinkGenerator(
|
|||
}
|
||||
}
|
||||
},
|
||||
{ link ->
|
||||
callback = { link ->
|
||||
Log.d(TAG, "Loaded ExtractorLink: $link")
|
||||
if (!currentLinks.contains(link.url)) {
|
||||
if (!currentLinkCache.contains(link)) {
|
||||
currentLinks.add(link.url)
|
||||
callback(Pair(link, null))
|
||||
if (allowedTypes.contains(link.type)) {
|
||||
callback(Pair(link, null))
|
||||
}
|
||||
currentLinkCache.add(link)
|
||||
//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.player.GeneratorPlayer
|
||||
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.SubtitleData
|
||||
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
|
||||
|
@ -591,7 +592,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
link,
|
||||
"$fileName ${link.name}",
|
||||
folder,
|
||||
if (link.url.contains(".srt")) ".srt" else "vtt",
|
||||
if (link.url.contains(".srt")) "srt" else "vtt",
|
||||
false,
|
||||
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 ->
|
||||
val fileName = VideoDownloadManager.getFileName(context, meta)
|
||||
downloadSubtitle(context, link, fileName, folder)
|
||||
|
@ -745,7 +746,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
val generator = RepoLinkGenerator(listOf(episode))
|
||||
val currentLinks = mutableSetOf<ExtractorLink>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
generator.generateLinks(clearCache = false, isCasting = false, callback = {
|
||||
generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = {
|
||||
it.first?.let { link ->
|
||||
currentLinks.add(link)
|
||||
}
|
||||
|
@ -825,7 +826,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
isVisible: Boolean = true
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -936,7 +937,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
private fun loadLinks(
|
||||
result: ResultEpisode,
|
||||
isVisible: Boolean,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
clearCache: Boolean = false,
|
||||
work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit)
|
||||
) {
|
||||
|
@ -945,7 +946,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
val links = loadLinks(
|
||||
result,
|
||||
isVisible = isVisible,
|
||||
isCasting = isCasting,
|
||||
type = type,
|
||||
clearCache = clearCache
|
||||
)
|
||||
if (!this.isActive) return@ioSafe
|
||||
|
@ -956,11 +957,11 @@ class ResultViewModel2 : ViewModel() {
|
|||
private var currentLoadLinkJob: Job? = null
|
||||
private fun acquireSingleLink(
|
||||
result: ResultEpisode,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
text: UiText,
|
||||
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
|
||||
) {
|
||||
loadLinks(result, isVisible = true, isCasting = isCasting) { links ->
|
||||
loadLinks(result, isVisible = true, type) { links ->
|
||||
postPopup(
|
||||
text,
|
||||
links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) {
|
||||
|
@ -971,11 +972,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
private fun acquireSingleSubtitle(
|
||||
result: ResultEpisode,
|
||||
isCasting: Boolean,
|
||||
text: UiText,
|
||||
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
|
||||
) {
|
||||
loadLinks(result, isVisible = true, isCasting = isCasting) { links ->
|
||||
loadLinks(result, isVisible = true, type = LoadType.Unknown) { links ->
|
||||
postPopup(
|
||||
text,
|
||||
links.subs.map { txt(it.name) })
|
||||
|
@ -988,7 +988,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
private suspend fun CoroutineScope.loadLinks(
|
||||
result: ResultEpisode,
|
||||
isVisible: Boolean,
|
||||
isCasting: Boolean,
|
||||
type: LoadType,
|
||||
clearCache: Boolean = false,
|
||||
): LinkLoadingResult {
|
||||
val tempGenerator = RepoLinkGenerator(listOf(result))
|
||||
|
@ -1002,7 +1002,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
try {
|
||||
updatePage()
|
||||
tempGenerator.generateLinks(clearCache, isCasting, { (link, _) ->
|
||||
tempGenerator.generateLinks(clearCache, type, { (link, _) ->
|
||||
if (link != null) {
|
||||
links += link
|
||||
updatePage()
|
||||
|
@ -1272,7 +1272,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
acquireSingleSubtitle(
|
||||
click.data,
|
||||
false,
|
||||
txt(R.string.episode_action_download_subtitle)
|
||||
) { (links, index) ->
|
||||
downloadSubtitle(
|
||||
|
@ -1317,7 +1316,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
val response = currentResponse ?: return
|
||||
acquireSingleLink(
|
||||
click.data,
|
||||
false,
|
||||
LoadType.InAppDownload,
|
||||
txt(R.string.episode_action_download_mirror)
|
||||
) { (result, index) ->
|
||||
ioSafe {
|
||||
|
@ -1347,7 +1346,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
loadLinks(
|
||||
click.data,
|
||||
isVisible = false,
|
||||
isCasting = false,
|
||||
type = LoadType.InApp,
|
||||
clearCache = true
|
||||
)
|
||||
}
|
||||
|
@ -1356,7 +1355,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
ACTION_CHROME_CAST_MIRROR -> {
|
||||
acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
LoadType.Chromecast,
|
||||
txt(R.string.episode_action_chromecast_mirror)
|
||||
) { (result, index) ->
|
||||
startChromecast(activity, click.data, result.links, result.subs, index)
|
||||
|
@ -1365,7 +1364,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
LoadType.Browser,
|
||||
txt(R.string.episode_action_play_in_browser)
|
||||
) { (result, index) ->
|
||||
try {
|
||||
|
@ -1380,7 +1379,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
ACTION_COPY_LINK -> {
|
||||
acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
LoadType.ExternalApp,
|
||||
txt(R.string.episode_action_copy_link)
|
||||
) { (result, index) ->
|
||||
val act = activity ?: return@acquireSingleLink
|
||||
|
@ -1399,7 +1398,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
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()) {
|
||||
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
return@loadLinks
|
||||
|
@ -1415,7 +1414,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
LoadType.Chromecast,
|
||||
txt(
|
||||
R.string.episode_action_play_in_format,
|
||||
txt(R.string.player_settings_play_in_web)
|
||||
|
@ -1432,7 +1431,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
LoadType.Chromecast,
|
||||
txt(
|
||||
R.string.episode_action_play_in_format,
|
||||
txt(R.string.player_settings_play_in_mpv)
|
||||
|
@ -1461,7 +1460,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
if (index >= 0)
|
||||
it.goto(index)
|
||||
}
|
||||
|
||||
} ?: return, list
|
||||
)
|
||||
)
|
||||
|
@ -2173,7 +2171,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
trailerData.extractorUrl,
|
||||
trailerData.referer ?: "",
|
||||
Qualities.Unknown.value,
|
||||
trailerData.extractorUrl.contains(".m3u8")
|
||||
type = INFER_TYPE
|
||||
)
|
||||
) to arrayListOf()
|
||||
} 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.VideoDownloadManager
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.SafeFile
|
||||
|
||||
fun getCurrentLocale(context: Context): String {
|
||||
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
|
||||
val appLanguages = arrayListOf(
|
||||
/* begin language list */
|
||||
Triple("", "ajp", "ajp"),
|
||||
Triple("", "አማርኛ", "am"),
|
||||
Triple("", "العربية", "ar"),
|
||||
Triple("", "ars", "ars"),
|
||||
Triple("", "български", "bg"),
|
||||
|
@ -96,6 +98,7 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "Soomaaliga", "so"),
|
||||
Triple("", "svenska", "sv"),
|
||||
Triple("", "தமிழ்", "ta"),
|
||||
Triple("", "ትግርኛ", "ti"),
|
||||
Triple("", "Tagalog", "tl"),
|
||||
Triple("", "Türkçe", "tr"),
|
||||
Triple("", "українська", "uk"),
|
||||
|
@ -335,7 +338,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
|
||||
val currentDir =
|
||||
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(
|
||||
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_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.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
|
||||
|
@ -143,65 +144,26 @@ object BackupUtils {
|
|||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun FragmentActivity.backup() {
|
||||
fun FragmentActivity.backup() = ioSafe {
|
||||
var fileStream: OutputStream? = null
|
||||
var printStream: PrintWriter? = null
|
||||
try {
|
||||
if (!checkWrite()) {
|
||||
showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG)
|
||||
showToast(R.string.backup_failed, Toast.LENGTH_LONG)
|
||||
requestRW()
|
||||
return
|
||||
return@ioSafe
|
||||
}
|
||||
|
||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||
val ext = "json"
|
||||
val ext = "txt"
|
||||
val displayName = "CS3_Backup_${date}"
|
||||
val backupFile = getBackup()
|
||||
val stream = setupStream(this, displayName, null, ext, false)
|
||||
val stream = setupStream(this@backup, displayName, null, ext, false)
|
||||
|
||||
fileStream = stream.openNew()
|
||||
printStream = PrintWriter(fileStream)
|
||||
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(
|
||||
R.string.backup_success,
|
||||
Toast.LENGTH_LONG
|
||||
|
@ -210,7 +172,7 @@ object BackupUtils {
|
|||
logError(e)
|
||||
try {
|
||||
showToast(
|
||||
getString(R.string.backup_failed_error_format).format(e.toString()),
|
||||
txt(R.string.backup_failed_error_format, e.toString()),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -55,7 +55,11 @@ object CastHelper {
|
|||
|
||||
val builder = MediaInfo.Builder(link.url)
|
||||
.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)
|
||||
.setMediaTracks(tracks)
|
||||
data?.let {
|
||||
|
|
|
@ -353,6 +353,12 @@ object DataStoreHelper {
|
|||
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>? {
|
||||
val folder = "$currentAccount/$RESULT_RESUME_WATCHING"
|
||||
return getKeys(folder)?.mapNotNull {
|
||||
|
@ -519,12 +525,10 @@ object DataStoreHelper {
|
|||
|
||||
fun setResultWatchState(id: Int?, status: Int) {
|
||||
if (id == null) return
|
||||
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
||||
if (status == WatchType.NONE.internalId) {
|
||||
removeKey(folder, id.toString())
|
||||
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
||||
deleteBookmarkedData(id)
|
||||
} 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.mvvm.logError
|
||||
import com.lagradost.cloudstream3.extractors.*
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URL
|
||||
import kotlin.collections.MutableList
|
||||
|
||||
/**
|
||||
|
@ -35,35 +37,101 @@ data class ExtractorLinkPlayList(
|
|||
val playlist: List<PlayListItem>,
|
||||
override val referer: String,
|
||||
override val quality: Int,
|
||||
override val isM3u8: Boolean = false,
|
||||
val isM3u8: Boolean = false,
|
||||
override val headers: Map<String, String> = mapOf(),
|
||||
/** Used for getExtractorVerifierJob() */
|
||||
override val extractorData: String? = null,
|
||||
override val type: ExtractorLinkType,
|
||||
) : ExtractorLink(
|
||||
source,
|
||||
name,
|
||||
// Blank as un-used
|
||||
"",
|
||||
referer,
|
||||
quality,
|
||||
isM3u8,
|
||||
headers,
|
||||
extractorData
|
||||
)
|
||||
source = source,
|
||||
name = name,
|
||||
url = "",
|
||||
referer = referer,
|
||||
quality = quality,
|
||||
headers = headers,
|
||||
extractorData = extractorData,
|
||||
type = type
|
||||
) {
|
||||
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 val source: String,
|
||||
open val name: String,
|
||||
override val url: String,
|
||||
override val referer: String,
|
||||
open val quality: Int,
|
||||
open val isM3u8: Boolean = false,
|
||||
override val headers: Map<String, String> = mapOf(),
|
||||
/** Used for getExtractorVerifierJob() */
|
||||
open val extractorData: String? = null,
|
||||
open val isDash: Boolean = false,
|
||||
open val type: ExtractorLinkType,
|
||||
) : 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.
|
||||
* Should be removed after all extensions have updated their cloudstream.jar
|
||||
|
@ -80,8 +148,30 @@ open class ExtractorLink constructor(
|
|||
extractorData: String? = null
|
||||
) : 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 {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
fun getStringByIntFull(quality: Int): String {
|
||||
return when (quality) {
|
||||
0 -> "Auto"
|
||||
|
@ -389,6 +480,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Acefile(),
|
||||
SpeedoStream(),
|
||||
SpeedoStream1(),
|
||||
SpeedoStream2(),
|
||||
Zorofile(),
|
||||
Embedgram(),
|
||||
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.removeKey
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.storage.MediaFileContentType
|
||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
||||
import com.lagradost.safefile.MediaFileContentType
|
||||
import com.lagradost.safefile.SafeFile
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -53,7 +53,7 @@ import java.io.Closeable
|
|||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.net.URL
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.*
|
||||
|
||||
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
|
||||
|
@ -62,6 +62,7 @@ const val DOWNLOAD_CHANNEL_DESCRIPT = "The download notification channel"
|
|||
|
||||
object VideoDownloadManager {
|
||||
var maxConcurrentDownloads = 3
|
||||
var maxConcurrentConnections = 3
|
||||
private var currentDownloads = mutableListOf<Int>()
|
||||
|
||||
private const val USER_AGENT =
|
||||
|
@ -504,7 +505,7 @@ object VideoDownloadManager {
|
|||
): List<Pair<String, Uri>>? {
|
||||
val base = basePathToFile(context, basePath)
|
||||
val folder = base?.gotoDirectory(relativePath, false) ?: return null
|
||||
if (folder.isDirectory() != false) return null
|
||||
//if (folder.isDirectory() != false) return null
|
||||
|
||||
return folder.listFiles()
|
||||
?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) }
|
||||
|
@ -553,9 +554,8 @@ object VideoDownloadManager {
|
|||
extension: String,
|
||||
tryResume: Boolean,
|
||||
): StreamData {
|
||||
val (base, _) = context.getBasePath()
|
||||
return setupStream(
|
||||
base ?: throw IOException("Bad config"),
|
||||
context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"),
|
||||
name,
|
||||
folder,
|
||||
extension,
|
||||
|
@ -951,7 +951,10 @@ object VideoDownloadManager {
|
|||
/** how many bytes every connection should be, by default it is 10 MiB */
|
||||
chuckSize: Long = (1 shl 20) * 10,
|
||||
/** 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 {
|
||||
// we don't want to make a separate connection for every 1kb
|
||||
require(chuckSize > 1000)
|
||||
|
@ -963,7 +966,7 @@ object VideoDownloadManager {
|
|||
var downloadLength: 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
|
||||
// connection
|
||||
LongArray(1) { startByte }
|
||||
|
@ -1024,6 +1027,7 @@ object VideoDownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
/** download a file that consist of a single stream of data*/
|
||||
suspend fun downloadThing(
|
||||
context: Context,
|
||||
link: IDownloadableMinimum,
|
||||
|
@ -1035,8 +1039,7 @@ object VideoDownloadManager {
|
|||
createNotificationCallback: (CreateNotificationMetadata) -> Unit,
|
||||
parallelConnections: Int = 3
|
||||
): DownloadStatus = withContext(Dispatchers.IO) {
|
||||
// we cant download torrents with this implementation, aria2c might be used in the future
|
||||
if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) {
|
||||
if (parallelConnections < 1) {
|
||||
return@withContext DOWNLOAD_INVALID_INPUT
|
||||
}
|
||||
|
||||
|
@ -1400,7 +1403,12 @@ object VideoDownloadManager {
|
|||
metadata.type = DownloadType.IsFailed
|
||||
}
|
||||
} finally {
|
||||
fileMutex.unlock()
|
||||
try {
|
||||
// may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling
|
||||
fileMutex.unlock()
|
||||
} catch (t : Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1524,6 +1532,11 @@ object VideoDownloadManager {
|
|||
notificationCallback: (Int, Notification) -> Unit,
|
||||
tryResume: Boolean = false,
|
||||
): 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)
|
||||
|
||||
// Make sure this is cancelled when download is done or cancelled.
|
||||
|
@ -1552,35 +1565,39 @@ object VideoDownloadManager {
|
|||
}
|
||||
|
||||
try {
|
||||
if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) {
|
||||
val startIndex = if (tryResume) {
|
||||
context.getKey<DownloadedFileInfo>(
|
||||
KEY_DOWNLOAD_INFO,
|
||||
ep.id.toString(),
|
||||
null
|
||||
)?.extraInfo?.toIntOrNull()
|
||||
} else null
|
||||
when(link.type) {
|
||||
ExtractorLinkType.M3U8 -> {
|
||||
val startIndex = if (tryResume) {
|
||||
context.getKey<DownloadedFileInfo>(
|
||||
KEY_DOWNLOAD_INFO,
|
||||
ep.id.toString(),
|
||||
null
|
||||
)?.extraInfo?.toIntOrNull()
|
||||
} else null
|
||||
|
||||
return downloadHLS(
|
||||
context,
|
||||
link,
|
||||
name,
|
||||
folder ?: "",
|
||||
ep.id,
|
||||
startIndex,
|
||||
callback
|
||||
)
|
||||
} else {
|
||||
return downloadThing(
|
||||
context,
|
||||
link,
|
||||
name,
|
||||
folder ?: "",
|
||||
"mp4",
|
||||
tryResume,
|
||||
ep.id,
|
||||
callback
|
||||
)
|
||||
return downloadHLS(
|
||||
context,
|
||||
link,
|
||||
name,
|
||||
folder ?: "",
|
||||
ep.id,
|
||||
startIndex,
|
||||
callback, parallelConnections = maxConcurrentConnections
|
||||
)
|
||||
}
|
||||
ExtractorLinkType.VIDEO -> {
|
||||
return downloadThing(
|
||||
context,
|
||||
link,
|
||||
name,
|
||||
folder ?: "",
|
||||
"mp4",
|
||||
tryResume,
|
||||
ep.id,
|
||||
callback, parallelConnections = maxConcurrentConnections
|
||||
)
|
||||
}
|
||||
else -> throw IllegalArgumentException("unsuported download type")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
return DOWNLOAD_FAILED
|
||||
|
@ -1682,7 +1699,7 @@ object VideoDownloadManager {
|
|||
|
||||
// only delete the key if the file is not found
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"?>
|
||||
<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="show_fillers_settings">Mostrar episódios de Filler em anime</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="automatic_plugin_updates">Atualizações de plugin automáticas</string>
|
||||
<string name="updates_settings">Mostrar atualizações do app</string>
|
||||
|
@ -183,7 +183,7 @@
|
|||
<string name="season_short">S</string>
|
||||
<string name="episode_short">E</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="pause">Pausar</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_nothing_to_download_format" formatted="true">Tudo %s já transferido</string>
|
||||
<string name="batch_download">Transferência em batch</string>
|
||||
<string name="plugin_singular">plugin</string>
|
||||
<string name="plugin">plugins</string>
|
||||
<string name="plugin_singular">Plugin</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">Apagar repositório</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_disabled" formatted="true">Desativado: %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_short">Lista pública</string>
|
||||
<string name="uppercase_all_subtitles">Todas as legendas em maiúsculas</string>
|
||||
|
@ -455,7 +459,7 @@
|
|||
<string name="edit">Editar</string>
|
||||
<string name="profiles">Perfis</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="sort_alphabetical_a">Alfabética(A => Z)</string>
|
||||
<string name="open_with">Abrir com</string>
|
||||
|
@ -468,7 +472,7 @@
|
|||
<string name="library">Biblioteca</string>
|
||||
<string name="no">Não</string>
|
||||
<string name="tracks">Trilhas Sonoras</string>
|
||||
<string name="sort_rating_asc">Votação(Baixa para Alta)</string>
|
||||
<string name="sort_rating_asc">Votação (Baixa para Alta)</string>
|
||||
<string name="update_started">Atualização iniciada</string>
|
||||
<string name="nsfw_singular">Conteúdo +18</string>
|
||||
<string name="help">Ajuda</string>
|
||||
|
@ -476,7 +480,7 @@
|
|||
<string name="update_notification_failed">Não pudemos instalar a nova versão do App</string>
|
||||
<string name="apk_installer_package_installer">instalador de pacotes</string>
|
||||
<string name="sort_by">Organizar por</string>
|
||||
<string name="sort_rating_desc">Votação(Alta para Baixa)</string>
|
||||
<string name="sort_rating_desc">Votação (Alta para Baixa)</string>
|
||||
<string name="sort_alphabetical_z">Alfabética(Z => A)</string>
|
||||
<string name="qualities">Qualidade</string>
|
||||
<string name="profile_background_des">Perfil de plano de fundo</string>
|
||||
|
@ -499,4 +503,51 @@
|
|||
<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="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>
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
<string name="sort_cancel">Abbrechen</string>
|
||||
<string name="sort_copy">Kopieren</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="player_speed">Player-Geschwindigkeit</string>
|
||||
<string name="subtitles_settings">Untertiteleinstellungen</string>
|
||||
|
@ -390,7 +390,7 @@
|
|||
<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="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="extensions">Erweiterungen</string>
|
||||
<string name="add_repository">Repository hinzufügen</string>
|
||||
|
@ -546,4 +546,10 @@
|
|||
\nWerden eine kombinierte Videopriorität von 10 haben.
|
||||
\n
|
||||
\nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird!</string>
|
||||
<string name="automatic_plugin_download_mode_title">Filtermodus für Plugin-Downloads auswählen</string>
|
||||
<string name="already_voted">Es wurde bereits abgestimmt</string>
|
||||
<string name="no_plugins_found_error">Keine Plugins im Repository gefunden</string>
|
||||
<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>
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
<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="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="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>
|
||||
|
@ -198,7 +198,7 @@
|
|||
<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="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_des">Csak előzetesen kiadott frissítések (prerelease) keresése a teljes kiadások helyett</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_download_subtitle">Feliratok letöltése</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_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="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_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_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_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_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_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="settings_info">Információ</string>
|
||||
<string name="resume">Folytatás</string>
|
||||
<string name="go_back_30">-30</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="bug_report_settings_off">Csak összeomlásokról küld adatokat</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="browser">Böngésző</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="pref_category_app_updates">Alkalmazásfrissítések</string>
|
||||
<string name="category_providers">Szolgáltatók</string>
|
||||
|
@ -496,4 +496,18 @@
|
|||
<string name="quality_hq">HQ</string>
|
||||
<string name="plugins_downloaded" formatted="true">%d letöltve</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>
|
||||
|
|
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_finish_format" formatted="true">Завантажено %d %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">плагіни</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 +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.
|
||||
Sie unterstützt mehrere Trailer-, Filmseiten und vieles mehr. Integrierte Features:
|
||||
Sie unterstützt zahlreiche Trailer, Filmseiten und vieles mehr, unter anderem:
|
||||
|
||||
Lesezeichen
|
||||
|
||||
Herunterladen und Streamen von Filmen, Fernsehsendungen und Animes
|
||||
Herunterladen und Streaming von Filmen, Fernsehsendungen und Animes
|
||||
|
||||
Downloads von Untertiteln
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue