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

View File

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

View File

@ -170,7 +170,7 @@ dependencies {
// Networking
// implementation "com.squareup.okhttp3:okhttp:4.9.2"
// 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 🙏
implementation "com.github.tachiyomiorg:unifile:17bec43"
@ -206,10 +206,28 @@ task androidSourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs//full sources
}
// this is used by the gradlew plugin
task makeJar(type: Copy) {
// after modifying here, you can export. Jar
from('build/intermediates/compile_app_classes_jar/debug')
into('build') // output location
include('classes.jar') // the classes file of the imported rack package
dependsOn build
into('build')
include('classes.jar')
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.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
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.Coroutines.runOnMainThread
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) {
ACRA.errorReporter.handleException(error)
try {
PrintStream(errorFile).use { ps ->
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)
}
} catch (ignored: FileNotFoundException) { }
} catch (ignored: FileNotFoundException) {
}
try {
onError.invoke()
} catch (ignored: Exception) { }
} catch (ignored: Exception) {
}
exitProcess(1)
}
@ -95,7 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
class AcraApplication : Application() {
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
})
@ -183,5 +194,15 @@ class AcraApplication : Application() {
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
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
// ConcurrentModificationException is possible!!!
val allProviders: MutableList<MainAPI> = arrayListOf()
fun initAll() {
@ -1118,6 +1119,11 @@ data class NextAiring(
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(
val season: Int,
val name: String? = null,
@ -1198,9 +1204,12 @@ data class AnimeLoadResponse(
override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse
/**
* If episodes already exist appends the list.
* */
fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) {
if (episodes.isNullOrEmpty()) return
this.episodes[status] = episodes
this.episodes[status] = (this.episodes[status] ?: emptyList()) + episodes
}
suspend fun MainAPI.newAnimeLoadResponse(

View File

@ -15,6 +15,7 @@ import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
@ -144,6 +145,68 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage
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) {
@ -348,56 +411,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (intent == null) return
val str = intent.dataString
loadCache()
if (str != null) {
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
}
}
}
}
}
handleAppIntentUrl(this, str, false)
}
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
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
} catch (e: Exception) {
logError(e)
@ -465,9 +480,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
lastError = errorFile.readText(Charset.defaultCharset())
errorFile.delete()
}
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
@ -501,7 +517,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
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)
} else {
PluginManager.loadAllOnlinePlugins(this@MainActivity)

View File

@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay
class DoodWfExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.wf"
}
class DoodCxExtractor : DoodLaExtractor() {
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.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.Qualities
class GMPlayer : ExtractorApi() {
override val name = "GM Player"
@ -25,11 +26,16 @@ class GMPlayer : ExtractorApi() {
data = mapOf("hash" to id, "r" to ref)
).parsed<GmResponse>().videoSource ?: return null
return M3u8Helper.generateM3u8(
name,
m3u8,
ref,
headers = mapOf("accept" to "*/*")
return listOf(
ExtractorLink(
this.name,
this.name,
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
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
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() {
override var mainUrl = "https://vidgomunime.xyz"
}
@ -104,31 +113,33 @@ open class StreamSB : ExtractorApi() {
@JsonProperty("status_code") val statusCode: Int,
)
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
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_-]+)")
override suspend fun getUrl(
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 {
it.value.replace(Regex("(embed-|\\/e\\/)"),"")
it.value.replace(Regex("(embed-|/e/)"), "")
}.first()
val bytes = id.toByteArray()
val bytesToHex = bytesToHex(bytes)
val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val master = "$mainUrl/sources48/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val headers = mapOf(
"watchsb" to "streamsb",
)
val urltext = app.get(master,
"watchsb" to "sbstream",
)
val mapped = app.get(
master.lowercase(),
headers = headers,
allowRedirects = false
).text
val mapped = urltext.let { parseJson<Main>(it) }
val testurl = app.get(mapped.streamData.file, headers = headers).text
referer = url,
).parsedSafe<Main>()
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
if (urltext.contains("m3u8") && testurl.contains("EXTM3U"))
return M3u8Helper.generateM3u8(
name,
mapped.streamData.file,
url,
headers = headers
)
return null
M3u8Helper.generateM3u8(
name,
mapped?.streamData?.file ?: return,
url,
headers = headers
).forEach(callback)
}
}

View File

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

View File

@ -5,15 +5,12 @@ import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.getCookies
import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.util.concurrent.TimeUnit
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.utils.Coroutines.ioSafe
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
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 fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest
.getInstance("SHA-256")
.digest("${url}#funny-salt".toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
MessageDigest
.getInstance("SHA-256")
.digest("${url}#funny-salt".toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
suspend fun SitePlugin.getVotes(): Int {
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 {
votesCache[pluginUrl] = it
} ?: (0.also {
ioSafe {
createBucket(pluginUrl)
}
ioSafe {
createBucket(pluginUrl)
}
})
}
@ -64,7 +67,8 @@ object VotingApi { // please do not cheat the votes lol
}
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")
app.get(url)
}
@ -74,32 +78,46 @@ object VotingApi { // please do not cheat the votes lol
return true
}
private val voteLock = Mutex()
suspend fun vote(pluginUrl: String, requestType: VoteType): Int {
if (!canVote(pluginUrl)) {
main {
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT).show()
// Prevent multiple requests at the same time.
voteLock.withLock {
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 = getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
var newType: VoteType = requestType
var changeValue = 0
if (requestType == savedType) {
newType = VoteType.NONE
changeValue = -requestType.value
} else if (savedType == VoteType.NONE) {
changeValue = requestType.value
} else if (savedType != requestType) {
changeValue = -savedType.value + requestType.value
}
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) {
val savedType: VoteType =
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
val newType = if (requestType == savedType) VoteType.NONE else requestType
val changeValue = if (requestType == savedType) {
-requestType.value
} else if (savedType == VoteType.NONE) {
requestType.value
} else if (savedType != requestType) {
-savedType.value + requestType.value
} else 0
// Pre-emptively set vote key
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(

View File

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

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
@ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
removeAccountKeys()
}
override fun authenticate() {
override fun authenticate(activity: FragmentActivity?) {
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 {

View File

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

View File

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
@ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return false
}
override fun authenticate() {
override fun authenticate(activity: FragmentActivity?) {
// It is recommended to use a URL-safe string as code_verifier.
// See section 4 of RFC 7636 for more details.
@ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val codeChallenge = codeVerifier
val request =
"$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

View File

@ -11,12 +11,12 @@ import android.webkit.WebViewClient
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import kotlinx.android.synthetic.main.fragment_webview.*
import java.net.URI
class WebviewFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -31,16 +31,8 @@ class WebviewFragment : Fragment() {
request: WebResourceRequest?
): Boolean {
val requestUrl = request?.url.toString()
val repoUrl = if (requestUrl.startsWith("https://cs.repo")) {
"https://" + requestUrl.substringAfter("?")
} else if (URI(requestUrl).scheme == appStringRepo) {
requestUrl.replaceFirst(appStringRepo, "https")
} else {
null
}
if (repoUrl != null) {
activity?.loadRepository(repoUrl)
val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true)
if (performedAction) {
findNavController().popBackStack()
return true
}
@ -50,6 +42,7 @@ class WebviewFragment : Fragment() {
}
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
web_view.settings.javaScriptEnabled = true
web_view.settings.userAgentString = USER_AGENT
web_view.settings.domStorageEnabled = true
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) {
return@launchSafe
}
// If the plugin isn't loaded yet. (Does not set the key)
if (api == null) {
loadAndCancel(noneApi)
} else if (preferredApiName == noneApi.name) {
if (preferredApiName == noneApi.name) {
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
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) {
val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) {

View File

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

View File

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

View File

@ -76,7 +76,7 @@ class RepoAdapter(
imageClickCallback(repositoryData)
}
itemView.setOnClickListener {
itemView.repository_item_root?.setOnClickListener {
clickCallback(repositoryData)
}
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.toSpanned
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -415,7 +416,7 @@ object AppUtils {
}
}
fun AppCompatActivity.loadResult(
fun FragmentActivity.loadResult(
url: String,
apiName: String,
startAction: Int = 0,

View File

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

View File

@ -200,16 +200,23 @@ class InAppUpdater {
private suspend fun Activity.downloadUpdate(url: String): Boolean {
try {
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()
updateLock.withLock {
sink.writeAll(app.get(url).body.source())
sink.close()
openApk(localContext, Uri.fromFile(downloadedFile))
openApk(this, Uri.fromFile(downloadedFile))
}
return true
} catch (e: Exception) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,57 +5,65 @@
<string name="episode_poster_img_des">ملصق الحلقة</string>
<string name="home_main_poster_img_des">الملصق الرئيسي</string>
<string name="home_next_random_img_des">التالي عشوائي</string>
<string name="go_back_img_des">ارجع للخلف</string>
<string name="home_change_provider_img_des">تغيير المصدر</string>
<string name="preview_background_img_des">معاينة الخلفية</string>
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">سرعة (%.2fx)</string>
<string name="rated_format" formatted="true">Rated: %.1f</string>
<string name="new_update_format" formatted="true">!تم إيجاد تحديث جديد\n%s -> %s</string>
<string name="duration_format" formatted="true">%d دقيقة</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">تشغيل بواسطة CloudStream</string>
<string name="title_home">الصفحة الرئيسية</string>
<string name="title_search">بحث</string>
<string name="title_search">البحث</string>
<string name="title_downloads">التحميلات</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="episode_more_options_des">المزيد من الخيارات</string>
<string name="next_episode">الحلقة التالية</string>
<string name="result_tags">النواع</string>
<string name="result_tags">أنواع</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="loading">…تحميل</string>
<string name="type_watching">مشاهدة</string>
<string name="type_watching">أشاهده</string>
<string name="type_on_hold">في الانتظار</string>
<string name="type_completed">مكتمل</string>
<string name="type_dropped">إسقاط</string>
<string name="type_plan_to_watch">تخطط للمشاهدة</string>
<string name="type_dropped">مهمل</string>
<string name="type_plan_to_watch">أخطط لمشاهدته</string>
<string name="type_none">لا شيء</string>
<string name="type_re_watching">إعادة المشاهدة</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_subtitle">الترجمة</string>
<string name="reload_error">…إعادة محاولة الاتصال</string>
<string name="go_back">ارجع للخلف</string>
<string name="play_episode">تشغيل الحلقة</string>
<!--<string name="need_storage">السماح بتحميل الحلقات</string>-->
<!--<string name="need_storage">السماح بتنزيل الحلقات</string>-->
<string name="download">تحميل</string>
<string name="downloaded">تم التنزيل</string>
<string name="downloading">جارى التحميل</string>
<string name="downloading">جاري التنزيل</string>
<string name="download_paused">توقف التنزيل مؤقتًا</string>
<string name="download_started">بدأ التنزيل</string>
<string name="download_failed">التحميل فشل</string>
<string name="download_failed">فشل التنزيل</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="download_storage_text">التخزين الداخلي</string>
@ -73,12 +81,18 @@
<string name="home_expanded_hide">إخفاء</string>
<string name="home_play">تشغيل</string>
<string name="home_info">معلومات</string>
<string name="filter_bookmarks">تصفية المواقع المفضلة</string>
<string name="filter_bookmarks">تصفية الاشارات المرجعية</string>
<string name="error_bookmarks_text">إشارات مرجعية</string>
<string name="action_remove_from_bookmarks">حذف</string>
<string name="action_add_to_bookmarks">إعداد حالة المشاهدة</string>
<string name="sort_apply">تطبيق</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="subs_text_color">لون الخط</string>
@ -90,65 +104,97 @@
<string name="subs_font">الخط</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="benene_count_text">%d البنينيس المعطاة الى المطورين</string>
<string name="benene_count_text_none">لم يتم إعطاء بنين</string>
<string name="benene_count_text">%d الموزات المعطاة الى المطورين</string>
<string name="benene_count_text_none">لم يتم إعطاء موز</string>
<string name="subs_auto_select_language">تحديد اللغة تلقائيًا</string>
<string name="subs_download_languages">تحميل اللغات</string>
<string name="subs_hold_to_reset_to_default">اضغط بإستمرار لإعادة التعيين</string>
<string name="continue_watching">استمر في المشاهدة</string>
<string name="subs_subtitle_languages">لغة الترجمة</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_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="provider_info_meta">.سيظهر إعلان مدته خمسة عشر ثانية إذا لم يتم توفير الفيديو في الموقع</string>
<string name="torrent_plot">الوصف</string>
<string name="normal_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_des">يستمر في التشغيل في مشغل مصغر فوق التطبيقات الأخرى</string>
<string name="player_size_settings">زر تغيير حجم المشغل</string>
<string name="player_size_settings_des">قم بإزالة الحدود السوداء</string>
<string name="picture_in_picture_des">يستمر في التشغيل في مُشغل مصغر فوق التطبيقات الأخرى</string>
<string name="player_size_settings">زر تغيير حجم المُشغل</string>
<string name="player_size_settings_des">إزالة الحدود السوداء</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_des">يضيف خيار السرعة في المشغل</string>
<string name="swipe_to_seek_settings">اسحب للسعي</string>
<string name="swipe_to_seek_settings_des">اسحب إلى اليسار أو اليمين للتحكم في الوقت في مشغل الفيديو</string>
<string name="swipe_to_change_settings">اسحب لتغيير الإعدادات</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="double_tap_to_seek_settings_des">اضغط مرتين على الجانب الأيمن أو الأيسر للسعي للأمام أو للخلف </string>
<string name="double_tap_to_pause_settings_des">اضغط في الوسط لإيقاف مؤقت</string>
<string name="eigengraumode_settings_des">يضيف خيار السرعة في المُشغل</string>
<string name="swipe_to_seek_settings">السحب للسعي</string>
<string name="swipe_to_seek_settings_des">إسحب إلى اليسار أو اليمين للتحكم في الوقت في مُشغل الفيديو</string>
<string name="swipe_to_change_settings">السحب لتغيير الإعدادات</string>
<string name="swipe_to_change_settings_des">إسحب على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت</string>
<string name="autoplay_next_settings">تشغيل الحلقة التالية تلقائيًا</string>
<string name="autoplay_next_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_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="category_account">الحسابات</string>
<string name="category_updates">التحديثات والنسخ الاحتياطية</string>
<string name="settings_info">معلومات</string>
<string name="advanced_search">البحث المتقدم</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="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_des">ابحث تلقائيًا عن التحديثات الجديدة عند البداية</string>
<string name="updates_settings_des">البحث تلقائيًا عن التحديثات الجديدة عند البداية</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="lightnovel">تطبيق رواية خفيف من نفس المطورين</string>
<string name="anim">تطبيق Anime من نفس المطورين</string>
<string name="discord">انضم إلى إلديسكورد</string>
<string name="benene">أعط موزة للمطورين</string>
<string name="benene_des">أعط الموز</string>
<string name="lightnovel">تطبيق روايات خفيف من نفس المطورين</string>
<string name="anim">تطبيق أنمي من نفس المطورين</string>
<string name="discord">انضم إلى الديسكورد</string>
<string name="benene">إعط موزة للمطورين</string>
<string name="benene_des">الموز المُعطي</string>
<string name="app_language">لغة التطبيق</string>
@ -158,23 +204,24 @@
<string name="play_episode_toast">تشغيل الحلقة</string>
<string name="subs_default_reset_toast">إعادة التعيين إلى القيمة الافتراضية</string>
<string name="acra_report_toast">عذرا ، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين</string>
<string name="season">موسم</string>
<string name="no_season">لا موسم</string>
<string name="episode">حلقة</string>
<string name="episodes">حلقات</string>
<string name="season_short">S</string>
<string name="episode_short">E</string>
<string name="season_short">ح</string>
<string name="episode_short">م</string>
<string name="no_episodes_found">لم يتم العثور على أي حلقات</string>
<string name="delete_file">حذف ملف</string>
<string name="delete_file">حذف الملف</string>
<string name="delete">حذف</string>
<string name="pause">إيقاف مؤقت</string>
<string name="resume">أكمل</string>
<string name="go_back_30">-30</string>
<string name="go_forward_30">+30</string>
<string name="delete_message">This will permanently delete %s\nAre you sure?</string>
<string name="resume_time_left" formatted="true">%dد\nمتبقية</string>
<string name="go_back_30">-٣٠</string>
<string name="go_forward_30">+٣٠</string>
<string name="delete_message">سوف يتم الحذف نهائيا %s\nهل أنت متأكد?</string>
<string name="resume_time_left" formatted="true">%dm\nمتبقية</string>
<string name="status_ongoing">جاري التنفيذ</string>
<string name="status_completed">اكتمل</string>
@ -189,28 +236,39 @@
<string name="no_subtitles">الترجمة ليست موجودة</string>
<string name="default_subtitles">الإفتراضي</string>
<string name="free_storage">حر</string>
<string name="free_storage">فارغ</string>
<string name="used_storage">مستخدم</string>
<string name="app_storage">تطبيق</string>
<string name="app_storage">التطبيق</string>
<!--plural-->
<string name="movies">أفلام</string>
<string name="tv_series">مسلسلات</string>
<string name="cartoons">رسوم متحركة</string>
<string name="anime">انمي</string>
<string name="anime">أنمي</string>
<string name="torrent">تورنت</string>
<string name="documentaries">الافلام الوثائقية</string>
<string name="ova">OVA</string>
<string name="documentaries">أفلام وثائقية</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="tv_series_singular">مسلسلات</string>
<string name="tv_series_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="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="remote_error">خطأ بعيد</string>
<string name="render_error">خطأ في جهاز العرض</string>
<string name="unexpected_error">خطأ غير متوقع في مشغل</string>
<string name="unexpected_error">خطأ غير متوقع في المُشغل</string>
<string name="storage_error">خطأ في التنزيل ، تحقق من أذونات التخزين</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_vlc">VLC تشغيل في</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_download_mirror">تحميل المرآة</string>
<string name="episode_action_download_mirror">تحميل بجودات مختلفة</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="check_for_update">تحقق من التحديثات</string>
@ -229,61 +298,244 @@
<string name="video_lock">قفل</string>
<string name="video_aspect_ratio_resize">تغيير الحجم</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="skip_update">تخطي هذا التحديث</string>
<string name="update">تحديث</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_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_summary">مفيد لتجاوز كتل مزود خدمة الإنترنت</string>
<string name="download_path_pref">موقع التنزيل</string>
<string name="dns_pref_summary">مفيد لتجاوز حجب مزود خدمة الإنترنت</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="resize_fit">تناسب الشاشة</string>
<string name="resize_fit">ملائمة الشاشة</string>
<string name="resize_fill">امتداد</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="random_button_settings">زر العشوائي</string>
<string name="random_button_settings_desc">إظهار زر العشوائي علي الشاشة الرئيسية</string>
<string name="provider_lang_settings">لغات الموفر</string>
<string name="app_layout">واجهة التطبيق</string>
<string name="preferred_media_settings">النوع المفضل من المشاهدة</string>
<string name="automatic">أوتوماتيك</string>
<string name="tv_layout">واجهة خاصة بتلفاز</string>
<string name="phone_layout">واجهة خاصة بهاتف</string>
<string name="app_layout">واجهة التطبيق</string>
<string name="preferred_media_settings">المحتوي المفضل</string>
<string name="enable_nsfw_on_providers">تفعيل محتوي البالغين داخل المزودين المدعومين</string>
<string name="subtitles_encoding">فك تشفير الترجمة</string>
<string name="category_providers">المصادر</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="app_theme_settings">مظهر التطبيق</string>
<string name="account">الحساب</string>
<string name="bottom_title_settings">موضع عنوان الملصق</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="login">تسجيل الدخول</string>
<string name="switch_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="normal">عادي</string>
<string name="all">الكل</string>
<string name="max">ماكس</string>
<string name="max">الحد الاقصي</string>
<string name="min">الحد الأدنى</string>
<string name="subtitles_outline">الخطوط العريضة</string>
<string name="subtitles_depressed">النمط المكتئب</string>
<string name="subtitles_none" translatable="false">@string/none</string>
<string name="subtitles_outline">الخطوط المحيطة</string>
<string name="subtitles_depressed">النمط المنخفض</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="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_online">تحميل من الانترنت</string>
<string name="downloaded_file">الملف الذي تم تنزيله</string>
<string name="actor_main">رئيسي</string>
<string name="actor_supporting">مساعد</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="actor_background">الخلفية</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>

View File

@ -20,7 +20,7 @@
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<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="filler" formatted="true">Filler</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