mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge remote-tracking branch 'origin/master' into bread_toast
This commit is contained in:
commit
7413f39c87
28 changed files with 519 additions and 149 deletions
|
@ -230,7 +230,7 @@ dependencies {
|
||||||
// Downloading & Networking
|
// Downloading & Networking
|
||||||
implementation("androidx.work:work-runtime:2.9.0")
|
implementation("androidx.work:work-runtime:2.9.0")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.5") // HTTP Lib
|
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("androidSourcesJar", Jar::class) {
|
tasks.register("androidSourcesJar", Jar::class) {
|
||||||
|
|
|
@ -1587,8 +1587,15 @@ data class AnimeLoadResponse(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
||||||
|
val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
|
||||||
|
|
||||||
return this.episodes.maxOf { (_, episodes) ->
|
return this.episodes.maxOf { (_, episodes) ->
|
||||||
episodes.count { ((it.season ?: Int.MIN_VALUE) < season) && it.season != 0 }
|
episodes.count { episodeData ->
|
||||||
|
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
|
||||||
|
val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||||
|
// Count all episodes from season 1 to below the current season.
|
||||||
|
episodeSeason in 1..<season
|
||||||
|
}
|
||||||
} + episode
|
} + episode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1895,8 +1902,13 @@ data class TvSeriesLoadResponse(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
||||||
return episodes.count {
|
val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
|
||||||
(it.season ?: Int.MIN_VALUE) < season && it.season != 0
|
|
||||||
|
return episodes.count { episodeData ->
|
||||||
|
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
|
||||||
|
val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||||
|
// Count all episodes from season 1 to below the current season.
|
||||||
|
episodeSeason in 1..<season
|
||||||
} + episode
|
} + episode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.initAll
|
import com.lagradost.cloudstream3.APIHolder.initAll
|
||||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
||||||
|
@ -287,9 +288,27 @@ var app = Requests(responseParser = object : ResponseParser {
|
||||||
class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "MAINACT"
|
const val TAG = "MAINACT"
|
||||||
const val ANIMATED_OUTLINE : Boolean = false
|
const val ANIMATED_OUTLINE: Boolean = false
|
||||||
var lastError: String? = null
|
var lastError: String? = null
|
||||||
|
|
||||||
|
private const val FILE_DELETE_KEY = "FILES_TO_DELETE_KEY"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transient files to delete on application exit.
|
||||||
|
* Deletes files on onDestroy().
|
||||||
|
*/
|
||||||
|
private var filesToDelete: Set<String>
|
||||||
|
// This needs to be persistent because the application may exit without calling onDestroy.
|
||||||
|
get() = getKey<Set<String>>(FILE_DELETE_KEY) ?: setOf()
|
||||||
|
private set(value) = setKey(FILE_DELETE_KEY, value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add file to delete on Exit.
|
||||||
|
*/
|
||||||
|
fun deleteFileOnExit(file: File) {
|
||||||
|
filesToDelete = filesToDelete + file.path
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setting this will automatically enter the query in the search
|
* Setting this will automatically enter the query in the search
|
||||||
* next time the search fragment is opened.
|
* next time the search fragment is opened.
|
||||||
|
@ -676,6 +695,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
filesToDelete.forEach { path ->
|
||||||
|
val result = File(path).deleteRecursively()
|
||||||
|
if (result) {
|
||||||
|
Log.d(TAG, "Deleted temporary file: $path")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Failed to delete temporary file: $path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filesToDelete = setOf()
|
||||||
val broadcastIntent = Intent()
|
val broadcastIntent = Intent()
|
||||||
broadcastIntent.action = "restart_service"
|
broadcastIntent.action = "restart_service"
|
||||||
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
|
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
|
||||||
|
@ -1654,7 +1682,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
// this ensures that no unnecessary space is taken
|
// this ensures that no unnecessary space is taken
|
||||||
loadCache()
|
loadCache()
|
||||||
File(filesDir, "exoplayer").deleteRecursively() // old cache
|
File(filesDir, "exoplayer").deleteRecursively() // old cache
|
||||||
File(cacheDir, "exoplayer").deleteOnExit() // current cache
|
deleteFileOnExit(File(cacheDir, "exoplayer")) // current cache
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.base64Decode
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
open class Acefile : ExtractorApi() {
|
open class Acefile : ExtractorApi() {
|
||||||
|
@ -16,22 +15,19 @@ open class Acefile : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val script = getAndUnpack(app.get(url).text)
|
val id = "/(?:d|download|player|f|file)/(\\w+)".toRegex().find(url)?.groupValues?.get(1)
|
||||||
val id = script.substringAfter("\"code\":\"").substringBefore("\",")
|
val script = getAndUnpack(app.get("$mainUrl/player/${id ?: return}").text)
|
||||||
val doc = app.get("https://drive.google.com/uc?id=${base64Decode(id)}&export=download").document
|
val service = """service\s*=\s*['"]([^'"]+)""".toRegex().find(script)?.groupValues?.get(1)
|
||||||
val form = doc.select("form#download-form").attr("action")
|
val serverUrl = """['"](\S+check&id\S+?)['"]""".toRegex().find(script)?.groupValues?.get(1)
|
||||||
val uc = doc.select("input#uc-download-link").attr("value")
|
?.replace("\"+service+\"", service ?: return)
|
||||||
val video = app.post(
|
|
||||||
form, data = mapOf(
|
val video = app.get(serverUrl ?: return, referer = "$mainUrl/").parsedSafe<Source>()?.data
|
||||||
"uc-download-link" to uc
|
|
||||||
)
|
|
||||||
).url
|
|
||||||
|
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
video,
|
video ?: return,
|
||||||
"",
|
"",
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
INFER_TYPE
|
INFER_TYPE
|
||||||
|
@ -40,4 +36,8 @@ open class Acefile : ExtractorApi() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val data: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -49,8 +49,23 @@ open class Chillx : ExtractorApi() {
|
||||||
val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||||
|
|
||||||
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||||
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
|
|
||||||
|
|
||||||
|
val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||||
|
val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex()
|
||||||
|
val matches = subtitlePattern.findAll(subtitles ?: "")
|
||||||
|
val languageUrlPairs = matches.map { matchResult ->
|
||||||
|
val (language, url) = matchResult.destructured
|
||||||
|
decodeUnicodeEscape(language) to url
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
languageUrlPairs.forEach{ (name, file) ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
name,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
// required
|
// required
|
||||||
val headers = mapOf(
|
val headers = mapOf(
|
||||||
"Accept" to "*/*",
|
"Accept" to "*/*",
|
||||||
|
@ -67,16 +82,13 @@ open class Chillx : ExtractorApi() {
|
||||||
"$mainUrl/",
|
"$mainUrl/",
|
||||||
headers = headers
|
headers = headers
|
||||||
).forEach(callback)
|
).forEach(callback)
|
||||||
|
}
|
||||||
|
|
||||||
AppUtils.tryParseJson<List<Tracks>>("[$tracks]")
|
private fun decodeUnicodeEscape(input: String): String {
|
||||||
?.filter { it.kind == "captions" }?.map { track ->
|
val regex = Regex("u([0-9a-fA-F]{4})")
|
||||||
subtitleCallback.invoke(
|
return regex.replace(input) {
|
||||||
SubtitleFile(
|
it.groupValues[1].toInt(16).toChar().toString()
|
||||||
track.label ?: "",
|
}
|
||||||
track.file ?: return@map null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getKey() = key ?: fetchKey().also { key = it }
|
suspend fun getKey() = key ?: fetchKey().also { key = it }
|
||||||
|
|
|
@ -22,9 +22,9 @@ open class Gofile : ExtractorApi() {
|
||||||
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1)
|
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1)
|
||||||
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
|
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
|
||||||
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
|
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
|
||||||
Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
|
Regex("fetchData.wt\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
|
||||||
}
|
}
|
||||||
app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=$websiteToken")
|
app.get("$mainApi/getContent?contentId=$id&token=$token&wt=$websiteToken")
|
||||||
.parsedSafe<Source>()?.data?.contents?.forEach {
|
.parsedSafe<Source>()?.data?.contents?.forEach {
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
|
|
|
@ -11,3 +11,13 @@ class FourCX : ContentX() {
|
||||||
override var name = "FourCX"
|
override var name = "FourCX"
|
||||||
override var mainUrl = "https://four.contentx.me"
|
override var mainUrl = "https://four.contentx.me"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PlayRu : ContentX() {
|
||||||
|
override var name = "PlayRu"
|
||||||
|
override var mainUrl = "https://playru.net"
|
||||||
|
}
|
||||||
|
|
||||||
|
class FourPlayRu : ContentX() {
|
||||||
|
override var name = "FourPlayRu"
|
||||||
|
override var mainUrl = "https://four.playru.net"
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.base64DecodeArray
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
@ -16,13 +17,52 @@ import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
// Code found in https://github.com/theonlymo/keys
|
|
||||||
// special credits to @theonlymo for providing key
|
|
||||||
class Megacloud : Rabbitstream() {
|
class Megacloud : Rabbitstream() {
|
||||||
override val name = "Megacloud"
|
override val name = "Megacloud"
|
||||||
override val mainUrl = "https://megacloud.tv"
|
override val mainUrl = "https://megacloud.tv"
|
||||||
override val embed = "embed-2/ajax/e-1"
|
override val embed = "embed-2/ajax/e-1"
|
||||||
override val key = "https://raw.githubusercontent.com/theonlymo/keys/e1/key"
|
private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js"
|
||||||
|
|
||||||
|
override suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = getKeys()
|
||||||
|
val sourcesArray = sources.toCharArray()
|
||||||
|
|
||||||
|
var extractedKey = ""
|
||||||
|
var currentIndex = 0
|
||||||
|
for (index in rawKeys) {
|
||||||
|
val start = index[0] + currentIndex
|
||||||
|
val end = start + index[1]
|
||||||
|
for (i in start until end) {
|
||||||
|
extractedKey += sourcesArray[i].toString()
|
||||||
|
sourcesArray[i] = ' '
|
||||||
|
}
|
||||||
|
currentIndex += index[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedKey to sourcesArray.joinToString("").replace(" ", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getKeys(): List<List<Int>> {
|
||||||
|
val script = app.get(scriptUrl).text
|
||||||
|
fun matchingKey(value: String): String {
|
||||||
|
return Regex(",$value=((?:0x)?([0-9a-fA-F]+))").find(script)?.groupValues?.get(1)
|
||||||
|
?.removePrefix("0x") ?: throw ErrorLoadingException("Failed to match the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
val regex = Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
|
||||||
|
val indexPairs = regex.findAll(script).toList().map { match ->
|
||||||
|
val matchKey1 = matchingKey(match.groupValues[1])
|
||||||
|
val matchKey2 = matchingKey(match.groupValues[2])
|
||||||
|
try {
|
||||||
|
listOf(matchKey1.toInt(16), matchKey2.toInt(16))
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
return indexPairs
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Dokicloud : Rabbitstream() {
|
class Dokicloud : Rabbitstream() {
|
||||||
|
@ -30,12 +70,14 @@ class Dokicloud : Rabbitstream() {
|
||||||
override val mainUrl = "https://dokicloud.one"
|
override val mainUrl = "https://dokicloud.one"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Code found in https://github.com/eatmynerds/key
|
||||||
|
// special credits to @eatmynerds for providing key
|
||||||
open class Rabbitstream : ExtractorApi() {
|
open class Rabbitstream : ExtractorApi() {
|
||||||
override val name = "Rabbitstream"
|
override val name = "Rabbitstream"
|
||||||
override val mainUrl = "https://rabbitstream.net"
|
override val mainUrl = "https://rabbitstream.net"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
open val embed = "ajax/embed-4"
|
open val embed = "ajax/embed-4"
|
||||||
open val key = "https://raw.githubusercontent.com/theonlymo/keys/e4/key"
|
open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt"
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -56,7 +98,7 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
||||||
response.parsedSafe()
|
response.parsedSafe()
|
||||||
} else {
|
} else {
|
||||||
val (key, encData) = extractRealKey(sources, getRawKey())
|
val (key, encData) = extractRealKey(sources)
|
||||||
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
||||||
SourcesResponses(
|
SourcesResponses(
|
||||||
sources = decrypted,
|
sources = decrypted,
|
||||||
|
@ -75,8 +117,8 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
decryptedSources?.tracks?.map { track ->
|
decryptedSources?.tracks?.map { track ->
|
||||||
subtitleCallback.invoke(
|
subtitleCallback.invoke(
|
||||||
SubtitleFile(
|
SubtitleFile(
|
||||||
track?.label ?: "",
|
track?.label ?: return@map,
|
||||||
track?.file ?: return@map
|
track.file ?: return@map
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -84,25 +126,10 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getRawKey(): String = app.get(key).text
|
open suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = parseJson<List<Int>>(app.get(key).text)
|
||||||
private fun extractRealKey(sources: String, stops: String): Pair<String, String> {
|
val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray())
|
||||||
val decryptKey = parseJson<List<List<Int>>>(stops)
|
return extractedKey to sources
|
||||||
val sourcesArray = sources.toCharArray()
|
|
||||||
|
|
||||||
var extractedKey = ""
|
|
||||||
var currentIndex = 0
|
|
||||||
for (index in decryptKey) {
|
|
||||||
val start = index[0] + currentIndex
|
|
||||||
val end = start + index[1]
|
|
||||||
for (i in start until end) {
|
|
||||||
extractedKey += sourcesArray[i].toString()
|
|
||||||
sourcesArray[i] = ' '
|
|
||||||
}
|
|
||||||
currentIndex += index[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return extractedKey to sourcesArray.joinToString("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import java.net.URI
|
||||||
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
|
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
|
||||||
* @param script pass custom js to execute
|
* @param script pass custom js to execute
|
||||||
* @param scriptCallback will be called with the result from custom js
|
* @param scriptCallback will be called with the result from custom js
|
||||||
|
* @param timeout close webview after timeout
|
||||||
* */
|
* */
|
||||||
class WebViewResolver(
|
class WebViewResolver(
|
||||||
val interceptUrl: Regex,
|
val interceptUrl: Regex,
|
||||||
|
@ -38,18 +39,29 @@ class WebViewResolver(
|
||||||
val userAgent: String? = USER_AGENT,
|
val userAgent: String? = USER_AGENT,
|
||||||
val useOkhttp: Boolean = true,
|
val useOkhttp: Boolean = true,
|
||||||
val script: String? = null,
|
val script: String? = null,
|
||||||
val scriptCallback: ((String) -> Unit)? = null
|
val scriptCallback: ((String) -> Unit)? = null,
|
||||||
|
val timeout: Long = DEFAULT_TIMEOUT
|
||||||
) :
|
) :
|
||||||
Interceptor {
|
Interceptor {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
interceptUrl: Regex,
|
||||||
|
additionalUrls: List<Regex> = emptyList(),
|
||||||
|
userAgent: String? = USER_AGENT,
|
||||||
|
useOkhttp: Boolean = true,
|
||||||
|
script: String? = null,
|
||||||
|
scriptCallback: ((String) -> Unit)? = null,
|
||||||
|
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
interceptUrl: Regex,
|
interceptUrl: Regex,
|
||||||
additionalUrls: List<Regex> = emptyList(),
|
additionalUrls: List<Regex> = emptyList(),
|
||||||
userAgent: String? = USER_AGENT,
|
userAgent: String? = USER_AGENT,
|
||||||
useOkhttp: Boolean = true
|
useOkhttp: Boolean = true
|
||||||
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null)
|
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val DEFAULT_TIMEOUT = 60_000L
|
||||||
var webViewUserAgent: String? = null
|
var webViewUserAgent: String? = null
|
||||||
|
|
||||||
@JvmName("getWebViewUserAgent1")
|
@JvmName("getWebViewUserAgent1")
|
||||||
|
@ -262,7 +274,7 @@ class WebViewResolver(
|
||||||
|
|
||||||
var loop = 0
|
var loop = 0
|
||||||
// Timeouts after this amount, 60s
|
// Timeouts after this amount, 60s
|
||||||
val totalTime = 60000L
|
val totalTime = timeout
|
||||||
|
|
||||||
val delayTime = 100L
|
val delayTime = 100L
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
package com.lagradost.cloudstream3.subtitles
|
package com.lagradost.cloudstream3.subtitles
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
|
import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
interface AbstractSubProvider {
|
interface AbstractSubProvider {
|
||||||
|
val idPrefix: String
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
|
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
|
@ -15,6 +27,98 @@ interface AbstractSubProvider {
|
||||||
suspend fun load(data: SubtitleEntity): String? {
|
suspend fun load(data: SubtitleEntity): String? {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun SubtitleResource.getResources(data: SubtitleEntity) {
|
||||||
|
this.addUrl(load(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
suspend fun getResource(data: SubtitleEntity): SubtitleResource {
|
||||||
|
return SubtitleResource().apply {
|
||||||
|
this.getResources(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for subtitle files.
|
||||||
|
* @see addUrl
|
||||||
|
* @see addFile
|
||||||
|
*/
|
||||||
|
class SubtitleResource {
|
||||||
|
fun downloadFile(source: BufferedSource): File {
|
||||||
|
val file = File.createTempFile("temp-subtitle", ".tmp").apply {
|
||||||
|
deleteFileOnExit(this)
|
||||||
|
}
|
||||||
|
val sink = file.sink().buffer()
|
||||||
|
sink.writeAll(source)
|
||||||
|
sink.close()
|
||||||
|
source.close()
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unzip(file: File): List<Pair<String, File>> {
|
||||||
|
val entries = mutableListOf<Pair<String, File>>()
|
||||||
|
|
||||||
|
ZipInputStream(file.inputStream()).use { zipInputStream ->
|
||||||
|
var zipEntry = zipInputStream.nextEntry
|
||||||
|
|
||||||
|
while (zipEntry != null) {
|
||||||
|
val tempFile = File.createTempFile("unzipped-subtitle", ".tmp").apply {
|
||||||
|
deleteFileOnExit(this)
|
||||||
|
}
|
||||||
|
entries.add(zipEntry.name to tempFile)
|
||||||
|
|
||||||
|
tempFile.sink().buffer().use { buffer ->
|
||||||
|
buffer.writeAll(zipInputStream.source())
|
||||||
|
}
|
||||||
|
|
||||||
|
zipEntry = zipInputStream.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SingleSubtitleResource(
|
||||||
|
val name: String?,
|
||||||
|
val url: String,
|
||||||
|
val origin: SubtitleOrigin
|
||||||
|
)
|
||||||
|
|
||||||
|
private var resources: MutableList<SingleSubtitleResource> = mutableListOf()
|
||||||
|
|
||||||
|
fun getSubtitles(): List<SingleSubtitleResource> {
|
||||||
|
return resources.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addUrl(url: String?, name: String? = null) {
|
||||||
|
if (url == null) return
|
||||||
|
this.resources.add(
|
||||||
|
SingleSubtitleResource(name, url, SubtitleOrigin.URL)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFile(file: File, name: String? = null) {
|
||||||
|
this.resources.add(
|
||||||
|
SingleSubtitleResource(name, file.toUri().toString(), SubtitleOrigin.DOWNLOADED_FILE)
|
||||||
|
)
|
||||||
|
deleteFileOnExit(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addZipUrl(
|
||||||
|
url: String,
|
||||||
|
nameGenerator: (String, File) -> String? = { _, _ -> null }
|
||||||
|
) {
|
||||||
|
val source = app.get(url).okhttpResponse.body.source()
|
||||||
|
val zip = downloadFile(source)
|
||||||
|
val realFiles = unzip(zip)
|
||||||
|
zip.deleteRecursively()
|
||||||
|
realFiles.forEach { (name, subtitleFile) ->
|
||||||
|
addFile(subtitleFile, nameGenerator(name, subtitleFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AbstractSubApi : AbstractSubProvider, AuthAPI
|
interface AbstractSubApi : AbstractSubProvider, AuthAPI
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.syncproviders
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.SubScene
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.*
|
import com.lagradost.cloudstream3.syncproviders.providers.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
val simklApi = SimklApi(0)
|
val simklApi = SimklApi(0)
|
||||||
val indexSubtitlesApi = IndexSubtitleApi()
|
val indexSubtitlesApi = IndexSubtitleApi()
|
||||||
val addic7ed = Addic7ed()
|
val addic7ed = Addic7ed()
|
||||||
|
val subScene = SubScene()
|
||||||
val localListApi = LocalList()
|
val localListApi = LocalList()
|
||||||
|
|
||||||
// used to login via app intent
|
// used to login via app intent
|
||||||
|
@ -41,7 +43,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
openSubtitlesApi,
|
openSubtitlesApi,
|
||||||
indexSubtitlesApi, // they got anti scraping measures in place :(
|
indexSubtitlesApi, // they got anti scraping measures in place :(
|
||||||
addic7ed
|
addic7ed,
|
||||||
|
subScene
|
||||||
)
|
)
|
||||||
|
|
||||||
const val appString = "cloudstreamapp"
|
const val appString = "cloudstreamapp"
|
||||||
|
|
|
@ -23,6 +23,47 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
companion object {
|
companion object {
|
||||||
const val host = "https://indexsubtitle.com"
|
const val host = "https://indexsubtitle.com"
|
||||||
const val TAG = "INDEXSUBS"
|
const val TAG = "INDEXSUBS"
|
||||||
|
|
||||||
|
fun getOrdinal(num: Int?): String? {
|
||||||
|
return when (num) {
|
||||||
|
1 -> "First"
|
||||||
|
2 -> "Second"
|
||||||
|
3 -> "Third"
|
||||||
|
4 -> "Fourth"
|
||||||
|
5 -> "Fifth"
|
||||||
|
6 -> "Sixth"
|
||||||
|
7 -> "Seventh"
|
||||||
|
8 -> "Eighth"
|
||||||
|
9 -> "Ninth"
|
||||||
|
10 -> "Tenth"
|
||||||
|
11 -> "Eleventh"
|
||||||
|
12 -> "Twelfth"
|
||||||
|
13 -> "Thirteenth"
|
||||||
|
14 -> "Fourteenth"
|
||||||
|
15 -> "Fifteenth"
|
||||||
|
16 -> "Sixteenth"
|
||||||
|
17 -> "Seventeenth"
|
||||||
|
18 -> "Eighteenth"
|
||||||
|
19 -> "Nineteenth"
|
||||||
|
20 -> "Twentieth"
|
||||||
|
21 -> "Twenty-First"
|
||||||
|
22 -> "Twenty-Second"
|
||||||
|
23 -> "Twenty-Third"
|
||||||
|
24 -> "Twenty-Fourth"
|
||||||
|
25 -> "Twenty-Fifth"
|
||||||
|
26 -> "Twenty-Sixth"
|
||||||
|
27 -> "Twenty-Seventh"
|
||||||
|
28 -> "Twenty-Eighth"
|
||||||
|
29 -> "Twenty-Ninth"
|
||||||
|
30 -> "Thirtieth"
|
||||||
|
31 -> "Thirty-First"
|
||||||
|
32 -> "Thirty-Second"
|
||||||
|
33 -> "Thirty-Third"
|
||||||
|
34 -> "Thirty-Fourth"
|
||||||
|
35 -> "Thirty-Fifth"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fixUrl(url: String): String {
|
private fun fixUrl(url: String): String {
|
||||||
|
@ -44,47 +85,6 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOrdinal(num: Int?): String? {
|
|
||||||
return when (num) {
|
|
||||||
1 -> "First"
|
|
||||||
2 -> "Second"
|
|
||||||
3 -> "Third"
|
|
||||||
4 -> "Fourth"
|
|
||||||
5 -> "Fifth"
|
|
||||||
6 -> "Sixth"
|
|
||||||
7 -> "Seventh"
|
|
||||||
8 -> "Eighth"
|
|
||||||
9 -> "Ninth"
|
|
||||||
10 -> "Tenth"
|
|
||||||
11 -> "Eleventh"
|
|
||||||
12 -> "Twelfth"
|
|
||||||
13 -> "Thirteenth"
|
|
||||||
14 -> "Fourteenth"
|
|
||||||
15 -> "Fifteenth"
|
|
||||||
16 -> "Sixteenth"
|
|
||||||
17 -> "Seventeenth"
|
|
||||||
18 -> "Eighteenth"
|
|
||||||
19 -> "Nineteenth"
|
|
||||||
20 -> "Twentieth"
|
|
||||||
21 -> "Twenty-First"
|
|
||||||
22 -> "Twenty-Second"
|
|
||||||
23 -> "Twenty-Third"
|
|
||||||
24 -> "Twenty-Fourth"
|
|
||||||
25 -> "Twenty-Fifth"
|
|
||||||
26 -> "Twenty-Sixth"
|
|
||||||
27 -> "Twenty-Seventh"
|
|
||||||
28 -> "Twenty-Eighth"
|
|
||||||
29 -> "Twenty-Ninth"
|
|
||||||
30 -> "Thirtieth"
|
|
||||||
31 -> "Thirty-First"
|
|
||||||
32 -> "Thirty-Second"
|
|
||||||
33 -> "Thirty-Third"
|
|
||||||
34 -> "Thirty-Fourth"
|
|
||||||
35 -> "Thirty-Fifth"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
|
private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
|
||||||
val FILTER_EPS_REGEX =
|
val FILTER_EPS_REGEX =
|
||||||
Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))")
|
Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))")
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
|
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal
|
||||||
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
|
||||||
|
class SubScene : AbstractSubProvider {
|
||||||
|
val mainUrl = "https://subscene.com"
|
||||||
|
val name = "Subscene"
|
||||||
|
override val idPrefix = "subscene"
|
||||||
|
|
||||||
|
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||||
|
val seasonName =
|
||||||
|
query.seasonNumber?.let { number ->
|
||||||
|
// Need to translate "7" to "Seventh Season"
|
||||||
|
getOrdinal(number)?.let { words -> " - $words Season" }
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
val fullQuery = query.query + seasonName
|
||||||
|
|
||||||
|
val doc = app.post(
|
||||||
|
"$mainUrl/subtitles/searchbytitle",
|
||||||
|
data = mapOf("query" to fullQuery, "l" to "")
|
||||||
|
).document
|
||||||
|
|
||||||
|
return doc.select("div.title a").map { element ->
|
||||||
|
val href = "$mainUrl${element.attr("href")}"
|
||||||
|
val title = element.text()
|
||||||
|
|
||||||
|
AbstractSubtitleEntities.SubtitleEntity(
|
||||||
|
idPrefix = idPrefix,
|
||||||
|
name = title,
|
||||||
|
source = name,
|
||||||
|
data = href,
|
||||||
|
lang = query.lang ?: "en",
|
||||||
|
epNumber = query.epNumber
|
||||||
|
)
|
||||||
|
}.distinctBy { it.data }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||||
|
val resultDoc = app.get(data.data).document
|
||||||
|
val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English"
|
||||||
|
|
||||||
|
val results = resultDoc.select("table tbody tr").mapNotNull { element ->
|
||||||
|
val anchor = element.select("a")
|
||||||
|
val href = anchor.attr("href") ?: return@mapNotNull null
|
||||||
|
val fixedHref = "$mainUrl${href}"
|
||||||
|
val spans = anchor.select("span")
|
||||||
|
val language = spans.firstOrNull()?.text()
|
||||||
|
val title = spans.getOrNull(1)?.text()
|
||||||
|
val isPositive = anchor.select("span.positive-icon").isNotEmpty()
|
||||||
|
|
||||||
|
TableElement(title, language, fixedHref, isPositive)
|
||||||
|
}.sortedBy {
|
||||||
|
it.getScore(queryLanguage, data.epNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint { "$name found subtitles: ${results.takeLast(3)}" }
|
||||||
|
// Last = highest score
|
||||||
|
val selectedResult = results.lastOrNull() ?: return
|
||||||
|
|
||||||
|
val subtitleDocument = app.get(selectedResult.href).document
|
||||||
|
val subtitleDownloadUrl =
|
||||||
|
"$mainUrl${subtitleDocument.select("div.download a").attr("href")}"
|
||||||
|
|
||||||
|
this.addZipUrl(subtitleDownloadUrl) { name, _ ->
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to manage the various different subtitle results and rank them.
|
||||||
|
*/
|
||||||
|
data class TableElement(
|
||||||
|
val title: String?,
|
||||||
|
val language: String?,
|
||||||
|
val href: String,
|
||||||
|
val isPositive: Boolean
|
||||||
|
) {
|
||||||
|
private fun matchesLanguage(other: String): Boolean {
|
||||||
|
return language != null && (language.contains(other, ignoreCase = true) ||
|
||||||
|
other.contains(language, ignoreCase = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scores in this order:
|
||||||
|
* Preferred Language > Episode number > Positive rating > English Language
|
||||||
|
*/
|
||||||
|
fun getScore(queryLanguage: String, episodeNum: Int?): Int {
|
||||||
|
var score = 0
|
||||||
|
if (this.matchesLanguage(queryLanguage)) {
|
||||||
|
score += 8
|
||||||
|
}
|
||||||
|
// Matches Episode 7 using "E07" with any number of leading zeroes
|
||||||
|
if (episodeNum != null && title != null && title.contains(
|
||||||
|
Regex(
|
||||||
|
"""E0*${episodeNum}""",
|
||||||
|
RegexOption.IGNORE_CASE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
score += 4
|
||||||
|
}
|
||||||
|
if (isPositive) {
|
||||||
|
score += 2
|
||||||
|
}
|
||||||
|
if (this.matchesLanguage("English")) {
|
||||||
|
score += 1
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -317,7 +317,7 @@ class HomeParentItemAdapterPreview(
|
||||||
homePreviewText.text = item.name
|
homePreviewText.text = item.name
|
||||||
populateChips(
|
populateChips(
|
||||||
homePreviewTags,
|
homePreviewTags,
|
||||||
item.tags ?: emptyList(),
|
item.tags?.take(6) ?: emptyList(),
|
||||||
R.style.ChipFilledSemiTransparent
|
R.style.ChipFilledSemiTransparent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||||
|
@ -657,7 +658,7 @@ class CS3IPlayer : IPlayer {
|
||||||
SimpleCache(
|
SimpleCache(
|
||||||
File(
|
File(
|
||||||
context.cacheDir, "exoplayer"
|
context.cacheDir, "exoplayer"
|
||||||
).also { it.deleteOnExit() }, // Ensures always fresh file
|
).also { deleteFileOnExit(it) }, // Ensures always fresh file
|
||||||
LeastRecentlyUsedCacheEvictor(cacheSize),
|
LeastRecentlyUsedCacheEvictor(cacheSize),
|
||||||
databaseProvider
|
databaseProvider
|
||||||
)
|
)
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.*
|
import com.lagradost.cloudstream3.mvvm.*
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
||||||
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
||||||
|
@ -69,7 +70,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val subsProviders
|
val subsProviders
|
||||||
get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null }
|
get() = subtitleProviders.filter { provider ->
|
||||||
|
(provider as? AbstractSubApi)?.let { !it.requiresLogin || it.loginInfo() != null }
|
||||||
|
?: true
|
||||||
|
}
|
||||||
val subsProvidersIsActive
|
val subsProvidersIsActive
|
||||||
get() = subsProviders.isNotEmpty()
|
get() = subsProviders.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
@ -147,7 +151,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playerStatusChanged() {
|
override fun playerStatusChanged() {
|
||||||
if(player.getIsPlaying()){
|
if (player.getIsPlaying()) {
|
||||||
viewModel.forceClearCache = false
|
viewModel.forceClearCache = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -473,17 +477,21 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
currentSubtitle?.let { currentSubtitle ->
|
currentSubtitle?.let { currentSubtitle ->
|
||||||
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
|
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
|
||||||
ioSafe {
|
ioSafe {
|
||||||
val url = api.load(currentSubtitle) ?: return@ioSafe
|
val subtitles =
|
||||||
val subtitle = SubtitleData(
|
api.getResource(currentSubtitle).getSubtitles().map { resource ->
|
||||||
name = getName(currentSubtitle, true),
|
SubtitleData(
|
||||||
url = url,
|
name = resource.name ?: getName(currentSubtitle, true),
|
||||||
origin = SubtitleOrigin.URL,
|
url = resource.url,
|
||||||
mimeType = url.toSubtitleMimeType(),
|
origin = resource.origin,
|
||||||
headers = currentSubtitle.headers,
|
mimeType = resource.url.toSubtitleMimeType(),
|
||||||
currentSubtitle.lang
|
headers = currentSubtitle.headers,
|
||||||
)
|
currentSubtitle.lang
|
||||||
runOnMainThread {
|
)
|
||||||
addAndSelectSubtitles(subtitle)
|
}
|
||||||
|
if (subtitles.isNotEmpty()) {
|
||||||
|
runOnMainThread {
|
||||||
|
addAndSelectSubtitles(*subtitles.toTypedArray())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -521,7 +529,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addAndSelectSubtitles(subtitleData: SubtitleData) {
|
private fun addAndSelectSubtitles(
|
||||||
|
vararg subtitleData: SubtitleData
|
||||||
|
) {
|
||||||
|
if (subtitleData.isEmpty()) return
|
||||||
|
val selectedSubtitle = subtitleData.first()
|
||||||
val ctx = context ?: return
|
val ctx = context ?: return
|
||||||
|
|
||||||
val subs = currentSubs + subtitleData
|
val subs = currentSubs + subtitleData
|
||||||
|
@ -533,13 +545,13 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
player.saveData()
|
player.saveData()
|
||||||
player.reloadPlayer(ctx)
|
player.reloadPlayer(ctx)
|
||||||
|
|
||||||
setSubtitles(subtitleData)
|
setSubtitles(selectedSubtitle)
|
||||||
viewModel.addSubtitles(setOf(subtitleData))
|
viewModel.addSubtitles(subtitleData.toSet())
|
||||||
|
|
||||||
selectSourceDialog?.dismissSafe()
|
selectSourceDialog?.dismissSafe()
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name),
|
String.format(ctx.getString(R.string.player_loaded_subtitles), selectedSubtitle.name),
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -919,7 +931,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
override fun playerError(exception: Throwable) {
|
override fun playerError(exception: Throwable) {
|
||||||
Log.i(TAG, "playerError = $currentSelectedLink")
|
Log.i(TAG, "playerError = $currentSelectedLink")
|
||||||
if(!hasNextMirror()){
|
if (!hasNextMirror()) {
|
||||||
viewModel.forceClearCache = true
|
viewModel.forceClearCache = true
|
||||||
}
|
}
|
||||||
super.playerError(exception)
|
super.playerError(exception)
|
||||||
|
|
|
@ -681,6 +681,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
resultMetaYear.setText(d.yearText)
|
resultMetaYear.setText(d.yearText)
|
||||||
resultMetaDuration.setText(d.durationText)
|
resultMetaDuration.setText(d.durationText)
|
||||||
resultMetaRating.setText(d.ratingText)
|
resultMetaRating.setText(d.ratingText)
|
||||||
|
resultMetaStatus.setText(d.onGoingText)
|
||||||
resultMetaContentRating.setText(d.contentRatingText)
|
resultMetaContentRating.setText(d.contentRatingText)
|
||||||
resultCastText.setText(d.actorsText)
|
resultCastText.setText(d.actorsText)
|
||||||
resultNextAiring.setText(d.nextAiringEpisode)
|
resultNextAiring.setText(d.nextAiringEpisode)
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings.extensions
|
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||||
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
||||||
|
@ -112,6 +119,17 @@ class RepoAdapter(
|
||||||
repositoryItemRoot.setOnClickListener {
|
repositoryItemRoot.setOnClickListener {
|
||||||
clickCallback(repositoryData)
|
clickCallback(repositoryData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositoryItemRoot.setOnLongClickListener {
|
||||||
|
val clipboardManager =
|
||||||
|
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?
|
||||||
|
clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url))
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
return@setOnLongClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
mainText.text = repositoryData.name
|
mainText.text = repositoryData.name
|
||||||
subText.text = repositoryData.url
|
subText.text = repositoryData.url
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.utils.TestingUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import okhttp3.internal.toImmutableList
|
|
||||||
|
|
||||||
class TestViewModel : ViewModel() {
|
class TestViewModel : ViewModel() {
|
||||||
data class TestProgress(
|
data class TestProgress(
|
||||||
|
|
|
@ -106,6 +106,8 @@ import com.lagradost.cloudstream3.extractors.ContentX
|
||||||
import com.lagradost.cloudstream3.extractors.EmturbovidExtractor
|
import com.lagradost.cloudstream3.extractors.EmturbovidExtractor
|
||||||
import com.lagradost.cloudstream3.extractors.Hotlinger
|
import com.lagradost.cloudstream3.extractors.Hotlinger
|
||||||
import com.lagradost.cloudstream3.extractors.FourCX
|
import com.lagradost.cloudstream3.extractors.FourCX
|
||||||
|
import com.lagradost.cloudstream3.extractors.PlayRu
|
||||||
|
import com.lagradost.cloudstream3.extractors.FourPlayRu
|
||||||
import com.lagradost.cloudstream3.extractors.HDMomPlayer
|
import com.lagradost.cloudstream3.extractors.HDMomPlayer
|
||||||
import com.lagradost.cloudstream3.extractors.HDPlayerSystem
|
import com.lagradost.cloudstream3.extractors.HDPlayerSystem
|
||||||
import com.lagradost.cloudstream3.extractors.VideoSeyred
|
import com.lagradost.cloudstream3.extractors.VideoSeyred
|
||||||
|
@ -704,6 +706,8 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
||||||
ContentX(),
|
ContentX(),
|
||||||
Hotlinger(),
|
Hotlinger(),
|
||||||
FourCX(),
|
FourCX(),
|
||||||
|
PlayRu(),
|
||||||
|
FourPlayRu(),
|
||||||
HDMomPlayer(),
|
HDMomPlayer(),
|
||||||
HDPlayerSystem(),
|
HDPlayerSystem(),
|
||||||
VideoSeyred(),
|
VideoSeyred(),
|
||||||
|
|
|
@ -23,6 +23,7 @@ import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -213,7 +214,7 @@ class InAppUpdater {
|
||||||
this.cacheDir.listFiles()?.filter {
|
this.cacheDir.listFiles()?.filter {
|
||||||
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
|
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
|
||||||
}?.forEach {
|
}?.forEach {
|
||||||
it.deleteOnExit()
|
deleteFileOnExit(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadedFile = File.createTempFile(appUpdateName, ".$appUpdateSuffix")
|
val downloadedFile = File.createTempFile(appUpdateName, ".$appUpdateSuffix")
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.lagradost.cloudstream3.MainActivity
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
||||||
|
@ -75,7 +76,7 @@ class PackageInstallerService : Service() {
|
||||||
this@PackageInstallerService.cacheDir.listFiles()?.filter {
|
this@PackageInstallerService.cacheDir.listFiles()?.filter {
|
||||||
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
|
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
|
||||||
}?.forEach {
|
}?.forEach {
|
||||||
it.deleteOnExit()
|
deleteFileOnExit(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,7 +137,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="5"
|
android:maxLines="3"
|
||||||
android:paddingBottom="5dp"
|
android:paddingBottom="5dp"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
tools:text="very nice tv series" />
|
tools:text="very nice tv series" />
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:elevation="10dp"
|
android:elevation="10dp"
|
||||||
|
android:tooltipText="@string/subscribe_tooltip"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/baseline_notifications_none_24"
|
android:src="@drawable/baseline_notifications_none_24"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:elevation="10dp"
|
android:elevation="10dp"
|
||||||
|
android:tooltipText="@string/action_add_to_favorites"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/ic_baseline_favorite_border_24"
|
android:src="@drawable/ic_baseline_favorite_border_24"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:elevation="10dp"
|
android:elevation="10dp"
|
||||||
|
android:tooltipText="@string/result_share"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/ic_outline_share_24"
|
android:src="@drawable/ic_outline_share_24"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:elevation="10dp"
|
android:elevation="10dp"
|
||||||
|
android:tooltipText="@string/result_open_in_browser"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/ic_baseline_public_24"
|
android:src="@drawable/ic_baseline_public_24"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
|
@ -153,7 +153,7 @@
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:elevation="10dp"
|
android:elevation="10dp"
|
||||||
|
android:tooltipText="@string/result_search_tooltip"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/search_icon"
|
android:src="@drawable/search_icon"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
android:layout_height="25dp"
|
android:layout_height="25dp"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:elevation="10dp"
|
android:elevation="10dp"
|
||||||
|
android:tooltipText="@string/recommendations_tooltip"
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/baseline_list_alt_24"
|
android:src="@drawable/baseline_list_alt_24"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
|
|
|
@ -387,6 +387,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
||||||
<com.lagradost.cloudstream3.widget.FlowLayout
|
<com.lagradost.cloudstream3.widget.FlowLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
app:itemSpacing="10dp">
|
app:itemSpacing="10dp">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
|
@ -399,6 +400,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
||||||
android:id="@+id/result_meta_content_rating"
|
android:id="@+id/result_meta_content_rating"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
style="@style/SmallWhiteButton"
|
style="@style/SmallWhiteButton"
|
||||||
|
android:focusable="false"
|
||||||
tools:text="PG-13" />
|
tools:text="PG-13" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
|
@ -148,7 +148,7 @@
|
||||||
<string name="download_done">Download Done</string>
|
<string name="download_done">Download Done</string>
|
||||||
<string name="download_format" translatable="false">%s - %s</string>
|
<string name="download_format" translatable="false">%s - %s</string>
|
||||||
<string name="update_started">Update Started</string>
|
<string name="update_started">Update Started</string>
|
||||||
<string name="stream">Stream</string>
|
<string name="stream">Network stream</string>
|
||||||
<string name="error_loading_links_toast">Error Loading Links</string>
|
<string name="error_loading_links_toast">Error Loading Links</string>
|
||||||
<string name="links_reloaded_toast">Links Reloaded</string>
|
<string name="links_reloaded_toast">Links Reloaded</string>
|
||||||
<string name="download_storage_text">Internal Storage</string>
|
<string name="download_storage_text">Internal Storage</string>
|
||||||
|
@ -174,6 +174,10 @@
|
||||||
<string name="sort_clear">Clear</string>
|
<string name="sort_clear">Clear</string>
|
||||||
<string name="sort_save">Save</string>
|
<string name="sort_save">Save</string>
|
||||||
<string name="copyTitle">Title copied!</string>
|
<string name="copyTitle">Title copied!</string>
|
||||||
|
<string name="copyRepoUrl">Repo URL copied!</string>
|
||||||
|
<string name="subscribe_tooltip">New episode notification</string>
|
||||||
|
<string name="result_search_tooltip">Search in other extensions</string>
|
||||||
|
<string name="recommendations_tooltip">Show recommendations</string>
|
||||||
<string name="player_speed">Player Speed</string>
|
<string name="player_speed">Player Speed</string>
|
||||||
<string name="subtitles_settings">Subtitle Settings</string>
|
<string name="subtitles_settings">Subtitle Settings</string>
|
||||||
<string name="subs_text_color">Text Color</string>
|
<string name="subs_text_color">Text Color</string>
|
||||||
|
@ -213,8 +217,8 @@
|
||||||
<string name="player_subtitles_settings_des">Player subtitles settings</string>
|
<string name="player_subtitles_settings_des">Player subtitles settings</string>
|
||||||
<string name="chromecast_subtitles_settings">Chromecast Subtitles</string>
|
<string name="chromecast_subtitles_settings">Chromecast Subtitles</string>
|
||||||
<string name="chromecast_subtitles_settings_des">Chromecast subtitles settings</string>
|
<string name="chromecast_subtitles_settings_des">Chromecast subtitles settings</string>
|
||||||
<string name="eigengraumode_settings">Eigengravy Mode</string>
|
<string name="eigengraumode_settings">Playback speed</string>
|
||||||
<string name="eigengraumode_settings_des">Adds a speed option in the player</string>
|
<string name="speed_setting_summary">Adds a speed option in the player</string>
|
||||||
<string name="swipe_to_seek_settings">Swipe to seek</string>
|
<string name="swipe_to_seek_settings">Swipe to seek</string>
|
||||||
<string name="swipe_to_seek_settings_des">Swipe from side to side to control your position in a video</string>
|
<string name="swipe_to_seek_settings_des">Swipe from side to side to control your position in a video</string>
|
||||||
<string name="swipe_to_change_settings">Swipe to change settings</string>
|
<string name="swipe_to_change_settings">Swipe to change settings</string>
|
||||||
|
@ -391,9 +395,9 @@
|
||||||
<string name="video_disk_description">Causes problems if set too high on devices with low storage space, such as Android TV.</string>
|
<string name="video_disk_description">Causes problems if set too high on devices with low storage space, such as Android TV.</string>
|
||||||
<string name="dns_pref">DNS over HTTPS</string>
|
<string name="dns_pref">DNS over HTTPS</string>
|
||||||
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
|
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
|
||||||
<string name="jsdelivr_proxy">raw.githubusercontent.com Proxy</string>
|
<string name="jsdelivr_proxy">GitHub Proxy</string>
|
||||||
<string name="jsdelivr_enabled">Could not reach GitHub. Turning on jsDelivr proxy…</string>
|
<string name="jsdelivr_enabled">Could not reach GitHub. Turning on jsDelivr proxy…</string>
|
||||||
<string name="jsdelivr_proxy_summary">Bypasses blocking of GitHub using jsDelivr. May cause updates to be delayed by few days.</string>
|
<string name="jsdelivr_proxy_summary">Bypass blocking of raw github URLs using jsDelivr. May cause updates to be delayed by few days.</string>
|
||||||
<string name="add_site_pref">Clone site</string>
|
<string name="add_site_pref">Clone site</string>
|
||||||
<string name="remove_site_pref">Remove site</string>
|
<string name="remove_site_pref">Remove site</string>
|
||||||
<string name="add_site_summary">Add a clone of an existing site, with a different URL</string>
|
<string name="add_site_summary">Add a clone of an existing site, with a different URL</string>
|
||||||
|
@ -439,13 +443,15 @@
|
||||||
<string name="category_general">General</string>
|
<string name="category_general">General</string>
|
||||||
<string name="random_button_settings">Random Button</string>
|
<string name="random_button_settings">Random Button</string>
|
||||||
<string name="random_button_settings_desc">Show random button on Homepage and Library</string>
|
<string name="random_button_settings_desc">Show random button on Homepage and Library</string>
|
||||||
<string name="provider_lang_settings">Provider languages</string>
|
<string name="provider_lang_settings">Extension languages</string>
|
||||||
<string name="app_layout">App Layout</string>
|
<string name="app_layout">App Layout</string>
|
||||||
<string name="preferred_media_settings">Preferred media</string>
|
<string name="preferred_media_settings">Preferred media</string>
|
||||||
<string name="enable_nsfw_on_providers">Enable NSFW on supported providers</string>
|
<string name="enable_nsfw_on_providers">Enable NSFW on supported Extensions</string>
|
||||||
<string name="subtitles_encoding">Subtitle encoding</string>
|
<string name="subtitles_encoding">Subtitle encoding</string>
|
||||||
<string name="category_providers">Providers</string>
|
<string name="category_providers">Providers</string>
|
||||||
<string name="category_provider_test">Provider test</string>
|
<string name="category_provider_test">Provider test</string>
|
||||||
|
<string name="test_extensions">Test all Extensions</string>
|
||||||
|
<string name="test_extensions_summary">This Test is meant for developers only and does not verifies or denies working of any extension.</string>
|
||||||
<string name="category_ui">Layout</string>
|
<string name="category_ui">Layout</string>
|
||||||
<string name="automatic">Auto</string>
|
<string name="automatic">Auto</string>
|
||||||
<string name="tv_layout">TV layout</string>
|
<string name="tv_layout">TV layout</string>
|
||||||
|
@ -462,11 +468,11 @@
|
||||||
<string name="opensubtitles_key" translatable="false">opensubtitles_key</string>
|
<string name="opensubtitles_key" translatable="false">opensubtitles_key</string>
|
||||||
<string name="nginx_key" translatable="false">nginx_key</string>
|
<string name="nginx_key" translatable="false">nginx_key</string>
|
||||||
<string name="example_password">password123</string>
|
<string name="example_password">password123</string>
|
||||||
<string name="example_username">MyCoolUsername</string>
|
<string name="example_username">Username</string>
|
||||||
<string name="example_email">hello@world.com</string>
|
<string name="example_email">hello@world.com</string>
|
||||||
<string name="example_ip">127.0.0.1</string>
|
<string name="example_ip">127.0.0.1</string>
|
||||||
<string name="example_site_name">MyCoolSite</string>
|
<string name="example_site_name">NewSiteName</string>
|
||||||
<string name="example_site_url">example.com</string>
|
<string name="example_site_url">https://example.com</string>
|
||||||
<string name="example_lang_name">Language code (en)</string>
|
<string name="example_lang_name">Language code (en)</string>
|
||||||
<!--
|
<!--
|
||||||
<string name="mal_account_settings" translatable="false">MAL</string>
|
<string name="mal_account_settings" translatable="false">MAL</string>
|
||||||
|
@ -558,8 +564,8 @@
|
||||||
<string name="subtitles_filter_lang">Filter by preferred media language</string>
|
<string name="subtitles_filter_lang">Filter by preferred media language</string>
|
||||||
<string name="extras">Extras</string>
|
<string name="extras">Extras</string>
|
||||||
<string name="trailer">Trailer</string>
|
<string name="trailer">Trailer</string>
|
||||||
<string name="network_adress_example">Link to stream</string>
|
<string name="network_adress_example">https://example.com/example.mp4</string>
|
||||||
<string name="referer">Referer</string>
|
<string name="referer">Referer (optional)</string>
|
||||||
<string name="next">Next</string>
|
<string name="next">Next</string>
|
||||||
<string name="provider_languages_tip">Watch videos in these languages</string>
|
<string name="provider_languages_tip">Watch videos in these languages</string>
|
||||||
<string name="previous">Previous</string>
|
<string name="previous">Previous</string>
|
||||||
|
@ -594,8 +600,6 @@
|
||||||
<string name="plugins_updated" formatted="true">Updated %d plugins</string>
|
<string name="plugins_updated" formatted="true">Updated %d plugins</string>
|
||||||
<string name="blank_repo_message">CloudStream has no sites installed by default. You need to install the sites from repositories.
|
<string name="blank_repo_message">CloudStream has no sites installed by default. You need to install the sites from repositories.
|
||||||
\n
|
\n
|
||||||
\nBecause of a brainless DMCA takedown by Sky UK Limited 🤮 we cannot link the repository site in app.
|
|
||||||
\n
|
|
||||||
\nJoin our Discord or search online.</string>
|
\nJoin our Discord or search online.</string>
|
||||||
<string name="view_public_repositories_button">View community repositories</string>
|
<string name="view_public_repositories_button">View community repositories</string>
|
||||||
<string name="view_public_repositories_button_short">Public list</string>
|
<string name="view_public_repositories_button_short">Public list</string>
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
app:key="@string/player_resize_enabled_key" />
|
app:key="@string/player_resize_enabled_key" />
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:icon="@drawable/ic_baseline_speed_24"
|
android:icon="@drawable/ic_baseline_speed_24"
|
||||||
android:summary="@string/eigengraumode_settings_des"
|
android:summary="@string/speed_setting_summary"
|
||||||
android:title="@string/eigengraumode_settings"
|
android:title="@string/eigengraumode_settings"
|
||||||
app:defaultValue="false"
|
app:defaultValue="false"
|
||||||
app:key="@string/playback_speed_enabled_key" />
|
app:key="@string/playback_speed_enabled_key" />
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/baseline_network_ping_24"
|
android:icon="@drawable/baseline_network_ping_24"
|
||||||
android:key="@string/test_providers_key"
|
android:key="@string/test_providers_key"
|
||||||
android:title="Test all providers" />
|
android:title="@string/test_extensions"
|
||||||
|
android:summary="@string/test_extensions_summary"/>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
Add table
Add a link
Reference in a new issue