Merge remote-tracking branch 'origin/master'

# Conflicts:
#	app/src/main/AndroidManifest.xml
This commit is contained in:
CranberrySoup 2023-09-15 13:12:58 +02:00
commit fb94cc8a86
51 changed files with 866 additions and 1151 deletions

View file

@ -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")

View file

@ -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"

View file

@ -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))

View file

@ -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 {

View file

@ -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()
}
}*/
}

View file

@ -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,

View file

@ -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,

View file

@ -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
)
)
}

View file

@ -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)

View file

@ -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,
)
}

View file

@ -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
)
)
}

View file

@ -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)
}
}
}

View file

@ -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
)
)
}
}

View file

@ -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
)
}

View file

@ -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 {

View file

@ -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)

View file

@ -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()

View file

@ -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)
}
)
}
}

View file

@ -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)
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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()
)
)
)

View file

@ -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

View file

@ -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()

View file

@ -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
}

View file

@ -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

View file

@ -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())

View file

@ -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
}

View file

@ -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 {

View file

@ -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"),

View file

@ -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) {

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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(),

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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?
}

View file

@ -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) }
}
}

View file

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

View 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>

View file

@ -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-&gt;%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>

View file

@ -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 =&gt; 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 =&gt; 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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -0,0 +1 @@
تمت إضافة سجل التغيير!

View file

@ -1,14 +1,10 @@
يتيح لك كلاود ستريم -3 بث وتنزيل الأفلام والمسلسلات التلفزيونية والأنيمي. يأتي التطبيق بدون أي إعلانات وتحليلات. و يدعم العديد من مواقع البث الاولي(التريلر) والأفلام والمزيد. وتشمل الميزات:
يسمح لك كلاود ستريم -3 ببث وتنزيل الأفلام, المسلسلات التلفزيونية, والأنيمي.
يأتي التطبيق بدون أي إعلانات وتحليلات و
يدعم العديد من مواقع البث الاولي(التريلر) ,والأفلام, والمزيد.
إشارات مرجعية
قم بتنزيل ودفق الأفلام والبرامج التلفزيونية والأنيمي
تنزيلات الترجمة
دعم كروم كاست

View file

@ -1 +1 @@
بث وتحميل الأفلام والأنمي والمسلسلات التلفزيونية.
بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية.

View file

@ -1 +1 @@
كلاود ستريم
كلاودستريم

View file

@ -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