Merge branch 'master' into feature/remote-sync

# Conflicts:
#	app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
#	app/src/main/res/values/strings.xml
This commit is contained in:
Martin Filo 2023-06-22 20:30:41 +02:00
commit 0fb2e68118
70 changed files with 2060 additions and 236 deletions

View file

@ -242,7 +242,7 @@ dependencies {
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204
implementation("com.github.TeamNewPipe:NewPipeExtractor:v0.22.6")
implementation("com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance

View file

@ -0,0 +1,135 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
class Bestx : Chillx() {
override val name = "Bestx"
override val mainUrl = "https://bestx.stream"
}
class Watchx : Chillx() {
override val name = "Watchx"
override val mainUrl = "https://watchx.top"
}
open class Chillx : ExtractorApi() {
override val name = "Chillx"
override val mainUrl = "https://chillx.top"
override val requiresReferer = true
companion object {
private const val KEY = "4VqE3#N7zt&HEP^a"
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val master = Regex("MasterJS\\s*=\\s*'([^']+)").find(
app.get(
url,
referer = referer
).text
)?.groupValues?.get(1)
val encData = AppUtils.tryParseJson<AESData>(base64Decode(master ?: return))
val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
val source = Regex("""sources:\s*\[\{"file":"([^"]+)""").find(decrypt)?.groupValues?.get(1)
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
// required
val headers = mapOf(
"Accept" to "*/*",
"Connection" to "keep-alive",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "cross-site",
"Origin" to mainUrl,
)
callback.invoke(
ExtractorLink(
name,
name,
source ?: return,
"$mainUrl/",
Qualities.P1080.value,
headers = headers,
isM3u8 = true
)
)
AppUtils.tryParseJson<List<Tracks>>("[$tracks]")
?.filter { it.kind == "captions" }?.map { track ->
subtitleCallback.invoke(
SubtitleFile(
track.label ?: "",
track.file ?: return@map null
)
)
}
}
private fun cryptoAESHandler(
data: AESData,
pass: String,
encrypt: Boolean = true
): String {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
val spec = PBEKeySpec(
pass.toCharArray(),
data.salt?.hexToByteArray(),
data.iterations?.toIntOrNull() ?: 1,
256
)
val key = factory.generateSecret(spec)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(key.encoded, "AES"),
IvParameterSpec(data.iv?.hexToByteArray())
)
String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString())))
} else {
cipher.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(key.encoded, "AES"),
IvParameterSpec(data.iv?.hexToByteArray())
)
base64Encode(cipher.doFinal(data.ciphertext?.toByteArray()))
}
}
private fun String.hexToByteArray(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
data class AESData(
@JsonProperty("ciphertext") val ciphertext: String? = null,
@JsonProperty("iv") val iv: String? = null,
@JsonProperty("salt") val salt: String? = null,
@JsonProperty("iterations") val iterations: String? = null,
)
data class Tracks(
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null,
@JsonProperty("kind") val kind: String? = null,
)
}

View file

@ -58,7 +58,7 @@ open class DoodLaExtractor : ExtractorApi() {
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
return listOf(
ExtractorLink(
trueUrl,
this.name,
this.name,
trueUrl,
mainUrl,

View file

@ -5,6 +5,25 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
class Moviesm4u : Filesim() {
override val mainUrl = "https://moviesm4u.com"
override val name = "Moviesm4u"
}
class FileMoonIn : Filesim() {
override val mainUrl = "https://filemoon.in"
override val name = "FileMoon"
}
class StreamhideCom : Filesim() {
override var name: String = "Streamhide"
override var mainUrl: String = "https://streamhide.com"
}
class Movhide : Filesim() {
override var name: String = "Movhide"
override var mainUrl: String = "https://movhide.pro"
}
class Ztreamhub : Filesim() {
override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works
@ -35,7 +54,7 @@ open class Filesim : ExtractorApi() {
response.select("script[type=text/javascript]").map { script ->
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
val unpackedscript = getAndUnpack(script.data())
val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"")
val m3u8Regex = Regex("file.\"(.*?m3u8.*?)\"")
val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: ""
if (m3u8.isNotEmpty()) {
generateM3u8(

View file

@ -0,0 +1,59 @@
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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
open class Gofile : ExtractorApi() {
override val name = "Gofile"
override val mainUrl = "https://gofile.io"
override val requiresReferer = false
private val mainApi = "https://api.gofile.io"
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1)
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=12345")
.parsedSafe<Source>()?.data?.contents?.forEach {
callback.invoke(
ExtractorLink(
this.name,
this.name,
it.value["link"] ?: return,
"",
getQuality(it.value["name"]),
headers = mapOf(
"Cookie" to "accountToken=$token"
)
)
)
}
}
private fun getQuality(str: String?): Int {
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
?: Qualities.Unknown.value
}
data class Account(
@JsonProperty("data") val data: HashMap<String, String>? = null,
)
data class Data(
@JsonProperty("contents") val contents: HashMap<String, HashMap<String, String>>? = null,
)
data class Source(
@JsonProperty("data") val data: Data? = null,
)
}

View file

@ -58,7 +58,7 @@ open class GuardareStream : ExtractorApi() {
jsonVideoData.data.forEach {
callback.invoke(
ExtractorLink(
it.file + ".${it.type}",
this.name,
this.name,
it.file + ".${it.type}",
mainUrl,

View file

@ -0,0 +1,37 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.httpsify
open class Krakenfiles : ExtractorApi() {
override val name = "Krakenfiles"
override val mainUrl = "https://krakenfiles.com"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val id = Regex("/(?:view|embed-video)/([\\da-zA-Z]+)").find(url)?.groupValues?.get(1)
val doc = app.get("$mainUrl/embed-video/$id").document
val link = doc.selectFirst("source")?.attr("src")
callback.invoke(
ExtractorLink(
this.name,
this.name,
httpsify(link ?: return),
"",
Qualities.Unknown.value
)
)
}
}

View file

@ -6,6 +6,26 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import kotlin.random.Random
class Vidgomunimesb : StreamSB() {
override var mainUrl = "https://vidgomunimesb.xyz"
}
class Sbasian : StreamSB() {
override var mainUrl = "https://sbasian.pro"
override var name = "Sbasian"
}
class Sbnet : StreamSB() {
override var name = "Sbnet"
override var mainUrl = "https://sbnet.one"
}
class Keephealth : StreamSB() {
override var name = "Keephealth"
override var mainUrl = "https://keephealth.info"
}
class Sbspeed : StreamSB() {
override var name = "Sbspeed"
@ -85,24 +105,62 @@ class Sblongvu : StreamSB() {
override var mainUrl = "https://sblongvu.com"
}
// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
open class StreamSB : ExtractorApi() {
override var name = "StreamSB"
override var mainUrl = "https://watchsb.com"
override val requiresReferer = false
private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
private val hexArray = "0123456789ABCDEF".toCharArray()
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val regexID =
Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)")
val id = regexID.findAll(url).map {
it.value.replace(Regex("(embed-|/e/)"), "")
}.first()
val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}"
val headers = mapOf(
"watchsb" to "sbstream",
)
val mapped = app.get(
master.lowercase(),
headers = headers,
referer = url,
).parsedSafe<Main>()
M3u8Helper.generateM3u8(
name,
mapped?.streamData?.file ?: return,
url,
headers = headers
).forEach(callback)
private fun bytesToHex(bytes: ByteArray): String {
val hexChars = CharArray(bytes.size * 2)
for (j in bytes.indices) {
val v = bytes[j].toInt() and 0xFF
hexChars[j * 2] = hexArray[v ushr 4]
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
mapped.streamData.subs?.map {sub ->
subtitleCallback.invoke(
SubtitleFile(
sub.label.toString(),
sub.file ?: return@map null,
)
)
}
}
private fun encodeId(id: String): String {
val code = "${createHashTable()}||$id||${createHashTable()}||streamsb"
return code.toCharArray().joinToString("") { char ->
char.code.toString(16)
}
}
private fun createHashTable(): String {
return buildString {
repeat(12) {
append(alphabet[Random.nextInt(alphabet.length)])
}
}
return String(hexChars)
}
data class Subs (
@ -126,42 +184,4 @@ open class StreamSB : ExtractorApi() {
@JsonProperty("status_code") val statusCode: Int,
)
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/)"), "")
}.first()
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val master = "$mainUrl/sources16/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val headers = mapOf(
"watchsb" to "sbstream",
)
val mapped = app.get(
master.lowercase(),
headers = headers,
referer = url,
).parsedSafe<Main>()
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
M3u8Helper.generateM3u8(
name,
mapped?.streamData?.file ?: return,
url,
headers = headers
).forEach(callback)
mapped.streamData.subs?.map {sub ->
subtitleCallback.invoke(
SubtitleFile(
sub.label.toString(),
sub.file ?: return@map null,
)
)
}
}
}

View file

@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
val jsonvideodata = parseJson<TantifilmJsonData>(response)
return jsonvideodata.data.map {
ExtractorLink(
it.file+".${it.type}",
this.name,
this.name,
it.file+".${it.type}",
mainUrl,

View file

@ -0,0 +1,51 @@
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
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
open class Uservideo : ExtractorApi() {
override val name: String = "Uservideo"
override val mainUrl: String = "https://uservideo.xyz"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val script = app.get(url).document.selectFirst("script:containsData(hosts =)")?.data()
val host = script?.substringAfter("hosts = [\"")?.substringBefore("\"];")
val servers = script?.substringAfter("servers = \"")?.substringBefore("\";")
val sources = app.get("$host/s/$servers").text.substringAfter("\"sources\":[").substringBefore("],").let {
AppUtils.tryParseJson<List<Sources>>("[$it]")
}
val quality = Regex("(\\d{3,4})[Pp]").find(url)?.groupValues?.getOrNull(1)?.toIntOrNull()
sources?.map { source ->
callback.invoke(
ExtractorLink(
name,
name,
source.src ?: return@map null,
url,
quality ?: Qualities.Unknown.value,
)
)
}
}
data class Sources(
@JsonProperty("src") val src: String? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("label") val label: String? = null,
)
}

View file

@ -0,0 +1,51 @@
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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
open class Vicloud : ExtractorApi() {
override val name: String = "Vicloud"
override val mainUrl: String = "https://vicloud.sbs"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
app.get(
"$mainUrl/api/?$id=&_=${System.currentTimeMillis()}",
headers = mapOf(
"X-Requested-With" to "XMLHttpRequest"
),
referer = url
).parsedSafe<Responses>()?.sources?.map { source ->
callback.invoke(
ExtractorLink(
name,
name,
source.file ?: return@map null,
url,
getQualityFromName(source.label),
)
)
}
}
private data class Sources(
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null,
)
private data class Responses(
@JsonProperty("sources") val sources: List<Sources>? = arrayListOf(),
)
}

View file

@ -6,6 +6,10 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class Tubeless : Voe() {
override var mainUrl = "https://tubelessceliolymph.com"
}
open class Voe : ExtractorApi() {
override val name = "Voe"
override val mainUrl = "https://voe.sx"
@ -18,8 +22,8 @@ open class Voe : ExtractorApi() {
callback: (ExtractorLink) -> Unit
) {
val res = app.get(url, referer = referer).document
val link = res.select("script").find { it.data().contains("const sources") }?.data()
?.substringAfter("\"hls\": \"")?.substringBefore("\",")
val script = res.select("script").find { it.data().contains("sources =") }?.data()
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
M3u8Helper.generateM3u8(
name,

View file

@ -1,54 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
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.getQualityFromName
open class VoeExtractor : ExtractorApi() {
override val name: String = "Voe"
override val mainUrl: String = "https://voe.sx"
override val requiresReferer = false
private data class ResponseLinks(
@JsonProperty("hls") val hls: String?,
@JsonProperty("mp4") val mp4: String?,
@JsonProperty("video_height") val label: Int?
//val type: String // Mp4
)
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val html = app.get(url).text
if (html.isNotBlank()) {
val src = html.substringAfter("const sources =").substringBefore(";")
// Remove last comma, it is not proper json otherwise
.replace("0,", "0")
// Make json use the proper quotes
.replace("'", "\"")
//Log.i(this.name, "Result => (src) ${src}")
parseJson<ResponseLinks?>(src)?.let { voeLink ->
//Log.i(this.name, "Result => (voeLink) ${voeLink}")
// Always defaults to the hls link, but returns the mp4 if null
val linkUrl = voeLink.hls ?: voeLink.mp4
val linkLabel = voeLink.label?.toString() ?: ""
if (!linkUrl.isNullOrEmpty()) {
return listOf(
ExtractorLink(
name = this.name,
source = this.name,
url = linkUrl,
quality = getQualityFromName(linkLabel),
referer = url,
isM3u8 = voeLink.hls != null
)
)
}
}
}
return emptyList()
}
}

View file

@ -8,6 +8,16 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
class StreamM4u : XStreamCdn() {
override val name: String = "StreamM4u"
override val mainUrl: String = "https://streamm4u.club"
}
class Fembed9hd : XStreamCdn() {
override var mainUrl = "https://fembed9hd.com"
override var name = "Fembed9hd"
}
class Cdnplayer: XStreamCdn() {
override val name: String = "Cdnplayer"
override val mainUrl: String = "https://cdnplayer.online"

View file

@ -0,0 +1,158 @@
package com.lagradost.cloudstream3.extractors.helper
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.base64DecodeArray
import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName
import org.jsoup.nodes.Document
import java.net.URI
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object GogoHelper {
/**
* @param id base64Decode(show_id) + IV
* @return the encryption key
* */
private fun getKey(id: String): String? {
return normalSafeApiCall {
id.map {
it.code.toString(16)
}.joinToString("").substring(0, 32)
}
}
// https://github.com/saikou-app/saikou/blob/45d0a99b8a72665a29a1eadfb38c506b842a29d7/app/src/main/java/ani/saikou/parsers/anime/extractors/GogoCDN.kt#L97
// No Licence on the function
private fun cryptoHandler(
string: String,
iv: String,
secretKeyString: String,
encrypt: Boolean = true
): String {
//println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
val ivParameterSpec = IvParameterSpec(iv.toByteArray())
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
String(cipher.doFinal(base64DecodeArray(string)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
base64Encode(cipher.doFinal(string.toByteArray()))
}
}
/**
* @param iframeUrl something like https://gogoplay4.com/streaming.php?id=XXXXXX
* @param mainApiName used for ExtractorLink names and source
* @param iv secret iv from site, required non-null if isUsingAdaptiveKeys is off
* @param secretKey secret key for decryption from site, required non-null if isUsingAdaptiveKeys is off
* @param secretDecryptKey secret key to decrypt the response json, required non-null if isUsingAdaptiveKeys is off
* @param isUsingAdaptiveKeys generates keys from IV and ID, see getKey()
* @param isUsingAdaptiveData generate encrypt-ajax data based on $("script[data-name='episode']")[0].dataset.value
* */
suspend fun extractVidstream(
iframeUrl: String,
mainApiName: String,
callback: (ExtractorLink) -> Unit,
iv: String?,
secretKey: String?,
secretDecryptKey: String?,
// This could be removed, but i prefer it verbose
isUsingAdaptiveKeys: Boolean,
isUsingAdaptiveData: Boolean,
// If you don't want to re-fetch the document
iframeDocument: Document? = null
) = safeApiCall {
if ((iv == null || secretKey == null || secretDecryptKey == null) && !isUsingAdaptiveKeys)
return@safeApiCall
val id = Regex("id=([^&]+)").find(iframeUrl)!!.value.removePrefix("id=")
var document: Document? = iframeDocument
val foundIv =
iv ?: (document ?: app.get(iframeUrl).document.also { document = it })
.select("""div.wrapper[class*=container]""")
.attr("class").split("-").lastOrNull() ?: return@safeApiCall
val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall
val foundDecryptKey = secretDecryptKey ?: foundKey
val uri = URI(iframeUrl)
val mainUrl = "https://" + uri.host
val encryptedId = cryptoHandler(id, foundIv, foundKey)
val encryptRequestData = if (isUsingAdaptiveData) {
// Only fetch the document if necessary
val realDocument = document ?: app.get(iframeUrl).document
val dataEncrypted =
realDocument.select("script[data-name='episode']").attr("data-value")
val headers = cryptoHandler(dataEncrypted, foundIv, foundKey, false)
"id=$encryptedId&alias=$id&" + headers.substringAfter("&")
} else {
"id=$encryptedId&alias=$id"
}
val jsonResponse =
app.get(
"$mainUrl/encrypt-ajax.php?$encryptRequestData",
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
)
val dataencrypted =
jsonResponse.text.substringAfter("{\"data\":\"").substringBefore("\"}")
val datadecrypted = cryptoHandler(dataencrypted, foundIv, foundDecryptKey, false)
val sources = AppUtils.parseJson<GogoSources>(datadecrypted)
suspend fun invokeGogoSource(
source: GogoSource,
sourceCallback: (ExtractorLink) -> Unit
) {
if (source.file.contains(".m3u8")) {
M3u8Helper.generateM3u8(
mainApiName,
source.file,
mainUrl,
headers = mapOf("Origin" to "https://plyr.link")
).forEach(sourceCallback)
} else {
sourceCallback.invoke(
ExtractorLink(
mainApiName,
mainApiName,
source.file,
mainUrl,
getQualityFromName(source.label),
)
)
}
}
sources.source?.forEach {
invokeGogoSource(it, callback)
}
sources.sourceBk?.forEach {
invokeGogoSource(it, callback)
}
}
data class GogoSources(
@JsonProperty("source") val source: List<GogoSource>?,
@JsonProperty("sourceBk") val sourceBk: List<GogoSource>?,
)
data class GogoSource(
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("default") val default: String? = null
)
}

View file

@ -39,6 +39,7 @@ import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -108,8 +109,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// get() = episodes.isNotEmpty()
// options for player
protected var currentPrefQuality =
Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
/**
* Default profile 1
* Decides how links should be sorted based on a priority system.
* This will be set in runtime based on settings.
**/
protected var currentQualityProfile = 1
// protected var currentPrefQuality =
// Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
protected var fastForwardTime = 10000L
protected var androidTVInterfaceOffSeekTime = 10000L;
protected var androidTVInterfaceOnSeekTime = 30000L;
@ -1221,10 +1229,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
.toLong() * 1000L
androidTVInterfaceOffSeekTime =
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_off_seek_key), 10)
settingsManager.getInt(
ctx.getString(R.string.android_tv_interface_off_seek_key),
10
)
.toLong() * 1000L
androidTVInterfaceOnSeekTime =
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_on_seek_key), 10)
settingsManager.getInt(
ctx.getString(R.string.android_tv_interface_on_seek_key),
10
)
.toLong() * 1000L
navigationBarHeight = ctx.getNavigationBarHeight()
@ -1257,10 +1271,20 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
ctx.getString(R.string.double_tap_pause_enabled_key),
false
)
currentPrefQuality = settingsManager.getInt(
ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key),
currentPrefQuality
)
val profiles = QualityDataHelper.getProfiles()
val type = if (ctx.isUsingMobileData())
QualityDataHelper.QualityProfileType.Data
else QualityDataHelper.QualityProfileType.WiFi
currentQualityProfile =
profiles.firstOrNull { it.type == type }?.id ?: profiles.firstOrNull()?.id
?: currentQualityProfile
// currentPrefQuality = settingsManager.getInt(
// ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key),
// currentPrefQuality
// )
// useSystemBrightness =
// settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
}

View file

@ -32,6 +32,10 @@ import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriority
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriorityDialog
import com.lagradost.cloudstream3.ui.result.*
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
@ -57,6 +61,9 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_cl
import kotlinx.android.synthetic.main.player_select_tracks.*
import kotlinx.coroutines.Job
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.abs
class GeneratorPlayer : FullScreenPlayer() {
companion object {
@ -188,17 +195,31 @@ class GeneratorPlayer : FullScreenPlayer() {
player.addTimeStamps(listOf()) // clear stamps
}
private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> {
return currentLinks.sortedBy {
val (linkData, _) = it
var quality = linkData?.quality ?: Qualities.Unknown.value
private fun closestQuality(target: Int?): Qualities {
if (target == null) return Qualities.Unknown
return Qualities.values().minBy { abs(it.value - target) }
}
// we set all qualities above current max as reverse
if (useQualitySettings && quality > currentPrefQuality) {
quality = currentPrefQuality - quality - 1
}
// negative because we want to sort highest quality first
-(quality)
private fun getLinkPriority(
qualityProfile: Int,
link: Pair<ExtractorLink?, ExtractorUri?>
): Int {
val (linkData, _) = link
val qualityPriority = QualityDataHelper.getQualityPriority(
qualityProfile,
closestQuality(linkData?.quality)
)
val sourcePriority =
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name)
// negative because we want to sort highest quality first
return qualityPriority + sourcePriority
}
private fun sortLinks(qualityProfile: Int): List<Pair<ExtractorLink?, ExtractorUri?>> {
return currentLinks.sortedBy {
-getLinkPriority(qualityProfile, it)
}
}
@ -219,6 +240,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
meta.name = newMeta.headerName
}
is ExtractorUri -> {
if (newMeta.tvType?.isMovieType() == false) {
meta.episode = newMeta.episode
@ -584,33 +606,39 @@ class GeneratorPlayer : FullScreenPlayer() {
var sourceIndex = 0
var startSource = 0
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
val sortedUrls = sortLinks(useQualitySettings = false)
if (sortedUrls.isEmpty()) {
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone = true
} else {
startSource = sortedUrls.indexOf(currentSelectedLink)
sourceIndex = startSource
fun refreshLinks(qualityProfile: Int) {
sortedUrls = sortLinks(qualityProfile)
if (sortedUrls.isEmpty()) {
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
true
} else {
startSource = sortedUrls.indexOf(currentSelectedLink)
sourceIndex = startSource
val sourcesArrayAdapter =
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
val sourcesArrayAdapter =
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
val name = link?.name ?: uri?.name ?: "NULL"
"$name ${Qualities.getStringByInt(link?.quality)}"
})
sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
val name = link?.name ?: uri?.name ?: "NULL"
"$name ${Qualities.getStringByInt(link?.quality)}"
})
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
providerList.adapter = sourcesArrayAdapter
providerList.setSelection(sourceIndex)
providerList.setItemChecked(sourceIndex, true)
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
providerList.adapter = sourcesArrayAdapter
providerList.setSelection(sourceIndex)
providerList.setItemChecked(sourceIndex, true)
providerList.setOnItemClickListener { _, _, which, _ ->
sourceIndex = which
providerList.setItemChecked(which, true)
providerList.setOnItemClickListener { _, _, which, _ ->
sourceIndex = which
providerList.setItemChecked(which, true)
}
}
}
refreshLinks(currentQualityProfile)
sourceDialog.setOnDismissListener {
if (shouldDismiss) dismiss()
selectSourceDialog = null
@ -650,6 +678,29 @@ class GeneratorPlayer : FullScreenPlayer() {
sourceDialog.dismissSafe(activity)
}
fun setProfileName(profile: Int) {
sourceDialog.source_settings_btt.setText(
QualityDataHelper.getProfileName(
profile
)
)
}
setProfileName(currentQualityProfile)
sourceDialog.profiles_click_settings.setOnClickListener {
val activity = activity ?: return@setOnClickListener
QualityProfileDialog(
activity,
R.style.AlertDialogCustomBlack,
currentLinks.mapNotNull { it.first },
currentQualityProfile
) { profile ->
currentQualityProfile = profile.id
setProfileName(profile.id)
refreshLinks(profile.id)
}.show()
}
sourceDialog.subtitles_encoding_format?.apply {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
@ -848,7 +899,7 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun startPlayer() {
if (isActive) return // we don't want double load when you skip loading
val links = sortLinks()
val links = sortLinks(currentQualityProfile)
if (links.isEmpty()) {
noLinksFound()
return
@ -869,12 +920,12 @@ class GeneratorPlayer : FullScreenPlayer() {
}
override fun hasNextMirror(): Boolean {
val links = sortLinks()
val links = sortLinks(currentQualityProfile)
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
}
override fun nextMirror() {
val links = sortLinks()
val links = sortLinks(currentQualityProfile)
if (links.isEmpty()) {
noLinksFound()
return
@ -933,6 +984,7 @@ class GeneratorPlayer : FullScreenPlayer() {
is ResultEpisode -> {
DataStoreHelper.removeLastWatched(newMeta.parentId)
}
is ExtractorUri -> {
DataStoreHelper.removeLastWatched(newMeta.parentId)
}
@ -949,6 +1001,7 @@ class GeneratorPlayer : FullScreenPlayer() {
isFromDownload = false
)
}
is ExtractorUri -> {
DataStoreHelper.setLastWatched(
resumeMeta.parentId,
@ -1080,6 +1133,7 @@ class GeneratorPlayer : FullScreenPlayer() {
season = meta.season
tvType = meta.tvType
}
is ExtractorUri -> {
headerName = meta.headerName
subName = meta.name
@ -1296,6 +1350,7 @@ class GeneratorPlayer : FullScreenPlayer() {
is Resource.Loading -> {
startLoading()
}
is Resource.Success -> {
// provider returned false
//if (it.value != true) {
@ -1303,6 +1358,7 @@ class GeneratorPlayer : FullScreenPlayer() {
//}
startPlayer()
}
is Resource.Failure -> {
showToast(activity, it.errorString, Toast.LENGTH_LONG)
startPlayer()
@ -1315,6 +1371,17 @@ class GeneratorPlayer : FullScreenPlayer() {
val turnVisible = it.isNotEmpty()
val wasGone = overlay_loading_skip_button?.isGone == true
overlay_loading_skip_button?.isVisible = turnVisible
normalSafeApiCall {
if (currentLinks.any { link ->
getLinkPriority(currentQualityProfile, link) >=
QualityDataHelper.AUTO_SKIP_PRIORITY
}
) {
startPlayer()
}
}
if (turnVisible && wasGone) {
overlay_loading_skip_button?.requestFocus()
}

View file

@ -156,18 +156,24 @@ class PlayerGeneratorViewModel : ViewModel() {
val currentSubs = mutableSetOf<SubtitleData>()
// clear old data
_currentSubs.postValue(currentSubs)
_currentLinks.postValue(currentLinks)
_currentSubs.postValue(emptySet())
_currentLinks.postValue(emptySet())
// load more data
_loadingLinks.postValue(Resource.Loading())
val loadingState = safeApiCall {
generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, {
currentLinks.add(it)
_currentLinks.postValue(currentLinks)
// Clone to prevent ConcurrentModificationException
normalSafeApiCall {
// Extra normalSafeApiCall since .toSet() iterates.
_currentLinks.postValue(currentLinks.toSet())
}
}, {
currentSubs.add(it)
// _currentSubs.postValue(currentSubs) // this causes ConcurrentModificationException, so fuck it
normalSafeApiCall {
_currentSubs.postValue(currentSubs.toSet())
}
})
}

View file

@ -0,0 +1,60 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.AppUtils
import kotlinx.android.synthetic.main.player_prioritize_item.view.*
data class SourcePriority<T>(
val data: T,
val name: String,
var priority: Int
)
class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
AppUtils.DiffAdapter<SourcePriority<T>>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PriorityViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is PriorityViewHolder -> holder.bind(items[position])
}
}
class PriorityViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
fun <T> bind(item: SourcePriority<T>) {
val plusButton: ImageView = itemView.add_button
val subtractButton: ImageView = itemView.subtract_button
val priorityText: TextView = itemView.priority_text
val priorityNumber: TextView = itemView.priority_number
priorityText.text = item.name
fun updatePriority() {
priorityNumber.text = item.priority.toString()
}
updatePriority()
plusButton.setOnClickListener {
// If someone clicks til the integer limit then they deserve to crash.
item.priority++
updatePriority()
}
subtractButton.setOnClickListener {
item.priority--
updatePriority()
}
}
}
}

View file

@ -0,0 +1,116 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.result.UiImage
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.player_quality_profile_item.view.card_view
import kotlinx.android.synthetic.main.player_quality_profile_item.view.outline
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_image_background
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_text
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_mobile_data
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_wifi
class ProfilesAdapter(
override val items: MutableList<QualityDataHelper.QualityProfile>,
val usedProfile: Int,
val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit,
) :
AppUtils.DiffAdapter<QualityDataHelper.QualityProfile>(
items,
comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile ->
first.id == second.id
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ProfilesViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.player_quality_profile_item, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ProfilesViewHolder -> holder.bind(items[position], position)
}
}
private var currentItem: Pair<Int, QualityDataHelper.QualityProfile>? = null
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
return currentItem?.second
}
inner class ProfilesViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
private val art = listOf(
R.drawable.profile_bg_teal,
R.drawable.profile_bg_blue,
R.drawable.profile_bg_dark_blue,
R.drawable.profile_bg_purple,
R.drawable.profile_bg_pink,
R.drawable.profile_bg_red,
R.drawable.profile_bg_orange,
)
fun bind(item: QualityDataHelper.QualityProfile, index: Int) {
val priorityText: TextView = itemView.profile_text
val profileBg: ImageView = itemView.profile_image_background
val wifiText: TextView = itemView.text_is_wifi
val dataText: TextView = itemView.text_is_mobile_data
val outline: View = itemView.outline
val cardView: View = itemView.card_view
priorityText.text = item.name.asString(itemView.context)
dataText.isVisible = item.type == QualityDataHelper.QualityProfileType.Data
wifiText.isVisible = item.type == QualityDataHelper.QualityProfileType.WiFi
fun setCurrentItem() {
val prevIndex = currentItem?.first
// Prevent UI bug when re-selecting the item quickly
if (prevIndex == index) {
return
}
currentItem = index to item
clickCallback.invoke(prevIndex, index)
}
outline.isVisible = currentItem?.second?.id == item.id
profileBg.setImage(UiImage.Drawable(art[index % art.size]), null, false) { palette ->
val color = palette.getDarkVibrantColor(
ContextCompat.getColor(
itemView.context,
R.color.dubColorBg
)
)
wifiText.backgroundTintList = ColorStateList.valueOf(color)
dataText.backgroundTintList = ColorStateList.valueOf(color)
}
val textStyle =
if (item.id == usedProfile) {
Typeface.BOLD
} else {
Typeface.NORMAL
}
priorityText.setTypeface(null, textStyle)
cardView.setOnClickListener {
setCurrentItem()
}
}
}
}

View file

@ -0,0 +1,159 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.content.Context
import androidx.annotation.StringRes
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.Qualities
object QualityDataHelper {
private const val VIDEO_SOURCE_PRIORITY = "video_source_priority"
private const val VIDEO_PROFILE_NAME = "video_profile_name"
private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority"
private const val VIDEO_PROFILE_TYPE = "video_profile_type"
private const val DEFAULT_SOURCE_PRIORITY = 1
/**
* Automatically skip loading links once this priority is reached
**/
const val AUTO_SKIP_PRIORITY = 10
/**
* Must be higher than amount of QualityProfileTypes
**/
private const val PROFILE_COUNT = 7
/**
* Unique guarantees that there will always be one of this type in the profile list.
**/
enum class QualityProfileType(@StringRes val stringRes: Int, val unique: Boolean) {
None(R.string.none, false),
WiFi(R.string.wifi, true),
Data(R.string.mobile_data, true)
}
data class QualityProfile(
val name: UiText,
val id: Int,
val type: QualityProfileType
)
fun getSourcePriority(profile: Int, name: String?): Int {
if (name == null) return DEFAULT_SOURCE_PRIORITY
return getKey(
"$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile",
name,
DEFAULT_SOURCE_PRIORITY
) ?: DEFAULT_SOURCE_PRIORITY
}
fun setSourcePriority(profile: Int, name: String, priority: Int) {
setKey("$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, priority)
}
fun setProfileName(profile: Int, name: String?) {
val path = "$currentAccount/$VIDEO_PROFILE_NAME/$profile"
if (name == null) {
removeKey(path)
} else {
setKey(path, name.trim())
}
}
fun getProfileName(profile: Int): UiText {
return getKey<String>("$currentAccount/$VIDEO_PROFILE_NAME/$profile")?.let { txt(it) }
?: txt(R.string.profile_number, profile)
}
fun getQualityPriority(profile: Int, quality: Qualities): Int {
return getKey(
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
quality.value.toString(),
quality.defaultPriority
) ?: quality.defaultPriority
}
fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) {
setKey(
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
quality.value.toString(),
priority
)
}
fun getQualityProfileType(profile: Int): QualityProfileType {
return getKey("$currentAccount/$VIDEO_PROFILE_TYPE/$profile") ?: QualityProfileType.None
}
fun setQualityProfileType(profile: Int, type: QualityProfileType?) {
val path = "$currentAccount/$VIDEO_PROFILE_TYPE/$profile"
if (type == QualityProfileType.None) {
removeKey(path)
} else {
setKey(path, type)
}
}
/**
* Gets all quality profiles, always includes one profile with WiFi and Data
* Must under all circumstances at least return one profile
**/
fun getProfiles(): List<QualityProfile> {
val availableTypes = QualityProfileType.values().toMutableList()
val profiles = (1..PROFILE_COUNT).map { profileNumber ->
// Get the real type
val type = getQualityProfileType(profileNumber)
// This makes it impossible to get more than one of each type
// Duplicates will be turned to None
val uniqueType = if (type.unique && !availableTypes.remove(type)) {
QualityProfileType.None
} else {
type
}
QualityProfile(
getProfileName(profileNumber),
profileNumber,
uniqueType
)
}.toMutableList()
/**
* If no profile of this type exists: insert it on the earliest profile with None type
**/
fun insertType(
list: MutableList<QualityProfile>,
type: QualityProfileType
) {
if (list.any { it.type == type }) return
val index =
list.indexOfFirst { it.type == QualityProfileType.None }
list.getOrNull(index)?.copy(type = type)
?.let { fixed ->
list.set(index, fixed)
}
}
QualityProfileType.values().forEach {
if (it.unique) insertType(profiles, it)
}
debugAssert({
!QualityProfileType.values().all { type ->
!type.unique || profiles.any { it.type == type }
}
}, { "All unique quality types do not exist" })
debugAssert({
profiles.isEmpty()
}, { "No profiles!" })
return profiles
}
}

View file

@ -0,0 +1,106 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.app.Dialog
import android.view.View
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlinx.android.synthetic.main.player_quality_profile_dialog.*
class QualityProfileDialog(
val activity: FragmentActivity,
@StyleRes val themeRes: Int,
private val links: List<ExtractorLink>,
private val usedProfile: Int,
private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit
) : Dialog(activity, themeRes) {
override fun show() {
setContentView(R.layout.player_quality_profile_dialog)
val profilesRecyclerView: RecyclerView = profiles_recyclerview
val useBtt: View = use_btt
val editBtt: View = edit_btt
val cancelBtt: View = cancel_btt
val defaultBtt: View = set_default_btt
val currentProfileText: TextView = currently_selected_profile_text
val selectedItemActionsHolder: View = selected_item_holder
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile()
}
fun refreshProfiles() {
currentProfileText.text = getProfileName(usedProfile).asString(context)
(profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles())
}
profilesRecyclerView.adapter = ProfilesAdapter(
mutableListOf(),
usedProfile,
) { oldIndex: Int?, newIndex: Int ->
profilesRecyclerView.adapter?.notifyItemChanged(newIndex)
selectedItemActionsHolder.alpha = 1f
if (oldIndex != null) {
profilesRecyclerView.adapter?.notifyItemChanged(oldIndex)
}
}
refreshProfiles()
editBtt.setOnClickListener {
getCurrentProfile()?.let { profile ->
SourcePriorityDialog(context, themeRes, links, profile) {
refreshProfiles()
}.show()
}
}
defaultBtt.setOnClickListener {
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
val choices = QualityDataHelper.QualityProfileType.values()
.filter { it != QualityDataHelper.QualityProfileType.None }
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
activity.showBottomDialog(
choiceNames,
choices.indexOf(currentProfile.type),
txt(R.string.set_default).asString(context),
false,
{},
{ index ->
val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog
// Remove previous picks
if (pickedChoice.unique) {
getProfiles().filter { it.type == pickedChoice }.forEach {
QualityDataHelper.setQualityProfileType(it.id, null)
}
}
QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice)
refreshProfiles()
})
}
cancelBtt.setOnClickListener {
this.dismissSafe()
}
useBtt.setOnClickListener {
getCurrentProfile()?.let {
profileSelectionCallback.invoke(it)
this.dismissSafe()
}
}
super.show()
}
}

View file

@ -0,0 +1,105 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.app.Dialog
import android.content.Context
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.work.impl.constraints.controllers.ConstraintController
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlinx.android.synthetic.main.player_select_source_priority.*
class SourcePriorityDialog(
ctx: Context,
@StyleRes themeRes: Int,
val links: List<ExtractorLink>,
private val profile: QualityDataHelper.QualityProfile,
/**
* Notify that the profile overview should be updated, for example if the name has been updated
* Should not be called excessively.
**/
private val updatedCallback: () -> Unit
) : Dialog(ctx, themeRes) {
override fun show() {
setContentView(R.layout.player_select_source_priority)
val sourcesRecyclerView: RecyclerView = sort_sources
val qualitiesRecyclerView: RecyclerView = sort_qualities
val profileText: EditText = profile_text_editable
val saveBtt: View = save_btt
val exitBtt: View = close_btt
val helpBtt: View = help_btt
profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context))
profileText.hint = txt(R.string.profile_number, profile.id).asString(context)
sourcesRecyclerView.adapter = PriorityAdapter(
links.map { link ->
SourcePriority(
null,
link.source,
QualityDataHelper.getSourcePriority(profile.id, link.source)
)
}.distinctBy { it.name }.sortedBy { -it.priority }.toMutableList()
)
qualitiesRecyclerView.adapter = PriorityAdapter(
Qualities.values().mapNotNull {
SourcePriority(
it,
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null },
QualityDataHelper.getQualityPriority(profile.id, it)
)
}.sortedBy { -it.priority }.toMutableList()
)
@Suppress("UNCHECKED_CAST") // We know the types
saveBtt.setOnClickListener {
val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter<Qualities>
val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter<Nothing>
val qualities = qualityAdapter?.items ?: emptyList()
val sources = sourcesAdapter?.items ?: emptyList()
qualities.forEach {
val data = it.data as? Qualities ?: return@forEach
QualityDataHelper.setQualityPriority(profile.id, data, it.priority)
}
sources.forEach {
QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority)
}
qualityAdapter?.updateList(qualities.sortedBy { -it.priority })
sourcesAdapter?.updateList(sources.sortedBy { -it.priority })
val savedProfileName = profileText.text.toString()
if (savedProfileName.isBlank()) {
QualityDataHelper.setProfileName(profile.id, null)
} else {
QualityDataHelper.setProfileName(profile.id, savedProfileName)
}
updatedCallback.invoke()
}
exitBtt.setOnClickListener {
this.dismissSafe()
}
helpBtt.setOnClickListener {
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setMessage(R.string.quality_profile_help)
}.show()
}
super.show()
}
}

View file

@ -72,7 +72,7 @@ sealed class UiImage {
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
when (value) {
is UiImage.Image -> setImageImage(value,fadeIn)
is UiImage.Image -> setImageImage(value, fadeIn)
is UiImage.Drawable -> setImageDrawable(value)
null -> {
this?.isVisible = false
@ -88,7 +88,7 @@ fun ImageView?.setImageImage(value: UiImage.Image, fadeIn: Boolean = true) {
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
if (this == null) return
this.isVisible = true
setImageResource(value.resId)
this.setImage(UiImage.Drawable(value.resId))
}
@JvmName("imgNull")

View file

@ -103,7 +103,7 @@ val appLanguages = arrayListOf(
Triple("", "اردو", "ur"),
Triple("", "Tiếng Việt", "vi"),
Triple("", "中文", "zh"),
Triple("\uD83C\uDDF9\uD83C\uDDFC", "文言", "zh-rTW"),
Triple("\uD83C\uDDF9\uD83C\uDDFC", "正體中文(臺灣)", "zh-rTW"),
/* end language list */
).sortedBy { it.second.lowercase() } //ye, we go alphabetical, so ppl don't put their lang on top

View file

@ -117,7 +117,7 @@ object DataStoreHelper {
/**
* A datastore wide account for future implementations of a multiple account system
**/
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
fun getAllWatchStateIds(): List<Int>? {
val folder = "$currentAccount/$RESULT_WATCH_STATE"

View file

@ -114,16 +114,16 @@ data class ExtractorSubtitleLink(
*/
val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
enum class Qualities(var value: Int) {
Unknown(400),
P144(144), // 144p
P240(240), // 240p
P360(360), // 360p
P480(480), // 480p
P720(720), // 720p
P1080(1080), // 1080p
P1440(1440), // 1440p
P2160(2160); // 4k or 2160p
enum class Qualities(var value: Int, val defaultPriority: Int) {
Unknown(400, 4),
P144(144, 0), // 144p
P240(240, 2), // 240p
P360(360, 3), // 360p
P480(480, 4), // 480p
P720(720, 5), // 720p
P1080(1080, 6), // 1080p
P1440(1440, 7), // 1440p
P2160(2160, 8); // 4k or 2160p
companion object {
fun getStringByInt(qual: Int?): String {
@ -135,6 +135,14 @@ enum class Qualities(var value: Int) {
else -> "${qual}p"
}
}
fun getStringByIntFull(quality: Int): String {
return when (quality) {
0 -> "Auto"
Unknown.value -> "Unknown"
P2160.value -> "4K"
else -> "${quality}p"
}
}
}
}
@ -236,6 +244,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
XStreamCdn(),
StreamSB(),
Vidgomunimesb(),
StreamSB1(),
StreamSB2(),
StreamSB3(),
@ -275,7 +284,6 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Uqload2(),
Evoload(),
Evoload1(),
VoeExtractor(),
UpstreamExtractor(),
Tomatomatela(),
@ -342,6 +350,24 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
DesuOdvip(),
DesuDrive(),
Chillx(),
Watchx(),
Bestx(),
Keephealth(),
Sbnet(),
Sbasian(),
Sblongvu(),
Fembed9hd(),
StreamM4u(),
Krakenfiles(),
Gofile(),
Vicloud(),
Uservideo(),
Movhide(),
StreamhideCom(),
FileMoonIn(),
Moviesm4u(),
Filesim(),
FileMoon(),
FileMoonSx(),
@ -357,6 +383,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Vidmoly(),
Vidmolyme(),
Voe(),
Tubeless(),
Moviehab(),
MoviehabNet(),
Jeniusplay(),

View file

@ -250,17 +250,6 @@ object SingleSelectionHelper {
)
}
fun showBottomDialog(
items: List<String>,
selectedIndex: Int,
name: String,
showApply: Boolean,
dismissCallback: () -> Unit,
callback: (Int) -> Unit,
) {
}
/** Only for a low amount of items */
fun Activity?.showBottomDialog(
items: List<String>,

View file

@ -44,12 +44,13 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions.bitmapTransform
import com.bumptech.glide.request.target.Target
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.UiImage
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.GlideOptions.bitmapTransform
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlin.math.roundToInt
@ -188,11 +189,30 @@ object UIHelper {
fadeIn: Boolean = true,
colorCallback: ((Palette) -> Unit)? = null
): Boolean {
if (this == null || url.isNullOrBlank()) return false
if (url.isNullOrBlank()) return false
this.setImage(UiImage.Image(url, headers, errorImageDrawable), errorImageDrawable, fadeIn, colorCallback)
return true
}
fun ImageView?.setImage(
uiImage: UiImage?,
@DrawableRes
errorImageDrawable: Int? = null,
fadeIn: Boolean = true,
colorCallback: ((Palette) -> Unit)? = null
): Boolean {
if (this == null || uiImage == null) return false
val (glideImage, identifier) =
(uiImage as? UiImage.Drawable)?.resId?.let {
it to it.toString()
} ?: (uiImage as? UiImage.Image)?.let { image ->
GlideUrl(image.url) { image.headers ?: emptyMap() } to image.url
} ?: return false
return try {
val builder = GlideApp.with(this)
.load(GlideUrl(url) { headers ?: emptyMap() })
.load(glideImage)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.ALL).let { req ->
if (fadeIn)
@ -211,7 +231,13 @@ object UIHelper {
isFirstResource: Boolean
): Boolean {
resource?.toBitmapOrNull()
?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) }
?.let { bitmap ->
createPaletteAsync(
identifier,
bitmap,
colorCallback
)
}
return false
}

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/white" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
</vector>

View file

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
android:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13H5v-2h14v2z"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View file

@ -0,0 +1,48 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="15dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/priority_text"
style="@style/NoCheckLabel"
android:layout_width="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/subtract_button"
tools:text="hello" />
<ImageView
android:id="@+id/subtract_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/priority_number"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="10dp"
android:src="@drawable/baseline_remove_24" />
<TextView
android:id="@+id/priority_number"
style="@style/NoCheckLabel"
android:layout_width="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/add_button"
android:gravity="center"
android:minWidth="50dp"
android:textColor="?attr/textColor"
tools:text="1" />
<ImageView
android:id="@+id/add_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="10dp"
android:src="@drawable/ic_baseline_add_24" />
</RelativeLayout>

View file

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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:id="@+id/profile_text_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingTop="10dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="10dp"
android:text="@string/profiles"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/currently_selected_profile_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textColor="?attr/textColor"
android:textSize="15sp"
tools:text="Profile 1" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/profiles_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/profile_button_bar"
android:layout_below="@+id/profile_text_bar"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/player_quality_profile_item" />
<LinearLayout
android:id="@+id/profile_button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_margin="10dp"
android:animateLayoutChanges="true"
android:gravity="end|bottom"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/selected_item_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:alpha="0.5">
<com.google.android.material.button.MaterialButton
android:id="@+id/edit_btt"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:text="@string/edit" />
<com.google.android.material.button.MaterialButton
android:id="@+id/set_default_btt"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:text="@string/set_default" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<com.google.android.material.button.MaterialButton
android:id="@+id/use_btt"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:text="@string/use" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_btt"
style="@style/BlackButton"
android:layout_width="wrap_content"
android:text="@string/sort_cancel" />
</LinearLayout>
</RelativeLayout>

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:focusable="false">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1"
android:layout_marginStart="10dp"
android:animateLayoutChanges="true"
android:backgroundTint="?attr/primaryGrayBackground"
android:foreground="?attr/selectableItemBackgroundBorderless"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/profile_image_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.4"
android:contentDescription="@string/profile_background_des"
android:scaleType="centerCrop" />
<View
android:id="@+id/outline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/outline"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/profile_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="10dp"
android:textSize="16sp"
tools:text="@string/mobile_data" />
<TextView
android:id="@+id/text_is_wifi"
style="@style/DubButton"
android:layout_gravity="end"
android:text="@string/wifi"
android:textColor="@color/textColor" />
<TextView
android:id="@+id/text_is_mobile_data"
style="@style/DubButton"
android:layout_gravity="end"
android:text="@string/mobile_data"
android:textColor="@color/textColor" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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"
@ -20,19 +21,44 @@
android:layout_weight="50"
android:orientation="vertical">
<TextView
<LinearLayout
android:id="@+id/profiles_click_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingTop="10dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="10dp"
android:text="@string/pick_source"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold" />
android:background="@drawable/outline_drawable_less"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingTop="10dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="10dp"
android:text="@string/pick_source"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/source_settings_btt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:drawablePadding="10dp"
android:gravity="center"
android:minWidth="140dp"
android:paddingHorizontal="10dp"
android:textColor="?attr/textColor"
android:textSize="15sp"
app:drawableEndCompat="@drawable/ic_outline_settings_24"
tools:text="@string/profile_number" />
</LinearLayout>
<ListView
android:id="@+id/sort_providers"

View file

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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:background="@null"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="60dp"
android:baselineAligned="false"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/sort_sources_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingTop="10dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="10dp"
android:text="@string/pick_source"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/sort_sources"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
android:listSelector="@drawable/outline_drawable_less"
android:nextFocusLeft="@id/sort_subtitles"
android:nextFocusRight="@id/apply_btt"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:layout_height="100dp"
tools:listitem="@layout/player_prioritize_item" />
</LinearLayout>
<LinearLayout
android:id="@+id/sort_subtitles_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:orientation="vertical">
<!-- android:id="@+id/subs_settings" android:foreground="?android:attr/selectableItemBackgroundBorderless"
-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<LinearLayout
android:id="@+id/subtitles_click_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingTop="10dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="10dp"
android:text="@string/qualities"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/help_btt"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/baseline_help_outline_24"
android:contentDescription="@string/help" />
</LinearLayout>
<ImageView
android:layout_width="25dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginTop="0dp"
android:layout_marginEnd="10dp"
android:contentDescription="@string/home_change_provider_img_des"
android:src="@drawable/ic_outline_settings_24"
android:visibility="gone" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/sort_qualities"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
android:listSelector="@drawable/outline_drawable_less"
android:nextFocusLeft="@id/sort_providers"
android:nextFocusRight="@id/cancel_btt"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:layout_height="200dp"
tools:listfooter="@layout/sort_bottom_footer_add_choice"
tools:listitem="@layout/player_prioritize_item" />
</LinearLayout>
</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:orientation="horizontal">
<EditText
android:id="@+id/profile_text_editable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:inputType="text"
android:maxLength="32"
android:layout_marginHorizontal="?android:attr/listPreferredItemPaddingStart"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="@string/profile_number"
android:autofillHints="username" />
<!-- <ImageView-->
<!-- android:layout_width="50dp"-->
<!-- android:layout_height="50dp"-->
<!-- android:padding="10dp"-->
<!-- android:layout_gravity="center"-->
<!-- android:src="@drawable/outline_edit_24" />-->
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
</Space>
<com.google.android.material.button.MaterialButton
android:id="@+id/save_btt"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:text="@string/sort_save" />
<com.google.android.material.button.MaterialButton
android:id="@+id/close_btt"
style="@style/BlackButton"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:text="@string/sort_close" />
</LinearLayout>
</LinearLayout>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string formatted="true" name="app_dub_sub_episode_text_format">%s еп. %d</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- KEYS DON'T TRANSLATE -->
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- KEYS DON'T TRANSLATE -->
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<string name="app_name">CloudStream</string>
<string name="title_home">Αρχική</string>

View file

@ -33,4 +33,16 @@
<string name="next_episode_time_hour_format" formatted="true">%dساعت %dدقیقه</string>
<string name="next_episode_time_min_format" formatted="true">%dدقیقه</string>
<string name="home_main_poster_img_des">پوستر اصلی</string>
<string name="torrent">تورنت</string>
<string name="free_storage">آزاد</string>
<string name="documentaries">مستند ها</string>
<string name="ova">انیمیشن ویدیویی اصلی</string>
<string name="max">حداکثر</string>
<string name="movies">فیلم‌ها</string>
<string name="tv_series">سریال های تلویزیونی</string>
<string name="asian_drama">درام های آسیایی</string>
<string name="anime">انیمه</string>
<string name="cartoons">کارتونها</string>
<string name="used_storage">استفاده شده</string>
<string name="app_storage">برنامه</string>
</resources>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<string name="app_name">CloudStream</string>
<string name="title_home">Accueil</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">रफ्तार (%.2fx)</string>
@ -147,4 +146,13 @@
<string name="next_episode_time_hour_format" formatted="true">%dh %dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="result_poster_img_des">विज्ञापन</string>
<string name="home_next_random_img_des">अगला रैंडम</string>
<string name="go_back_img_des">वापस जाओ</string>
<string name="search_poster_img_des">पोस्टर</string>
<string name="preview_background_img_des">पृष्ठभूमि का पूर्वावलोकन करें</string>
<string name="home_change_provider_img_des">प्रदाता बदलें</string>
<string name="cast_format" formatted="true">Cast: %s</string>
<string name="home_main_poster_img_des">मुख्य पोस्टर</string>
<string name="episode_poster_img_des">एपिसोड का पोस्टर</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
</resources>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>

View file

@ -198,4 +198,7 @@
\nSky UK Limitedによる無脳なDMCAテイクダウンのため🤮、アプリ内でリポジトリサイトをリンクすることができません。
\n
\n私たちのDiscordに参加するか、オンラインで検索してください。</string>
<string name="preview_background_img_des">バックグラウンドをプレビュー</string>
<string name="play_livestream_button">ライブストリームの再生</string>
<string name="home_change_provider_img_des">プロバイダーの変更</string>
</resources>

View file

@ -125,4 +125,11 @@
<string name="download_started">ಡೌನ್‌ಲೋಡ್ ಪ್ರಾರಂಭವಾಗಿದೆ</string>
<string name="download_canceled">ಡೌನ್‌ಲೋಡ್ ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ</string>
<string name="home_next_random_img_des">ಮುಂದಿನ ರಾಂಡಮ್</string>
<string name="swipe_to_seek_settings">ಮುಂದಕ್ಕೆ ಹೋಗಲು ಸ್ವೈಪ್ ಮಾಡಿ</string>
<string name="swipe_to_seek_settings_des">ವೀಡಿಯೊದಲ್ಲಿ ನಿಮ್ಮ ಸ್ಥಾನವನ್ನು ನಿಯಂತ್ರಿಸಲು ಅಕ್ಕಪಕ್ಕಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ</string>
<string name="autoplay_next_settings">ಮುಂದಿನ ಸಂಚಿಕೆಯನ್ನು ಆಟೋ ಪ್ಲೇ ಮಾಡಿ</string>
<string name="double_tap_to_seek_settings">ಮುಂದೂಡಲು ಅಥವಾ ಇಂದೂಡಲು ಎರಡು ಬಾರಿ ಟ್ಯಾಪ್ ಮಾಡಿ</string>
<string name="swipe_to_change_settings_des">Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ</string>
<string name="autoplay_next_settings_des">ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ</string>
<string name="swipe_to_change_settings">ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ</string>
</resources>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">Брзина (%.2fx)</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">വേഗം (%.2fx)</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>

View file

@ -1,2 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<resources/>
<resources>
<string name="episode_more_options_des">ଅଧିକ ଵିକଳ୍ପ</string>
<string name="type_watching">ଦେଖୁଛନ୍ତି</string>
<string name="next_episode_time_day_format" formatted="true">%dଦି %dଘ %dମି</string>
<string name="next_episode_time_hour_format" formatted="true">%dଘ %dମି</string>
<string name="next_episode_time_min_format" formatted="true">%dମି</string>
<string name="type_re_watching">ପୁନଃଦେଖୁଛନ୍ତି</string>
<string name="home_expanded_hide">ଲୁଚାଅ</string>
<string name="home_play">ଚଲାଅ</string>
<string name="home_info">ସୂଚନା</string>
<string name="title_home">ଗୃହ</string>
<string name="title_search">ସନ୍ଧାନ</string>
<string name="result_tags">ଧରଣ</string>
<string name="type_on_hold">ସ୍ଥଗିତ</string>
<string name="type_completed">ସାରିଛନ୍ତି</string>
<string name="title_settings">ସେଟିଂ</string>
<string name="duration_format" formatted="true">%d ମିନିଟ୍</string>
<string name="player_speed_text_format" formatted="true">ଵେଗ (%.2fଗୁଣ)</string>
<string name="type_dropped">ତ୍ୟାଗିଛନ୍ତି</string>
<string name="type_plan_to_watch">ଦେଖିବା ପାଇଁ ଇଚ୍ଛୁକ</string>
<string name="type_none">କିଛି ନାହିଁ</string>
<string name="home_more_info">ଅଧିକ ସୂଚନା</string>
<string name="cast_format" formatted="true">ପାତ୍ର: %s</string>
<string name="result_poster_img_des">ପୋଷ୍ଟର୍</string>
<string name="search_poster_img_des">ପୋଷ୍ଟର୍</string>
<string name="play_episode_toast">ଅଧ୍ୟାୟ ଚଲାଅ</string>
<string name="no_episodes_found">କୌଣସି ଅଧ୍ୟାୟ ମିଳିଲା ନାହିଁ</string>
<string name="episodes">ଟି ଅଧ୍ୟାୟ</string>
<string name="episode">ଟିଏ ଅଧ୍ୟାୟ</string>
<string name="episode_action_play_in_format">%sରେ ଚଲାଅ</string>
<string name="episode_action_play_in_browser">ବ୍ରାଉଜର୍‌ରେ ଚଲାଅ</string>
<string name="episode_action_download_subtitle">ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା</string>
<string name="sync_total_episodes_some" formatted="true">/%d</string>
<string name="sync_total_episodes_none">/\?\?</string>
<string name="subscription_episode_released">ଅଧ୍ୟାୟ %d ମୁକ୍ତିଲାଭ କଲା!</string>
<string name="episode_action_auto_download">ସ୍ୱତଃ ଡାଉନଲୋଡ୍</string>
<string name="episode_action_reload_links">ଲିଙ୍କ୍‌ଗୁଡ଼ିକୁ ପୁନଃଲୋଡ୍ କରିବା</string>
<string name="episode_action_copy_link">ଲିଙ୍କ୍ କପି କରିନେବା</string>
<string name="episode_action_play_in_app">ଆପ୍‌ରେ ଚଲାଅ</string>
<string name="episode_action_chromecast_episode">Chromecast ଅଧ୍ୟାୟ</string>
<string name="episode_short"></string>
<string name="episode_poster_img_des">ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍</string>
<string name="home_main_poster_img_des">ମୁଖ୍ୟ ପୋଷ୍ଟର୍</string>
<string name="default_subtitles">ଡିଫଲ୍ଟ</string>
<string name="extension_language">ଭାଷା</string>
<string name="no">ନାହିଁ</string>
<string name="extension_description">ଵର୍ଣ୍ଣନା</string>
<string name="yes">ହଁ</string>
<string name="library">ଲାଇବ୍ରେରୀ</string>
<string name="history">ଇତିଵୃତ୍ତି</string>
<string name="extension_authors">ଲେଖକ</string>
<string name="skip_type_format" formatted="true">%s ବାଦ୍ ଦିଅ</string>
<string name="subs_subtitle_languages">ଉପଶୀର୍ଷକ ଭାଷା</string>
<string name="single_plugin_disabled" formatted="true">%s (ଅକ୍ଷମ)</string>
<string name="extension_status">ସ୍ଥିତି</string>
<string name="extension_size">ଆକାର</string>
<string name="extension_types">ସମର୍ଥିତ</string>
<string name="hls_playlist">HLS ଚାଳନାତାଲିକା</string>
<string name="player_settings_play_in_app">ଅନ୍ତଃ-ଚାଳକ</string>
<string name="skip_type_op">ଆଦ୍ୟ</string>
<string name="skip_type_ed">ପ୍ରାନ୍ତ</string>
<string name="app_not_found_error">ଆପ୍ ମିଳିଲା ନାହିଁ</string>
<string name="all_languages_preference">ସବୁ ଭାଷା</string>
<string name="player_settings_play_in_vlc">VLC</string>
<string name="player_settings_play_in_mpv">MPV</string>
<string name="skip_type_mixed_ed">ମିଶ୍ରିତ ପ୍ରାନ୍ତ</string>
<string name="skip_type_mixed_op">ମିଶ୍ରିତ ଆଦ୍ୟ</string>
<string name="skip_type_creddits">ଶ୍ରେୟ</string>
<string name="skip_type_intro">ଉପକ୍ରମ</string>
<string name="provider_languages_tip">ଏହି ଭାଷାଗୁଡ଼ିକରେ ଵିଡ଼ିଓ ଦେଖନ୍ତୁ</string>
<string name="extension_version">ସଂସ୍କରଣ</string>
<string name="app_language">ଆପ୍ ଭାଷା</string>
<string name="play_episode">ଅଧ୍ୟାୟ ଚଲାଅ</string>
<string name="season_short"></string>
<string name="status_ongoing">ଚଳିତ</string>
<string name="copy_link_toast">ଲିଙ୍କ୍ କ୍ଲିପ୍‌ବୋର୍ଡରେ କପି କରିନିଆଗଲା</string>
<string name="movies">ଚଳଚ୍ଚିତ୍ର</string>
<string name="livestreams">ସିଧାପ୍ରସାରଣ</string>
<string name="video_source">ଉତ୍ସ</string>
<string name="no_update_found">କୌଣସି ଅଦ୍ୟତନ ମିଳିଲା ନାହିଁ</string>
<string name="category_general">ସାଧାରଣ</string>
<string name="dont_show_again">ପୁନଃ ଦେଖାଅନି</string>
<string name="automatic">ସ୍ୱତଃ</string>
<string name="error">ତ୍ରୁଟି</string>
<string name="restore_settings">ବ୍ୟାକଅପ୍‌ରୁ ତଥ୍ୟ ପୁନରୁଦ୍ଧାର କରିବା</string>
<string name="backup_failed">ଷ୍ଟୋରେଜ୍ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ। ଦୟାକରି ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ।</string>
<string name="category_updates">ଅଦ୍ୟତନ ଏଵଂ ବ୍ୟାକଅପ୍</string>
<string name="pref_category_backup">ବ୍ୟାକଅପ୍</string>
<string name="pref_category_android_tv">ଆଣ୍ଡ୍ରଏଡ୍ ଟିଵି</string>
<string name="pref_category_gestures">ଅଙ୍ଗଭଙ୍ଗୀ</string>
<string name="new_update_format" formatted="true">ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା!
\n%s -&gt; %s</string>
<string name="duration">ଅଵଧି</string>
<string name="app_storage">ଆପ୍</string>
<string name="restore_success">ବ୍ୟାକଅପ୍ ଫାଇଲ୍ ଧାରଣ ହେଲା</string>
<string name="backup_success">ତଥ୍ୟ ଗଚ୍ଛିତ ହୋଇଛି</string>
<string name="backup_failed_error_format">%s ବ୍ୟାକଅପ୍ ନେବାରେ ତ୍ରୁଟି ଘଟିଲା</string>
<string name="season">ଋତୁ</string>
<string name="no_season">କୌଣସି ଋତୁ ନାହିଁ</string>
<string name="delete_file">ଫାଇଲ୍ ଵିଲୋପ କରିବେ</string>
<string name="test_passed">ପାରିତ ହେଲା</string>
<string name="go_back_30">-୩୦</string>
<string name="status">ସ୍ଥିତି</string>
<string name="used_storage">ଵ୍ୟଵହୃତ</string>
<string name="tv_series">ଟିଵି ଧାରାଵାହିକ</string>
<string name="asian_drama">ଏସୀୟ ନାଟକ</string>
<string name="others">ଅନ୍ୟାନ୍ୟ</string>
<string name="other_singular">ଵିଡ଼ିଓ</string>
<string name="source_error">ଉତ୍ସ ତ୍ରୁଟି</string>
<string name="unexpected_error">ଅପ୍ରତ୍ୟାଶିତ ଚାଳକ ତ୍ରୁଟି</string>
<string name="show_title">ଆଖ୍ୟା</string>
<string name="check_for_update">ଅଦ୍ୟତନ ପାଇଁ ଯାଞ୍ଚ କରିବା</string>
<string name="video_lock">ତାଲା</string>
<string name="video_aspect_ratio_resize">ଆକାର ଠିକ୍ କରିବା</string>
<string name="skip_update">ଏହି ଅଦ୍ୟତନଟିକୁ ବାଦ୍ ଦିଅ</string>
<string name="pref_category_actions">କୃତ୍ୟ</string>
<string name="pref_category_subtitles">ଉପଶୀର୍ଷକ</string>
<string name="pref_category_ui_features">ଵୈଶିଷ୍ଟ୍ୟସବୁ</string>
<string name="pref_category_looks">ଵେଶ</string>
<string name="pref_category_defaults">ଡିଫଲ୍ଟଗୁଡ଼ା</string>
<string name="primary_color_settings">ପ୍ରାଥମିକ ରଙ୍ଗ</string>
<string name="added_sync_format" formatted="true">%s ଯୋଡ଼ାଗଲା</string>
<string name="title">ଆଖ୍ୟା</string>
<string name="setup_done">ହେଲା</string>
<string name="update_notification_downloading">ଆପ୍ ଅଦ୍ୟତନ ଡାଉନଲୋଡ୍ ଚାଲିଛି…</string>
<string name="update_notification_installing">ଆପ୍ ଅଦ୍ୟତନ ଅଧିସ୍ଥାପନ ଚାଲିଛି…</string>
<string name="update_notification_failed">ଆପ୍‌ର ନୂଆ ସଂସ୍କରଣ ଅଧିସ୍ଥାପନ କରିହେଲା ନାହିଁ</string>
<string name="test_failed">ଵିଫଳ ହେଲା</string>
<string name="category_player">ଚାଳକ</string>
<string name="backup_settings">ତଥ୍ୟର ବ୍ୟାକଅପ୍ ନେବା</string>
<string name="delete">ଵିଲୋପ କର</string>
<string name="documentaries">ଵୃତ୍ତଚିତ୍ର</string>
<string name="anime_singular">ଅନିମେ</string>
<string name="tv_series_singular">ଧାରାଵାହିକ</string>
<string name="movies_singular">ଚଳଚ୍ଚିତ୍ର</string>
<string name="documentaries_singular">ଵୃତ୍ତଚିତ୍ର</string>
<string name="asian_drama_singular">ଏସୀୟ ନାଟକ</string>
<string name="live_singular">ସିଧାପ୍ରସାରଣ</string>
<string name="show_hd">ଗୁଣଵତ୍ତା ଲେବଲ୍</string>
<string name="update">ଅଦ୍ୟତନ କରିବା</string>
<string name="pref_category_player_features">ଚାଳକ ଵୈଶିଷ୍ଟ୍ୟସବୁ</string>
<string name="app_theme_settings">ଆପ୍ ଥିମ୍</string>
<string name="subs_auto_select_language">ଭାଷା ସ୍ୱତଃ-ଚୟନ</string>
<string name="anime">ଅନିମେ</string>
<string name="player_subtitles_settings">ଉପଶୀର୍ଷକ</string>
<string name="go_forward_30">+୩୦</string>
<string name="year">ଵର୍ଷ</string>
</resources>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<string name="player_speed_text_format" formatted="true">Prędkość (%.2fx)</string>
<string name="rated_format" formatted="true">Ocena: %.1f</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<string name="rated_format">Betygsatt: %.1f</string>
<string name="player_speed_text_format">Hastighet (%.2fx)</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- KEYS DON'T TRANSLATE -->
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>

View file

@ -453,22 +453,22 @@
<string name="player_settings_play_in_mpv">MPV</string>
<string name="player_settings_play_in_web">Відтворення веб-відео</string>
<string name="player_settings_play_in_browser">Веб-браузер</string>
<string name="skip_type_ed">Кінець</string>
<string name="skip_type_ed">Ендінґ</string>
<string name="skip_type_recap">Коротке повторення</string>
<string name="skip_type_format" formatted="true">Пропустити %s</string>
<string name="skip_type_mixed_ed">Змішаний кінець</string>
<string name="skip_type_mixed_ed">Змішаний ендінґ</string>
<string name="skip_type_creddits">Подяки</string>
<string name="skip_type_op">Опенінг</string>
<string name="skip_type_op">Опенінґ</string>
<string name="skip_type_intro">Вступ</string>
<string name="clear_history">Очистити історію</string>
<string name="history">Історія</string>
<string name="enable_skip_op_from_database_des">Показувати спливаючі вікна для опенінгу/кінця</string>
<string name="enable_skip_op_from_database_des">Показувати спливаючі вікна для опенінґу/кінця</string>
<string name="clipboard_too_large">Забагато тексту. Не вдалося зберегти в буфер обміну.</string>
<string name="action_mark_as_watched">Позначити як переглянуте</string>
<string name="confirm_exit_dialog">Ви впевнені що хочете вийти\?</string>
<string name="yes">Так</string>
<string name="no">Ні</string>
<string name="update_notification_installing">Установлення оновлення програми…</string>
<string name="update_notification_installing">Встановлення оновлення програми…</string>
<string name="update_notification_failed">Не вдалося встановити нову версію програми</string>
<string name="apk_installer_legacy">Старий</string>
<string name="apk_installer_package_installer">Інсталятор пакетів</string>
@ -487,7 +487,7 @@
<string name="update_notification_downloading">Завантаження оновлення програми…</string>
<string name="safe_mode_description">Усі розширення були вимкнені через збій, щоб допомогти вам знайти те, що стало причиною проблеми.</string>
<string name="app_not_found_error">Програму не знайдено</string>
<string name="skip_type_mixed_op">Змішаний опенінг</string>
<string name="skip_type_mixed_op">Змішаний опенінґ</string>
<string name="action_remove_from_watched">Видалити з переглянутого</string>
<string name="sort_updated_old">За оновленням (від старого до нового)</string>
<string name="sort_updated_new">За оновленням (від нового до старого)</string>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- KEYS DON'T TRANSLATE -->
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>
@ -143,14 +142,14 @@
<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_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_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>
@ -178,7 +177,7 @@
<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="github">Github</string>
@ -245,8 +244,8 @@
<string name="movies_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="anime_singular">動畫</string>
<string name="ova_singular">OVA</string>
<string name="torrent_singular">種子</string>
<string name="documentaries_singular">紀錄片</string>
<string name="asian_drama_singular">亞洲劇</string>
@ -286,7 +285,7 @@
<string name="dont_show_again">不再顯示</string>
<string name="skip_update">跳過此更新</string>
<string name="update">更新</string>
<string name="watch_quality_pref">偏好播放畫質</string>
<string name="watch_quality_pref">偏好播放畫質 (WiFi)</string>
<string name="limit_title">影片播放器標題最大字數</string>
<string name="limit_title_rez">影片播放器標題</string>
<string name="video_buffer_size_settings">影片緩衝大小</string>
@ -535,4 +534,47 @@
<string name="pref_category_looks">外觀</string>
<string name="pref_category_ui_features">功能</string>
<string name="browser">瀏覽器</string>
<string name="subscription_episode_released">第 %d 集已發行!</string>
<string name="library">媒體庫</string>
<string name="start">開始</string>
<string name="android_tv_interface_on_seek_settings">播放器顯示 - 快轉快退秒數</string>
<string name="open_with">開啟方式</string>
<string name="delayed_update_notice">應用程式將在關閉時更新</string>
<string name="sort_rating_asc">評分(從低到高)</string>
<string name="update_started">更新開始</string>
<string name="plugin_downloaded">外掛程式已下載</string>
<string name="action_remove_from_watched">從觀看中刪除</string>
<string name="sort_by">排序方式</string>
<string name="sort">排序</string>
<string name="sort_rating_desc">評分(從高到低)</string>
<string name="android_tv_interface_on_seek_settings_summary">播放器可見時使用的快轉快退秒數</string>
<string name="android_tv_interface_off_seek_settings">播放器隱藏 - 快轉快退秒數</string>
<string name="sort_updated_new">更新(從新到舊)</string>
<string name="sort_updated_old">更新(從舊到新)</string>
<string name="sort_alphabetical_a">按字母順序A 到 Z</string>
<string name="sort_alphabetical_z">按字母順序Z 到 A</string>
<string name="select_library">選擇媒體庫</string>
<string name="safe_mode_file">找到安全模式檔案!
\n在刪除檔案之前不在啟動時載入任何擴充功能。</string>
<string name="test_log">日誌</string>
<string name="test_failed">失敗</string>
<string name="test_passed">通過</string>
<string name="android_tv_interface_off_seek_settings_summary">播放器隱藏時使用的快轉快退秒數</string>
<string name="pref_category_android_tv">Android TV</string>
<string name="category_provider_test">片源測試</string>
<string name="restart">重新啟動</string>
<string name="stop">停止</string>
<string name="subscription_list_name">訂閱</string>
<string name="subscription_new">已訂閱 %s</string>
<string name="subscription_deleted">已取消訂閱 %s</string>
<string name="watch_quality_pref_data">偏好播放畫質 (行動數據)</string>
<string name="jsdelivr_proxy">raw.githubusercontent.com Proxy</string>
<string name="pref_category_bypass">繞過 ISP</string>
<string name="revert">還原</string>
<string name="jsdelivr_enabled">無法訪問 GitHub。 正在開啟 jsDelivr proxy…</string>
<string name="jsdelivr_proxy_summary">使用 jsDelivr 繞過 GitHub 的阻擋。 可能導致更新延遲幾天。</string>
<string name="empty_library_no_accounts_message">您的媒體庫是空的:(
\n登入媒體庫帳戶或將節目添加到您本機的媒體庫。</string>
<string name="empty_library_logged_in_message">此列表是空的。 嘗試切換到另一個。</string>
<string name="subscription_in_progress_notification">正在更新訂閱節目</string>
</resources>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>

View file

@ -664,8 +664,27 @@
<string name="subscription_new">Subscribed to %s</string>
<string name="subscription_deleted">Unsubscribed from %s</string>
<string name="subscription_episode_released">Episode %d released!</string>
<string name="profile_number">Profile %d</string>
<string name="wifi">Wi-Fi</string>
<string name="mobile_data">Mobile data</string>
<string name="set_default">Set default</string>
<string name="use">Use</string>
<string name="edit">Edit</string>
<string name="profiles">Profiles</string>
<string name="help">Help</string>
<string name="quality_profile_help">
Here you can change how the sources are ordered. If a video has a higher priority it will appear higher in the source selection.
The sum of the source priority and the quality priority is the video priority.
\n\nSource A: 3
\nQuality B: 7
\nWill have a combined video priority of 10.
\n\nNOTE: If the sum is 10 or more the player will automatically skip loading when that link is loaded!
</string>
<string name="qualities">Qualities</string>
<string name="profile_background_des">Profile background</string>
<string name="example_login_file_name_full">Sync file name (optional)</string>
<string name="example_login_redirect_url_full">Oauth redirect url (optional)</string>
<string name="example_redirect_url" translatable="false">https://recloudstream.github.io/cloudstream-sync/google-drive</string>
<string name="info_button">Info</string>
</resources>
</resources>

View file

@ -11,14 +11,14 @@
<PreferenceCategory
android:title="@string/pref_category_defaults">
<Preference
android:icon="@drawable/ic_baseline_hd_24"
android:key="@string/quality_pref_key"
android:title="@string/watch_quality_pref" />
<Preference
android:icon="@drawable/ic_baseline_hd_24"
android:key="@string/quality_pref_mobile_data_key"
android:title="@string/watch_quality_pref_data" />
<!-- <Preference-->
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
<!-- android:key="@string/quality_pref_key"-->
<!-- android:title="@string/watch_quality_pref" />-->
<!-- <Preference-->
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
<!-- android:key="@string/quality_pref_mobile_data_key"-->
<!-- android:title="@string/watch_quality_pref_data" />-->
<Preference
android:icon="@drawable/netflix_play"