mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
move to async get requests + remade search
This commit is contained in:
parent
06801ba6ab
commit
302185093c
53 changed files with 497 additions and 360 deletions
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.google.auto.service.AutoService
|
import com.google.auto.service.AutoService
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
|
@ -12,6 +13,7 @@ import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
|
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.acra.ReportField
|
import org.acra.ReportField
|
||||||
import org.acra.config.CoreConfiguration
|
import org.acra.config.CoreConfiguration
|
||||||
import org.acra.data.CrashReportData
|
import org.acra.data.CrashReportData
|
||||||
|
@ -33,11 +35,13 @@ class CustomReportSender : ReportSender {
|
||||||
)
|
)
|
||||||
|
|
||||||
thread { // to not run it on main thread
|
thread { // to not run it on main thread
|
||||||
normalSafeApiCall {
|
runBlocking {
|
||||||
|
suspendSafeApiCall {
|
||||||
val post = app.post(url, data = data)
|
val post = app.post(url, data = data)
|
||||||
println("Report response: $post")
|
println("Report response: $post")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runOnMainThread { // to run it on main looper
|
runOnMainThread { // to run it on main looper
|
||||||
normalSafeApiCall {
|
normalSafeApiCall {
|
||||||
|
|
|
@ -2,13 +2,9 @@ package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
|
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
|
||||||
|
/*
|
||||||
fun <T, R> Iterable<T>.pmap(
|
fun <T, R> Iterable<T>.pmap(
|
||||||
numThreads: Int = maxOf(Runtime.getRuntime().availableProcessors() - 2, 1),
|
numThreads: Int = maxOf(Runtime.getRuntime().availableProcessors() - 2, 1),
|
||||||
exec: ExecutorService = Executors.newFixedThreadPool(numThreads),
|
exec: ExecutorService = Executors.newFixedThreadPool(numThreads),
|
||||||
|
@ -27,14 +23,14 @@ fun <T, R> Iterable<T>.pmap(
|
||||||
exec.awaitTermination(1, TimeUnit.DAYS)
|
exec.awaitTermination(1, TimeUnit.DAYS)
|
||||||
|
|
||||||
return ArrayList<R>(destination)
|
return ArrayList<R>(destination)
|
||||||
}
|
}*/
|
||||||
|
|
||||||
fun <A, B> List<A>.apmap(f: suspend (A) -> B): List<B> = runBlocking {
|
fun <A, B> List<A>.apmap(f: suspend (A) -> B): List<B> = runBlocking {
|
||||||
map { async { f(it) } }.map { it.await() }
|
map { async { f(it) } }.map { it.await() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// run code in parallel
|
// run code in parallel
|
||||||
fun <R> argpmap(
|
/*fun <R> argpmap(
|
||||||
vararg transforms: () -> R,
|
vararg transforms: () -> R,
|
||||||
numThreads: Int = maxOf(Runtime.getRuntime().availableProcessors() - 2, 1),
|
numThreads: Int = maxOf(Runtime.getRuntime().availableProcessors() - 2, 1),
|
||||||
exec: ExecutorService = Executors.newFixedThreadPool(numThreads)
|
exec: ExecutorService = Executors.newFixedThreadPool(numThreads)
|
||||||
|
@ -45,10 +41,10 @@ fun <R> argpmap(
|
||||||
|
|
||||||
exec.shutdown()
|
exec.shutdown()
|
||||||
exec.awaitTermination(1, TimeUnit.DAYS)
|
exec.awaitTermination(1, TimeUnit.DAYS)
|
||||||
}
|
}*/
|
||||||
|
|
||||||
//fun <R> argamap(
|
fun <R> argamap(
|
||||||
// vararg transforms: () -> R,
|
vararg transforms: suspend () -> R,
|
||||||
//) = runBlocking {
|
) = runBlocking {
|
||||||
// transforms.map { async { it.invoke() } }.map { it.await() }
|
transforms.map { async { it.invoke() } }.map { it.await() }
|
||||||
//}
|
}
|
|
@ -3,7 +3,7 @@ package com.lagradost.cloudstream3.animeproviders
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.network.AppResponse
|
import com.lagradost.cloudstream3.network.AppResponse
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
@ -23,7 +23,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
else TvType.Anime
|
else TvType.Anime
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateSession(): Boolean {
|
suspend fun generateSession(): Boolean {
|
||||||
if (cookies.isNotEmpty()) return true
|
if (cookies.isNotEmpty()) return true
|
||||||
return try {
|
return try {
|
||||||
val response = app.get("$MAIN_URL/")
|
val response = app.get("$MAIN_URL/")
|
||||||
|
@ -124,7 +124,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
@JsonProperty("data") val data: List<AnimePaheSearchData>
|
@JsonProperty("data") val data: List<AnimePaheSearchData>
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getAnimeByIdAndTitle(title: String, animeId: Int): String? {
|
private suspend fun getAnimeByIdAndTitle(title: String, animeId: Int): String? {
|
||||||
val url = "$mainUrl/api?m=search&l=8&q=$title"
|
val url = "$mainUrl/api?m=search&l=8&q=$title"
|
||||||
val headers = mapOf("referer" to "$mainUrl/")
|
val headers = mapOf("referer" to "$mainUrl/")
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
private fun generateListOfEpisodes(link: String): ArrayList<AnimeEpisode> {
|
private suspend fun generateListOfEpisodes(link: String): ArrayList<AnimeEpisode> {
|
||||||
try {
|
try {
|
||||||
val attrs = link.split('/')
|
val attrs = link.split('/')
|
||||||
val id = attrs[attrs.size - 1].split("?")[0]
|
val id = attrs[attrs.size - 1].split("?")[0]
|
||||||
|
@ -243,8 +243,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
return normalSafeApiCall {
|
return suspendSafeApiCall {
|
||||||
|
|
||||||
val regex = Regex("""a/(\d+)\?slug=(.+)""")
|
val regex = Regex("""a/(\d+)\?slug=(.+)""")
|
||||||
val (animeId, animeTitle) = regex.find(url)!!.destructured
|
val (animeId, animeTitle) = regex.find(url)!!.destructured
|
||||||
val link = getAnimeByIdAndTitle(animeTitle, animeId.toInt())!!
|
val link = getAnimeByIdAndTitle(animeTitle, animeId.toInt())!!
|
||||||
|
@ -436,7 +435,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
@JsonProperty("data") val data: List<Map<String, VideoQuality>>
|
@JsonProperty("data") val data: List<Map<String, VideoQuality>>
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun bypassAdfly(adflyUri: String): String {
|
private suspend fun bypassAdfly(adflyUri: String): String {
|
||||||
if (!generateSession()) {
|
if (!generateSession()) {
|
||||||
return bypassAdfly(adflyUri)
|
return bypassAdfly(adflyUri)
|
||||||
}
|
}
|
||||||
|
@ -461,7 +460,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
return decodeAdfly(YTSM.find(adflyContent?.text.toString())!!.destructured.component1())
|
return decodeAdfly(YTSM.find(adflyContent?.text.toString())!!.destructured.component1())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStreamUrlFromKwik(adflyUri: String): String {
|
private suspend fun getStreamUrlFromKwik(adflyUri: String): String {
|
||||||
val fContent =
|
val fContent =
|
||||||
app.get(
|
app.get(
|
||||||
bypassAdfly(adflyUri),
|
bypassAdfly(adflyUri),
|
||||||
|
@ -496,7 +495,7 @@ class AnimePaheProvider : MainAPI() {
|
||||||
return content?.headers?.values("location").toString()
|
return content?.headers?.values("location").toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractVideoLinks(episodeLink: String): List<ExtractorLink> {
|
private suspend fun extractVideoLinks(episodeLink: String): List<ExtractorLink> {
|
||||||
var link = episodeLink
|
var link = episodeLink
|
||||||
val headers = mapOf("referer" to "$mainUrl/")
|
val headers = mapOf("referer" to "$mainUrl/")
|
||||||
|
|
||||||
|
@ -507,7 +506,6 @@ class AnimePaheProvider : MainAPI() {
|
||||||
val episodeNum = regex.find(link)?.destructured?.component1()?.toIntOrNull()
|
val episodeNum = regex.find(link)?.destructured?.component1()?.toIntOrNull()
|
||||||
link = link.replace(regex, "")
|
link = link.replace(regex, "")
|
||||||
|
|
||||||
|
|
||||||
val req = app.get(link, headers = headers).text
|
val req = app.get(link, headers = headers).text
|
||||||
val jsonResponse = req.let { mapper.readValue<AnimePaheAnimeData>(it) }
|
val jsonResponse = req.let { mapper.readValue<AnimePaheAnimeData>(it) }
|
||||||
val ep = ((jsonResponse.data.map {
|
val ep = ((jsonResponse.data.map {
|
||||||
|
|
|
@ -54,7 +54,7 @@ class DubbedAnimeProvider : MainAPI() {
|
||||||
@JsonProperty("tags") val tags: String,*/
|
@JsonProperty("tags") val tags: String,*/
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun parseDocumentTrending(url: String): List<SearchResponse> {
|
private suspend fun parseDocumentTrending(url: String): List<SearchResponse> {
|
||||||
val response = app.get(url).text
|
val response = app.get(url).text
|
||||||
val document = Jsoup.parse(response)
|
val document = Jsoup.parse(response)
|
||||||
return document.select("li > a").map {
|
return document.select("li > a").map {
|
||||||
|
@ -73,7 +73,7 @@ class DubbedAnimeProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDocument(url: String, trimEpisode: Boolean = false): List<SearchResponse> {
|
private suspend fun parseDocument(url: String, trimEpisode: Boolean = false): List<SearchResponse> {
|
||||||
val response = app.get(url).text
|
val response = app.get(url).text
|
||||||
val document = Jsoup.parse(response)
|
val document = Jsoup.parse(response)
|
||||||
return document.select("a.grid__link").map {
|
return document.select("a.grid__link").map {
|
||||||
|
@ -109,7 +109,7 @@ class DubbedAnimeProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getAnimeEpisode(slug: String, isMovie: Boolean): EpisodeInfo {
|
private suspend fun getAnimeEpisode(slug: String, isMovie: Boolean): EpisodeInfo {
|
||||||
val url =
|
val url =
|
||||||
mainUrl + (if (isMovie) "/movies/jsonMovie" else "/xz/v3/jsonEpi") + ".php?slug=$slug&_=$unixTime"
|
mainUrl + (if (isMovie) "/movies/jsonMovie" else "/xz/v3/jsonEpi") + ".php?slug=$slug&_=$unixTime"
|
||||||
val response = app.get(url).text
|
val response = app.get(url).text
|
||||||
|
|
|
@ -232,17 +232,17 @@ class GogoanimeProvider : MainAPI() {
|
||||||
val default: String? = null
|
val default: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun extractVideos(uri: String, callback: (ExtractorLink) -> Unit) {
|
private suspend fun extractVideos(uri: String, callback: (ExtractorLink) -> Unit) {
|
||||||
val doc = app.get(uri).document
|
val doc = app.get(uri).document
|
||||||
|
|
||||||
val iframe = fixUrlNull(doc.selectFirst("div.play-video > iframe").attr("src")) ?: return
|
val iframe = fixUrlNull(doc.selectFirst("div.play-video > iframe").attr("src")) ?: return
|
||||||
|
|
||||||
argpmap(
|
argamap(
|
||||||
{
|
{
|
||||||
val link = iframe.replace("streaming.php", "download")
|
val link = iframe.replace("streaming.php", "download")
|
||||||
val page = app.get(link, headers = mapOf("Referer" to iframe))
|
val page = app.get(link, headers = mapOf("Referer" to iframe))
|
||||||
|
|
||||||
page.document.select(".dowload > a").pmap {
|
page.document.select(".dowload > a").apmap {
|
||||||
if (it.hasAttr("download")) {
|
if (it.hasAttr("download")) {
|
||||||
val qual = if (it.text()
|
val qual = if (it.text()
|
||||||
.contains("HDP")
|
.contains("HDP")
|
||||||
|
@ -266,7 +266,7 @@ class GogoanimeProvider : MainAPI() {
|
||||||
}, {
|
}, {
|
||||||
val streamingResponse = app.get(iframe, headers = mapOf("Referer" to iframe))
|
val streamingResponse = app.get(iframe, headers = mapOf("Referer" to iframe))
|
||||||
val streamingDocument = streamingResponse.document
|
val streamingDocument = streamingResponse.document
|
||||||
argpmap({
|
argamap({
|
||||||
streamingDocument.select(".list-server-items > .linkserver")
|
streamingDocument.select(".list-server-items > .linkserver")
|
||||||
?.forEach { element ->
|
?.forEach { element ->
|
||||||
val status = element.attr("data-status") ?: return@forEach
|
val status = element.attr("data-status") ?: return@forEach
|
||||||
|
@ -312,11 +312,9 @@ class GogoanimeProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sources.source?.forEach {
|
sources.source?.forEach {
|
||||||
println("${this.name} ${it.label ?: ""}")
|
|
||||||
invokeGogoSource(it, callback)
|
invokeGogoSource(it, callback)
|
||||||
}
|
}
|
||||||
sources.sourceBk?.forEach {
|
sources.sourceBk?.forEach {
|
||||||
println("${this.name} ${it.label ?: ""}")
|
|
||||||
invokeGogoSource(it, callback)
|
invokeGogoSource(it, callback)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -257,7 +257,7 @@ class ZoroProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getM3u8FromRapidCloud(url: String): String {
|
private suspend fun getM3u8FromRapidCloud(url: String): String {
|
||||||
return Regex("""/(embed-\d+)/(.*?)\?z=""").find(url)?.groupValues?.let {
|
return Regex("""/(embed-\d+)/(.*?)\?z=""").find(url)?.groupValues?.let {
|
||||||
val jsonLink = "https://rapid-cloud.ru/ajax/${it[1]}/getSources?id=${it[2]}"
|
val jsonLink = "https://rapid-cloud.ru/ajax/${it[1]}/getSources?id=${it[2]}"
|
||||||
app.get(jsonLink).text
|
app.get(jsonLink).text
|
||||||
|
@ -295,7 +295,7 @@ class ZoroProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent duplicates
|
// Prevent duplicates
|
||||||
servers.distinctBy { it.second }.pmap {
|
servers.distinctBy { it.second }.apmap {
|
||||||
val link =
|
val link =
|
||||||
"$mainUrl/ajax/v2/episode/sources?id=${it.second}"
|
"$mainUrl/ajax/v2/episode/sources?id=${it.second}"
|
||||||
val extractorLink = app.get(
|
val extractorLink = app.get(
|
||||||
|
@ -316,7 +316,7 @@ class ZoroProvider : MainAPI() {
|
||||||
extractorLink
|
extractorLink
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response.contains("<html")) return@pmap
|
if (response.contains("<html")) return@apmap
|
||||||
val mapped = mapper.readValue<SflixProvider.SourceObject>(response)
|
val mapped = mapper.readValue<SflixProvider.SourceObject>(response)
|
||||||
|
|
||||||
mapped.tracks?.forEach { track ->
|
mapped.tracks?.forEach { track ->
|
||||||
|
|
|
@ -10,7 +10,7 @@ class AsianLoad : ExtractorApi() {
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
with(app.get(url, referer = referer)) {
|
with(app.get(url, referer = referer)) {
|
||||||
sourceRegex.findAll(this.text).forEach { sourceMatch ->
|
sourceRegex.findAll(this.text).forEach { sourceMatch ->
|
||||||
|
|
|
@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import java.lang.Thread.sleep
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
class DoodToExtractor : DoodLaExtractor() {
|
class DoodToExtractor : DoodLaExtractor() {
|
||||||
override val mainUrl = "https://dood.to"
|
override val mainUrl = "https://dood.to"
|
||||||
|
@ -28,13 +28,13 @@ open class DoodLaExtractor : ExtractorApi() {
|
||||||
return "$mainUrl/d/$id"
|
return "$mainUrl/d/$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val id = url.removePrefix("$mainUrl/e/").removePrefix("$mainUrl/d/")
|
val id = url.removePrefix("$mainUrl/e/").removePrefix("$mainUrl/d/")
|
||||||
val trueUrl = getExtractorUrl(id)
|
val trueUrl = getExtractorUrl(id)
|
||||||
val response = app.get(trueUrl).text
|
val response = app.get(trueUrl).text
|
||||||
Regex("href=\".*/download/(.*?)\"").find(response)?.groupValues?.get(1)?.let { link ->
|
Regex("href=\".*/download/(.*?)\"").find(response)?.groupValues?.get(1)?.let { link ->
|
||||||
if (link.isEmpty()) return null
|
if (link.isEmpty()) return null
|
||||||
sleep(5000) // might need this to not trigger anti bot
|
delay(5000) // might need this to not trigger anti bot
|
||||||
val downloadLink = "$mainUrl/download/$link"
|
val downloadLink = "$mainUrl/download/$link"
|
||||||
val downloadResponse = app.get(downloadLink).text
|
val downloadResponse = app.get(downloadLink).text
|
||||||
Regex("onclick=\"window\\.open\\((['\"])(.*?)(['\"])").find(downloadResponse)?.groupValues?.get(2)
|
Regex("onclick=\"window\\.open\\((['\"])(.*?)(['\"])").find(downloadResponse)?.groupValues?.get(2)
|
||||||
|
|
|
@ -15,7 +15,7 @@ open class Evoload : ExtractorApi() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val id = url.replace("https://evoload.io/e/", "") // wanted media id
|
val id = url.replace("https://evoload.io/e/", "") // wanted media id
|
||||||
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
|
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
|
||||||
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
|
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
|
||||||
|
|
|
@ -13,7 +13,7 @@ class MixDrop : ExtractorApi() {
|
||||||
return "$mainUrl/e/$id"
|
return "$mainUrl/e/$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
with(app.get(url)) {
|
with(app.get(url)) {
|
||||||
getAndUnpack(this.text).let { unpackedText ->
|
getAndUnpack(this.text).let { unpackedText ->
|
||||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Mp4Upload : ExtractorApi() {
|
||||||
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
with(app.get(url)) {
|
with(app.get(url)) {
|
||||||
getAndUnpack(this.text).let { unpackedText ->
|
getAndUnpack(this.text).let { unpackedText ->
|
||||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||||
|
|
|
@ -19,7 +19,7 @@ class MultiQuality : ExtractorApi() {
|
||||||
return "$mainUrl/loadserver.php?id=$id"
|
return "$mainUrl/loadserver.php?id=$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
with(app.get(url)) {
|
with(app.get(url)) {
|
||||||
sourceRegex.findAll(this.text).forEach { sourceMatch ->
|
sourceRegex.findAll(this.text).forEach { sourceMatch ->
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.apmap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.pmap
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
@ -27,9 +27,9 @@ class Pelisplus(val mainUrl: String) {
|
||||||
private val normalApis = arrayListOf(MultiQuality())
|
private val normalApis = arrayListOf(MultiQuality())
|
||||||
|
|
||||||
// https://gogo-stream.com/streaming.php?id=MTE3NDg5
|
// https://gogo-stream.com/streaming.php?id=MTE3NDg5
|
||||||
fun getUrl(id: String, isCasting: Boolean = false, callback: (ExtractorLink) -> Unit): Boolean {
|
suspend fun getUrl(id: String, isCasting: Boolean = false, callback: (ExtractorLink) -> Unit): Boolean {
|
||||||
try {
|
try {
|
||||||
normalApis.pmap { api ->
|
normalApis.apmap { api ->
|
||||||
val url = api.getExtractorUrl(id)
|
val url = api.getExtractorUrl(id)
|
||||||
val source = api.getSafeUrl(url)
|
val source = api.getSafeUrl(url)
|
||||||
source?.forEach { callback.invoke(it) }
|
source?.forEach { callback.invoke(it) }
|
||||||
|
@ -37,7 +37,7 @@ class Pelisplus(val mainUrl: String) {
|
||||||
val extractorUrl = getExtractorUrl(id)
|
val extractorUrl = getExtractorUrl(id)
|
||||||
|
|
||||||
/** Stolen from GogoanimeProvider.kt extractor */
|
/** Stolen from GogoanimeProvider.kt extractor */
|
||||||
normalSafeApiCall {
|
suspendSafeApiCall {
|
||||||
val link = getDownloadUrl(id)
|
val link = getDownloadUrl(id)
|
||||||
println("Generated vidstream download link: $link")
|
println("Generated vidstream download link: $link")
|
||||||
val page = app.get(link, referer = extractorUrl)
|
val page = app.get(link, referer = extractorUrl)
|
||||||
|
@ -46,8 +46,8 @@ class Pelisplus(val mainUrl: String) {
|
||||||
val qualityRegex = Regex("(\\d+)P")
|
val qualityRegex = Regex("(\\d+)P")
|
||||||
|
|
||||||
//a[download]
|
//a[download]
|
||||||
pageDoc.select(".dowload > a")?.pmap { element ->
|
pageDoc.select(".dowload > a")?.apmap { element ->
|
||||||
val href = element.attr("href") ?: return@pmap
|
val href = element.attr("href") ?: return@apmap
|
||||||
val qual = if (element.text()
|
val qual = if (element.text()
|
||||||
.contains("HDP")
|
.contains("HDP")
|
||||||
) "1080" else qualityRegex.find(element.text())?.destructured?.component1().toString()
|
) "1080" else qualityRegex.find(element.text())?.destructured?.component1().toString()
|
||||||
|
@ -78,7 +78,7 @@ class Pelisplus(val mainUrl: String) {
|
||||||
//val name = element.text()
|
//val name = element.text()
|
||||||
|
|
||||||
// Matches vidstream links with extractors
|
// Matches vidstream links with extractors
|
||||||
extractorApis.filter { !it.requiresReferer || !isCasting }.pmap { api ->
|
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
|
||||||
if (link.startsWith(api.mainUrl)) {
|
if (link.startsWith(api.mainUrl)) {
|
||||||
val extractedLinks = api.getSafeUrl(link, extractorUrl)
|
val extractedLinks = api.getSafeUrl(link, extractorUrl)
|
||||||
if (extractedLinks?.isNotEmpty() == true) {
|
if (extractedLinks?.isNotEmpty() == true) {
|
||||||
|
|
|
@ -26,7 +26,7 @@ open class SBPlay : ExtractorApi() {
|
||||||
override val name = "SBPlay"
|
override val name = "SBPlay"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val response = app.get(url, referer = referer).text
|
val response = app.get(url, referer = referer).text
|
||||||
val document = Jsoup.parse(response)
|
val document = Jsoup.parse(response)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ class StreamSB : ExtractorApi() {
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
// https://sbembed.com/embed-ns50b0cukf9j.html -> https://sbvideo.net/play/ns50b0cukf9j
|
// https://sbembed.com/embed-ns50b0cukf9j.html -> https://sbvideo.net/play/ns50b0cukf9j
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
val newUrl = url.replace("sbplay.org/embed-", "sbplay.org/play/").removeSuffix(".html")
|
val newUrl = url.replace("sbplay.org/embed-", "sbplay.org/play/").removeSuffix(".html")
|
||||||
with(app.get(newUrl, timeout = 10)) {
|
with(app.get(newUrl, timeout = 10)) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ class StreamTape : ExtractorApi() {
|
||||||
private val linkRegex =
|
private val linkRegex =
|
||||||
Regex("""'robotlink'\)\.innerHTML = '(.+?)'\+ \('(.+?)'\)""")
|
Regex("""'robotlink'\)\.innerHTML = '(.+?)'\+ \('(.+?)'\)""")
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
with(app.get(url)) {
|
with(app.get(url)) {
|
||||||
linkRegex.find(this.text)?.let {
|
linkRegex.find(this.text)?.let {
|
||||||
val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
|
val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
|
||||||
|
|
|
@ -16,7 +16,7 @@ class Streamhub : ExtractorApi() {
|
||||||
return "$mainUrl/e/$id"
|
return "$mainUrl/e/$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val response = app.get(url).text
|
val response = app.get(url).text
|
||||||
Regex("eval((.|\\n)*?)</script>").find(response)?.groupValues?.get(1)?.let { jsEval ->
|
Regex("eval((.|\\n)*?)</script>").find(response)?.groupValues?.get(1)?.let { jsEval ->
|
||||||
JsUnpacker("eval$jsEval").unpack()?.let { unPacked ->
|
JsUnpacker("eval$jsEval").unpack()?.let { unPacked ->
|
||||||
|
|
|
@ -10,7 +10,7 @@ class UpstreamExtractor: ExtractorApi() {
|
||||||
override val mainUrl: String = "https://upstream.to"
|
override val mainUrl: String = "https://upstream.to"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
// WIP: m3u8 link fetched but sometimes not playing
|
// WIP: m3u8 link fetched but sometimes not playing
|
||||||
//Log.i(this.name, "Result => (no extractor) ${url}")
|
//Log.i(this.name, "Result => (no extractor) ${url}")
|
||||||
val sources: MutableList<ExtractorLink> = mutableListOf()
|
val sources: MutableList<ExtractorLink> = mutableListOf()
|
||||||
|
|
|
@ -13,7 +13,7 @@ open class Uqload : ExtractorApi() {
|
||||||
private val srcRegex = Regex("""sources:.\[(.*?)\]""") // would be possible to use the parse and find src attribute
|
private val srcRegex = Regex("""sources:.\[(.*?)\]""") // would be possible to use the parse and find src attribute
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
|
with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
|
||||||
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
|
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
|
||||||
return listOf(
|
return listOf(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.apmap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.pmap
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
@ -27,9 +27,9 @@ class Vidstream(val mainUrl: String) {
|
||||||
private val normalApis = arrayListOf(MultiQuality())
|
private val normalApis = arrayListOf(MultiQuality())
|
||||||
|
|
||||||
// https://gogo-stream.com/streaming.php?id=MTE3NDg5
|
// https://gogo-stream.com/streaming.php?id=MTE3NDg5
|
||||||
fun getUrl(id: String, isCasting: Boolean = false, callback: (ExtractorLink) -> Unit): Boolean {
|
suspend fun getUrl(id: String, isCasting: Boolean = false, callback: (ExtractorLink) -> Unit): Boolean {
|
||||||
try {
|
try {
|
||||||
normalApis.pmap { api ->
|
normalApis.apmap { api ->
|
||||||
val url = api.getExtractorUrl(id)
|
val url = api.getExtractorUrl(id)
|
||||||
val source = api.getSafeUrl(url)
|
val source = api.getSafeUrl(url)
|
||||||
source?.forEach { callback.invoke(it) }
|
source?.forEach { callback.invoke(it) }
|
||||||
|
@ -37,7 +37,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
val extractorUrl = getExtractorUrl(id)
|
val extractorUrl = getExtractorUrl(id)
|
||||||
|
|
||||||
/** Stolen from GogoanimeProvider.kt extractor */
|
/** Stolen from GogoanimeProvider.kt extractor */
|
||||||
normalSafeApiCall {
|
suspendSafeApiCall {
|
||||||
val link = getDownloadUrl(id)
|
val link = getDownloadUrl(id)
|
||||||
println("Generated vidstream download link: $link")
|
println("Generated vidstream download link: $link")
|
||||||
val page = app.get(link, referer = extractorUrl)
|
val page = app.get(link, referer = extractorUrl)
|
||||||
|
@ -46,8 +46,8 @@ class Vidstream(val mainUrl: String) {
|
||||||
val qualityRegex = Regex("(\\d+)P")
|
val qualityRegex = Regex("(\\d+)P")
|
||||||
|
|
||||||
//a[download]
|
//a[download]
|
||||||
pageDoc.select(".dowload > a")?.pmap { element ->
|
pageDoc.select(".dowload > a")?.apmap { element ->
|
||||||
val href = element.attr("href") ?: return@pmap
|
val href = element.attr("href") ?: return@apmap
|
||||||
val qual = if (element.text()
|
val qual = if (element.text()
|
||||||
.contains("HDP")
|
.contains("HDP")
|
||||||
) "1080" else qualityRegex.find(element.text())?.destructured?.component1().toString()
|
) "1080" else qualityRegex.find(element.text())?.destructured?.component1().toString()
|
||||||
|
@ -78,7 +78,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
//val name = element.text()
|
//val name = element.text()
|
||||||
|
|
||||||
// Matches vidstream links with extractors
|
// Matches vidstream links with extractors
|
||||||
extractorApis.filter { !it.requiresReferer || !isCasting }.pmap { api ->
|
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
|
||||||
if (link.startsWith(api.mainUrl)) {
|
if (link.startsWith(api.mainUrl)) {
|
||||||
val extractedLinks = api.getSafeUrl(link, extractorUrl)
|
val extractedLinks = api.getSafeUrl(link, extractorUrl)
|
||||||
if (extractedLinks?.isNotEmpty() == true) {
|
if (extractedLinks?.isNotEmpty() == true) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ open class VoeExtractor : ExtractorApi() {
|
||||||
//val type: String // Mp4
|
//val type: String // Mp4
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
val doc = app.get(url).text
|
val doc = app.get(url).text
|
||||||
if (doc.isNotEmpty()) {
|
if (doc.isNotEmpty()) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ open class WatchSB : ExtractorApi() {
|
||||||
override val mainUrl = "https://watchsb.com"
|
override val mainUrl = "https://watchsb.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val response = app.get(
|
val response = app.get(
|
||||||
url, interceptor = WebViewResolver(
|
url, interceptor = WebViewResolver(
|
||||||
Regex("""master\.m3u8""")
|
Regex("""master\.m3u8""")
|
||||||
|
|
|
@ -12,7 +12,7 @@ class WcoStream : ExtractorApi() {
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
private val hlsHelper = M3u8Helper()
|
private val hlsHelper = M3u8Helper()
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val baseUrl = url.split("/e/")[0]
|
val baseUrl = url.split("/e/")[0]
|
||||||
|
|
||||||
val html = app.get(url, headers = mapOf("Referer" to "https://wcostream.cc/")).text
|
val html = app.get(url, headers = mapOf("Referer" to "https://wcostream.cc/")).text
|
||||||
|
|
|
@ -29,7 +29,7 @@ open class XStreamCdn : ExtractorApi() {
|
||||||
return "$domainUrl/api/source/$id"
|
return "$domainUrl/api/source/$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val headers = mapOf(
|
val headers = mapOf(
|
||||||
"Referer" to url,
|
"Referer" to url,
|
||||||
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
|
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
|
||||||
class AsianEmbedHelper {
|
class AsianEmbedHelper {
|
||||||
companion object {
|
companion object {
|
||||||
fun getUrls(url: String, callback: (ExtractorLink) -> Unit) {
|
suspend fun getUrls(url: String, callback: (ExtractorLink) -> Unit) {
|
||||||
if (url.startsWith("https://asianembed.io")) {
|
if (url.startsWith("https://asianembed.io")) {
|
||||||
// Fetch links
|
// Fetch links
|
||||||
val doc = app.get(url).document
|
val doc = app.get(url).document
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.extractors.helper
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
@ -11,7 +10,7 @@ class VstreamhubHelper {
|
||||||
private val baseUrl: String = "https://vstreamhub.com"
|
private val baseUrl: String = "https://vstreamhub.com"
|
||||||
private val baseName: String = "Vstreamhub"
|
private val baseName: String = "Vstreamhub"
|
||||||
|
|
||||||
fun getUrls(url: String, callback: (ExtractorLink) -> Unit) {
|
suspend fun getUrls(url: String, callback: (ExtractorLink) -> Unit) {
|
||||||
if (url.startsWith(baseUrl)) {
|
if (url.startsWith(baseUrl)) {
|
||||||
// Fetch links
|
// Fetch links
|
||||||
val doc = app.get(url).document.select("script")
|
val doc = app.get(url).document.select("script")
|
||||||
|
|
|
@ -41,7 +41,7 @@ class AkwamProvider : MainAPI() {
|
||||||
"Series" to "$mainUrl/series",
|
"Series" to "$mainUrl/series",
|
||||||
"Shows" to "$mainUrl/shows"
|
"Shows" to "$mainUrl/shows"
|
||||||
)
|
)
|
||||||
val pages = moviesUrl.pmap {
|
val pages = moviesUrl.apmap {
|
||||||
val doc = app.get(it.second).document
|
val doc = app.get(it.second).document
|
||||||
val list = doc.select("div.col-lg-auto.col-md-4.col-6.mb-12").mapNotNull { element ->
|
val list = doc.select("div.col-lg-auto.col-md-4.col-6.mb-12").mapNotNull { element ->
|
||||||
element.toSearchResponse()
|
element.toSearchResponse()
|
||||||
|
@ -150,7 +150,7 @@ class AkwamProvider : MainAPI() {
|
||||||
|
|
||||||
|
|
||||||
// Maybe possible to not use the url shortener but cba investigating that.
|
// Maybe possible to not use the url shortener but cba investigating that.
|
||||||
private fun skipUrlShortener(url: String): AppResponse {
|
private suspend fun skipUrlShortener(url: String): AppResponse {
|
||||||
return app.get(app.get(url).document.select("a.download-link").attr("href"))
|
return app.get(app.get(url).document.select("a.download-link").attr("href"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ class AkwamProvider : MainAPI() {
|
||||||
}.filter { link -> link.first.contains("/link/") }
|
}.filter { link -> link.first.contains("/link/") }
|
||||||
}.flatten()
|
}.flatten()
|
||||||
|
|
||||||
links.pmap {
|
links.map {
|
||||||
val linkDoc = skipUrlShortener(it.first).document
|
val linkDoc = skipUrlShortener(it.first).document
|
||||||
val button = linkDoc.select("div.btn-loader > a")
|
val button = linkDoc.select("div.btn-loader > a")
|
||||||
val url = button.attr("href")
|
val url = button.attr("href")
|
||||||
|
|
|
@ -110,7 +110,7 @@ class LookMovieProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<SearchResponse> {
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
fun search(query: String, isMovie: Boolean): ArrayList<SearchResponse> {
|
suspend fun search(query: String, isMovie: Boolean): ArrayList<SearchResponse> {
|
||||||
val url = "$mainUrl/${if (isMovie) "movies" else "shows"}/search/?q=$query"
|
val url = "$mainUrl/${if (isMovie) "movies" else "shows"}/search/?q=$query"
|
||||||
val response = app.get(url).text
|
val response = app.get(url).text
|
||||||
val document = Jsoup.parse(response)
|
val document = Jsoup.parse(response)
|
||||||
|
@ -158,7 +158,7 @@ class LookMovieProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCurrentLinks(url: String, callback: (ExtractorLink) -> Unit) {
|
private suspend fun loadCurrentLinks(url: String, callback: (ExtractorLink) -> Unit) {
|
||||||
val response = app.get(url.replace("\$unixtime", unixTime.toString())).text
|
val response = app.get(url.replace("\$unixtime", unixTime.toString())).text
|
||||||
M3u8Manifest.extractLinks(response).forEach {
|
M3u8Manifest.extractLinks(response).forEach {
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
|
|
|
@ -151,7 +151,7 @@ open class PelisplusProviderTemplate : MainAPI() {
|
||||||
val urls = homePageUrlList
|
val urls = homePageUrlList
|
||||||
val homePageList = ArrayList<HomePageList>()
|
val homePageList = ArrayList<HomePageList>()
|
||||||
// .pmap {} is used to fetch the different pages in parallel
|
// .pmap {} is used to fetch the different pages in parallel
|
||||||
urls.pmap { url ->
|
urls.apmap { url ->
|
||||||
val response = app.get(url, timeout = 20).text
|
val response = app.get(url, timeout = 20).text
|
||||||
val document = Jsoup.parse(response)
|
val document = Jsoup.parse(response)
|
||||||
document.select("div.main-inner")?.forEach { inner ->
|
document.select("div.main-inner")?.forEach { inner ->
|
||||||
|
|
|
@ -228,7 +228,7 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() {
|
||||||
}
|
}
|
||||||
} ?: tryParseJson<List<String>>(data))?.distinct()
|
} ?: tryParseJson<List<String>>(data))?.distinct()
|
||||||
|
|
||||||
urls?.pmap { url ->
|
urls?.apmap { url ->
|
||||||
val sources = app.get(
|
val sources = app.get(
|
||||||
url,
|
url,
|
||||||
interceptor = WebViewResolver(
|
interceptor = WebViewResolver(
|
||||||
|
|
|
@ -59,7 +59,7 @@ class VfFilmProvider : MainAPI() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDirect(original: String): String { // original data, https://vf-film.org/?trembed=1&trid=55313&trtype=1 for example
|
private suspend fun getDirect(original: String): String { // original data, https://vf-film.org/?trembed=1&trid=55313&trtype=1 for example
|
||||||
val response = app.get(original).text
|
val response = app.get(original).text
|
||||||
val url = "iframe .*src=\"(.*?)\"".toRegex().find(response)?.groupValues?.get(1)
|
val url = "iframe .*src=\"(.*?)\"".toRegex().find(response)?.groupValues?.get(1)
|
||||||
.toString() // https://vudeo.net/embed-uweno86lzx8f.html for example
|
.toString() // https://vudeo.net/embed-uweno86lzx8f.html for example
|
||||||
|
|
|
@ -42,7 +42,7 @@ class VfSerieProvider : MainAPI() {
|
||||||
return returnValue
|
return returnValue
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDirect(original: String): String { // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example
|
private suspend fun getDirect(original: String): String { // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example
|
||||||
val response = app.get(original).text
|
val response = app.get(original).text
|
||||||
val url = "iframe .*src=\"(.*?)\"".toRegex().find(response)?.groupValues?.get(1)
|
val url = "iframe .*src=\"(.*?)\"".toRegex().find(response)?.groupValues?.get(1)
|
||||||
.toString() // https://vudeo.net/embed-7jdb1t5b2mvo.html for example
|
.toString() // https://vudeo.net/embed-7jdb1t5b2mvo.html for example
|
||||||
|
|
|
@ -148,7 +148,7 @@ open class VidstreamProviderTemplate : MainAPI() {
|
||||||
val urls = homePageUrlList
|
val urls = homePageUrlList
|
||||||
val homePageList = ArrayList<HomePageList>()
|
val homePageList = ArrayList<HomePageList>()
|
||||||
// .pmap {} is used to fetch the different pages in parallel
|
// .pmap {} is used to fetch the different pages in parallel
|
||||||
urls.pmap { url ->
|
urls.apmap { url ->
|
||||||
val response = app.get(url, timeout = 20).text
|
val response = app.get(url, timeout = 20).text
|
||||||
val document = Jsoup.parse(response)
|
val document = Jsoup.parse(response)
|
||||||
document.select("div.main-inner")?.forEach { inner ->
|
document.select("div.main-inner")?.forEach { inner ->
|
||||||
|
|
|
@ -191,7 +191,7 @@ class WatchAsianProvider : MainAPI() {
|
||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getServerLinks(url: String) : String {
|
private suspend fun getServerLinks(url: String) : String {
|
||||||
val moviedoc = app.get(url, referer = mainUrl).document
|
val moviedoc = app.get(url, referer = mainUrl).document
|
||||||
return moviedoc.select("div.anime_muti_link > ul > li")
|
return moviedoc.select("div.anime_muti_link > ul > li")
|
||||||
?.mapNotNull {
|
?.mapNotNull {
|
||||||
|
|
|
@ -51,6 +51,15 @@ fun <T> normalSafeApiCall(apiCall: () -> T): T? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> suspendSafeApiCall(apiCall: suspend () -> T): T? {
|
||||||
|
return try {
|
||||||
|
apiCall.invoke()
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
logError(throwable)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun <T> safeFail(throwable: Throwable): Resource<T> {
|
fun <T> safeFail(throwable: Throwable): Resource<T> {
|
||||||
val stackTraceMsg = (throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
|
val stackTraceMsg = (throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
|
||||||
separator = "\n"
|
separator = "\n"
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.lagradost.cloudstream3.network
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.network.Requests.Companion.await
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
@ -18,17 +20,17 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
|
||||||
|
|
||||||
private var ddosBypassPath: String? = null
|
private var ddosBypassPath: String? = null
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
if (alwaysBypass) return bypassDdosGuard(request)
|
if (alwaysBypass) return@runBlocking bypassDdosGuard(request)
|
||||||
|
|
||||||
val response = chain.proceed(request)
|
val response = chain.proceed(request)
|
||||||
return if (response.code == 403) {
|
return@runBlocking if (response.code == 403) {
|
||||||
bypassDdosGuard(request)
|
bypassDdosGuard(request)
|
||||||
} else response
|
} else response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bypassDdosGuard(request: Request): Response {
|
private suspend fun bypassDdosGuard(request: Request): Response {
|
||||||
ddosBypassPath = ddosBypassPath ?: Regex("'(.*?)'").find(
|
ddosBypassPath = ddosBypassPath ?: Regex("'(.*?)'").find(
|
||||||
app.get(
|
app.get(
|
||||||
"https://check.ddos-guard.net/check.js"
|
"https://check.ddos-guard.net/check.js"
|
||||||
|
@ -49,6 +51,6 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
|
||||||
request.newBuilder()
|
request.newBuilder()
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
.build()
|
.build()
|
||||||
).execute()
|
).await()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,13 +7,19 @@ import com.lagradost.cloudstream3.R
|
||||||
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.mapper
|
import com.lagradost.cloudstream3.mapper
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.CompletionHandler
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.Headers.Companion.toHeaders
|
import okhttp3.Headers.Companion.toHeaders
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
|
||||||
class Session(
|
class Session(
|
||||||
|
@ -234,7 +240,41 @@ open class Requests {
|
||||||
return baseClient
|
return baseClient
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(
|
class ContinuationCallback(
|
||||||
|
private val call: Call,
|
||||||
|
private val continuation: CancellableContinuation<Response>
|
||||||
|
) : Callback, CompletionHandler {
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
continuation.resume(response, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
if (!call.isCanceled()) {
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invoke(cause: Throwable?) {
|
||||||
|
try {
|
||||||
|
call.cancel()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
suspend inline fun Call.await(): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
val callback = ContinuationCallback(this, continuation)
|
||||||
|
enqueue(callback)
|
||||||
|
continuation.invokeOnCancellation(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(
|
||||||
url: String,
|
url: String,
|
||||||
headers: Map<String, String> = emptyMap(),
|
headers: Map<String, String> = emptyMap(),
|
||||||
referer: String? = null,
|
referer: String? = null,
|
||||||
|
@ -255,7 +295,7 @@ open class Requests {
|
||||||
if (interceptor != null) client.addInterceptor(interceptor)
|
if (interceptor != null) client.addInterceptor(interceptor)
|
||||||
val request =
|
val request =
|
||||||
getRequestCreator(url, headers, referer, params, cookies, cacheTime, cacheUnit)
|
getRequestCreator(url, headers, referer, params, cookies, cacheTime, cacheUnit)
|
||||||
val response = client.build().newCall(request).execute()
|
val response = client.build().newCall(request).await()
|
||||||
return AppResponse(response)
|
return AppResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +303,7 @@ open class Requests {
|
||||||
return AppResponse(baseClient.newCall(request).execute())
|
return AppResponse(baseClient.newCall(request).execute())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun post(
|
suspend fun post(
|
||||||
url: String,
|
url: String,
|
||||||
headers: Map<String, String> = mapOf(),
|
headers: Map<String, String> = mapOf(),
|
||||||
referer: String? = null,
|
referer: String? = null,
|
||||||
|
@ -283,11 +323,11 @@ open class Requests {
|
||||||
.build()
|
.build()
|
||||||
val request =
|
val request =
|
||||||
postRequestCreator(url, headers, referer, params, cookies, data, cacheTime, cacheUnit)
|
postRequestCreator(url, headers, referer, params, cookies, data, cacheTime, cacheUnit)
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).await()
|
||||||
return AppResponse(response)
|
return AppResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(
|
suspend fun put(
|
||||||
url: String,
|
url: String,
|
||||||
headers: Map<String, String> = mapOf(),
|
headers: Map<String, String> = mapOf(),
|
||||||
referer: String? = null,
|
referer: String? = null,
|
||||||
|
@ -307,7 +347,7 @@ open class Requests {
|
||||||
.build()
|
.build()
|
||||||
val request =
|
val request =
|
||||||
putRequestCreator(url, headers, referer, params, cookies, data, cacheTime, cacheUnit)
|
putRequestCreator(url, headers, referer, params, cookies, data, cacheTime, cacheUnit)
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).await()
|
||||||
return AppResponse(response)
|
return AppResponse(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ class WebViewResolver(val interceptUrl: Regex, val additionalUrls: List<Regex> =
|
||||||
override fun shouldInterceptRequest(
|
override fun shouldInterceptRequest(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
request: WebResourceRequest
|
request: WebResourceRequest
|
||||||
): WebResourceResponse? {
|
): WebResourceResponse? = runBlocking {
|
||||||
val webViewUrl = request.url.toString()
|
val webViewUrl = request.url.toString()
|
||||||
// println("Loading WebView URL: $webViewUrl")
|
// println("Loading WebView URL: $webViewUrl")
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class WebViewResolver(val interceptUrl: Regex, val additionalUrls: List<Regex> =
|
||||||
fixedRequest = request.toRequest().also(requestCallBack)
|
fixedRequest = request.toRequest().also(requestCallBack)
|
||||||
println("Web-view request finished: $webViewUrl")
|
println("Web-view request finished: $webViewUrl")
|
||||||
destroyWebView()
|
destroyWebView()
|
||||||
return null
|
return@runBlocking null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) {
|
if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) {
|
||||||
|
@ -128,7 +128,13 @@ class WebViewResolver(val interceptUrl: Regex, val additionalUrls: List<Regex> =
|
||||||
* Overriding with okhttp might fuck up otherwise working requests,
|
* Overriding with okhttp might fuck up otherwise working requests,
|
||||||
* e.g the recaptcha request.
|
* e.g the recaptcha request.
|
||||||
* **/
|
* **/
|
||||||
return try {
|
|
||||||
|
/** NOTE! request.requestHeaders is not perfect!
|
||||||
|
* They don't contain all the headers the browser actually gives.
|
||||||
|
* Overriding with okhttp might fuck up otherwise working requests,
|
||||||
|
* e.g the recaptcha request.
|
||||||
|
* **/
|
||||||
|
return@runBlocking try {
|
||||||
when {
|
when {
|
||||||
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
|
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
|
||||||
"/favicon.ico"
|
"/favicon.ico"
|
||||||
|
@ -152,7 +158,7 @@ class WebViewResolver(val interceptUrl: Regex, val additionalUrls: List<Regex> =
|
||||||
webViewUrl,
|
webViewUrl,
|
||||||
headers = request.requestHeaders
|
headers = request.requestHeaders
|
||||||
).response.toWebResourceResponse()
|
).response.toWebResourceResponse()
|
||||||
else -> return super.shouldInterceptRequest(view, request)
|
else -> return@runBlocking super.shouldInterceptRequest(view, request)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
|
|
|
@ -66,7 +66,7 @@ interface SyncAPI : OAuth2API {
|
||||||
val icon: Int
|
val icon: Int
|
||||||
|
|
||||||
val mainUrl: String
|
val mainUrl: String
|
||||||
fun search(name: String): List<SyncSearchResult>?
|
suspend fun search(name: String): List<SyncSearchResult>?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
-1 -> None
|
-1 -> None
|
||||||
|
@ -77,9 +77,9 @@ interface SyncAPI : OAuth2API {
|
||||||
4 -> PlanToWatch
|
4 -> PlanToWatch
|
||||||
5 -> ReWatching
|
5 -> ReWatching
|
||||||
*/
|
*/
|
||||||
fun score(id: String, status: SyncStatus): Boolean
|
suspend fun score(id: String, status: SyncStatus): Boolean
|
||||||
|
|
||||||
fun getStatus(id: String): SyncStatus?
|
suspend fun getStatus(id: String): SyncStatus?
|
||||||
|
|
||||||
fun getResult(id: String): SyncResult?
|
suspend fun getResult(id: String): SyncResult?
|
||||||
}
|
}
|
|
@ -75,7 +75,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
||||||
val data = searchShows(name) ?: return null
|
val data = searchShows(name) ?: return null
|
||||||
return data.data.Page.media.map {
|
return data.data.Page.media.map {
|
||||||
SyncAPI.SyncSearchResult(
|
SyncAPI.SyncSearchResult(
|
||||||
|
@ -88,7 +88,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResult(id: String): SyncAPI.SyncResult? {
|
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
val season = getSeason(internalId)?.data?.Media ?: return null
|
val season = getSeason(internalId)?.data?.Media ?: return null
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(id: String): SyncAPI.SyncStatus? {
|
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
val data = getDataAboutId(internalId) ?: return null
|
val data = getDataAboutId(internalId) ?: return null
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
|
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
|
||||||
return postDataAboutId(
|
return postDataAboutId(
|
||||||
id.toIntOrNull() ?: return false,
|
id.toIntOrNull() ?: return false,
|
||||||
fromIntToAnimeStatus(status.status),
|
fromIntToAnimeStatus(status.status),
|
||||||
|
@ -143,7 +143,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
.replace("[^a-zA-Z0-9]".toRegex(), "")
|
.replace("[^a-zA-Z0-9]".toRegex(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchShows(name: String): GetSearchRoot? {
|
private suspend fun searchShows(name: String): GetSearchRoot? {
|
||||||
try {
|
try {
|
||||||
val query = """
|
val query = """
|
||||||
query (${"$"}id: Int, ${"$"}page: Int, ${"$"}search: String, ${"$"}type: MediaType) {
|
query (${"$"}id: Int, ${"$"}page: Int, ${"$"}search: String, ${"$"}type: MediaType) {
|
||||||
|
@ -225,7 +225,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should use https://gist.github.com/purplepinapples/5dc60f15f2837bf1cea71b089cfeaa0a
|
// Should use https://gist.github.com/purplepinapples/5dc60f15f2837bf1cea71b089cfeaa0a
|
||||||
fun getShowId(malId: String?, name: String, year: Int?): GetSearchMedia? {
|
suspend fun getShowId(malId: String?, name: String, year: Int?): GetSearchMedia? {
|
||||||
// Strips these from the name
|
// Strips these from the name
|
||||||
val blackList = listOf(
|
val blackList = listOf(
|
||||||
"TV Dubbed",
|
"TV Dubbed",
|
||||||
|
@ -293,7 +293,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getSeason(id: Int): SeasonResponse? {
|
private suspend fun getSeason(id: Int): SeasonResponse? {
|
||||||
val q: String = """
|
val q: String = """
|
||||||
query (${'$'}id: Int = $id) {
|
query (${'$'}id: Int = $id) {
|
||||||
Media (id: ${'$'}id, type: ANIME) {
|
Media (id: ${'$'}id, type: ANIME) {
|
||||||
|
@ -351,7 +351,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)!!
|
)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDataAboutId(id: Int): AniListTitleHolder? {
|
suspend fun getDataAboutId(id: Int): AniListTitleHolder? {
|
||||||
val q =
|
val q =
|
||||||
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
|
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
|
||||||
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
||||||
|
@ -410,7 +410,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postApi(url: String, q: String, cache: Boolean = false): String {
|
private suspend fun postApi(url: String, q: String, cache: Boolean = false): String {
|
||||||
return try {
|
return try {
|
||||||
if (!checkToken()) {
|
if (!checkToken()) {
|
||||||
// println("VARS_ " + vars)
|
// println("VARS_ " + vars)
|
||||||
|
@ -514,7 +514,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
|
return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAnilistAnimeListSmart(): Array<Lists>? {
|
suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
|
||||||
if (getKey<String>(
|
if (getKey<String>(
|
||||||
accountId,
|
accountId,
|
||||||
ANILIST_TOKEN_KEY,
|
ANILIST_TOKEN_KEY,
|
||||||
|
@ -535,7 +535,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFullAnilistList(): FullAnilistList? {
|
private suspend fun getFullAnilistList(): FullAnilistList? {
|
||||||
try {
|
try {
|
||||||
var userID: Int? = null
|
var userID: Int? = null
|
||||||
/** WARNING ASSUMES ONE USER! **/
|
/** WARNING ASSUMES ONE USER! **/
|
||||||
|
@ -597,7 +597,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleLike(id: Int): Boolean {
|
suspend fun toggleLike(id: Int): Boolean {
|
||||||
val q = """mutation (${'$'}animeId: Int = $id) {
|
val q = """mutation (${'$'}animeId: Int = $id) {
|
||||||
ToggleFavourite (animeId: ${'$'}animeId) {
|
ToggleFavourite (animeId: ${'$'}animeId) {
|
||||||
anime {
|
anime {
|
||||||
|
@ -614,7 +614,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return data != ""
|
return data != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postDataAboutId(
|
private suspend fun postDataAboutId(
|
||||||
id: Int,
|
id: Int,
|
||||||
type: AniListStatusType,
|
type: AniListStatusType,
|
||||||
score: Int?,
|
score: Int?,
|
||||||
|
@ -643,7 +643,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUser(setSettings: Boolean = true): AniListUser? {
|
private suspend fun getUser(setSettings: Boolean = true): AniListUser? {
|
||||||
val q = """
|
val q = """
|
||||||
{
|
{
|
||||||
Viewer {
|
Viewer {
|
||||||
|
@ -686,9 +686,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllSeasons(id: Int): List<SeasonResponse?> {
|
suspend fun getAllSeasons(id: Int): List<SeasonResponse?> {
|
||||||
val seasons = mutableListOf<SeasonResponse?>()
|
val seasons = mutableListOf<SeasonResponse?>()
|
||||||
fun getSeasonRecursive(id: Int) {
|
suspend fun getSeasonRecursive(id: Int) {
|
||||||
val season = getSeason(id)
|
val season = getSeason(id)
|
||||||
if (season != null) {
|
if (season != null) {
|
||||||
seasons.add(season)
|
seasons.add(season)
|
||||||
|
|
|
@ -50,7 +50,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(name: String): List<SyncAPI.SyncSearchResult> {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> {
|
||||||
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
||||||
val auth = getKey<String>(
|
val auth = getKey<String>(
|
||||||
accountId,
|
accountId,
|
||||||
|
@ -73,7 +73,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun score(id: String, status : SyncAPI.SyncStatus): Boolean {
|
override suspend fun score(id: String, status : SyncAPI.SyncStatus): Boolean {
|
||||||
return setScoreRequest(
|
return setScoreRequest(
|
||||||
id.toIntOrNull() ?: return false,
|
id.toIntOrNull() ?: return false,
|
||||||
fromIntToAnimeStatus(status.status),
|
fromIntToAnimeStatus(status.status),
|
||||||
|
@ -82,12 +82,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResult(id: String): SyncAPI.SyncResult? {
|
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatus(id: String): SyncAPI.SyncStatus? {
|
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
|
|
||||||
val data = getDataAboutMalId(internalId)?.my_list_status ?: return null
|
val data = getDataAboutMalId(internalId)?.my_list_status ?: return null
|
||||||
|
@ -182,7 +182,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshToken() {
|
private suspend fun refreshToken() {
|
||||||
try {
|
try {
|
||||||
val res = app.post(
|
val res = app.post(
|
||||||
"https://myanimelist.net/v1/oauth2/token",
|
"https://myanimelist.net/v1/oauth2/token",
|
||||||
|
@ -281,7 +281,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMalAnimeListSmart(): Array<Data>? {
|
suspend fun getMalAnimeListSmart(): Array<Data>? {
|
||||||
if (getKey<String>(
|
if (getKey<String>(
|
||||||
accountId,
|
accountId,
|
||||||
MAL_TOKEN_KEY
|
MAL_TOKEN_KEY
|
||||||
|
@ -299,7 +299,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMalAnimeList(): Array<Data>? {
|
private suspend fun getMalAnimeList(): Array<Data>? {
|
||||||
return try {
|
return try {
|
||||||
checkMalToken()
|
checkMalToken()
|
||||||
var offset = 0
|
var offset = 0
|
||||||
|
@ -321,7 +321,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
|
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMalAnimeListSlice(offset: Int = 0): MalList? {
|
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
|
||||||
val user = "@me"
|
val user = "@me"
|
||||||
val auth = getKey<String>(
|
val auth = getKey<String>(
|
||||||
accountId,
|
accountId,
|
||||||
|
@ -344,7 +344,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDataAboutMalId(id: Int): MalAnime? {
|
private suspend fun getDataAboutMalId(id: Int): MalAnime? {
|
||||||
return try {
|
return try {
|
||||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
||||||
val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
|
val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
|
||||||
|
@ -362,7 +362,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAllMalData() {
|
suspend fun setAllMalData() {
|
||||||
val user = "@me"
|
val user = "@me"
|
||||||
var isDone = false
|
var isDone = false
|
||||||
var index = 0
|
var index = 0
|
||||||
|
@ -426,7 +426,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkMalToken() {
|
private suspend fun checkMalToken() {
|
||||||
if (unixTime > getKey(
|
if (unixTime > getKey(
|
||||||
accountId,
|
accountId,
|
||||||
MAL_UNIXTIME_KEY
|
MAL_UNIXTIME_KEY
|
||||||
|
@ -436,7 +436,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMalUser(setSettings: Boolean = true): MalUser? {
|
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
|
||||||
checkMalToken()
|
checkMalToken()
|
||||||
return try {
|
return try {
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
|
@ -483,7 +483,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setScoreRequest(
|
suspend fun setScoreRequest(
|
||||||
id: Int,
|
id: Int,
|
||||||
status: MalStatusType? = null,
|
status: MalStatusType? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
|
@ -514,7 +514,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setScoreRequest(
|
private suspend fun setScoreRequest(
|
||||||
id: Int,
|
id: Int,
|
||||||
status: String? = null,
|
status: String? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
|
|
|
@ -8,8 +8,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
|
||||||
class APIRepository(val api: MainAPI) {
|
class APIRepository(val api: MainAPI) {
|
||||||
companion object {
|
companion object {
|
||||||
var providersActive = HashSet<String>()
|
|
||||||
var typesActive = HashSet<TvType>()
|
|
||||||
var dubStatusActive = HashSet<DubStatus>()
|
var dubStatusActive = HashSet<DubStatus>()
|
||||||
|
|
||||||
val noneApi = object : MainAPI() {
|
val noneApi = object : MainAPI() {
|
||||||
|
|
|
@ -138,6 +138,22 @@ class HomeFragment : Fragment() {
|
||||||
bottomSheetDialogBuilder.show()
|
bottomSheetDialogBuilder.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPairList(
|
||||||
|
anime: MaterialButton?,
|
||||||
|
cartoons: MaterialButton?,
|
||||||
|
tvs: MaterialButton?,
|
||||||
|
docs: MaterialButton?,
|
||||||
|
movies: MaterialButton?
|
||||||
|
): List<Pair<MaterialButton?, List<TvType>>> {
|
||||||
|
return listOf(
|
||||||
|
Pair(anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)),
|
||||||
|
Pair(cartoons, listOf(TvType.Cartoon)),
|
||||||
|
Pair(tvs, listOf(TvType.TvSeries)),
|
||||||
|
Pair(docs, listOf(TvType.Documentary)),
|
||||||
|
Pair(movies, listOf(TvType.Movie, TvType.Torrent))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.selectHomepage(selectedApiName: String?, callback: (String) -> Unit) {
|
fun Context.selectHomepage(selectedApiName: String?, callback: (String) -> Unit) {
|
||||||
val validAPIs = filterProviderByPreferredMedia().toMutableList()
|
val validAPIs = filterProviderByPreferredMedia().toMutableList()
|
||||||
|
|
||||||
|
@ -169,6 +185,8 @@ class HomeFragment : Fragment() {
|
||||||
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
|
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
|
||||||
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
|
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
|
||||||
|
|
||||||
|
val pairList = getPairList(anime, cartoons, tvs, docs, movies)
|
||||||
|
|
||||||
cancelBtt?.setOnClickListener {
|
cancelBtt?.setOnClickListener {
|
||||||
dialog.dismissSafe()
|
dialog.dismissSafe()
|
||||||
}
|
}
|
||||||
|
@ -194,14 +212,6 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val pairList = listOf(
|
|
||||||
Pair(anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)),
|
|
||||||
Pair(cartoons, listOf(TvType.Cartoon)),
|
|
||||||
Pair(tvs, listOf(TvType.TvSeries)),
|
|
||||||
Pair(docs, listOf(TvType.Documentary)),
|
|
||||||
Pair(movies, listOf(TvType.Movie, TvType.Torrent))
|
|
||||||
)
|
|
||||||
|
|
||||||
fun updateList() {
|
fun updateList() {
|
||||||
this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes)
|
this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes)
|
||||||
|
|
||||||
|
@ -210,12 +220,11 @@ class HomeFragment : Fragment() {
|
||||||
api.hasMainPage && api.supportedTypes.any {
|
api.hasMainPage && api.supportedTypes.any {
|
||||||
preSelectedTypes.contains(it)
|
preSelectedTypes.contains(it)
|
||||||
}
|
}
|
||||||
}.toMutableList()
|
}.sortedBy { it.name }.toMutableList()
|
||||||
currentValidApis.addAll(0, validAPIs.subList(0, 2))
|
currentValidApis.addAll(0, validAPIs.subList(0, 2))
|
||||||
|
|
||||||
val names = currentValidApis.map { it.name }
|
val names = currentValidApis.map { it.name }
|
||||||
val index = names.indexOf(currentApiName)
|
val index = names.indexOf(currentApiName)
|
||||||
println("INDEX: $index")
|
|
||||||
listView?.setItemChecked(index, true)
|
listView?.setItemChecked(index, true)
|
||||||
arrayAdapter.notifyDataSetChanged()
|
arrayAdapter.notifyDataSetChanged()
|
||||||
arrayAdapter.addAll(names)
|
arrayAdapter.addAll(names)
|
||||||
|
|
|
@ -6,8 +6,10 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.*
|
import android.widget.AbsListView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.ListView
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -15,18 +17,15 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiFromName
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiSettings
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiTypeSettings
|
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive
|
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.typesActive
|
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
|
||||||
|
@ -34,7 +33,6 @@ import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
import com.lagradost.cloudstream3.utils.SEARCH_PROVIDER_TOGGLE
|
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||||
|
@ -42,6 +40,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import kotlinx.android.synthetic.main.fragment_search.*
|
import kotlinx.android.synthetic.main.fragment_search.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
|
const val SEARCH_PREF_TAGS = "search_pref_tags"
|
||||||
|
const val SEARCH_PREF_PROVIDERS = "search_pref_providers"
|
||||||
|
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -90,6 +90,9 @@ class SearchFragment : Fragment() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var selectedSearchTypes = mutableListOf<TvType>()
|
||||||
|
var selectedApis = mutableSetOf<String>()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
@ -108,188 +111,190 @@ class SearchFragment : Fragment() {
|
||||||
search_autofit_results.adapter = adapter
|
search_autofit_results.adapter = adapter
|
||||||
search_loading_bar.alpha = 0f
|
search_loading_bar.alpha = 0f
|
||||||
|
|
||||||
val searchExitIcon = main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
val searchExitIcon =
|
||||||
val searchMagIcon = main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||||
|
val searchMagIcon =
|
||||||
|
main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||||
searchMagIcon.scaleX = 0.65f
|
searchMagIcon.scaleX = 0.65f
|
||||||
searchMagIcon.scaleY = 0.65f
|
searchMagIcon.scaleY = 0.65f
|
||||||
|
|
||||||
|
context?.let { ctx ->
|
||||||
|
val validAPIs = ctx.filterProviderByPreferredMedia()
|
||||||
|
selectedApis = ctx.getKey(
|
||||||
|
SEARCH_PREF_PROVIDERS,
|
||||||
|
defVal = validAPIs.map { it.name }
|
||||||
|
)!!.toMutableSet()
|
||||||
|
}
|
||||||
|
|
||||||
search_filter.setOnClickListener { searchView ->
|
search_filter.setOnClickListener { searchView ->
|
||||||
val apiNamesSetting = activity?.getApiSettings()
|
searchView?.context?.let { ctx ->
|
||||||
val langs = activity?.getApiProviderLangSettings()
|
val validAPIs = ctx.filterProviderByPreferredMedia()
|
||||||
if (apiNamesSetting != null && langs != null) {
|
var currentValidApis = listOf<MainAPI>()
|
||||||
val apiNames = apis.filter { langs.contains(it.lang) }.map { it.name }
|
val currentSelectedApis = if (selectedApis.isEmpty()) validAPIs.map { it.name }
|
||||||
|
.toMutableSet() else selectedApis
|
||||||
val builder =
|
val builder =
|
||||||
AlertDialog.Builder(searchView.context).setView(R.layout.provider_list)
|
BottomSheetDialog(ctx)
|
||||||
|
|
||||||
val dialog = builder.create()
|
builder.setContentView(R.layout.home_select_mainpage)
|
||||||
dialog.show()
|
builder.show()
|
||||||
|
builder.let { dialog ->
|
||||||
|
val anime = dialog.findViewById<MaterialButton>(R.id.home_select_anime)
|
||||||
|
val cartoons = dialog.findViewById<MaterialButton>(R.id.home_select_cartoons)
|
||||||
|
val tvs = dialog.findViewById<MaterialButton>(R.id.home_select_tv_series)
|
||||||
|
val docs = dialog.findViewById<MaterialButton>(R.id.home_select_documentaries)
|
||||||
|
val movies = dialog.findViewById<MaterialButton>(R.id.home_select_movies)
|
||||||
|
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
|
||||||
|
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
|
||||||
|
|
||||||
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
|
val pairList = HomeFragment.getPairList(anime, cartoons, tvs, docs, movies)
|
||||||
val listView2 = dialog.findViewById<ListView>(R.id.listview2)!!
|
|
||||||
val toggle = dialog.findViewById<SwitchMaterial>(R.id.toggle1)!!
|
|
||||||
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
|
||||||
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
|
||||||
// val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
|
|
||||||
|
|
||||||
val arrayAdapter = ArrayAdapter<String>(searchView.context, R.layout.sort_bottom_single_choice)
|
cancelBtt?.setOnClickListener {
|
||||||
arrayAdapter.addAll(apiNames)
|
dialog.dismissSafe()
|
||||||
|
|
||||||
listView.adapter = arrayAdapter
|
|
||||||
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
|
||||||
|
|
||||||
val typeChoices = listOf(
|
|
||||||
Pair(R.string.movies, listOf(TvType.Movie)),
|
|
||||||
Pair(R.string.tv_series, listOf(TvType.TvSeries, TvType.Documentary)),
|
|
||||||
Pair(R.string.cartoons, listOf(TvType.Cartoon)),
|
|
||||||
Pair(R.string.anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)),
|
|
||||||
Pair(R.string.torrent, listOf(TvType.Torrent)),
|
|
||||||
).filter { item -> apis.any { api -> api.supportedTypes.any { type -> item.second.contains(type) } } }
|
|
||||||
|
|
||||||
val arrayAdapter2 = ArrayAdapter<String>(searchView.context, R.layout.sort_bottom_single_choice)
|
|
||||||
arrayAdapter2.addAll(typeChoices.map { getString(it.first) })
|
|
||||||
|
|
||||||
listView2.adapter = arrayAdapter2
|
|
||||||
listView2.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
|
||||||
|
|
||||||
for ((index, item) in apiNames.withIndex()) {
|
|
||||||
listView.setItemChecked(index, apiNamesSetting.contains(item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((index, item) in typeChoices.withIndex()) {
|
cancelBtt?.setOnClickListener {
|
||||||
listView2.setItemChecked(index, item.second.any { typesActive.contains(it) })
|
dialog.dismissSafe()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSearch(isOn: Boolean) {
|
applyBtt?.setOnClickListener {
|
||||||
toggle.text =
|
//if (currentApiName != selectedApiName) {
|
||||||
getString(if (isOn) R.string.search_provider_text_types else R.string.search_provider_text_providers)
|
// currentApiName?.let(callback)
|
||||||
|
//}
|
||||||
if (isOn) {
|
dialog.dismissSafe()
|
||||||
listView2.visibility = View.VISIBLE
|
|
||||||
listView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
listView.visibility = View.VISIBLE
|
|
||||||
listView2.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val defVal = context?.getKey(SEARCH_PROVIDER_TOGGLE, true) ?: true
|
|
||||||
toggleSearch(defVal)
|
|
||||||
|
|
||||||
toggle.isChecked = defVal
|
|
||||||
toggle.setOnCheckedChangeListener { _, isOn ->
|
|
||||||
toggleSearch(isOn)
|
|
||||||
}
|
|
||||||
|
|
||||||
listView.setOnItemClickListener { _, _, _, _ ->
|
|
||||||
val types = HashSet<TvType>()
|
|
||||||
for ((index, api) in apis.withIndex()) {
|
|
||||||
if (listView.checkedItemPositions[index]) {
|
|
||||||
types.addAll(api.supportedTypes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for ((typeIndex, type) in typeChoices.withIndex()) {
|
|
||||||
listView2.setItemChecked(typeIndex, type.second.any { types.contains(it) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listView2.setOnItemClickListener { _, _, _, _ ->
|
|
||||||
for ((index, api) in apis.withIndex()) {
|
|
||||||
var isSupported = false
|
|
||||||
|
|
||||||
for ((typeIndex, type) in typeChoices.withIndex()) {
|
|
||||||
if (listView2.checkedItemPositions[typeIndex]) {
|
|
||||||
if (api.supportedTypes.any { type.second.contains(it) }) {
|
|
||||||
isSupported = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listView.setItemChecked(
|
|
||||||
index,
|
|
||||||
isSupported
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
context?.setKey(SEARCH_PROVIDER_TOGGLE, toggle.isChecked)
|
context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList())
|
||||||
|
selectedApis = currentSelectedApis
|
||||||
}
|
}
|
||||||
|
|
||||||
applyButton.setOnClickListener {
|
val selectedSearchTypes = context?.getKey<List<String>>(SEARCH_PREF_TAGS)
|
||||||
val settingsManagerLocal = PreferenceManager.getDefaultSharedPreferences(activity)
|
?.mapNotNull { listName ->
|
||||||
|
TvType.values().firstOrNull { it.name == listName }
|
||||||
val activeTypes = HashSet<TvType>()
|
|
||||||
for ((index, _) in typeChoices.withIndex()) {
|
|
||||||
if (listView2.checkedItemPositions[index]) {
|
|
||||||
activeTypes.addAll(typeChoices[index].second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
?.toMutableList()
|
||||||
|
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||||
|
|
||||||
if (activeTypes.size == 0) {
|
val listView = dialog.findViewById<ListView>(R.id.listview1)
|
||||||
activeTypes.addAll(TvType.values())
|
val arrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||||
}
|
listView?.adapter = arrayAdapter
|
||||||
|
listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||||
|
|
||||||
|
listView?.setOnItemClickListener { _, _, i, _ ->
|
||||||
val activeApis = HashSet<String>()
|
if (!currentValidApis.isNullOrEmpty()) {
|
||||||
for ((index, name) in apiNames.withIndex()) {
|
val api = currentValidApis[i].name
|
||||||
if (listView.checkedItemPositions[index]) {
|
if (currentSelectedApis.contains(api)) {
|
||||||
activeApis.add(name)
|
listView.setItemChecked(i, false)
|
||||||
}
|
currentSelectedApis -= api
|
||||||
}
|
|
||||||
|
|
||||||
if (activeApis.size == 0) {
|
|
||||||
activeApis.addAll(apiNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
val edit = settingsManagerLocal.edit()
|
|
||||||
edit.putStringSet(
|
|
||||||
getString(R.string.search_providers_list_key),
|
|
||||||
activeApis
|
|
||||||
)
|
|
||||||
edit.putStringSet(
|
|
||||||
getString(R.string.search_types_list_key),
|
|
||||||
activeTypes.map { it.name }.toSet()
|
|
||||||
)
|
|
||||||
edit.apply()
|
|
||||||
providersActive = activeApis
|
|
||||||
typesActive = activeTypes
|
|
||||||
|
|
||||||
dialog.dismissSafe(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelButton.setOnClickListener {
|
|
||||||
dialog.dismissSafe(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
//listView.setSelection(selectedIndex)
|
|
||||||
// listView.setItemChecked(selectedIndex, true)
|
|
||||||
/*
|
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
|
|
||||||
|
|
||||||
builder.setMultiChoiceItems(
|
|
||||||
apiNames.toTypedArray(),
|
|
||||||
apiNames.map { a -> apiNamesSetting.contains(a) }.toBooleanArray()
|
|
||||||
) { _, position: Int, checked: Boolean ->
|
|
||||||
val apiNamesSettingLocal = activity?.getApiSettings()
|
|
||||||
if (apiNamesSettingLocal != null) {
|
|
||||||
val settingsManagerLocal = PreferenceManager.getDefaultSharedPreferences(activity)
|
|
||||||
if (checked) {
|
|
||||||
apiNamesSettingLocal.add(apiNames[position])
|
|
||||||
} else {
|
} else {
|
||||||
apiNamesSettingLocal.remove(apiNames[position])
|
listView.setItemChecked(i, true)
|
||||||
|
currentSelectedApis += api
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val edit = settingsManagerLocal.edit()
|
fun updateList() {
|
||||||
edit.putStringSet(
|
arrayAdapter.clear()
|
||||||
getString(R.string.search_providers_list_key),
|
currentValidApis = validAPIs.filter { api ->
|
||||||
apiNames.filter { a -> apiNamesSettingLocal.contains(a) }.toSet()
|
api.hasMainPage && api.supportedTypes.any {
|
||||||
|
selectedSearchTypes.contains(it)
|
||||||
|
}
|
||||||
|
}.sortedBy { it.name }
|
||||||
|
|
||||||
|
val names = currentValidApis.map { it.name }
|
||||||
|
|
||||||
|
for ((index, api) in names.withIndex()) {
|
||||||
|
listView?.setItemChecked(index, currentSelectedApis.contains(api))
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayAdapter.notifyDataSetChanged()
|
||||||
|
arrayAdapter.addAll(names)
|
||||||
|
arrayAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((button, validTypes) in pairList) {
|
||||||
|
val isValid =
|
||||||
|
validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } }
|
||||||
|
button?.isVisible = isValid
|
||||||
|
if (isValid) {
|
||||||
|
fun buttonContains(): Boolean {
|
||||||
|
return selectedSearchTypes.any { validTypes.contains(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
button?.isSelected = buttonContains()
|
||||||
|
button?.setOnClickListener {
|
||||||
|
selectedSearchTypes.clear()
|
||||||
|
selectedSearchTypes.addAll(validTypes)
|
||||||
|
for ((otherButton, _) in pairList) {
|
||||||
|
otherButton?.isSelected = false
|
||||||
|
}
|
||||||
|
button.isSelected = true
|
||||||
|
updateList()
|
||||||
|
}
|
||||||
|
|
||||||
|
button?.setOnLongClickListener {
|
||||||
|
if (!buttonContains()) {
|
||||||
|
button.isSelected = true
|
||||||
|
selectedSearchTypes.addAll(validTypes)
|
||||||
|
} else {
|
||||||
|
button.isSelected = false
|
||||||
|
selectedSearchTypes.removeAll(validTypes)
|
||||||
|
}
|
||||||
|
updateList()
|
||||||
|
return@setOnLongClickListener true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pairList = HomeFragment.getPairList(
|
||||||
|
search_select_anime,
|
||||||
|
search_select_cartoons,
|
||||||
|
search_select_tv_series,
|
||||||
|
search_select_documentaries,
|
||||||
|
search_select_movies
|
||||||
)
|
)
|
||||||
edit.apply()
|
|
||||||
providersActive = apiNamesSettingLocal
|
selectedSearchTypes = context?.getKey<List<String>>(SEARCH_PREF_TAGS)
|
||||||
|
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
|
||||||
|
?.toMutableList()
|
||||||
|
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||||
|
context?.filterProviderByPreferredMedia()?.let { validAPIs ->
|
||||||
|
for ((button, validTypes) in pairList) {
|
||||||
|
val isValid =
|
||||||
|
validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } }
|
||||||
|
button?.isVisible = isValid
|
||||||
|
if (isValid) {
|
||||||
|
fun buttonContains(): Boolean {
|
||||||
|
return selectedSearchTypes.any { validTypes.contains(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
button?.isSelected = buttonContains()
|
||||||
|
button?.setOnClickListener {
|
||||||
|
selectedSearchTypes.clear()
|
||||||
|
selectedSearchTypes.addAll(validTypes)
|
||||||
|
for ((otherButton, _) in pairList) {
|
||||||
|
otherButton?.isSelected = false
|
||||||
|
}
|
||||||
|
it?.context?.setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
|
||||||
|
it?.isSelected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
button?.setOnLongClickListener {
|
||||||
|
if (!buttonContains()) {
|
||||||
|
it?.isSelected = true
|
||||||
|
selectedSearchTypes.addAll(validTypes)
|
||||||
|
} else {
|
||||||
|
it?.isSelected = false
|
||||||
|
selectedSearchTypes.removeAll(validTypes)
|
||||||
|
}
|
||||||
|
it?.context?.setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
|
||||||
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setTitle("Search Providers")
|
|
||||||
builder.setNegativeButton("Ok") { _, _ -> }
|
|
||||||
builder.show()*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,7 +305,12 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
searchViewModel.searchAndCancel(query = query)
|
searchViewModel.searchAndCancel(
|
||||||
|
query = query,
|
||||||
|
providersActive = selectedApis.filter { name ->
|
||||||
|
getApiFromName(name).supportedTypes.any { selectedSearchTypes.contains(it) }
|
||||||
|
}.toSet()
|
||||||
|
)
|
||||||
|
|
||||||
main_search?.let {
|
main_search?.let {
|
||||||
hideKeyboard(it)
|
hideKeyboard(it)
|
||||||
|
@ -363,10 +373,6 @@ class SearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activity?.let {
|
|
||||||
providersActive = it.getApiSettings()
|
|
||||||
typesActive = it.getApiTypeSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*main_search.setOnQueryTextFocusChangeListener { _, b ->
|
/*main_search.setOnQueryTextFocusChangeListener { _, b ->
|
||||||
if (b) {
|
if (b) {
|
||||||
|
@ -376,20 +382,21 @@ class SearchFragment : Fragment() {
|
||||||
}*/
|
}*/
|
||||||
//main_search.onActionViewExpanded()*/
|
//main_search.onActionViewExpanded()*/
|
||||||
|
|
||||||
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = ParentItemAdapter(listOf(), { callback ->
|
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||||
|
ParentItemAdapter(listOf(), { callback ->
|
||||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||||
}, { item ->
|
}, { item ->
|
||||||
activity?.loadHomepageList(item)
|
activity?.loadHomepageList(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
search_master_recycler.adapter = masterAdapter
|
search_master_recycler?.adapter = masterAdapter
|
||||||
search_master_recycler.layoutManager = GridLayoutManager(context, 1)
|
search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
|
||||||
|
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true)
|
val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true)
|
||||||
|
|
||||||
search_master_recycler.isVisible = isAdvancedSearch
|
search_master_recycler?.isVisible = isAdvancedSearch
|
||||||
search_autofit_results.isVisible = !isAdvancedSearch
|
search_autofit_results?.isVisible = !isAdvancedSearch
|
||||||
|
|
||||||
// SubtitlesFragment.push(activity)
|
// SubtitlesFragment.push(activity)
|
||||||
//searchViewModel.search("iron man")
|
//searchViewModel.search("iron man")
|
||||||
|
|
|
@ -14,7 +14,6 @@ import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -26,7 +25,8 @@ data class OnGoingSearch(
|
||||||
)
|
)
|
||||||
|
|
||||||
class SearchViewModel : ViewModel() {
|
class SearchViewModel : ViewModel() {
|
||||||
private val _searchResponse: MutableLiveData<Resource<ArrayList<SearchResponse>>> = MutableLiveData()
|
private val _searchResponse: MutableLiveData<Resource<ArrayList<SearchResponse>>> =
|
||||||
|
MutableLiveData()
|
||||||
val searchResponse: LiveData<Resource<ArrayList<SearchResponse>>> get() = _searchResponse
|
val searchResponse: LiveData<Resource<ArrayList<SearchResponse>>> get() = _searchResponse
|
||||||
|
|
||||||
private val _currentSearch: MutableLiveData<ArrayList<OnGoingSearch>> = MutableLiveData()
|
private val _currentSearch: MutableLiveData<ArrayList<OnGoingSearch>> = MutableLiveData()
|
||||||
|
@ -40,9 +40,14 @@ class SearchViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var onGoingSearch: Job? = null
|
var onGoingSearch: Job? = null
|
||||||
fun searchAndCancel(query: String, isMainApis: Boolean = true, ignoreSettings: Boolean = false) {
|
fun searchAndCancel(
|
||||||
|
query: String,
|
||||||
|
isMainApis: Boolean = true,
|
||||||
|
providersActive: Set<String> = setOf(),
|
||||||
|
ignoreSettings: Boolean = false
|
||||||
|
) {
|
||||||
onGoingSearch?.cancel()
|
onGoingSearch?.cancel()
|
||||||
onGoingSearch = search(query, isMainApis, ignoreSettings)
|
onGoingSearch = search(query, isMainApis, providersActive, ignoreSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SyncSearchResultSearchResponse(
|
data class SyncSearchResultSearchResponse(
|
||||||
|
@ -65,7 +70,12 @@ class SearchViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun search(query: String, isMainApis: Boolean = true, ignoreSettings: Boolean = false) =
|
private fun search(
|
||||||
|
query: String,
|
||||||
|
isMainApis: Boolean = true,
|
||||||
|
providersActive: Set<String>,
|
||||||
|
ignoreSettings: Boolean = false
|
||||||
|
) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (query.length <= 1) {
|
if (query.length <= 1) {
|
||||||
clearSearch()
|
clearSearch()
|
||||||
|
@ -81,7 +91,7 @@ class SearchViewModel : ViewModel() {
|
||||||
withContext(Dispatchers.IO) { // This interrupts UI otherwise
|
withContext(Dispatchers.IO) { // This interrupts UI otherwise
|
||||||
if (isMainApis) {
|
if (isMainApis) {
|
||||||
repos.filter { a ->
|
repos.filter { a ->
|
||||||
ignoreSettings || (providersActive.size == 0 || providersActive.contains(a.name))
|
ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))
|
||||||
}.apmap { a -> // Parallel
|
}.apmap { a -> // Parallel
|
||||||
val search = a.search(query)
|
val search = a.search(query)
|
||||||
currentList.add(OnGoingSearch(a.name, search))
|
currentList.add(OnGoingSearch(a.name, search))
|
||||||
|
@ -102,7 +112,8 @@ class SearchViewModel : ViewModel() {
|
||||||
|
|
||||||
val list = ArrayList<SearchResponse>()
|
val list = ArrayList<SearchResponse>()
|
||||||
val nestedList =
|
val nestedList =
|
||||||
currentList.map { it.data }.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
|
currentList.map { it.data }
|
||||||
|
.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
|
||||||
|
|
||||||
// I do it this way to move the relevant search results to the top
|
// I do it this way to move the relevant search results to the top
|
||||||
var index = 0
|
var index = 0
|
||||||
|
|
|
@ -22,7 +22,6 @@ import com.hippo.unifile.UniFile
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiSettings
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.restrictedApis
|
import com.lagradost.cloudstream3.APIHolder.restrictedApis
|
||||||
import com.lagradost.cloudstream3.AcraApplication
|
import com.lagradost.cloudstream3.AcraApplication
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
|
@ -294,7 +293,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
this.getString(R.string.provider_lang_key),
|
this.getString(R.string.provider_lang_key),
|
||||||
selectedList.map { names[it].first }.toMutableSet()
|
selectedList.map { names[it].first }.toMutableSet()
|
||||||
).apply()
|
).apply()
|
||||||
APIRepository.providersActive = it.context.getApiSettings()
|
//APIRepository.providersActive = it.context.getApiSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ import com.lagradost.cloudstream3.TvType
|
||||||
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.extractors.*
|
import com.lagradost.cloudstream3.extractors.*
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
data class ExtractorLink(
|
data class ExtractorLink(
|
||||||
|
@ -78,7 +79,7 @@ fun getAndUnpack(string: String): String {
|
||||||
/**
|
/**
|
||||||
* Tries to load the appropriate extractor based on link, returns true if any extractor is loaded.
|
* Tries to load the appropriate extractor based on link, returns true if any extractor is loaded.
|
||||||
* */
|
* */
|
||||||
fun loadExtractor(url: String, referer: String?, callback: (ExtractorLink) -> Unit) : Boolean {
|
suspend fun loadExtractor(url: String, referer: String?, callback: (ExtractorLink) -> Unit) : Boolean {
|
||||||
for (extractor in extractorApis) {
|
for (extractor in extractorApis) {
|
||||||
if (url.startsWith(extractor.mainUrl)) {
|
if (url.startsWith(extractor.mainUrl)) {
|
||||||
extractor.getSafeUrl(url, referer)?.forEach(callback)
|
extractor.getSafeUrl(url, referer)?.forEach(callback)
|
||||||
|
@ -138,7 +139,7 @@ fun httpsify(url: String): String {
|
||||||
return if (url.startsWith("//")) "https:$url" else url
|
return if (url.startsWith("//")) "https:$url" else url
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPostForm(requestUrl : String, html : String) : String? {
|
suspend fun getPostForm(requestUrl : String, html : String) : String? {
|
||||||
val document = Jsoup.parse(html)
|
val document = Jsoup.parse(html)
|
||||||
val inputs = document.select("Form > input")
|
val inputs = document.select("Form > input")
|
||||||
if (inputs.size < 4) return null
|
if (inputs.size < 4) return null
|
||||||
|
@ -160,7 +161,7 @@ fun getPostForm(requestUrl : String, html : String) : String? {
|
||||||
if (op == null || id == null || mode == null || hash == null) {
|
if (op == null || id == null || mode == null || hash == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
Thread.sleep(5000) // ye this is needed, wont work with 0 delay
|
delay(5000) // ye this is needed, wont work with 0 delay
|
||||||
|
|
||||||
val postResponse = app.post(
|
val postResponse = app.post(
|
||||||
requestUrl,
|
requestUrl,
|
||||||
|
@ -181,14 +182,14 @@ abstract class ExtractorApi {
|
||||||
abstract val mainUrl: String
|
abstract val mainUrl: String
|
||||||
abstract val requiresReferer: Boolean
|
abstract val requiresReferer: Boolean
|
||||||
|
|
||||||
fun getSafeUrl(url: String, referer: String? = null): List<ExtractorLink>? {
|
suspend fun getSafeUrl(url: String, referer: String? = null): List<ExtractorLink>? {
|
||||||
return normalSafeApiCall { getUrl(url, referer) }
|
return suspendSafeApiCall { getUrl(url, referer) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will throw errors, use getSafeUrl if you don't want to handle the exception yourself
|
* Will throw errors, use getSafeUrl if you don't want to handle the exception yourself
|
||||||
*/
|
*/
|
||||||
abstract fun getUrl(url: String, referer: String? = null): List<ExtractorLink>?
|
abstract suspend fun getUrl(url: String, referer: String? = null): List<ExtractorLink>?
|
||||||
|
|
||||||
open fun getExtractorUrl(id: String): String {
|
open fun getExtractorUrl(id: String): String {
|
||||||
return id
|
return id
|
||||||
|
|
|
@ -16,7 +16,7 @@ object FillerEpisodeCheck {
|
||||||
return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ").replace("[^a-zA-Z0-9 ]".toRegex(), "")
|
return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ").replace("[^a-zA-Z0-9 ]".toRegex(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFillerList(): Boolean {
|
private suspend fun getFillerList(): Boolean {
|
||||||
if (list != null) return true
|
if (list != null) return true
|
||||||
try {
|
try {
|
||||||
val result = app.get("$MAIN_URL/shows").text
|
val result = app.get("$MAIN_URL/shows").text
|
||||||
|
@ -59,7 +59,7 @@ object FillerEpisodeCheck {
|
||||||
return q + "cache" + z
|
return q + "cache" + z
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? {
|
suspend fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? {
|
||||||
try {
|
try {
|
||||||
if (!getFillerList()) return null
|
if (!getFillerList()) return null
|
||||||
val localList = list ?: return null
|
val localList = list ?: return null
|
||||||
|
|
|
@ -24,6 +24,7 @@ import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
@ -83,7 +84,12 @@ class InAppUpdater {
|
||||||
val url = "https://api.github.com/repos/LagradOst/CloudStream-3/releases"
|
val url = "https://api.github.com/repos/LagradOst/CloudStream-3/releases"
|
||||||
val headers = mapOf("Accept" to "application/vnd.github.v3+json")
|
val headers = mapOf("Accept" to "application/vnd.github.v3+json")
|
||||||
val response =
|
val response =
|
||||||
mapper.readValue<List<GithubRelease>>(app.get(url, headers = headers).text)
|
mapper.readValue<List<GithubRelease>>(runBlocking {
|
||||||
|
app.get(
|
||||||
|
url,
|
||||||
|
headers = headers
|
||||||
|
).text
|
||||||
|
})
|
||||||
|
|
||||||
val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""")
|
val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""")
|
||||||
val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""")
|
val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""")
|
||||||
|
@ -139,7 +145,7 @@ class InAppUpdater {
|
||||||
return Update(false, null, null, null)
|
return Update(false, null, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Activity.getPreReleaseUpdate(): Update {
|
private fun Activity.getPreReleaseUpdate(): Update = runBlocking {
|
||||||
val tagUrl =
|
val tagUrl =
|
||||||
"https://api.github.com/repos/LagradOst/CloudStream-3/git/ref/tags/pre-release"
|
"https://api.github.com/repos/LagradOst/CloudStream-3/git/ref/tags/pre-release"
|
||||||
val releaseUrl = "https://api.github.com/repos/LagradOst/CloudStream-3/releases"
|
val releaseUrl = "https://api.github.com/repos/LagradOst/CloudStream-3/releases"
|
||||||
|
@ -159,7 +165,7 @@ class InAppUpdater {
|
||||||
val shouldUpdate =
|
val shouldUpdate =
|
||||||
(getString(R.string.prerelease_commit_hash) != tagResponse.github_object.sha)
|
(getString(R.string.prerelease_commit_hash) != tagResponse.github_object.sha)
|
||||||
|
|
||||||
return if (foundAsset != null) {
|
return@runBlocking if (foundAsset != null) {
|
||||||
Update(
|
Update(
|
||||||
shouldUpdate,
|
shouldUpdate,
|
||||||
foundAsset.browser_download_url,
|
foundAsset.browser_download_url,
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.network.text
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
@ -82,7 +82,9 @@ class M3u8Helper {
|
||||||
fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean): List<M3u8Stream> {
|
fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean): List<M3u8Stream> {
|
||||||
val generate = sequence {
|
val generate = sequence {
|
||||||
val m3u8Parent = getParentLink(m3u8.streamUrl)
|
val m3u8Parent = getParentLink(m3u8.streamUrl)
|
||||||
val response = app.get(m3u8.streamUrl, headers = m3u8.headers).text
|
val response = runBlocking {
|
||||||
|
app.get(m3u8.streamUrl, headers = m3u8.headers).text
|
||||||
|
}
|
||||||
|
|
||||||
for (match in QUALITY_REGEX.findAll(response)) {
|
for (match in QUALITY_REGEX.findAll(response)) {
|
||||||
var (quality, m3u8Link, m3u8Link2) = match.destructured
|
var (quality, m3u8Link, m3u8Link2) = match.destructured
|
||||||
|
@ -146,7 +148,7 @@ class M3u8Helper {
|
||||||
|
|
||||||
val secondSelection = selectBest(streams.ifEmpty { listOf(selected) })
|
val secondSelection = selectBest(streams.ifEmpty { listOf(selected) })
|
||||||
if (secondSelection != null) {
|
if (secondSelection != null) {
|
||||||
val m3u8Response = app.get(secondSelection.streamUrl, headers = headers).text
|
val m3u8Response = runBlocking {app.get(secondSelection.streamUrl, headers = headers).text}
|
||||||
|
|
||||||
var encryptionUri: String?
|
var encryptionUri: String?
|
||||||
var encryptionIv = byteArrayOf()
|
var encryptionIv = byteArrayOf()
|
||||||
|
@ -164,7 +166,7 @@ class M3u8Helper {
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptionIv = match.component3().toByteArray()
|
encryptionIv = match.component3().toByteArray()
|
||||||
val encryptionKeyResponse = app.get(encryptionUri, headers = headers)
|
val encryptionKeyResponse = runBlocking { app.get(encryptionUri, headers = headers) }
|
||||||
encryptionData = encryptionKeyResponse.body?.bytes() ?: byteArrayOf()
|
encryptionData = encryptionKeyResponse.body?.bytes() ?: byteArrayOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +189,7 @@ class M3u8Helper {
|
||||||
|
|
||||||
while (lastYield != c) {
|
while (lastYield != c) {
|
||||||
try {
|
try {
|
||||||
val tsResponse = app.get(url, headers = headers)
|
val tsResponse = runBlocking { app.get(url, headers = headers) }
|
||||||
var tsData = tsResponse.body?.bytes() ?: byteArrayOf()
|
var tsData = tsResponse.body?.bytes() ?: byteArrayOf()
|
||||||
|
|
||||||
if (encryptionState) {
|
if (encryptionState) {
|
||||||
|
|
|
@ -5,13 +5,12 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mapper
|
import com.lagradost.cloudstream3.mapper
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.network.text
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
object SyncUtil {
|
object SyncUtil {
|
||||||
/** first. Mal, second. Anilist,
|
/** first. Mal, second. Anilist,
|
||||||
* valid sites are: Gogoanime, Twistmoe and 9anime*/
|
* valid sites are: Gogoanime, Twistmoe and 9anime*/
|
||||||
fun getIdsFromSlug(slug: String, site : String = "Gogoanime"): Pair<String?, String?>? {
|
suspend fun getIdsFromSlug(slug: String, site : String = "Gogoanime"): Pair<String?, String?>? {
|
||||||
try {
|
try {
|
||||||
//Gogoanime, Twistmoe and 9anime
|
//Gogoanime, Twistmoe and 9anime
|
||||||
val url =
|
val url =
|
||||||
|
|
|
@ -81,6 +81,59 @@
|
||||||
app:tint="?attr/textColor"
|
app:tint="?attr/textColor"
|
||||||
android:contentDescription="@string/change_providers_img_des" />
|
android:contentDescription="@string/change_providers_img_des" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingEnd="10dp"
|
||||||
|
android:fadingEdge="horizontal"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:nextFocusRight="@id/search_select_tv_series"
|
||||||
|
|
||||||
|
android:id="@+id/search_select_movies"
|
||||||
|
android:text="@string/movies"
|
||||||
|
style="@style/RoundedSelectableButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:nextFocusLeft="@id/search_select_movies"
|
||||||
|
android:nextFocusRight="@id/search_select_anime"
|
||||||
|
|
||||||
|
android:id="@+id/search_select_tv_series"
|
||||||
|
android:text="@string/tv_series"
|
||||||
|
style="@style/RoundedSelectableButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:nextFocusLeft="@id/search_select_tv_series"
|
||||||
|
android:nextFocusRight="@id/search_select_cartoons"
|
||||||
|
|
||||||
|
android:id="@+id/search_select_anime"
|
||||||
|
android:text="@string/anime"
|
||||||
|
style="@style/RoundedSelectableButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:nextFocusLeft="@id/search_select_anime"
|
||||||
|
android:nextFocusRight="@id/search_select_documentaries"
|
||||||
|
|
||||||
|
android:id="@+id/search_select_cartoons"
|
||||||
|
android:text="@string/cartoons"
|
||||||
|
style="@style/RoundedSelectableButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:nextFocusLeft="@id/search_select_cartoons"
|
||||||
|
|
||||||
|
android:id="@+id/search_select_documentaries"
|
||||||
|
android:text="@string/documentaries"
|
||||||
|
style="@style/RoundedSelectableButton" />
|
||||||
|
</LinearLayout>
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
|
||||||
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||||
android:nextFocusLeft="@id/nav_rail_view"
|
android:nextFocusLeft="@id/nav_rail_view"
|
||||||
|
|
|
@ -13,7 +13,7 @@ class ProviderTests {
|
||||||
return allApis.filter { !it.usesWebView }
|
return allApis.filter { !it.usesWebView }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLinks(api: MainAPI, url: String?): Boolean {
|
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
|
||||||
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
|
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
|
||||||
if (url == null) return true
|
if (url == null) return true
|
||||||
var linksLoaded = 0
|
var linksLoaded = 0
|
||||||
|
@ -39,7 +39,7 @@ class ProviderTests {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testSingleProviderApi(api: MainAPI): Boolean {
|
private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
|
||||||
val searchQueries = listOf("over", "iron", "guy")
|
val searchQueries = listOf("over", "iron", "guy")
|
||||||
var correctResponses = 0
|
var correctResponses = 0
|
||||||
var searchResult: List<SearchResponse>? = null
|
var searchResult: List<SearchResponse>? = null
|
||||||
|
@ -144,7 +144,7 @@ class ProviderTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
getAllProviders().pmap { api ->
|
getAllProviders().apmap { api ->
|
||||||
if (api.hasMainPage) {
|
if (api.hasMainPage) {
|
||||||
try {
|
try {
|
||||||
val homepage = api.getMainPage()
|
val homepage = api.getMainPage()
|
||||||
|
@ -175,10 +175,10 @@ class ProviderTests {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrect() {
|
suspend fun providerCorrect() {
|
||||||
val invalidProvider = ArrayList<Pair<MainAPI,Exception?>>()
|
val invalidProvider = ArrayList<Pair<MainAPI,Exception?>>()
|
||||||
val providers = getAllProviders()
|
val providers = getAllProviders()
|
||||||
providers.pmap { api ->
|
providers.apmap { api ->
|
||||||
try {
|
try {
|
||||||
println("Trying $api")
|
println("Trying $api")
|
||||||
if (testSingleProviderApi(api)) {
|
if (testSingleProviderApi(api)) {
|
||||||
|
|
Loading…
Reference in a new issue