Merge branch 'recloudstream-master'

This commit is contained in:
KillerDogeEmpire 2022-09-20 15:28:15 -07:00
commit 7198a0eb2a
34 changed files with 1065 additions and 483 deletions

View File

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

View File

@ -36,9 +36,7 @@ jobs:
echo "::set-output name=key_pwd::$KEY_PWD" echo "::set-output name=key_pwd::$KEY_PWD"
- name: Run Gradle - name: Run Gradle
run: | run: |
./gradlew assemblePrerelease ./gradlew assemblePrerelease makeJar androidSourcesJar
./gradlew androidSourcesJar
./gradlew makeJar
env: env:
SIGNING_KEY_ALIAS: "key10" SIGNING_KEY_ALIAS: "key10"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}

View File

@ -170,7 +170,7 @@ dependencies {
// Networking // Networking
// implementation "com.squareup.okhttp3:okhttp:4.9.2" // implementation "com.squareup.okhttp3:okhttp:4.9.2"
// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1" // implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
implementation 'com.github.Blatzar:NiceHttp:0.3.2' implementation 'com.github.Blatzar:NiceHttp:0.3.3'
// Util to skip the URI file fuckery 🙏 // Util to skip the URI file fuckery 🙏
implementation "com.github.tachiyomiorg:unifile:17bec43" implementation "com.github.tachiyomiorg:unifile:17bec43"
@ -206,10 +206,28 @@ task androidSourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs//full sources from android.sourceSets.main.java.srcDirs//full sources
} }
// this is used by the gradlew plugin
task makeJar(type: Copy) { task makeJar(type: Copy) {
// after modifying here, you can export. Jar
from('build/intermediates/compile_app_classes_jar/debug') from('build/intermediates/compile_app_classes_jar/debug')
into('build') // output location into('build')
include('classes.jar') // the classes file of the imported rack package include('classes.jar')
dependsOn build dependsOn('build')
} }
dokkaHtml {
moduleName.set("Cloudstream")
dokkaSourceSets {
main {
sourceLink {
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
localDirectory.set(file("src/main/java"))
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(new URL(
"https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
}
}
}

View File

@ -7,10 +7,12 @@ import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.auto.service.AutoService import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -74,19 +76,28 @@ class CustomSenderFactory : ReportSenderFactory {
} }
} }
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.UncaughtExceptionHandler { class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, error: Throwable) { override fun uncaughtException(thread: Thread, error: Throwable) {
ACRA.errorReporter.handleException(error) ACRA.errorReporter.handleException(error)
try { try {
PrintStream(errorFile).use { ps -> PrintStream(errorFile).use { ps ->
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
ps.println(String.format("Fatal exception on thread %s (%d)", thread.name, thread.id)) ps.println(
String.format(
"Fatal exception on thread %s (%d)",
thread.name,
thread.id
)
)
error.printStackTrace(ps) error.printStackTrace(ps)
} }
} catch (ignored: FileNotFoundException) { } } catch (ignored: FileNotFoundException) {
}
try { try {
onError.invoke() onError.invoke()
} catch (ignored: Exception) { } } catch (ignored: Exception) {
}
exitProcess(1) exitProcess(1)
} }
@ -95,7 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
class AcraApplication : Application() { class AcraApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component)) startActivity(Intent.makeRestartActivityTask(intent!!.component))
}) })
@ -183,5 +194,15 @@ class AcraApplication : Application() {
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) { fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
context?.openBrowser(url, fallbackWebview, fragment) context?.openBrowser(url, fallbackWebview, fragment)
} }
/** Will fallback to webview if in TV layout */
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
isTvSettings(),
activity?.supportFragmentManager?.fragments?.lastOrNull()
)
}
} }
} }

View File

@ -40,6 +40,7 @@ object APIHolder {
private const val defProvider = 0 private const val defProvider = 0
// ConcurrentModificationException is possible!!!
val allProviders: MutableList<MainAPI> = arrayListOf() val allProviders: MutableList<MainAPI> = arrayListOf()
fun initAll() { fun initAll() {
@ -1118,6 +1119,11 @@ data class NextAiring(
val unixTime: Long, val unixTime: Long,
) )
/**
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
* @param name To be shown next to the season like "Season $displaySeason $name" but if displaySeason is null then "$name"
* @param displaySeason What to be displayed next to the season name, if null then the name is the only thing shown.
* */
data class SeasonData( data class SeasonData(
val season: Int, val season: Int,
val name: String? = null, val name: String? = null,
@ -1198,9 +1204,12 @@ data class AnimeLoadResponse(
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse ) : LoadResponse, EpisodeResponse
/**
* If episodes already exist appends the list.
* */
fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) { fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) {
if (episodes.isNullOrEmpty()) return if (episodes.isNullOrEmpty()) return
this.episodes[status] = episodes this.episodes[status] = (this.episodes[status] ?: emptyList()) + episodes
} }
suspend fun MainAPI.newAnimeLoadResponse( suspend fun MainAPI.newAnimeLoadResponse(

View File

@ -15,6 +15,7 @@ import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
@ -144,6 +145,68 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val mainPluginsLoadedEvent = val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage Event<Boolean>() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event<Boolean>() val afterRepositoryLoadedEvent = Event<Boolean>()
/**
* @return true if the str has launched an app task (be it successful or not)
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
* */
fun handleAppIntentUrl(activity: FragmentActivity?, str: String?, isWebview: Boolean): Boolean =
with(activity) {
if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
return true
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
if (isSuccessful) {
Log.i(TAG, "authenticated ${api.name}")
} else {
Log.i(TAG, "failed to authenticate ${api.name}")
}
this@with.runOnUiThread {
try {
showToast(
this@with,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
return true
}
}
} else if (URI(str).scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
return true
} else if (!isWebview){
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
return true
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
return true
}
}
}
}
}
return false
}
} }
override fun onColorSelected(dialogId: Int, color: Int) { override fun onColorSelected(dialogId: Int, color: Int) {
@ -348,56 +411,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (intent == null) return if (intent == null) return
val str = intent.dataString val str = intent.dataString
loadCache() loadCache()
if (str != null) { handleAppIntentUrl(this, str, false)
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
val activity = this
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
if (isSuccessful) {
Log.i(TAG, "authenticated ${api.name}")
} else {
Log.i(TAG, "failed to authenticate ${api.name}")
}
activity.runOnUiThread {
try {
showToast(
activity,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
}
}
} else if (URI(str).scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
} else {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
break
}
}
}
}
}
} }
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean = private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
@ -445,7 +459,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
// it.hashCode() is not enough to make sure they are distinct // it.hashCode() is not enough to make sure they are distinct
apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } apis =
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
APIHolder.apiMap = null APIHolder.apiMap = null
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -465,9 +480,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
lastError = errorFile.readText(Charset.defaultCharset()) lastError = errorFile.readText(Charset.defaultCharset())
errorFile.delete() errorFile.delete()
} }
val settingsForProvider = SettingsJson() val settingsForProvider = SettingsJson()
settingsForProvider.enableAdult = settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false) settingsForProvider.enableAdult =
settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
MainAPI.settingsForProvider = settingsForProvider MainAPI.settingsForProvider = settingsForProvider
@ -501,7 +517,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
ioSafe { ioSafe {
if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) { if (settingsManager.getBoolean(
getString(R.string.auto_update_plugins_key),
true
)
) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else { } else {
PluginManager.loadAllOnlinePlugins(this@MainActivity) PluginManager.loadAllOnlinePlugins(this@MainActivity)

View File

@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class DoodWfExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.wf"
}
class DoodCxExtractor : DoodLaExtractor() { class DoodCxExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.cx" override var mainUrl = "https://dood.cx"
} }

View File

@ -4,6 +4,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.Qualities
class GMPlayer : ExtractorApi() { class GMPlayer : ExtractorApi() {
override val name = "GM Player" override val name = "GM Player"
@ -25,11 +26,16 @@ class GMPlayer : ExtractorApi() {
data = mapOf("hash" to id, "r" to ref) data = mapOf("hash" to id, "r" to ref)
).parsed<GmResponse>().videoSource ?: return null ).parsed<GmResponse>().videoSource ?: return null
return M3u8Helper.generateM3u8( return listOf(
name, ExtractorLink(
m3u8, this.name,
ref, this.name,
headers = mapOf("accept" to "*/*") m3u8,
ref,
Qualities.Unknown.value,
headers = mapOf("accept" to "*/*"),
isM3u8 = true
)
) )
} }

View File

@ -0,0 +1,182 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
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 DatabaseGdrive : Gdriveplayer() {
override var mainUrl = "https://series.databasegdriveplayer.co"
}
class Gdriveplayerapi: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayerapi.com"
}
class Gdriveplayerapp: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.app"
}
class Gdriveplayerfun: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.fun"
}
class Gdriveplayerio: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.io"
}
class Gdriveplayerme: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.me"
}
class Gdriveplayerbiz: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.biz"
}
class Gdriveplayerorg: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.org"
}
class Gdriveplayerus: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.us"
}
class Gdriveplayerco: Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.co"
}
open class Gdriveplayer : ExtractorApi() {
override val name = "Gdrive"
override val mainUrl = "https://gdriveplayer.to"
override val requiresReferer = false
private fun unpackJs(script: Element): String? {
return script.select("script").find { it.data().contains("eval(function(p,a,c,k,e,d)") }
?.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)
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = app.get(url).document
val eval = unpackJs(document)?.replace("\\", "") ?: return
val data = AppUtils.tryParseJson<AesData>(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("\\", "")
?.substringAfter("sources:[")?.substringBefore("],")
Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(decryptedData ?: return).map {
it.groupValues[1] to it.groupValues[2]
}.toList().distinctBy { it.second }.map { (link, quality) ->
callback.invoke(
ExtractorLink(
source = this.name,
name = this.name,
url = "${httpsify(link)}&res=$quality",
referer = mainUrl,
quality = quality.toIntOrNull() ?: Qualities.Unknown.value,
headers = mapOf("Range" to "bytes=0-")
)
)
}
}
data class AesData(
@JsonProperty("ct") val ct: String,
@JsonProperty("iv") val iv: String,
@JsonProperty("s") val s: String
)
}

View File

@ -1,12 +1,21 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class Streamsss : StreamSB() {
override var mainUrl = "https://streamsss.net"
}
class Sbflix : StreamSB() {
override var mainUrl = "https://sbflix.xyz"
override var name = "Sbflix"
}
class Vidgomunime : StreamSB() { class Vidgomunime : StreamSB() {
override var mainUrl = "https://vidgomunime.xyz" override var mainUrl = "https://vidgomunime.xyz"
} }
@ -104,31 +113,33 @@ open class StreamSB : ExtractorApi() {
@JsonProperty("status_code") val statusCode: Int, @JsonProperty("status_code") val statusCode: Int,
) )
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(
val regexID = Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|\\/e\\/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)") url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val regexID =
Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|/e/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
val id = regexID.findAll(url).map { val id = regexID.findAll(url).map {
it.value.replace(Regex("(embed-|\\/e\\/)"),"") it.value.replace(Regex("(embed-|/e/)"), "")
}.first() }.first()
val bytes = id.toByteArray() // val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val bytesToHex = bytesToHex(bytes) val master = "$mainUrl/sources48/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val headers = mapOf( val headers = mapOf(
"watchsb" to "streamsb", "watchsb" to "sbstream",
) )
val urltext = app.get(master, val mapped = app.get(
master.lowercase(),
headers = headers, headers = headers,
allowRedirects = false referer = url,
).text ).parsedSafe<Main>()
val mapped = urltext.let { parseJson<Main>(it) }
val testurl = app.get(mapped.streamData.file, headers = headers).text
// val urlmain = mapped.streamData.file.substringBefore("/hls/") // val urlmain = mapped.streamData.file.substringBefore("/hls/")
if (urltext.contains("m3u8") && testurl.contains("EXTM3U")) M3u8Helper.generateM3u8(
return M3u8Helper.generateM3u8( name,
name, mapped?.streamData?.file ?: return,
mapped.streamData.file, url,
url, headers = headers
headers = headers ).forEach(callback)
)
return null
} }
} }

View File

@ -7,6 +7,7 @@ import com.bumptech.glide.load.HttpException
import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.InterruptedIOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLHandshakeException
@ -157,7 +158,7 @@ suspend fun <T> safeApiCall(
} }
safeFail(throwable) safeFail(throwable)
} }
is SocketTimeoutException -> { is SocketTimeoutException, is InterruptedIOException -> {
Resource.Failure( Resource.Failure(
true, true,
null, null,
@ -192,7 +193,7 @@ suspend fun <T> safeApiCall(
true, true,
null, null,
null, null,
(throwable.message ?: "SSLHandshakeException") + "\nTry again later." (throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
) )
} }
else -> safeFail(throwable) else -> safeFail(throwable)

View File

@ -5,15 +5,12 @@ import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.getCookies
import com.lagradost.nicehttp.ignoreAllSSLErrors import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache import okhttp3.Cache
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
fun Requests.initClient(context: Context): OkHttpClient { fun Requests.initClient(context: Context): OkHttpClient {

View File

@ -10,6 +10,9 @@ import java.security.MessageDigest
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object VotingApi { // please do not cheat the votes lol object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi" private const val LOGKEY = "VotingApi"
@ -23,10 +26,10 @@ object VotingApi { // please do not cheat the votes lol
private val apiDomain = "https://api.countapi.xyz" private val apiDomain = "https://api.countapi.xyz"
private fun transformUrl(url: String): String = // dont touch or all votes get reset private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest MessageDigest
.getInstance("SHA-256") .getInstance("SHA-256")
.digest("${url}#funny-salt".toByteArray()) .digest("${url}#funny-salt".toByteArray())
.fold("") { str, it -> str + "%02x".format(it) } .fold("") { str, it -> str + "%02x".format(it) }
suspend fun SitePlugin.getVotes(): Int { suspend fun SitePlugin.getVotes(): Int {
return getVotes(url) return getVotes(url)
@ -53,9 +56,9 @@ object VotingApi { // please do not cheat the votes lol
return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also { return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also {
votesCache[pluginUrl] = it votesCache[pluginUrl] = it
} ?: (0.also { } ?: (0.also {
ioSafe { ioSafe {
createBucket(pluginUrl) createBucket(pluginUrl)
} }
}) })
} }
@ -64,7 +67,8 @@ object VotingApi { // please do not cheat the votes lol
} }
private suspend fun createBucket(pluginUrl: String) { private suspend fun createBucket(pluginUrl: String) {
val url = "${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0" val url =
"${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0"
Log.d(LOGKEY, "Requesting: $url") Log.d(LOGKEY, "Requesting: $url")
app.get(url) app.get(url)
} }
@ -74,32 +78,46 @@ object VotingApi { // please do not cheat the votes lol
return true return true
} }
private val voteLock = Mutex()
suspend fun vote(pluginUrl: String, requestType: VoteType): Int { suspend fun vote(pluginUrl: String, requestType: VoteType): Int {
if (!canVote(pluginUrl)) { // Prevent multiple requests at the same time.
main { voteLock.withLock {
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT).show() if (!canVote(pluginUrl)) {
main {
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT)
.show()
}
return getVotes(pluginUrl)
} }
return getVotes(pluginUrl)
} val savedType: VoteType =
val savedType: VoteType = getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
var newType: VoteType = requestType
var changeValue = 0 val newType = if (requestType == savedType) VoteType.NONE else requestType
if (requestType == savedType) { val changeValue = if (requestType == savedType) {
newType = VoteType.NONE -requestType.value
changeValue = -requestType.value } else if (savedType == VoteType.NONE) {
} else if (savedType == VoteType.NONE) { requestType.value
changeValue = requestType.value } else if (savedType != requestType) {
} else if (savedType != requestType) { -savedType.value + requestType.value
changeValue = -savedType.value + requestType.value } else 0
}
val url = "${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}" // Pre-emptively set vote key
Log.d(LOGKEY, "Requesting: $url")
val res = app.get(url).parsedSafe<Result>()?.value
if (res != null) {
setKey("cs3-votes/${transformUrl(pluginUrl)}", newType) setKey("cs3-votes/${transformUrl(pluginUrl)}", newType)
votesCache[pluginUrl] = res
val url =
"${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
Log.d(LOGKEY, "Requesting: $url")
val res = app.get(url).parsedSafe<Result>()?.value
if (res == null) {
// "Refund" key if the response is invalid
setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType)
} else {
votesCache[pluginUrl] = res
}
return res ?: 0
} }
return res ?: 0
} }
private data class Result( private data class Result(

View File

@ -1,9 +1,11 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import androidx.fragment.app.FragmentActivity
interface OAuth2API : AuthAPI { interface OAuth2API : AuthAPI {
val key: String val key: String
val redirectUrl: String val redirectUrl: String
suspend fun handleRedirect(url: String) : Boolean suspend fun handleRedirect(url: String) : Boolean
fun authenticate() fun authenticate(activity: FragmentActivity?)
} }

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
@ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
removeAccountKeys() removeAccountKeys()
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token" val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
openBrowser(request) openBrowser(request, activity)
} }
override suspend fun handleRedirect(url: String): Boolean { override suspend fun handleRedirect(url: String): Boolean {

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API
@ -15,7 +16,7 @@ class Dropbox : OAuth2API {
override val icon: Int override val icon: Int
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View File

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64 import android.util.Base64
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
@ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return false return false
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
// It is recommended to use a URL-safe string as code_verifier. // It is recommended to use a URL-safe string as code_verifier.
// See section 4 of RFC 7636 for more details. // See section 4 of RFC 7636 for more details.
@ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val codeChallenge = codeVerifier val codeChallenge = codeVerifier
val request = val request =
"$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
openBrowser(request) openBrowser(request, activity)
} }
private var requestId = 0 private var requestId = 0

View File

@ -11,12 +11,12 @@ import android.webkit.WebViewClient
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import kotlinx.android.synthetic.main.fragment_webview.* import kotlinx.android.synthetic.main.fragment_webview.*
import java.net.URI
class WebviewFragment : Fragment() { class WebviewFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -31,16 +31,8 @@ class WebviewFragment : Fragment() {
request: WebResourceRequest? request: WebResourceRequest?
): Boolean { ): Boolean {
val requestUrl = request?.url.toString() val requestUrl = request?.url.toString()
val repoUrl = if (requestUrl.startsWith("https://cs.repo")) { val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true)
"https://" + requestUrl.substringAfter("?") if (performedAction) {
} else if (URI(requestUrl).scheme == appStringRepo) {
requestUrl.replaceFirst(appStringRepo, "https")
} else {
null
}
if (repoUrl != null) {
activity?.loadRepository(repoUrl)
findNavController().popBackStack() findNavController().popBackStack()
return true return true
} }
@ -50,6 +42,7 @@ class WebviewFragment : Fragment() {
} }
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
web_view.settings.javaScriptEnabled = true web_view.settings.javaScriptEnabled = true
web_view.settings.userAgentString = USER_AGENT
web_view.settings.domStorageEnabled = true web_view.settings.domStorageEnabled = true
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString WebViewResolver.webViewUserAgent = web_view.settings.userAgentString

View File

@ -272,12 +272,13 @@ class HomeViewModel : ViewModel() {
if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) {
return@launchSafe return@launchSafe
} }
// If the plugin isn't loaded yet. (Does not set the key)
if (api == null) { if (preferredApiName == noneApi.name) {
loadAndCancel(noneApi)
} else if (preferredApiName == noneApi.name) {
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
loadAndCancel(noneApi) loadAndCancel(noneApi)
// If the plugin isn't loaded yet. (Does not set the key)
} else if (api == null) {
loadAndCancel(noneApi)
} else if (preferredApiName == randomApi.name) { } else if (preferredApiName == randomApi.name) {
val validAPIs = context?.filterProviderByPreferredMedia() val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) { if (validAPIs.isNullOrEmpty()) {

View File

@ -1483,15 +1483,20 @@ class ResultViewModel2 : ViewModel() {
0 -> txt(R.string.no_season) 0 -> txt(R.string.no_season)
else -> { else -> {
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
val seasonData = val seasonData = seasonNames.getSeason(indexer.season)
seasonNames.getSeason(indexer.season)
val suffix = seasonData?.name?.let { " $it" } ?: "" // If displaySeason is null then only show the name!
txt( if (seasonData?.name != null && seasonData.displaySeason == null) {
R.string.season_format, txt(seasonData.name)
txt(R.string.season), } else {
seasonData?.displaySeason ?: indexer.season, val suffix = seasonData?.name?.let { " $it" } ?: ""
suffix txt(
) R.string.season_format,
txt(R.string.season),
seasonData?.displaySeason ?: indexer.season,
suffix
)
}
} }
} }
) )
@ -1587,8 +1592,8 @@ class ResultViewModel2 : ViewModel() {
val idIndex = ep.key.id val idIndex = ep.key.id
for ((index, i) in ep.value.withIndex()) { for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1) val episode = i.episode ?: (index + 1)
val id = mainId + episode + idIndex * 1000000 val id = mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) ?: 0)
if (!existingEpisodes.contains(episode)) { if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id) existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season) val seasonData = loadResponse.seasonNames.getSeason(i.season)
val eps = val eps =
@ -1597,8 +1602,8 @@ class ResultViewModel2 : ViewModel() {
filterName(i.name), filterName(i.name),
i.posterUrl, i.posterUrl,
episode, episode,
null, seasonData?.season ?: i.season,
seasonData?.displaySeason ?: i.season, if (seasonData != null) seasonData.displaySeason else i.season,
i.data, i.data,
loadResponse.apiName, loadResponse.apiName,
id, id,
@ -1610,7 +1615,7 @@ class ResultViewModel2 : ViewModel() {
mainId mainId
) )
val season = eps.season ?: 0 val season = eps.seasonIndex ?: 0
val indexer = EpisodeIndexer(ep.key, season) val indexer = EpisodeIndexer(ep.key, season)
episodes[indexer]?.add(eps) ?: run { episodes[indexer]?.add(eps) ?: run {
episodes[indexer] = mutableListOf(eps) episodes[indexer] = mutableListOf(eps)
@ -1625,15 +1630,14 @@ class ResultViewModel2 : ViewModel() {
mutableMapOf() mutableMapOf()
val existingEpisodes = HashSet<Int>() val existingEpisodes = HashSet<Int>()
for ((index, episode) in loadResponse.episodes.sortedBy { for ((index, episode) in loadResponse.episodes.sortedBy {
(it.season?.times(10000) ?: 0) + (it.episode ?: 0) (it.season?.times(10_000) ?: 0) + (it.episode ?: 0)
}.withIndex()) { }.withIndex()) {
val episodeIndex = episode.episode ?: (index + 1) val episodeIndex = episode.episode ?: (index + 1)
val id = val id =
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1 mainId + (episode.season?.times(100_000) ?: 0) + episodeIndex + 1
if (!existingEpisodes.contains(id)) { if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id) existingEpisodes.add(id)
val seasonIndex = episode.season?.minus(1) val seasonData =
val currentSeason =
loadResponse.seasonNames.getSeason(episode.season) loadResponse.seasonNames.getSeason(episode.season)
val ep = val ep =
@ -1642,8 +1646,8 @@ class ResultViewModel2 : ViewModel() {
filterName(episode.name), filterName(episode.name),
episode.posterUrl, episode.posterUrl,
episodeIndex, episodeIndex,
seasonIndex, seasonData?.season ?: episode.season,
currentSeason?.displaySeason ?: episode.season, if (seasonData != null) seasonData.displaySeason else episode.season,
episode.data, episode.data,
loadResponse.apiName, loadResponse.apiName,
id, id,
@ -1655,7 +1659,7 @@ class ResultViewModel2 : ViewModel() {
mainId mainId
) )
val season = episode.season ?: 0 val season = ep.seasonIndex ?: 0
val indexer = EpisodeIndexer(DubStatus.None, season) val indexer = EpisodeIndexer(DubStatus.None, season)
episodes[indexer]?.add(ep) ?: kotlin.run { episodes[indexer]?.add(ep) ?: kotlin.run {
@ -1747,16 +1751,17 @@ class ResultViewModel2 : ViewModel() {
val seasonData = loadResponse.seasonNames.getSeason(seasonNumber) val seasonData = loadResponse.seasonNames.getSeason(seasonNumber)
val fixedSeasonNumber = seasonData?.displaySeason ?: seasonNumber val fixedSeasonNumber = seasonData?.displaySeason ?: seasonNumber
val suffix = seasonData?.name?.let { " $it" } ?: "" val suffix = seasonData?.name?.let { " $it" } ?: ""
// If displaySeason is null then only show the name!
val name = val name = if (seasonData?.name != null && seasonData.displaySeason == null) {
/*loadResponse.seasonNames?.firstOrNull { it.season == seasonNumber }?.name?.let { seasonData -> txt(seasonData.name)
txt(seasonData) } else {
} ?:*/txt( txt(
R.string.season_format, R.string.season_format,
txt(R.string.season), txt(R.string.season),
fixedSeasonNumber, fixedSeasonNumber,
suffix suffix
) )
}
name to seasonNumber name to seasonNumber
}) })
} }
@ -1812,7 +1817,12 @@ class ResultViewModel2 : ViewModel() {
} }
private fun loadTrailers(loadResponse: LoadResponse) = ioSafe { private fun loadTrailers(loadResponse: LoadResponse) = ioSafe {
_trailers.postValue(getTrailers(loadResponse, 3)) // we dont want to fetch too many trailers _trailers.postValue(
getTrailers(
loadResponse,
3
)
) // we dont want to fetch too many trailers
} }
private suspend fun getTrailers( private suspend fun getTrailers(

View File

@ -1,8 +1,5 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
@ -12,8 +9,10 @@ import androidx.annotation.UiThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -39,7 +38,11 @@ import kotlinx.android.synthetic.main.add_account_input.*
class SettingsAccount : PreferenceFragmentCompat() { class SettingsAccount : PreferenceFragmentCompat() {
companion object { companion object {
/** Used by nginx plugin too */ /** Used by nginx plugin too */
fun showLoginInfo(activity: Activity?, api: AccountManager, info: AuthAPI.LoginInfo) { fun showLoginInfo(
activity: FragmentActivity?,
api: AccountManager,
info: AuthAPI.LoginInfo
) {
val builder = val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment) .setView(R.layout.account_managment)
@ -62,9 +65,13 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
showAccountSwitch(activity, api) showAccountSwitch(activity, api)
} }
if (isTvSettings()) {
dialog.account_switch_account?.requestFocus()
}
} }
fun showAccountSwitch(activity: Activity, api: AccountManager) { fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) {
val accounts = api.getAccounts() ?: return val accounts = api.getAccounts() ?: return
val builder = val builder =
@ -98,11 +105,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
@UiThread @UiThread
fun addAccount(activity: Activity?, api: AccountManager) { fun addAccount(activity: FragmentActivity?, api: AccountManager) {
try { try {
when (api) { when (api) {
is OAuth2API -> { is OAuth2API -> {
api.authenticate() api.authenticate(activity)
} }
is InAppAuthAPI -> { is InAppAuthAPI -> {
val builder = val builder =
@ -144,13 +151,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.login_username_input?.isVisible = api.requiresUsername dialog.login_username_input?.isVisible = api.requiresUsername
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
dialog.create_account?.setOnClickListener { dialog.create_account?.setOnClickListener {
val i = Intent(Intent.ACTION_VIEW) openBrowser(
i.data = Uri.parse(api.createAccountUrl) api.createAccountUrl ?: return@setOnClickListener,
try { activity
activity.startActivity(i) )
} catch (e: Exception) { dialog.dismissSafe()
logError(e)
}
} }
dialog.text1?.text = api.name dialog.text1?.text = api.name
@ -181,9 +186,10 @@ class SettingsAccount : PreferenceFragmentCompat() {
try { try {
showToast( showToast(
activity, activity,
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
api.name .format(
) api.name
)
) )
} catch (e: Exception) { } catch (e: Exception) {
logError(e) // format might fail logError(e) // format might fail

View File

@ -76,7 +76,7 @@ class RepoAdapter(
imageClickCallback(repositoryData) imageClickCallback(repositoryData)
} }
itemView.setOnClickListener { itemView.repository_item_root?.setOnClickListener {
clickCallback(repositoryData) clickCallback(repositoryData)
} }
itemView.main_text?.text = repositoryData.name itemView.main_text?.text = repositoryData.name

View File

@ -31,6 +31,7 @@ import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -415,7 +416,7 @@ object AppUtils {
} }
} }
fun AppCompatActivity.loadResult( fun FragmentActivity.loadResult(
url: String, url: String,
apiName: String, apiName: String,
startAction: Int = 0, startAction: Int = 0,

View File

@ -236,6 +236,8 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Ssbstream(), Ssbstream(),
Sbthe(), Sbthe(),
Vidgomunime(), Vidgomunime(),
Sbflix(),
Streamsss(),
Fastream(), Fastream(),
@ -269,6 +271,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
DoodWsExtractor(), DoodWsExtractor(),
DoodShExtractor(), DoodShExtractor(),
DoodWatchExtractor(), DoodWatchExtractor(),
DoodWfExtractor(),
AsianLoad(), AsianLoad(),
@ -321,6 +324,18 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Mvidoo(), Mvidoo(),
Streamplay(), Streamplay(),
Gdriveplayerapi(),
Gdriveplayerapp(),
Gdriveplayerfun(),
Gdriveplayerio(),
Gdriveplayerme(),
Gdriveplayerbiz(),
Gdriveplayerorg(),
Gdriveplayerus(),
Gdriveplayerco(),
Gdriveplayer(),
DatabaseGdrive(),
YoutubeExtractor(), YoutubeExtractor(),
YoutubeShortLinkExtractor(), YoutubeShortLinkExtractor(),
YoutubeMobileExtractor(), YoutubeMobileExtractor(),

View File

@ -200,16 +200,23 @@ class InAppUpdater {
private suspend fun Activity.downloadUpdate(url: String): Boolean { private suspend fun Activity.downloadUpdate(url: String): Boolean {
try { try {
Log.d(LOG_TAG, "Downloading update: $url") Log.d(LOG_TAG, "Downloading update: $url")
val appUpdateName = "CloudStream"
val appUpdateSuffix = "apk"
val localContext = this // Delete all old updates
this.cacheDir.listFiles()?.filter {
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
}?.forEach {
it.deleteOnExit()
}
val downloadedFile = File.createTempFile("CloudStream", ".apk") val downloadedFile = File.createTempFile(appUpdateName, ".$appUpdateSuffix")
val sink: BufferedSink = downloadedFile.sink().buffer() val sink: BufferedSink = downloadedFile.sink().buffer()
updateLock.withLock { updateLock.withLock {
sink.writeAll(app.get(url).body.source()) sink.writeAll(app.get(url).body.source())
sink.close() sink.close()
openApk(localContext, Uri.fromFile(downloadedFile)) openApk(this, Uri.fromFile(downloadedFile))
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -1,131 +1,134 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="match_parent"
android:layout_height="match_parent"> android:orientation="vertical">
<FrameLayout
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_rowWeight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Test" />
<com.google.android.material.button.MaterialButton
style="@style/WhiteButton"
android:layout_gravity="center_vertical|end"
app:icon="@drawable/ic_baseline_add_24"
android:text="@string/create_account"
android:id="@+id/create_account"
android:layout_width="wrap_content" />
</FrameLayout>
<LinearLayout <LinearLayout
android:orientation="vertical" android:layout_width="match_parent"
android:layout_marginBottom="60dp" android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp" android:layout_marginTop="20dp"
android:layout_width="match_parent" android:layout_marginBottom="10dp"
android:layout_height="wrap_content"> android:orientation="horizontal">
<EditText <TextView
android:textColorHint="?attr/grayTextColor" android:id="@+id/text1"
android:hint="@string/example_username" android:layout_weight="1"
android:autofillHints="username" android:layout_width="0dp"
android:id="@+id/login_username_input" android:layout_height="wrap_content"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusDown="@id/login_email_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
tools:ignore="LabelFor" />
<EditText android:layout_rowWeight="1"
android:textColorHint="?attr/grayTextColor" android:layout_gravity="center_vertical"
android:autofillHints="emailAddress"
android:hint="@string/example_email"
android:id="@+id/login_email_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/login_username_input"
android:nextFocusDown="@id/login_server_input"
android:requiresFadingEdge="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:layout_width="match_parent" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_height="wrap_content" android:textColor="?attr/textColor"
android:inputType="textEmailAddress" android:textSize="20sp"
tools:ignore="LabelFor" /> android:textStyle="bold"
tools:text="Test" />
<EditText <com.google.android.material.button.MaterialButton
android:textColorHint="?attr/grayTextColor" android:id="@+id/create_account"
android:hint="@string/example_ip" style="@style/WhiteButton"
android:id="@+id/login_server_input" android:layout_width="wrap_content"
android:nextFocusRight="@id/cancel_btt" android:layout_gravity="center_vertical|end"
android:nextFocusLeft="@id/apply_btt" android:text="@string/create_account"
android:nextFocusUp="@id/login_email_input" app:icon="@drawable/ic_baseline_add_24" />
android:nextFocusDown="@id/login_password_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
tools:ignore="LabelFor" />
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_password"
android:id="@+id/login_password_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/login_server_input"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textVisiblePassword"
tools:ignore="LabelFor"
android:autofillHints="password" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/apply_btt_holder" android:layout_width="match_parent"
android:orientation="horizontal" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_marginHorizontal="10dp"
android:gravity="bottom|end" android:layout_marginBottom="60dp"
android:layout_marginTop="-60dp" android:orientation="vertical">
<EditText
android:id="@+id/login_username_input"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="60dp"> android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="@string/example_username"
android:inputType="text"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusDown="@id/login_email_input"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<EditText
android:id="@+id/login_email_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="emailAddress"
android:hint="@string/example_email"
android:inputType="textEmailAddress"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusUp="@id/login_username_input"
android:nextFocusDown="@id/login_server_input"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<EditText
android:id="@+id/login_server_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/example_ip"
android:inputType="textUri"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusUp="@id/login_email_input"
android:nextFocusDown="@id/login_password_input"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<EditText
android:id="@+id/login_password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:hint="@string/example_password"
android:inputType="textVisiblePassword"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusUp="@id/login_server_input"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:id="@+id/apply_btt_holder"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="bottom"
android:layout_marginTop="-60dp"
android:gravity="bottom|end"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/WhiteButton" android:id="@+id/apply_btt"
android:layout_gravity="center_vertical|end" style="@style/WhiteButton"
android:text="@string/login" android:layout_width="wrap_content"
android:id="@+id/apply_btt" android:layout_gravity="center_vertical|end"
android:layout_width="wrap_content" /> android:text="@string/login" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/BlackButton" android:id="@+id/cancel_btt"
android:layout_gravity="center_vertical|end" style="@style/BlackButton"
android:text="@string/sort_cancel" android:layout_width="wrap_content"
android:id="@+id/cancel_btt" android:layout_gravity="center_vertical|end"
android:layout_width="wrap_content" /> android:text="@string/sort_cancel" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -102,18 +102,21 @@
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_cartoons" android:nextFocusLeft="@id/home_select_cartoons"
android:nextFocusRight="@id/home_select_livestreams"
android:text="@string/documentaries" /> android:text="@string/documentaries" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_livestreams" android:id="@+id/home_select_livestreams"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_documentaries" android:nextFocusLeft="@id/home_select_documentaries"
android:nextFocusRight="@id/home_select_nsfw"
android:text="@string/livestreams" /> android:text="@string/livestreams" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_nsfw" android:id="@+id/home_select_nsfw"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_livestreams" android:nextFocusLeft="@id/home_select_livestreams"
android:nextFocusRight="@id/home_select_nsfw"
android:text="@string/nsfw" /> android:text="@string/nsfw" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@ -136,21 +136,23 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/search_select_documentaries" android:id="@+id/search_select_documentaries"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/search_select_cartoons" android:nextFocusLeft="@id/search_select_cartoons"
android:nextFocusRight="@id/search_select_livestreams"
android:text="@string/documentaries" /> android:text="@string/documentaries" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/search_select_livestreams" android:id="@+id/search_select_livestreams"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/search_select_documentaries" android:nextFocusLeft="@id/search_select_documentaries"
android:nextFocusRight="@id/search_select_nsfw"
android:text="@string/livestreams" /> android:text="@string/livestreams" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/search_select_nsfw" android:id="@+id/search_select_nsfw"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/search_select_livestreams" android:nextFocusLeft="@id/search_select_livestreams"
android:nextFocusRight="@id/search_select_others"
android:text="@string/nsfw" /> android:text="@string/nsfw" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@ -31,7 +31,7 @@
android:paddingEnd="10dp" android:paddingEnd="10dp"
android:requiresFadingEdge="horizontal"> android:requiresFadingEdge="horizontal">
<!-- Man what the fuck we need a recyclerview --> <!-- Man what the fuck we need a recyclerview -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -93,21 +93,23 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_documentaries" android:id="@+id/home_select_documentaries"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_cartoons" android:nextFocusLeft="@id/home_select_cartoons"
android:nextFocusRight="@id/home_select_livestreams"
android:text="@string/documentaries" /> android:text="@string/documentaries" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_livestreams" android:id="@+id/home_select_livestreams"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_documentaries" android:nextFocusLeft="@id/home_select_documentaries"
android:nextFocusRight="@id/home_select_nsfw"
android:text="@string/livestreams" /> android:text="@string/livestreams" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_select_nsfw" android:id="@+id/home_select_nsfw"
style="@style/RoundedSelectableButton" style="@style/RoundedSelectableButton"
android:nextFocusLeft="@id/home_select_livestreams" android:nextFocusLeft="@id/home_select_livestreams"
android:nextFocusRight="@id/home_select_others"
android:text="@string/nsfw" /> android:text="@string/nsfw" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@ -101,7 +101,7 @@
<TextView <TextView
android:id="@+id/settings_extensions" android:id="@+id/settings_extensions"
style="@style/SettingsItem" style="@style/SettingsItem"
android:nextFocusUp="@id/settings_updates" android:nextFocusUp="@id/settings_credits"
android:text="@string/extensions" /> android:text="@string/extensions" />
<LinearLayout <LinearLayout

View File

@ -8,6 +8,8 @@
android:background="@drawable/outline_drawable" android:background="@drawable/outline_drawable"
android:nextFocusRight="@id/action_button" android:nextFocusRight="@id/action_button"
android:orientation="horizontal" android:orientation="horizontal"
android:clickable="true"
android:focusable="true"
android:padding="12dp"> android:padding="12dp">
<ImageView <ImageView

View File

@ -5,57 +5,65 @@
<string name="episode_poster_img_des">ملصق الحلقة</string> <string name="episode_poster_img_des">ملصق الحلقة</string>
<string name="home_main_poster_img_des">الملصق الرئيسي</string> <string name="home_main_poster_img_des">الملصق الرئيسي</string>
<string name="home_next_random_img_des">التالي عشوائي</string> <string name="home_next_random_img_des">التالي عشوائي</string>
<string name="go_back_img_des">ارجع للخلف</string> <string name="go_back_img_des">ارجع للخلف</string>
<string name="home_change_provider_img_des">تغيير المصدر</string> <string name="home_change_provider_img_des">تغيير المصدر</string>
<string name="preview_background_img_des">معاينة الخلفية</string> <string name="preview_background_img_des">معاينة الخلفية</string>
<!-- TRANSLATE, BUT DON'T FORGET FORMAT --> <!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">سرعة (%.2fx)</string> <string name="player_speed_text_format" formatted="true">سرعة (%.2fx)</string>
<string name="rated_format" formatted="true">Rated: %.1f</string> <string name="rated_format" formatted="true">Rated: %.1f</string>
<string name="new_update_format" formatted="true">!تم إيجاد تحديث جديد\n%s -> %s</string> <string name="new_update_format" formatted="true">!تم إيجاد تحديث جديد\n%s -> %s</string>
<string name="duration_format" formatted="true">%d دقيقة</string> <string name="duration_format" formatted="true">%d دقيقة</string>
<string name="app_name">CloudStream</string> <string name="app_name">CloudStream</string>
<string name="play_with_app_name">تشغيل بواسطة CloudStream</string>
<string name="title_home">الصفحة الرئيسية</string> <string name="title_home">الصفحة الرئيسية</string>
<string name="title_search">بحث</string> <string name="title_search">البحث</string>
<string name="title_downloads">التحميلات</string> <string name="title_downloads">التحميلات</string>
<string name="title_settings">الإعدادات</string> <string name="title_settings">الإعدادات</string>
<string name="search_hint">…بحث</string> <string name="search_hint">…بحث</string>
<string name="search_hint_site" formatted="true">بحث %s…</string>
<string name="no_data">لايوجد بيانات</string> <string name="no_data">لايوجد بيانات</string>
<string name="episode_more_options_des">المزيد من الخيارات</string> <string name="episode_more_options_des">المزيد من الخيارات</string>
<string name="next_episode">الحلقة التالية</string> <string name="next_episode">الحلقة التالية</string>
<string name="result_tags">النواع</string> <string name="result_tags">أنواع</string>
<string name="result_share">شارك</string> <string name="result_share">شارك</string>
<string name="result_open_in_browser">فتح في الويب </string> <string name="result_open_in_browser">فتح في الويب</string>
<string name="skip_loading">تخطي التحميل</string> <string name="skip_loading">تخطي التحميل</string>
<string name="loading">…تحميل</string> <string name="loading">…تحميل</string>
<string name="type_watching">مشاهدة</string> <string name="type_watching">أشاهده</string>
<string name="type_on_hold">في الانتظار</string> <string name="type_on_hold">في الانتظار</string>
<string name="type_completed">مكتمل</string> <string name="type_completed">مكتمل</string>
<string name="type_dropped">إسقاط</string> <string name="type_dropped">مهمل</string>
<string name="type_plan_to_watch">تخطط للمشاهدة</string> <string name="type_plan_to_watch">أخطط لمشاهدته</string>
<string name="type_none">لا شيء</string> <string name="type_none">لا شيء</string>
<string name="type_re_watching">إعادة المشاهدة</string> <string name="type_re_watching">إعادة المشاهدة</string>
<string name="play_movie_button">مشاهدة الفيلم</string> <string name="play_movie_button">مشاهدة الفيلم</string>
<string name="play_torrent_button">تشغيل التورنت</string> <string name="play_livestream_button">تشغيل بث حي</string>
<string name="play_torrent_button">تشغيل تورنت</string>
<string name="pick_source">المصادر</string> <string name="pick_source">المصادر</string>
<string name="pick_subtitle">الترجمة</string> <string name="pick_subtitle">الترجمة</string>
<string name="reload_error">…إعادة محاولة الاتصال</string> <string name="reload_error">…إعادة محاولة الاتصال</string>
<string name="go_back">ارجع للخلف</string> <string name="go_back">ارجع للخلف</string>
<string name="play_episode">تشغيل الحلقة</string> <string name="play_episode">تشغيل الحلقة</string>
<!--<string name="need_storage">السماح بتحميل الحلقات</string>--> <!--<string name="need_storage">السماح بتنزيل الحلقات</string>-->
<string name="download">تحميل</string> <string name="download">تحميل</string>
<string name="downloaded">تم التنزيل</string> <string name="downloaded">تم التنزيل</string>
<string name="downloading">جارى التحميل</string> <string name="downloading">جاري التنزيل</string>
<string name="download_paused">توقف التنزيل مؤقتًا</string> <string name="download_paused">توقف التنزيل مؤقتًا</string>
<string name="download_started">بدأ التنزيل</string> <string name="download_started">بدأ التنزيل</string>
<string name="download_failed">التحميل فشل</string> <string name="download_failed">فشل التنزيل</string>
<string name="download_canceled">تم إلغاء التنزيل</string> <string name="download_canceled">تم إلغاء التنزيل</string>
<string name="download_done">تنزيل تم</string> <string name="download_done">تم التنزيل</string>
<string name="stream">تشغيل</string>
<string name="error_loading_links_toast">خطأ في تحميل الرابط</string> <string name="error_loading_links_toast">خطأ في تحميل الرابط</string>
<string name="download_storage_text">التخزين الداخلي</string> <string name="download_storage_text">التخزين الداخلي</string>
@ -73,12 +81,18 @@
<string name="home_expanded_hide">إخفاء</string> <string name="home_expanded_hide">إخفاء</string>
<string name="home_play">تشغيل</string> <string name="home_play">تشغيل</string>
<string name="home_info">معلومات</string> <string name="home_info">معلومات</string>
<string name="filter_bookmarks">تصفية المواقع المفضلة</string> <string name="filter_bookmarks">تصفية الاشارات المرجعية</string>
<string name="error_bookmarks_text">إشارات مرجعية</string> <string name="error_bookmarks_text">إشارات مرجعية</string>
<string name="action_remove_from_bookmarks">حذف</string> <string name="action_remove_from_bookmarks">حذف</string>
<string name="action_add_to_bookmarks">إعداد حالة المشاهدة</string>
<string name="sort_apply">تطبيق</string> <string name="sort_apply">تطبيق</string>
<string name="sort_cancel">إلغاء</string> <string name="sort_cancel">إلغاء</string>
<string name="player_speed">سرعة المشغل</string> <string name="sort_copy">نسخ</string>
<string name="sort_close">إغلاق</string>
<string name="sort_clear">مسح</string>
<string name="sort_save">حفظ</string>
<string name="player_speed">سرعة المُشغل</string>
<string name="subtitles_settings">إعدادات الترجمة</string> <string name="subtitles_settings">إعدادات الترجمة</string>
<string name="subs_text_color">لون الخط</string> <string name="subs_text_color">لون الخط</string>
@ -90,65 +104,97 @@
<string name="subs_font">الخط</string> <string name="subs_font">الخط</string>
<string name="subs_font_size">حجم الخط</string> <string name="subs_font_size">حجم الخط</string>
<string name="search_provider_text_providers">ابحث باستخدام المصادر</string> <string name="search_provider_text_providers">البحث باستخدام المصادر</string>
<string name="search_provider_text_types">البحث باستخدام الأنواع</string> <string name="search_provider_text_types">البحث باستخدام الأنواع</string>
<string name="benene_count_text">%d البنينيس المعطاة الى المطورين</string> <string name="benene_count_text">%d الموزات المعطاة الى المطورين</string>
<string name="benene_count_text_none">لم يتم إعطاء بنين</string> <string name="benene_count_text_none">لم يتم إعطاء موز</string>
<string name="subs_auto_select_language">تحديد اللغة تلقائيًا</string> <string name="subs_auto_select_language">تحديد اللغة تلقائيًا</string>
<string name="subs_download_languages">تحميل اللغات</string> <string name="subs_download_languages">تحميل اللغات</string>
<string name="subs_hold_to_reset_to_default">اضغط بإستمرار لإعادة التعيين</string> <string name="subs_subtitle_languages">لغة الترجمة</string>
<string name="continue_watching">استمر في المشاهدة</string> <string name="subs_hold_to_reset_to_default">إضغط بإستمرار لإعادة التعيين للإعدادات الافتراضية</string>
<string name="subs_import_text" formatted="true">إستيراد خطوط بوضعها هنا %s</string>
<string name="continue_watching">متابعة المشاهدة</string>
<string name="action_remove_watching">حذف</string> <string name="action_remove_watching">حذف</string>
<string name="action_open_watching">مزيد من المعلومات</string> <string name="action_open_watching">مزيد من المعلومات</string>
<string name="vpn_might_be_needed">قد تكون هناك حاجة إلى شبكة ظاهرية خاصة لكي يعمل هذا المزود بشكل صحيح</string> <string name="vpn_might_be_needed">قد تكون هناك حاجة إلى VPN لكي يعمل هذا المزود بشكل صحيح</string>
<string name="vpn_torrent">هذا المزود هو تورنت ، يوصى باستخدام شبكة ظاهرية خاصة</string> <string name="vpn_torrent">هذا المزود هو تورنت ، يوصى باستخدام شبكة ظاهرية خاصة</string>
<string name="provider_info_meta">.سيظهر إعلان مدته خمسة عشر ثانية إذا لم يتم توفير الفيديو في الموقع</string> <string name="provider_info_meta">.سيظهر إعلان مدته خمسة عشر ثانية إذا لم يتم توفير الفيديو في الموقع</string>
<string name="torrent_plot">الوصف</string> <string name="torrent_plot">الوصف</string>
<string name="normal_no_plot">لم يتم العثور على وصف</string> <string name="normal_no_plot">لم يتم العثور على وصف</string>
<string name="torrent_no_plot">لم يتم العثور على وصف</string> <string name="torrent_no_plot">لم يتم العثور على وصف</string>
<string name="show_log_cat">🐈 logcat عرض</string>
<string name="picture_in_picture">نافذة منبثقة</string> <string name="picture_in_picture">نافذة منبثقة</string>
<string name="picture_in_picture_des">يستمر في التشغيل في مشغل مصغر فوق التطبيقات الأخرى</string> <string name="picture_in_picture_des">يستمر في التشغيل في مُشغل مصغر فوق التطبيقات الأخرى</string>
<string name="player_size_settings">زر تغيير حجم المشغل</string> <string name="player_size_settings">زر تغيير حجم المُشغل</string>
<string name="player_size_settings_des">قم بإزالة الحدود السوداء</string> <string name="player_size_settings_des">إزالة الحدود السوداء</string>
<string name="player_subtitles_settings">الترجمة</string> <string name="player_subtitles_settings">الترجمة</string>
<string name="player_subtitles_settings_des">إعدادات ترجمة المشغل</string> <string name="player_subtitles_settings_des">إعدادات ترجمة المُشغل</string>
<string name="chromecast_subtitles_settings">ترجمة كروم كاست</string>
<string name="chromecast_subtitles_settings_des">إعدادات ترجمة كروم كاست</string>
<string name="eigengraumode_settings">وضع إيغنغرافي</string> <string name="eigengraumode_settings">وضع إيغنغرافي</string>
<string name="eigengraumode_settings_des">يضيف خيار السرعة في المشغل</string> <string name="eigengraumode_settings_des">يضيف خيار السرعة في المُشغل</string>
<string name="swipe_to_seek_settings">اسحب للسعي</string> <string name="swipe_to_seek_settings">السحب للسعي</string>
<string name="swipe_to_seek_settings_des">اسحب إلى اليسار أو اليمين للتحكم في الوقت في مشغل الفيديو</string> <string name="swipe_to_seek_settings_des">إسحب إلى اليسار أو اليمين للتحكم في الوقت في مُشغل الفيديو</string>
<string name="swipe_to_change_settings">اسحب لتغيير الإعدادات</string> <string name="swipe_to_change_settings">السحب لتغيير الإعدادات</string>
<string name="swipe_to_change_settings_des">اسحب على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت</string> <string name="swipe_to_change_settings_des">إسحب على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت</string>
<string name="double_tap_to_seek_settings">انقر مرتين للسعي للأمام أو للخلف</string>
<string name="double_tap_to_pause_settings">اضغط مرتين لإيقاف مؤقت</string> <string name="autoplay_next_settings">تشغيل الحلقة التالية تلقائيًا</string>
<string name="double_tap_to_seek_settings_des">اضغط مرتين على الجانب الأيمن أو الأيسر للسعي للأمام أو للخلف </string> <string name="autoplay_next_settings_des">تبدأ الحلقة التالية عندما تنتهي الحالية</string>
<string name="double_tap_to_pause_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="double_tap_to_seek_settings_des">إضغط مرتين على الجانب الأيمن أو الأيسر للسعي للأمام أو للخلف </string>
<string name="double_tap_to_pause_settings_des">إضغط في الوسط لإيقاف مؤقت</string>
<string name="use_system_brightness_settings">استخدم سطوع النظام</string> <string name="use_system_brightness_settings">استخدم سطوع النظام</string>
<string name="use_system_brightness_settings_des">استخدم سطوع النظام في مشغل التطبيق بدلاً من التراكب الداكن</string> <string name="use_system_brightness_settings_des">استخدم سطوع النظام في مُشغل التطبيق بدلاً من التراكب الداكن</string>
<string name="episode_sync_settings">تحديث تقدم المشاهدة</string>
<string name="episode_sync_settings_des">مزامنة التقدم في الحلقة الحالية تلقائيًا</string>
<string name="restore_settings">إسترجاع البيانات من نسخة إحتياطية</string>
<string name="backup_settings">نسخ إحتياطي</string>
<string name="restore_success">تم تحميل ملف النسخة الاحتياطية</string>
<string name="restore_failed_format" formatted="true">فشل استيراد البيانات من الملف %s</string>
<string name="backup_success">تم تخزين البيانات بنجاح</string>
<string name="backup_failed">إذن الوصول الي ذاكرة التخزين مفقود, من فضلك حاول مجددا</string>
<string name="backup_failed_error_format">فشل إنشاء نسخة احتياطية %s</string>
<string name="search">بحث</string> <string name="search">بحث</string>
<string name="category_account">الحسابات</string>
<string name="category_updates">التحديثات والنسخ الاحتياطية</string>
<string name="settings_info">معلومات</string> <string name="settings_info">معلومات</string>
<string name="advanced_search">البحث المتقدم</string> <string name="advanced_search">البحث المتقدم</string>
<string name="advanced_search_des">يعطيك نتائج البحث مفصولة عن طريق المزود</string> <string name="advanced_search_des">يعطيك نتائج البحث مفصولة عن طريق المزود</string>
<string name="bug_report_settings_off">إرسال البيانات عن الأعطال فقط</string> <string name="bug_report_settings_off">إرسال البيانات عند الأعطال فقط</string>
<string name="bug_report_settings_on">لا ترسل أي بيانات</string> <string name="bug_report_settings_on">لا ترسل أي بيانات</string>
<string name="show_fillers_settings">عرض حلقة فلر لأنيمي</string> <string name="show_fillers_settings">عرض حلقات الفلر للأنمي</string>
<string name="show_trailers_settings">عرض المقاطع الدعائية</string>
<string name="kitsu_settings">عرض ملصقات من kitsu</string>
<string name="pref_filter_search_quality">إخفاء جودة الفيديو المختارة من نتائج البحث</string>
<string name="automatic_plugin_updates">تحديث الإضافات تلقائيًا</string>
<string name="updates_settings">التحديث التلقائي</string> <string name="updates_settings">التحديث التلقائي</string>
<string name="updates_settings_des">ابحث تلقائيًا عن التحديثات الجديدة عند البداية</string> <string name="updates_settings_des">البحث تلقائيًا عن التحديثات الجديدة عند البداية</string>
<string name="uprereleases_settings">التحديث إلى الاصدارات التجريبيه (بيتا)</string> <string name="uprereleases_settings">التحديث إلى الاصدارات التجريبيه (بيتا)</string>
<string name="uprereleases_settings_des">ابحث عن التحديثات التجريبية بدلاً من الإصدارات الكاملة فقط</string> <string name="uprereleases_settings_des">البحث عن التحديثات التجريبية بدلاً من الإصدارات الكاملة فقط</string>
<string name="github">Github</string> <string name="github">Github</string>
<string name="lightnovel">تطبيق رواية خفيف من نفس المطورين</string> <string name="lightnovel">تطبيق روايات خفيف من نفس المطورين</string>
<string name="anim">تطبيق Anime من نفس المطورين</string> <string name="anim">تطبيق أنمي من نفس المطورين</string>
<string name="discord">انضم إلى إلديسكورد</string> <string name="discord">انضم إلى الديسكورد</string>
<string name="benene">أعط موزة للمطورين</string> <string name="benene">إعط موزة للمطورين</string>
<string name="benene_des">أعط الموز</string> <string name="benene_des">الموز المُعطي</string>
<string name="app_language">لغة التطبيق</string> <string name="app_language">لغة التطبيق</string>
@ -158,23 +204,24 @@
<string name="play_episode_toast">تشغيل الحلقة</string> <string name="play_episode_toast">تشغيل الحلقة</string>
<string name="subs_default_reset_toast">إعادة التعيين إلى القيمة الافتراضية</string> <string name="subs_default_reset_toast">إعادة التعيين إلى القيمة الافتراضية</string>
<string name="acra_report_toast">عذرا ، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين</string> <string name="acra_report_toast">عذرا ، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين</string>
<string name="season">موسم</string> <string name="season">موسم</string>
<string name="no_season">لا موسم</string> <string name="no_season">لا موسم</string>
<string name="episode">حلقة</string> <string name="episode">حلقة</string>
<string name="episodes">حلقات</string> <string name="episodes">حلقات</string>
<string name="season_short">S</string> <string name="season_short">ح</string>
<string name="episode_short">E</string> <string name="episode_short">م</string>
<string name="no_episodes_found">لم يتم العثور على أي حلقات</string> <string name="no_episodes_found">لم يتم العثور على أي حلقات</string>
<string name="delete_file">حذف ملف</string> <string name="delete_file">حذف الملف</string>
<string name="delete">حذف</string> <string name="delete">حذف</string>
<string name="pause">إيقاف مؤقت</string> <string name="pause">إيقاف مؤقت</string>
<string name="resume">أكمل</string> <string name="resume">أكمل</string>
<string name="go_back_30">-30</string> <string name="go_back_30">-٣٠</string>
<string name="go_forward_30">+30</string> <string name="go_forward_30">+٣٠</string>
<string name="delete_message">This will permanently delete %s\nAre you sure?</string> <string name="delete_message">سوف يتم الحذف نهائيا %s\nهل أنت متأكد?</string>
<string name="resume_time_left" formatted="true">%dد\nمتبقية</string> <string name="resume_time_left" formatted="true">%dm\nمتبقية</string>
<string name="status_ongoing">جاري التنفيذ</string> <string name="status_ongoing">جاري التنفيذ</string>
<string name="status_completed">اكتمل</string> <string name="status_completed">اكتمل</string>
@ -189,28 +236,39 @@
<string name="no_subtitles">الترجمة ليست موجودة</string> <string name="no_subtitles">الترجمة ليست موجودة</string>
<string name="default_subtitles">الإفتراضي</string> <string name="default_subtitles">الإفتراضي</string>
<string name="free_storage">حر</string> <string name="free_storage">فارغ</string>
<string name="used_storage">مستخدم</string> <string name="used_storage">مستخدم</string>
<string name="app_storage">تطبيق</string> <string name="app_storage">التطبيق</string>
<!--plural-->
<string name="movies">أفلام</string> <string name="movies">أفلام</string>
<string name="tv_series">مسلسلات</string> <string name="tv_series">مسلسلات</string>
<string name="cartoons">رسوم متحركة</string> <string name="cartoons">رسوم متحركة</string>
<string name="anime">انمي</string> <string name="anime">أنمي</string>
<string name="torrent">تورنت</string> <string name="torrent">تورنت</string>
<string name="documentaries">الافلام الوثائقية</string> <string name="documentaries">أفلام وثائقية</string>
<string name="ova">OVA</string> <string name="ova">أوفا</string>
<string name="asian_drama">دراما آسيوية</string>
<string name="livestreams">بث حي</string>
<string name="nsfw">+18</string>
<string name="others">أخري</string>
<!--singular-->
<string name="movies_singular">فيلم</string> <string name="movies_singular">فيلم</string>
<string name="tv_series_singular">مسلسلات</string> <string name="tv_series_singular">مسلسل</string>
<string name="cartoons_singular">كارتون</string> <string name="cartoons_singular">كارتون</string>
<string name="anime_singular">@string/anime</string>
<string name="ova_singular">@string/ova</string>
<string name="torrent_singular">تورنت</string> <string name="torrent_singular">تورنت</string>
<string name="documentaries_singular">وثائقي</string> <string name="documentaries_singular">وثائقي</string>
<string name="asian_drama_singular">دراما آسيوية</string>
<string name="live_singular">بث حي</string>
<string name="nsfw_singular">+18</string>
<string name="other_singular">فيديو</string>
<string name="source_error">خطأ في المصدر</string> <string name="source_error">خطأ في المصدر</string>
<string name="remote_error">خطأ بعيد</string> <string name="remote_error">خطأ بعيد</string>
<string name="render_error">خطأ في جهاز العرض</string> <string name="render_error">خطأ في جهاز العرض</string>
<string name="unexpected_error">خطأ غير متوقع في مشغل</string> <string name="unexpected_error">خطأ غير متوقع في المُشغل</string>
<string name="storage_error">خطأ في التنزيل ، تحقق من أذونات التخزين</string> <string name="storage_error">خطأ في التنزيل ، تحقق من أذونات التخزين</string>
<string name="episode_action_chromecast_episode">حلقة كروم كاست</string> <string name="episode_action_chromecast_episode">حلقة كروم كاست</string>
@ -218,10 +276,21 @@
<string name="episode_action_play_in_app">تشغيل في التطبيق</string> <string name="episode_action_play_in_app">تشغيل في التطبيق</string>
<string name="episode_action_play_in_vlc">VLC تشغيل في</string> <string name="episode_action_play_in_vlc">VLC تشغيل في</string>
<string name="episode_action_play_in_browser">تشغيل في الويب </string> <string name="episode_action_play_in_browser">تشغيل في الويب </string>
<string name="episode_action_copy_link">انسخ الرابط</string> <string name="episode_action_copy_link">نسخ الرابط</string>
<string name="episode_action_auto_download">التحميل التلقائي</string> <string name="episode_action_auto_download">التحميل التلقائي</string>
<string name="episode_action_download_mirror">تحميل المرآة</string> <string name="episode_action_download_mirror">تحميل بجودات مختلفة</string>
<string name="episode_action_reload_links">إعادة تحميل الروابط</string> <string name="episode_action_reload_links">إعادة تحميل الروابط</string>
<string name="episode_action_download_subtitle">تحميل الترجمة</string>
<string name="show_hd">ملصق الجودة</string>
<string name="show_dub">ملصق مدبلج</string>
<string name="show_sub">ملصق مترجم</string>
<string name="show_title">العنوان</string>
<string name="show_hd_key" translatable="false">show_hd_key</string>
<string name="show_dub_key" translatable="false">show_dub_key</string>
<string name="show_sub_key" translatable="false">show_sub_key</string>
<string name="show_title_key" translatable="false">show_title_key</string>
<string name="poster_ui_settings">التحكم في عناصر الواجهة علي الملصق </string>
<string name="no_update_found">لم يتم العثور على تحديث</string> <string name="no_update_found">لم يتم العثور على تحديث</string>
<string name="check_for_update">تحقق من التحديثات</string> <string name="check_for_update">تحقق من التحديثات</string>
@ -229,61 +298,244 @@
<string name="video_lock">قفل</string> <string name="video_lock">قفل</string>
<string name="video_aspect_ratio_resize">تغيير الحجم</string> <string name="video_aspect_ratio_resize">تغيير الحجم</string>
<string name="video_source">مصدر</string> <string name="video_source">مصدر</string>
<string name="video_skip_op">OP تخطي</string> <string name="video_skip_op">تخطي المقدمة</string>
<string name="dont_show_again">لا تظهر مرة أخرى</string> <string name="dont_show_again">لا تظهر مرة أخرى</string>
<string name="skip_update">تخطي هذا التحديث</string>
<string name="update">تحديث</string> <string name="update">تحديث</string>
<string name="watch_quality_pref">جودة المشاهدة المفضلة</string> <string name="watch_quality_pref">جودة المشاهدة المفضلة</string>
<string name="limit_title">أقصي عدد حروف لعنوان مُشغل الفيديو</string>
<string name="limit_title_rez">أبعاد مُشغل الفيديو</string>
<string name="video_buffer_size_settings">حجم ذاكرة التخزين المؤقت للفيديو</string> <string name="video_buffer_size_settings">حجم ذاكرة التخزين المؤقت للفيديو</string>
<string name="video_buffer_length_settings">طول التخزين المؤقت</string>
<string name="video_buffer_disk_settings">التخزين المؤقت للفيديو علي القرص</string>
<string name="video_buffer_clear_settings">مسح التخزين المؤقت للصورة والفيديو</string>
<string name="video_ram_description">سوف يسبب بعض الأعطال إذا تم إعداد قيمة عالية جدًا. لا\تغير القيمة إذا كان لديك ذاكرة تخزين عشوائية منخفضة كتلفاز أندرويد أو هاتف قديم</string>
<string name="video_disk_description">قد يسبب مشاكل مع حجم ذاكرة التخزين المنخفضة كأجهزة تلفاز الأندرويد إذا أعددت قيمة عالية جدًا</string>
<string name="dns_pref">DNS فوق HTTPS</string> <string name="dns_pref">DNS فوق HTTPS</string>
<string name="dns_pref_summary">مفيد لتجاوز كتل مزود خدمة الإنترنت</string> <string name="dns_pref_summary">مفيد لتجاوز حجب مزود خدمة الإنترنت</string>
<string name="download_path_pref">موقع التنزيل</string> <string name="add_site_pref">نسخ موقع</string>
<string name="remove_site_pref">حذف موقع</string>
<string name="add_site_summary">إضافة نسخة من موقع موجود, بعنوان رابط مختلف</string>
<string name="download_path_pref">مسار التنزيل</string>
<string name="nginx_url_pref">عنوان رابط سيرفر Nginx</string>
<string name="display_subbed_dubbed_settings">عرض أنمي مدبلج / مترجم</string> <string name="display_subbed_dubbed_settings">عرض أنمي مدبلج / مترجم</string>
<string name="resize_fit">تناسب الشاشة</string> <string name="resize_fit">ملائمة الشاشة</string>
<string name="resize_fill">امتداد</string> <string name="resize_fill">امتداد</string>
<string name="resize_zoom">تكبير</string> <string name="resize_zoom">تكبير</string>
<string name="legal_notice">إخلاء مسؤولية</string>
<string name="legal_notice_key" translatable="false">legal_notice_key</string>
<string name="legal_notice_text" translatable="false">Any legal issues regarding the content on this application
should be taken up with the actual file hosts and providers themselves as we are not affiliated with them.
In case of copyright infringement, please directly contact the responsible parties or the streaming websites.
The app is purely for educational and personal use.
CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down.
CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or
manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient,
user-friendly interface.
It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the
responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use
CloudStream 3 at your own risk.
</string>
<string name="category_general">عام</string> <string name="category_general">عام</string>
<string name="random_button_settings">زر العشوائي</string>
<string name="random_button_settings_desc">إظهار زر العشوائي علي الشاشة الرئيسية</string>
<string name="provider_lang_settings">لغات الموفر</string> <string name="provider_lang_settings">لغات الموفر</string>
<string name="app_layout">واجهة التطبيق</string> <string name="app_layout">واجهة التطبيق</string>
<string name="preferred_media_settings">النوع المفضل من المشاهدة</string> <string name="preferred_media_settings">المحتوي المفضل</string>
<string name="enable_nsfw_on_providers">تفعيل محتوي البالغين داخل المزودين المدعومين</string>
<string name="automatic">أوتوماتيك</string> <string name="subtitles_encoding">فك تشفير الترجمة</string>
<string name="tv_layout">واجهة خاصة بتلفاز</string> <string name="category_providers">المصادر</string>
<string name="phone_layout">واجهة خاصة بهاتف</string> <string name="category_ui">الواجهة</string>
<string name="automatic">أوتوماتيك</string>
<string name="tv_layout">واجهة تلفاز</string>
<string name="phone_layout">واجهة هاتف</string>
<string name="emulator_layout">واجهة محاكي</string>
<string name="primary_color_settings">اللون الأساسي</string> <string name="primary_color_settings">اللون الأساسي</string>
<string name="app_theme_settings">مظهر التطبيق</string> <string name="app_theme_settings">مظهر التطبيق</string>
<string name="bottom_title_settings">موضع عنوان الملصق</string>
<string name="account">الحساب</string> <string name="bottom_title_settings_des">وضع العنوان تحت الملصق</string>
<!-- account stuff -->
<string name="anilist_key" translatable="false">anilist_key</string>
<string name="mal_key" translatable="false">mal_key</string>
<string name="opensubtitles_key" translatable="false">opensubtitles_key</string>
<string name="nginx_key" translatable="false">nginx_key</string>
<string name="example_password">password123</string>
<string name="example_username">MyCoolUsername</string>
<string name="example_email">hello@world.com</string>
<string name="example_ip">127.0.0.1</string>
<string name="example_site_name">MyCoolSite</string>
<string name="example_site_url">example.com</string>
<string name="example_lang_name">Language code (en)</string>
<!--
<string name="mal_account_settings" translatable="false">MAL</string>
<string name="anilist_account_settings" translatable="false">AniList</string>
<string name="tmdb_account_settings" translatable="false">TMDB</string>
<string name="imdb_account_settings" translatable="false">IMDB</string>
<string name="kitsu_account_settings" translatable="false">Kitsu</string>
<string name="trakt_account_settings" translatable="false">Trakt</string>
-->
<string name="login_format" formatted="true">%s %s</string>
<string name="account">حساب</string>
<string name="logout">تسجيل خروج</string> <string name="logout">تسجيل خروج</string>
<string name="login">تسجيل الدخول</string> <string name="login">تسجيل الدخول</string>
<string name="switch_account">تبديل الحساب</string> <string name="switch_account">تبديل الحساب</string>
<string name="add_account">إضافة حساب</string> <string name="add_account">إضافة حساب</string>
<string name="add_sync">Add tracking</string> <string name="create_account">إنشاء حساب</string>
<string name="add_sync">إضافة تتبع</string>
<string name="added_sync_format" formatted="true">تم إضافة %s</string>
<string name="upload_sync">مزامنة</string>
<string name="sync_score">مقيّم</string>
<string name="sync_score_format" formatted="true">%d / 10</string>
<string name="sync_total_episodes_none">/??</string>
<string name="sync_total_episodes_some" formatted="true">/%d</string>
<string name="authenticated_user" formatted="true">تم توثيق %s</string>
<string name="authenticated_user_fail" formatted="true">فشل توثيق %s</string>
<!-- ============ --> <!-- ============ -->
<string name="none">لا شيء</string> <string name="none">لا شيء</string>
<string name="normal">عادي</string> <string name="normal">عادي</string>
<string name="all">الكل</string> <string name="all">الكل</string>
<string name="max">ماكس</string> <string name="max">الحد الاقصي</string>
<string name="min">الحد الأدنى</string> <string name="min">الحد الأدنى</string>
<string name="subtitles_outline">الخطوط العريضة</string> <string name="subtitles_none" translatable="false">@string/none</string>
<string name="subtitles_depressed">النمط المكتئب</string> <string name="subtitles_outline">الخطوط المحيطة</string>
<string name="subtitles_depressed">النمط المنخفض</string>
<string name="subtitles_shadow">ظل</string> <string name="subtitles_shadow">ظل</string>
<string name="subtitles_raised">رفع</string> <string name="subtitles_raised">رفع</string>
<string name="subtitle_offset">مزامنة الترجمة</string>
<string name="subtitle_offset_hint">1000ms</string>
<string name="subtitle_offset_title">تأخير الترجمة</string>
<string name="subtitle_offset_extra_hint_later_format">استخدم هذا إذا كانت الترجمة تُعرض %dms مبكرًا جدًا</string>
<string name="subtitle_offset_extra_hint_before_format">استخدم هذا إذا كانت الترجمة تُعرض %dms متأخرًا جدًا</string>
<string name="subtitle_offset_extra_hint_none_format">لا تأخير في الترجمة</string>
<!--
Example text (pangram) can optionally be translated; if you do, include all the letters in the alphabet,
see:
https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300
https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog
-->
<string name="subtitles_example_text">نصٌّ حكيمٌ لهُ سِرٌّ قاطِعٌ وَذُو شَأنٍ عَظيمٍ مكتوبٌ على ثوبٍ أخضرَ ومُغلفٌ بجلدٍ أزرق</string> <string name="subtitles_example_text">نصٌّ حكيمٌ لهُ سِرٌّ قاطِعٌ وَذُو شَأنٍ عَظيمٍ مكتوبٌ على ثوبٍ أخضرَ ومُغلفٌ بجلدٍ أزرق</string>
<string name="recommended">موصى به</string> <string name="recommended">مُوصي به</string>
<string name="player_loaded_subtitles" formatted="true">تم تحميل %s</string>
<string name="player_load_subtitles">إختيار ملف</string> <string name="player_load_subtitles">إختيار ملف</string>
<string name="player_load_subtitles_online">تحميل من الانترنت</string>
<string name="downloaded_file">الملف الذي تم تنزيله</string> <string name="downloaded_file">الملف الذي تم تنزيله</string>
<string name="actor_main">رئيسي</string> <string name="actor_main">رئيسي</string>
<string name="actor_supporting">مساعد</string> <string name="actor_supporting">مساعد</string>
<string name="actor_background">خلفية</string> <string name="actor_background">الخلفية</string>
<string name="chromecast_subtitles_settings">ترجمة كروم كاست</string>
<string name="chromecast_subtitles_settings_des">إعدادات ترجمة الكروم كاست</string> <string name="home_source">مصدر</string>
<string name="home_source">المصادر</string> <string name="home_random">عشوائي</string>
<string name="coming_soon">قريبا…</string>
<string name="quality_cam">Cam</string>
<string name="quality_cam_rip">Cam</string>
<string name="quality_cam_hd">Cam</string>
<string name="quality_hq">HQ</string>
<string name="quality_hd">HD</string>
<string name="quality_ts">TS</string>
<string name="quality_tc">TC</string>
<string name="quality_blueray">BlueRay</string>
<string name="quality_workprint">WP</string>
<string name="quality_dvd">DVD</string>
<string name="quality_4k">4K</string>
<string name="quality_sd">SD</string>
<string name="quality_uhd">UHD</string>
<string name="quality_hdr">HDR</string>
<string name="quality_sdr">SDR</string>
<string name="quality_webrip">Web</string>
<string name="poster_image">صورة الملصق</string>
<string name="category_player">المُشغل</string>
<string name="resolution_and_title">الأبعاد والعنوان</string>
<string name="title">العنوان</string>
<string name="resolution">الأبعاد</string>
<string name="error_invalid_id">هوية غير صالحة</string>
<string name="error_invalid_data">بيانات غير صالحة</string>
<string name="error_invalid_url">عنون رابط غير صالح</string>
<string name="error">خطأ</string>
<string name="subtitles_remove_captions">ازالة التسميات التوضيحية من الترجمة</string>
<string name="subtitles_remove_bloat">ازالة البرمجيات الخبيثة من الترجمة</string>
<string name="subtitles_filter_lang">فلترة تبعا للغة المحتوي المفضلة</string>
<string name="extras">اكسترا</string>
<string name="trailer">مقطع دعائي</string>
<string name="network_adress_example">رابط الفيديو</string>
<string name="referer">Referer</string>
<string name="next">التالي</string>
<string name="provider_languages_tip">شاهد الفيديوهات بهذه اللغات</string>
<string name="previous">السابق</string>
<string name="skip_setup">تخطي الإعداد</string>
<string name="app_layout_subtext">تغيير شكل البرنامح حتي يلائم جهازك</string>
<string name="crash_reporting_title">ابلاغ الاعطال</string>
<string name="preferred_media_subtext">ماذا تريد ان تري</string>
<string name="setup_done">تم</string>
<string name="extensions">الإضافات</string>
<string name="add_repository">إضافة مستودع</string>
<string name="repository_name_hint">إسم المستودع</string>
<string name="repository_url_hint">عنوان رابط المستودع</string>
<string name="plugin_loaded">تم تحميل الإضافة</string>
<string name="plugin_deleted">تم إزالة الإضافة</string>
<string name="plugin_load_fail" formatted="true">فشل التحميل %s</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">بدأ تنزيل %d %s</string>
<string name="batch_download_finish_format" formatted="true">تم تنزيل %d %s بنجاح</string>
<string name="batch_download_nothing_to_download_format" formatted="true">جميع %s محملة بالفعل</string>
<string name="batch_download">تحميل مكثف</string>
<string name="plugin_singular">إضافة</string>
<string name="plugin">إضافات</string>
<string name="delete_repository_plugins">سوف يتم محو جميع إضافات المستودع</string>
<string name="delete_repository">ازالة مستودع</string>
<string name="setup_extensions_subtext">تحميل قائمة المواقع التي تريد استخدامها</string>
<string name="plugins_downloaded" formatted="true">تم تحميل: %d</string>
<string name="plugins_disabled" formatted="true">مُعطل %d</string>
<string name="plugins_not_downloaded" formatted="true">غير مُحمل: %d</string>
<string name="blank_repo_message">قم بإضافة مستودع لكي يتم تثبيت إضافات المواقع</string>
<string name="view_public_repositories_button">عرض مستودعات المجتمع</string>
<string name="view_public_repositories_button_short">قائمة عامة</string>
<string name="uppercase_all_subtitles">جميع الترجمات حروف كبيرة</string>
<string name="download_all_plugins_from_repo">تحميل جميع الإضافات من هذا المستودع?</string>
<string name="single_plugin_disabled" formatted="true">%s (Disabled)</string>
<string name="tracks">المسارات</string>
<string name="audio_tracks">مسار الصوت</string>
<string name="video_tracks">مسار الفيديو</string>
<string name="apply_on_restart">تطبيق بعد إعادة الفتح</string>
<string name="safe_mode_title">وضع الامان مفعل</string>
<string name="safe_mode_description">An unrecoverable crash occurred and we\'ve automatically disabled all extensions, so you can find and remove the extension which is causing trouble.</string>
<string name="safe_mode_crash_info">عرض بيانات الاعطال</string>
<string name="extension_rating" formatted="true">تقييم: %s</string>
<string name="extension_description">الوصف</string>
<string name="extension_version">إصدار</string>
<string name="extension_status">الحالة</string>
<string name="extension_size">الحجم</string>
<string name="extension_authors">المؤلفون</string>
<string name="extension_types">مدعوم</string>
<string name="extension_language">اللغة</string>
<string name="hls_playlist">قائمة HLS</string>
</resources> </resources>

View File

@ -20,7 +20,7 @@
<!-- TRANSLATE, BUT DON'T FORGET FORMAT --> <!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">Snelheid (%.2fx)</string> <string name="player_speed_text_format" formatted="true">Snelheid (%.2fx)</string>
<string name="rated_format" formatted="true">Beoordeeld: %.Als</string> <string name="rated_format" formatted="true">Beoordeeld: %.1fAls</string>
<string name="new_update_format" formatted="true">Nieuwe update gevonden!\n%s -> %s</string> <string name="new_update_format" formatted="true">Nieuwe update gevonden!\n%s -> %s</string>
<string name="filler" formatted="true">Filler</string> <string name="filler" formatted="true">Filler</string>
<string name="duration_format" formatted="true">%d min</string> <string name="duration_format" formatted="true">%d min</string>

View File

@ -1,5 +0,0 @@
files:
- source: /app/src/main/res/values/strings.xml
translation: /app/src/main/res/values-%android_code%/strings.xml
- source: /app/src/main/res/values/array.xml
translation: /app/src/main/res/values-%android_code%/array.xml