Merge remote-tracking branch 'origin/master' into bread_toast

This commit is contained in:
IndusAryan 2024-03-01 19:34:13 +05:30
commit 7413f39c87
28 changed files with 519 additions and 149 deletions

View file

@ -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) {

View file

@ -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
} }

View file

@ -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
@ -290,6 +291,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
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)
} }

View file

@ -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,
)
} }

View file

@ -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,15 +82,12 @@ 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
)
)
} }
} }

View file

@ -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(

View file

@ -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"
}

View file

@ -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? {

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -23,28 +23,8 @@ 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"
}
private fun fixUrl(url: String): String { fun getOrdinal(num: Int?): String? {
if (url.startsWith("http")) {
return url
}
if (url.isEmpty()) {
return ""
}
val startsWithNoHttp = url.startsWith("//")
if (startsWithNoHttp) {
return "https:$url"
} else {
if (url.startsWith('/')) {
return host + url
}
return "$host/$url"
}
}
private fun getOrdinal(num: Int?): String? {
return when (num) { return when (num) {
1 -> "First" 1 -> "First"
2 -> "Second" 2 -> "Second"
@ -84,6 +64,26 @@ class IndexSubtitleApi : AbstractSubApi {
else -> null else -> null
} }
} }
}
private fun fixUrl(url: String): String {
if (url.startsWith("http")) {
return url
}
if (url.isEmpty()) {
return ""
}
val startsWithNoHttp = url.startsWith("//")
if (startsWithNoHttp) {
return "https:$url"
} else {
if (url.startsWith('/')) {
return host + url
}
return "$host/$url"
}
}
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 =

View file

@ -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
}
}
}

View file

@ -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
) )

View file

@ -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
) )

View file

@ -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()
} }
@ -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,
mimeType = resource.url.toSubtitleMimeType(),
headers = currentSubtitle.headers, headers = currentSubtitle.headers,
currentSubtitle.lang currentSubtitle.lang
) )
}
if (subtitles.isNotEmpty()) {
runOnMainThread { runOnMainThread {
addAndSelectSubtitles(subtitle) 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
) )
} }

View file

@ -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)

View file

@ -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
} }

View file

@ -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(

View file

@ -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(),

View file

@ -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")

View file

@ -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)
} }
} }

View file

@ -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" />

View file

@ -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"

View file

@ -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

View file

@ -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>

View file

@ -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" />

View file

@ -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>