diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0896bdae..571e133f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt new file mode 100644 index 00000000..1c548e74 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -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(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>("[$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, + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 0d94eb08..24495a40 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -58,7 +58,7 @@ open class DoodLaExtractor : ExtractorApi() { val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0) return listOf( ExtractorLink( - trueUrl, + this.name, this.name, trueUrl, mainUrl, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt index 84fd0552..4c1791a8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -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( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt new file mode 100644 index 00000000..2ec185e0 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -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()?.data?.get("token") + app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=12345") + .parsedSafe()?.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? = null, + ) + + data class Data( + @JsonProperty("contents") val contents: HashMap>? = null, + ) + + data class Source( + @JsonProperty("data") val data: Data? = null, + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt index 2adc00d5..3d046267 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt new file mode 100644 index 00000000..b6887259 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt @@ -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 + ) + ) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index 1c6c7b94..3d2a81b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -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
() + 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
() - // 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, - ) - ) - } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt index d721dea8..13aa48c6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt @@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() { val jsonvideodata = parseJson(response) return jsonvideodata.data.map { ExtractorLink( - it.file+".${it.type}", + this.name, this.name, it.file+".${it.type}", mainUrl, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt new file mode 100644 index 00000000..37a7edb5 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt @@ -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>("[$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, + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt new file mode 100644 index 00000000..c8b2ae07 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt @@ -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()?.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? = arrayListOf(), + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 12a76a9b..2c6998de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt deleted file mode 100644 index ad3f0150..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt +++ /dev/null @@ -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 { - 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(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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt index 15ff0436..ccb2fde7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt @@ -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" diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt new file mode 100644 index 00000000..1766af6c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt @@ -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(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?, + @JsonProperty("sourceBk") val sourceBk: List?, + ) + + data class GogoSource( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("default") val default: String? = null + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 86e21fd6..9ff1c52d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -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) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index a5cd0ab7..645cecbc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -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> { - 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 + ): 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> { + 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>() - val sortedUrls = sortLinks(useQualitySettings = false) - if (sortedUrls.isEmpty()) { - sourceDialog.findViewById(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(R.id.sort_sources_holder)?.isGone = + true + } else { + startSource = sortedUrls.indexOf(currentSelectedLink) + sourceIndex = startSource - val sourcesArrayAdapter = - ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) + val sourcesArrayAdapter = + ArrayAdapter(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() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 7faf0cf5..1b13b519 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -156,18 +156,24 @@ class PlayerGeneratorViewModel : ViewModel() { val currentSubs = mutableSetOf() // 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()) + } }) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt new file mode 100644 index 00000000..8e0ce67c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -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( + val data: T, + val name: String, + var priority: Int +) + +class PriorityAdapter(override val items: MutableList>) : + AppUtils.DiffAdapter>(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 bind(item: SourcePriority) { + 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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt new file mode 100644 index 00000000..ff84c1f5 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -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, + val usedProfile: Int, + val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit, +) : + AppUtils.DiffAdapter( + 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? = 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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt new file mode 100644 index 00000000..96249db4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -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("$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 { + 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, + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt new file mode 100644 index 00000000..28a6365f --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -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, + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt new file mode 100644 index 00000000..efc1f1b8 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -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, + 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 + val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter + + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 81ef8d57..f2eca5b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -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") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 07520a99..2a3e2497 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 516cd990..3bdb64e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -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? { val folder = "$currentAccount/$RESULT_WATCH_STATE" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index b03c9fb7..f6373dce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -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 = arrayListOf( XStreamCdn(), StreamSB(), + Vidgomunimesb(), StreamSB1(), StreamSB2(), StreamSB3(), @@ -275,7 +284,6 @@ val extractorApis: MutableList = arrayListOf( Uqload2(), Evoload(), Evoload1(), - VoeExtractor(), UpstreamExtractor(), Tomatomatela(), @@ -342,6 +350,24 @@ val extractorApis: MutableList = 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 = arrayListOf( Vidmoly(), Vidmolyme(), Voe(), + Tubeless(), Moviehab(), MoviehabNet(), Jeniusplay(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 2dc6846c..1f6d726d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -250,17 +250,6 @@ object SingleSelectionHelper { ) } - fun showBottomDialog( - items: List, - selectedIndex: Int, - name: String, - showApply: Boolean, - dismissCallback: () -> Unit, - callback: (Int) -> Unit, - ) { - - } - /** Only for a low amount of items */ fun Activity?.showBottomDialog( items: List, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index c300d615..7d798204 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -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 } diff --git a/app/src/main/res/drawable/baseline_help_outline_24.xml b/app/src/main/res/drawable/baseline_help_outline_24.xml new file mode 100644 index 00000000..3a72cda0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_help_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_remove_24.xml b/app/src/main/res/drawable/baseline_remove_24.xml index 791a2f81..f4455598 100644 --- a/app/src/main/res/drawable/baseline_remove_24.xml +++ b/app/src/main/res/drawable/baseline_remove_24.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:tint="?attr/white"> diff --git a/app/src/main/res/drawable/profile_bg_blue.jpg b/app/src/main/res/drawable/profile_bg_blue.jpg new file mode 100644 index 00000000..e573439b Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_blue.jpg differ diff --git a/app/src/main/res/drawable/profile_bg_dark_blue.jpg b/app/src/main/res/drawable/profile_bg_dark_blue.jpg new file mode 100644 index 00000000..d59e4888 Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_dark_blue.jpg differ diff --git a/app/src/main/res/drawable/profile_bg_orange.jpg b/app/src/main/res/drawable/profile_bg_orange.jpg new file mode 100644 index 00000000..a97e7179 Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_orange.jpg differ diff --git a/app/src/main/res/drawable/profile_bg_pink.jpg b/app/src/main/res/drawable/profile_bg_pink.jpg new file mode 100644 index 00000000..9d4940f0 Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_pink.jpg differ diff --git a/app/src/main/res/drawable/profile_bg_purple.jpg b/app/src/main/res/drawable/profile_bg_purple.jpg new file mode 100644 index 00000000..15723dba Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_purple.jpg differ diff --git a/app/src/main/res/drawable/profile_bg_red.jpg b/app/src/main/res/drawable/profile_bg_red.jpg new file mode 100644 index 00000000..6a27ff31 Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_red.jpg differ diff --git a/app/src/main/res/drawable/profile_bg_teal.jpg b/app/src/main/res/drawable/profile_bg_teal.jpg new file mode 100644 index 00000000..93236650 Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_teal.jpg differ diff --git a/app/src/main/res/layout/player_prioritize_item.xml b/app/src/main/res/layout/player_prioritize_item.xml new file mode 100644 index 00000000..b78863f8 --- /dev/null +++ b/app/src/main/res/layout/player_prioritize_item.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_quality_profile_dialog.xml b/app/src/main/res/layout/player_quality_profile_dialog.xml new file mode 100644 index 00000000..7bd7a680 --- /dev/null +++ b/app/src/main/res/layout/player_quality_profile_dialog.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_quality_profile_item.xml b/app/src/main/res/layout/player_quality_profile_item.xml new file mode 100644 index 00000000..3fad69ac --- /dev/null +++ b/app/src/main/res/layout/player_quality_profile_item.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index 067e4ad5..550b08d5 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -1,5 +1,6 @@ - + android:background="@drawable/outline_drawable_less" + android:foreground="?attr/selectableItemBackgroundBorderless" + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index d3bb648e..0543a94e 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,5 +1,4 @@ - %s еп. %d diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 16df53a6..38424e56 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7ad80259..16ceff2d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 67e81957..5e02924f 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1,5 +1,4 @@ - CloudStream Αρχική diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 6a6b5243..2e4b89b3 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -33,4 +33,16 @@ %dساعت %dدقیقه %dدقیقه پوستر اصلی + تورنت + آزاد + مستند ها + انیمیشن ویدیویی اصلی + حداکثر + فیلم‌ها + سریال های تلویزیونی + درام های آسیایی + انیمه + کارتونها + استفاده شده + برنامه diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 62e41fdb..36c1cf1f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,4 @@ - CloudStream Accueil diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e4b9fe46..1401b3d8 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,5 +1,4 @@ - रफ्तार (%.2fx) @@ -147,4 +146,13 @@ %dh %dm %dm विज्ञापन + अगला रैंडम + वापस जाओ + पोस्टर + पृष्ठभूमि का पूर्वावलोकन करें + प्रदाता बदलें + Cast: %s + मुख्य पोस्टर + एपिसोड का पोस्टर + %s Ep %d diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 41b95aad..754b7a3a 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 15c09228..a8c6a197 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 25b8ca5a..6dca2e3a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 347712d8..5fcc14da 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -198,4 +198,7 @@ \nSky UK Limitedによる無脳なDMCAテイクダウンのため🤮、アプリ内でリポジトリサイトをリンクすることができません。 \n \n私たちのDiscordに参加するか、オンラインで検索してください。 + バックグラウンドをプレビュー + ライブストリームの再生 + プロバイダーの変更 diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 1236dbba..399aafb1 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -125,4 +125,11 @@ ಡೌನ್‌ಲೋಡ್ ಪ್ರಾರಂಭವಾಗಿದೆ ಡೌನ್‌ಲೋಡ್ ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ ಮುಂದಿನ ರಾಂಡಮ್ + ಮುಂದಕ್ಕೆ ಹೋಗಲು ಸ್ವೈಪ್ ಮಾಡಿ + ವೀಡಿಯೊದಲ್ಲಿ ನಿಮ್ಮ ಸ್ಥಾನವನ್ನು ನಿಯಂತ್ರಿಸಲು ಅಕ್ಕಪಕ್ಕಕ್ಕೆ ಸ್ವೈಪ್ ಮಾಡಿ + ಮುಂದಿನ ಸಂಚಿಕೆಯನ್ನು ಆಟೋ ಪ್ಲೇ ಮಾಡಿ + ಮುಂದೂಡಲು ಅಥವಾ ಇಂದೂಡಲು ಎರಡು ಬಾರಿ ಟ್ಯಾಪ್ ಮಾಡಿ + Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ + ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ + ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index d217f97f..66a6b9ba 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -1,5 +1,4 @@ - Брзина (%.2fx) diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 1f117af6..3d6240f9 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1,5 +1,4 @@ - വേഗം (%.2fx) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e640a28a..f56b0bfb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 42eba3cc..eaa76652 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -1,2 +1,149 @@ - + + ଅଧିକ ଵିକଳ୍ପ + ଦେଖୁଛନ୍ତି + %dଦି %dଘ %dମି + %dଘ %dମି + %dମି + ପୁନଃଦେଖୁଛନ୍ତି + ଲୁଚାଅ + ଚଲାଅ + ସୂଚନା + ଗୃହ + ସନ୍ଧାନ + ଧରଣ + ସ୍ଥଗିତ + ସାରିଛନ୍ତି + ସେଟିଂ + %d ମିନିଟ୍ + ଵେଗ (%.2fଗୁଣ) + ତ୍ୟାଗିଛନ୍ତି + ଦେଖିବା ପାଇଁ ଇଚ୍ଛୁକ + କିଛି ନାହିଁ + ଅଧିକ ସୂଚନା + ପାତ୍ର: %s + ପୋଷ୍ଟର୍ + ପୋଷ୍ଟର୍ + ଅଧ୍ୟାୟ ଚଲାଅ + କୌଣସି ଅଧ୍ୟାୟ ମିଳିଲା ନାହିଁ + ଟି ଅଧ୍ୟାୟ + ଟିଏ ଅଧ୍ୟାୟ + %s‌ରେ ଚଲାଅ + ବ୍ରାଉଜର୍‌ରେ ଚଲାଅ + ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା + /%d + /\?\? + ଅଧ୍ୟାୟ %d ମୁକ୍ତିଲାଭ କଲା! + ସ୍ୱତଃ ଡାଉନଲୋଡ୍ + ଲିଙ୍କ୍‌ଗୁଡ଼ିକୁ ପୁନଃଲୋଡ୍ କରିବା + ଲିଙ୍କ୍ କପି କରିନେବା + ଆପ୍‌ରେ ଚଲାଅ + Chromecast ଅଧ୍ୟାୟ + + ଅଧ୍ୟାୟର ପୋଷ୍ଟର୍ + ମୁଖ୍ୟ ପୋଷ୍ଟର୍ + ଡିଫଲ୍ଟ + ଭାଷା + ନାହିଁ + ଵର୍ଣ୍ଣନା + ହଁ + ଲାଇବ୍ରେରୀ + ଇତିଵୃତ୍ତି + ଲେଖକ + %s ବାଦ୍ ଦିଅ + ଉପଶୀର୍ଷକ ଭାଷା + %s (ଅକ୍ଷମ) + ସ୍ଥିତି + ଆକାର + ସମର୍ଥିତ + HLS ଚାଳନାତାଲିକା + ଅନ୍ତଃ-ଚାଳକ + ଆଦ୍ୟ + ପ୍ରାନ୍ତ + ଆପ୍ ମିଳିଲା ନାହିଁ + ସବୁ ଭାଷା + VLC + MPV + ମିଶ୍ରିତ ପ୍ରାନ୍ତ + ମିଶ୍ରିତ ଆଦ୍ୟ + ଶ୍ରେୟ + ଉପକ୍ରମ + ଏହି ଭାଷାଗୁଡ଼ିକରେ ଵିଡ଼ିଓ ଦେଖନ୍ତୁ + ସଂସ୍କରଣ + ଆପ୍ ଭାଷା + ଅଧ୍ୟାୟ ଚଲାଅ + + ଚଳିତ + ଲିଙ୍କ୍ କ୍ଲିପ୍‌ବୋର୍ଡରେ କପି କରିନିଆଗଲା + ଚଳଚ୍ଚିତ୍ର + ସିଧାପ୍ରସାରଣ + ଉତ୍ସ + କୌଣସି ଅଦ୍ୟତନ ମିଳିଲା ନାହିଁ + ସାଧାରଣ + ପୁନଃ ଦେଖାଅନି + ସ୍ୱତଃ + ତ୍ରୁଟି + ବ୍ୟାକଅପ୍‌ରୁ ତଥ୍ୟ ପୁନରୁଦ୍ଧାର କରିବା + ଷ୍ଟୋରେଜ୍ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ। ଦୟାକରି ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ। + ଅଦ୍ୟତନ ଏଵଂ ବ୍ୟାକଅପ୍ + ବ୍ୟାକଅପ୍ + ଆଣ୍ଡ୍ରଏଡ୍ ଟିଵି + ଅଙ୍ଗଭଙ୍ଗୀ + ନୂଆ ଅଦ୍ୟତନ ମିଳିଲା! +\n%s -> %s + ଅଵଧି + ଆପ୍ + ବ୍ୟାକଅପ୍ ଫାଇଲ୍ ଧାରଣ ହେଲା + ତଥ୍ୟ ଗଚ୍ଛିତ ହୋଇଛି + %s ବ୍ୟାକଅପ୍ ନେବାରେ ତ୍ରୁଟି ଘଟିଲା + ଋତୁ + କୌଣସି ଋତୁ ନାହିଁ + ଫାଇଲ୍ ଵିଲୋପ କରିବେ + ପାରିତ ହେଲା + -୩୦ + ସ୍ଥିତି + ଵ୍ୟଵହୃତ + ଟିଵି ଧାରାଵାହିକ + ଏସୀୟ ନାଟକ + ଅନ୍ୟାନ୍ୟ + ଵିଡ଼ିଓ + ଉତ୍ସ ତ୍ରୁଟି + ଅପ୍ରତ୍ୟାଶିତ ଚାଳକ ତ୍ରୁଟି + ଆଖ୍ୟା + ଅଦ୍ୟତନ ପାଇଁ ଯାଞ୍ଚ କରିବା + ତାଲା + ଆକାର ଠିକ୍ କରିବା + ଏହି ଅଦ୍ୟତନଟିକୁ ବାଦ୍ ଦିଅ + କୃତ୍ୟ + ଉପଶୀର୍ଷକ + ଵୈଶିଷ୍ଟ୍ୟସବୁ + ଵେଶ + ଡିଫଲ୍ଟଗୁଡ଼ା + ପ୍ରାଥମିକ ରଙ୍ଗ + %s ଯୋଡ଼ାଗଲା + ଆଖ୍ୟା + ହେଲା + ଆପ୍ ଅଦ୍ୟତନ ଡାଉନଲୋଡ୍ ଚାଲିଛି… + ଆପ୍ ଅଦ୍ୟତନ ଅଧିସ୍ଥାପନ ଚାଲିଛି… + ଆପ୍‌ର ନୂଆ ସଂସ୍କରଣ ଅଧିସ୍ଥାପନ କରିହେଲା ନାହିଁ + ଵିଫଳ ହେଲା + ଚାଳକ + ତଥ୍ୟର ବ୍ୟାକଅପ୍ ନେବା + ଵିଲୋପ କର + ଵୃତ୍ତଚିତ୍ର + ଅନିମେ + ଧାରାଵାହିକ + ଚଳଚ୍ଚିତ୍ର + ଵୃତ୍ତଚିତ୍ର + ଏସୀୟ ନାଟକ + ସିଧାପ୍ରସାରଣ + ଗୁଣଵତ୍ତା ଲେବଲ୍ + ଅଦ୍ୟତନ କରିବା + ଚାଳକ ଵୈଶିଷ୍ଟ୍ୟସବୁ + ଆପ୍ ଥିମ୍ + ଭାଷା ସ୍ୱତଃ-ଚୟନ + ଅନିମେ + ଉପଶୀର୍ଷକ + +୩୦ + ଵର୍ଷ + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1071a9b3..2961cb47 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,5 +1,4 @@ - Prędkość (%.2fx) Ocena: %.1f diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index bd22fb33..294abcfd 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1,5 +1,4 @@ - %s Ep %d diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 736f27ce..168e23fa 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1,5 +1,4 @@ - Betygsatt: %.1f Hastighet (%.2fx) diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index a1faf3e1..95d38478 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2ee7b65f..170c3679 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c58dd334..82527c95 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -453,22 +453,22 @@ MPV Відтворення веб-відео Веб-браузер - Кінець + Ендінґ Коротке повторення Пропустити %s - Змішаний кінець + Змішаний ендінґ Подяки - Опенінг + Опенінґ Вступ Очистити історію Історія - Показувати спливаючі вікна для опенінгу/кінця + Показувати спливаючі вікна для опенінґу/кінця Забагато тексту. Не вдалося зберегти в буфер обміну. Позначити як переглянуте Ви впевнені що хочете вийти\? Так Ні - Установлення оновлення програми… + Встановлення оновлення програми… Не вдалося встановити нову версію програми Старий Інсталятор пакетів @@ -487,7 +487,7 @@ Завантаження оновлення програми… Усі розширення були вимкнені через збій, щоб допомогти вам знайти те, що стало причиною проблеми. Програму не знайдено - Змішаний опенінг + Змішаний опенінґ Видалити з переглянутого За оновленням (від старого до нового) За оновленням (від нового до старого) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index d7795713..f896e5c1 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 01b3b682..1fd01d8a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s @@ -143,14 +142,14 @@ 播放速度 在播放器中添加播放速度選項 活動控制進度 - 左右滑動控制播放進度 + 從一側滑動到另一側以控制影片中的位置 滑動更改設定 上下滑動更改亮度或音量 自動播放下一集 播放完畢後播放下一集 輕按兩下以控制進度 輕按兩下以暫停 - 輕按兩下以控制進度時間 + 輕按兩下以控制進度時間(秒) 在右側或左側輕按兩次以向前或向後快轉 輕按兩下中間以暫停 使用系統亮度 @@ -178,7 +177,7 @@ 在搜尋結果中隱藏選中的影片畫質 自動更新外掛程式 顯示應用更新 - 啟動時自動搜尋更新 + 啟動應用程式後自動搜尋更新。 更新至預覽版 搜尋預覽版更新而不是僅搜尋正式版 Github @@ -245,8 +244,8 @@ 電影 電視劇 卡通 - @string/anime - @string/ova + 動畫 + OVA 種子 紀錄片 亞洲劇 @@ -286,7 +285,7 @@ 不再顯示 跳過此更新 更新 - 偏好播放畫質 + 偏好播放畫質 (WiFi) 影片播放器標題最大字數 影片播放器標題 影片緩衝大小 @@ -535,4 +534,47 @@ 外觀 功能 瀏覽器 + 第 %d 集已發行! + 媒體庫 + 開始 + 播放器顯示 - 快轉快退秒數 + 開啟方式 + 應用程式將在關閉時更新 + 評分(從低到高) + 更新開始 + 外掛程式已下載 + 從觀看中刪除 + 排序方式 + 排序 + 評分(從高到低) + 播放器可見時使用的快轉快退秒數 + 播放器隱藏 - 快轉快退秒數 + 更新(從新到舊) + 更新(從舊到新) + 按字母順序(A 到 Z) + 按字母順序(Z 到 A) + 選擇媒體庫 + 找到安全模式檔案! +\n在刪除檔案之前不在啟動時載入任何擴充功能。 + 日誌 + 失敗 + 通過 + 播放器隱藏時使用的快轉快退秒數 + Android TV + 片源測試 + 重新啟動 + 停止 + 訂閱 + 已訂閱 %s + 已取消訂閱 %s + 偏好播放畫質 (行動數據) + raw.githubusercontent.com Proxy + 繞過 ISP + 還原 + 無法訪問 GitHub。 正在開啟 jsDelivr proxy… + 使用 jsDelivr 繞過 GitHub 的阻擋。 可能導致更新延遲幾天。 + 您的媒體庫是空的:( +\n登入媒體庫帳戶或將節目添加到您本機的媒體庫。 + 此列表是空的。 嘗試切換到另一個。 + 正在更新訂閱節目 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index f63e1d75..dbd96827 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,5 +1,4 @@ - %d %s | %s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fd95e84..4dad8702 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -664,8 +664,27 @@ Subscribed to %s Unsubscribed from %s Episode %d released! + Profile %d + Wi-Fi + Mobile data + Set default + Use + Edit + Profiles + 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! + + Qualities + Profile background Sync file name (optional) Oauth redirect url (optional) https://recloudstream.github.io/cloudstream-sync/google-drive Info - \ No newline at end of file + diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 2d2905ea..ad33e036 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -11,14 +11,14 @@ - - + + + + + + + +