package com.lagradost.cloudstream3 import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.util.Base64.encodeToString import androidx.annotation.WorkerThread import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.animeproviders.* import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider import com.lagradost.cloudstream3.movieproviders.* import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.ExtractorLink import java.util.* const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" //val baseHeader = mapOf("User-Agent" to USER_AGENT) val mapper = JsonMapper.builder().addModule(KotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! object APIHolder { val unixTime: Long get() = System.currentTimeMillis() / 1000L val unixTimeMS: Long get() = System.currentTimeMillis() private const val defProvider = 0 val allProviders by lazy { arrayListOf( // Movie providers PelisplusProvider(), PelisplusHDProvider(), PeliSmartProvider(), MeloMovieProvider(), // Captcha for links DoramasYTProvider(), CinecalidadProvider(), CuevanaProvider(), EntrepeliculasyseriesProvider(), PelisflixProvider(), SeriesflixProvider(), IHaveNoTvProvider(), // Documentaries provider LookMovieProvider(), // RECAPTCHA (Please allow up to 5 seconds...) VMoveeProvider(), AllMoviesForYouProvider(), VidEmbedProvider(), VfFilmProvider(), VfSerieProvider(), FrenchStreamProvider(), AsianLoadProvider(), AsiaFlixProvider(), // restricted BflixProvider(), FmoviesToProvider(), SflixProProvider(), FilmanProvider(), SflixProvider(), DopeboxProvider(), SolarmovieProvider(), PinoyMoviePediaProvider(), PinoyHDXyzProvider(), PinoyMoviesEsProvider(), TrailersTwoProvider(), TwoEmbedProvider(), DramaSeeProvider(), WatchAsianProvider(), KdramaHoodProvider(), AkwamProvider(), MyCimaProvider(), EgyBestProvider(), SoaptwoDayProvider(), HDMProvider(),// disabled due to cloudflare // Metadata providers //TmdbProvider(), CrossTmdbProvider(), ApiMDBProvider(), // Anime providers WatchCartoonOnlineProvider(), GogoanimeProvider(), AllAnimeProvider(), AnimekisaProvider(), //ShiroProvider(), // v2 fucked me AnimeFlickProvider(), AnimeflvnetProvider(), TenshiProvider(), WcoProvider(), AnimePaheProvider(), NineAnimeProvider(), AnimeWorldProvider(), ZoroProvider(), DubbedAnimeProvider(), MonoschinosProvider(), KawaiifuProvider(), // disabled due to cloudflare ) } var apis: List = arrayListOf() fun getApiFromName(apiName: String?): MainAPI { return getApiFromNameNull(apiName) ?: apis[defProvider] } fun getApiFromNameNull(apiName: String?): MainAPI? { if (apiName == null) return null for (api in allProviders) { if (apiName == api.name) return api } return null } fun getApiFromUrlNull(url : String?) : MainAPI? { if (url == null) return null for (api in allProviders) { if(url.startsWith(api.mainUrl)) return api } return null } fun LoadResponse.getId(): Int { return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode() } /** * Gets the website captcha token * discovered originally by https://github.com/ahmedgamal17 * optimized by https://github.com/justfoolingaround * * @param url the main url, likely the same website you found the key from. * @param key used to fill https://www.google.com/recaptcha/api.js?render=.... * * @param referer the referer for the google.com/recaptcha/api.js... request, optional. * */ // Try document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]").attr("src").substringAfter("render=") // To get the key suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { val uri = Uri.parse(url) val domain = encodeToString( (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), 0 ).replace("\n", "").replace("=", ".") val vToken = app.get( "https://www.google.com/recaptcha/api.js?render=$key", referer = referer, cacheTime = 0 ) .text .substringAfter("releases/") .substringBefore("/") val recapToken = app.get("https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=cs3&k=$key&co=$domain&v=$vToken") .document .selectFirst("#recaptcha-token")?.attr("value") if (recapToken != null) { return app.post( "https://www.google.com/recaptcha/api2/reload?k=$key", data = mapOf( "v" to vToken, "k" to key, "c" to recapToken, "co" to domain, "sa" to "", "reason" to "q" ), cacheTime = 0 ).text .substringAfter("rresp\",\"") .substringBefore("\"") } return null } fun Context.getApiSettings(): HashSet { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name }) /*val set = settingsManager.getStringSet( this.getString(R.string.search_providers_list_key), hashSet )?.toHashSet() ?: hashSet val list = HashSet() for (name in set) { val api = getApiFromNameNull(name) ?: continue if (activeLangs.contains(api.lang)) { list.add(name) } }*/ //if (list.isEmpty()) return hashSet //return list return hashSet } fun Context.getApiDubstatusSettings(): HashSet { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val hashSet = HashSet() hashSet.addAll(DubStatus.values()) val list = settingsManager.getStringSet( this.getString(R.string.display_sub_key), hashSet.map { it.name }.toMutableSet() ) ?: return hashSet val names = DubStatus.values().map { it.name }.toHashSet() //if(realSet.isEmpty()) return hashSet return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() } fun Context.getApiProviderLangSettings(): HashSet { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val hashSet = HashSet() hashSet.add("en") // def is only en val list = settingsManager.getStringSet( this.getString(R.string.provider_lang_key), hashSet.toMutableSet() ) if (list.isNullOrEmpty()) return hashSet return list.toHashSet() } fun Context.getApiTypeSettings(): HashSet { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val hashSet = HashSet() hashSet.addAll(TvType.values()) val list = settingsManager.getStringSet( this.getString(R.string.search_types_list_key), hashSet.map { it.name }.toMutableSet() ) if (list.isNullOrEmpty()) return hashSet val names = TvType.values().map { it.name }.toHashSet() val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() if (realSet.isEmpty()) return hashSet return realSet } fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val currentPrefMedia = settingsManager.getInt(this.getString(R.string.prefer_media_type_key), 0) val langs = this.getApiProviderLangSettings() val allApis = apis.filter { langs.contains(it.lang) } .filter { api -> api.hasMainPage || !hasHomePageIsRequired } return if (currentPrefMedia < 1) { allApis } else { // Filter API depending on preferred media type val listEnumAnime = listOf(TvType.Anime, TvType.AnimeMovie, TvType.OVA) val listEnumMovieTv = listOf(TvType.Movie, TvType.TvSeries, TvType.Cartoon) val mediaTypeList = if (currentPrefMedia == 1) listEnumMovieTv else listEnumAnime val filteredAPI = allApis.filter { api -> api.supportedTypes.any { it in mediaTypeList } } filteredAPI } } } /* 0 = Site not good 1 = All good 2 = Slow, heavy traffic 3 = restricted, must donate 30 benenes to use */ const val PROVIDER_STATUS_KEY = "PROVIDER_STATUS_KEY" const val PROVIDER_STATUS_URL = "https://raw.githubusercontent.com/LagradOst/CloudStream-3/master/providers.json" const val PROVIDER_STATUS_BETA_ONLY = 3 const val PROVIDER_STATUS_SLOW = 2 const val PROVIDER_STATUS_OK = 1 const val PROVIDER_STATUS_DOWN = 0 data class ProvidersInfoJson( @JsonProperty("name") var name: String, @JsonProperty("url") var url: String, @JsonProperty("status") var status: Int, ) /**Every provider will **not** have try catch built in, so handle exceptions when calling these functions*/ abstract class MainAPI { companion object { var overrideData: HashMap? = null } public fun overrideWithNewData(data: ProvidersInfoJson) { this.name = data.name this.mainUrl = data.url } init { overrideData?.get(this.javaClass.simpleName)?.let { data -> overrideWithNewData(data) } } open var name = "NONE" open var mainUrl = "NONE" //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id open val lang = "en" // ISO_639_1 check SubtitleHelper /**If link is stored in the "data" string, so links can be instantly loaded*/ open val instantLinkLoading = false /**Set false if links require referer or for some reason cant be played on a chromecast*/ open val hasChromecastSupport = true /**If all links are encrypted then set this to false*/ open val hasDownloadSupport = true /**Used for testing and can be used to disable the providers if WebView is not available*/ open val usesWebView = false open val hasMainPage = false open val hasQuickSearch = false open val supportedTypes = setOf( TvType.Movie, TvType.TvSeries, TvType.Cartoon, TvType.Anime, TvType.OVA, ) open val vpnStatus = VPNStatus.None open val providerType = ProviderType.DirectProvider @WorkerThread open suspend fun getMainPage(): HomePageResponse? { throw NotImplementedError() } @WorkerThread open suspend fun search(query: String): List? { throw NotImplementedError() } @WorkerThread open suspend fun quickSearch(query: String): List? { throw NotImplementedError() } @WorkerThread /** * Based on data from search() or getMainPage() it generates a LoadResponse, * basically opening the info page from a link. * */ open suspend fun load(url: String): LoadResponse? { throw NotImplementedError() } /** * Largely redundant feature for most providers. * * This job runs in the background when a link is playing in exoplayer. * First implemented to do polling for sflix to keep the link from getting expired. * * This function might be updated to include exoplayer timestamps etc in the future * if the need arises. * */ @WorkerThread open suspend fun extractorVerifierJob(extractorData: String?) { throw NotImplementedError() } /**Callback is fired once a link is found, will return true if method is executed successfully*/ @WorkerThread open suspend fun loadLinks( data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ): Boolean { throw NotImplementedError() } } /** Might need a different implementation for desktop*/ @SuppressLint("NewApi") fun base64Decode(string: String): String { return String(base64DecodeArray(string), Charsets.ISO_8859_1) } @SuppressLint("NewApi") fun base64DecodeArray(string: String): ByteArray { return try { android.util.Base64.decode(string, android.util.Base64.DEFAULT) } catch (e: Exception) { Base64.getDecoder().decode(string) } } @SuppressLint("NewApi") fun base64Encode(array: ByteArray): String { return try { String(android.util.Base64.encode(array, android.util.Base64.NO_WRAP), Charsets.ISO_8859_1) } catch (e: Exception) { String(Base64.getEncoder().encode(array)) } } class ErrorLoadingException(message: String? = null) : Exception(message) fun parseRating(ratingString: String?): Int? { if (ratingString == null) return null val floatRating = ratingString.toFloatOrNull() ?: return null return (floatRating * 10).toInt() } fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null } return fixUrl(url) } fun MainAPI.fixUrl(url: String): String { if (url.startsWith("http")) { return url } if (url.isEmpty()) { return "" } val startsWithNoHttp = url.startsWith("//") if (startsWithNoHttp) { return "https:$url" } else { if (url.startsWith('/')) { return mainUrl + url } return "$mainUrl/$url" } } fun sortUrls(urls: Set): List { return urls.sortedBy { t -> -t.quality } } fun sortSubs(subs: Set): List { return subs.sortedBy { it.name } } fun capitalizeString(str: String): String { return capitalizeStringNullable(str) ?: str } fun capitalizeStringNullable(str: String?): String? { if (str == null) return null return try { str.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } catch (e: Exception) { str } } /** https://www.imdb.com/title/tt2861424/ -> tt2861424 */ fun imdbUrlToId(url: String): String? { return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1) ?: Regex("tt[0-9]{5,}").find(url)?.groupValues?.get(0) } fun imdbUrlToIdNullable(url: String?): String? { if (url == null) return null return imdbUrlToId(url) } enum class ProviderType { // When data is fetched from a 3rd party site like imdb MetaProvider, // When all data is from the site DirectProvider, } enum class VPNStatus { None, MightBeNeeded, Torrent, } enum class ShowStatus { Completed, Ongoing, } enum class DubStatus { Dubbed, Subbed, } enum class TvType { Movie, AnimeMovie, TvSeries, Cartoon, Anime, OVA, Torrent, Documentary, } // IN CASE OF FUTURE ANIME MOVIE OR SMTH fun TvType.isMovieType(): Boolean { return this == TvType.Movie || this == TvType.AnimeMovie || this == TvType.Torrent } // returns if the type has an anime opening fun TvType.isAnimeOp(): Boolean { return this == TvType.Anime || this == TvType.OVA } data class SubtitleFile(val lang: String, val url: String) class HomePageResponse( val items: List ) class HomePageList( val name: String, var list: List ) enum class SearchQuality { //https://en.wikipedia.org/wiki/Pirated_movie_release_types Cam, CamRip, HdCam, Telesync, // TS WorkPrint, Telecine, // TC HQ, HD, BlueRay, DVD, } interface SearchResponse { val name: String val url: String val apiName: String var type: TvType? var posterUrl: String? var id: Int? var quality : SearchQuality? } enum class ActorRole { Main, Supporting, Background, } data class Actor( val name: String, val image: String? = null, ) data class ActorData( val actor: Actor, val role: ActorRole? = null, val roleString: String? = null, val voiceActor: Actor? = null, ) data class AnimeSearchResponse( override val name: String, override val url: String, override val apiName: String, override var type: TvType?, override var posterUrl: String?, val year: Int? = null, val dubStatus: EnumSet? = null, val otherName: String? = null, val dubEpisodes: Int? = null, val subEpisodes: Int? = null, override var id: Int? = null, override var quality: SearchQuality? = null, ) : SearchResponse data class TorrentSearchResponse( override val name: String, override val url: String, override val apiName: String, override var type: TvType?, override var posterUrl: String?, override var id: Int? = null, override var quality: SearchQuality? = null, ) : SearchResponse data class MovieSearchResponse( override val name: String, override val url: String, override val apiName: String, override var type: TvType?, override var posterUrl: String?, val year: Int? = null, override var id: Int? = null, override var quality: SearchQuality? = null, ) : SearchResponse data class TvSeriesSearchResponse( override val name: String, override val url: String, override val apiName: String, override var type: TvType?, override var posterUrl: String?, val year: Int?, val episodes: Int?, override var id: Int? = null, override var quality: SearchQuality? = null, ) : SearchResponse interface LoadResponse { val name: String val url: String val apiName: String val type: TvType val posterUrl: String? val year: Int? val plot: String? val rating: Int? // 1-1000 val tags: List? var duration: Int? // in minutes val trailerUrl: String? var recommendations: List? var actors: List? var comingSoon: Boolean companion object { @JvmName("addActorNames") fun LoadResponse.addActors(actors: List?) { this.actors = actors?.map { ActorData(Actor(it)) } } @JvmName("addActors") fun LoadResponse.addActors(actors: List>?) { this.actors = actors?.map { (actor, role) -> ActorData(actor, roleString = role) } } @JvmName("addActorsRole") fun LoadResponse.addActors(actors: List>?) { this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) } } @JvmName("addActorsOnly") fun LoadResponse.addActors(actors: List?) { this.actors = actors?.map { actor -> ActorData(actor) } } fun LoadResponse.setDuration(input: String?) { val cleanInput = input?.trim()?.replace(" ", "") ?: return Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values -> if (values.size == 3) { val hours = values[1].toIntOrNull() val minutes = values[2].toIntOrNull() this.duration = if (minutes != null && hours != null) { hours * 60 + minutes } else null if (this.duration != null) return } } Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values -> if (values.size == 2) { this.duration = values[1].toIntOrNull() if (this.duration != null) return } } } } } fun LoadResponse?.isEpisodeBased(): Boolean { if (this == null) return false return (this is AnimeLoadResponse || this is TvSeriesLoadResponse) && this.type.isEpisodeBased() } fun LoadResponse?.isAnimeBased(): Boolean { if (this == null) return false return (this.type == TvType.Anime || this.type == TvType.OVA) // && (this is AnimeLoadResponse) } fun TvType?.isEpisodeBased(): Boolean { if (this == null) return false return (this == TvType.TvSeries || this == TvType.Anime) } data class AnimeEpisode( val url: String, var name: String? = null, var posterUrl: String? = null, var date: String? = null, var rating: Int? = null, var description: String? = null, var episode: Int? = null, ) data class TorrentLoadResponse( override var name: String, override var url: String, override var apiName: String, var magnet: String?, var torrent: String?, override var plot: String?, override var type: TvType = TvType.Torrent, override var posterUrl: String? = null, override var year: Int? = null, override var rating: Int? = null, override var tags: List? = null, override var duration: Int? = null, override var trailerUrl: String? = null, override var recommendations: List? = null, override var actors: List? = null, override var comingSoon: Boolean = false, ) : LoadResponse data class AnimeLoadResponse( var engName: String? = null, var japName: String? = null, override var name: String, override var url: String, override var apiName: String, override var type: TvType, override var posterUrl: String? = null, override var year: Int? = null, var episodes: HashMap> = hashMapOf(), var showStatus: ShowStatus? = null, override var plot: String? = null, override var tags: List? = null, var synonyms: List? = null, var malId: Int? = null, var anilistId: Int? = null, override var rating: Int? = null, override var duration: Int? = null, override var trailerUrl: String? = null, override var recommendations: List? = null, override var actors: List? = null, override var comingSoon: Boolean = false, ) : LoadResponse fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List?) { if (episodes == null) return this.episodes[status] = episodes } fun MainAPI.newAnimeLoadResponse( name: String, url: String, type: TvType, comingSoonIfNone : Boolean, initializer: AnimeLoadResponse.() -> Unit = { }, ): AnimeLoadResponse { val builder = AnimeLoadResponse(name = name, url = url, apiName = this.name, type = type) builder.initializer() if(comingSoonIfNone) { builder.comingSoon = true for (key in builder.episodes.keys) if(!builder.episodes[key].isNullOrEmpty()) { builder.comingSoon = false break } } return builder } fun MainAPI.newAnimeLoadResponse( name: String, url: String, type: TvType, initializer: AnimeLoadResponse.() -> Unit = { }, ): AnimeLoadResponse { return newAnimeLoadResponse(name, url, type, true, initializer) } data class MovieLoadResponse( override var name: String, override var url: String, override var apiName: String, override var type: TvType, var dataUrl: String, override var posterUrl: String? = null, override var year: Int? = null, override var plot: String? = null, var imdbId: String? = null, override var rating: Int? = null, override var tags: List? = null, override var duration: Int? = null, override var trailerUrl: String? = null, override var recommendations: List? = null, override var actors: List? = null, override var comingSoon: Boolean = false, ) : LoadResponse fun MainAPI.newMovieLoadResponse( name: String, url: String, type: TvType, dataUrl: String, initializer: MovieLoadResponse.() -> Unit = { } ): MovieLoadResponse { val builder = MovieLoadResponse( name = name, url = url, apiName = this.name, type = type, dataUrl = dataUrl, comingSoon = dataUrl.isBlank() ) builder.initializer() return builder } data class TvSeriesEpisode( val name: String? = null, val season: Int? = null, val episode: Int? = null, val data: String, val posterUrl: String? = null, val date: String? = null, val rating: Int? = null, val description: String? = null, ) data class TvSeriesLoadResponse( override var name: String, override var url: String, override var apiName: String, override var type: TvType, var episodes: List, override var posterUrl: String? = null, override var year: Int? = null, override var plot: String? = null, var showStatus: ShowStatus? = null, var imdbId: String? = null, override var rating: Int? = null, override var tags: List? = null, override var duration: Int? = null, override var trailerUrl: String? = null, override var recommendations: List? = null, override var actors: List? = null, override var comingSoon: Boolean = false, ) : LoadResponse fun MainAPI.newTvSeriesLoadResponse( name: String, url: String, type: TvType, episodes: List, initializer: TvSeriesLoadResponse.() -> Unit = { } ): TvSeriesLoadResponse { val builder = TvSeriesLoadResponse( name = name, url = url, apiName = this.name, type = type, episodes = episodes, comingSoon = episodes.isEmpty(), ) builder.initializer() return builder } fun fetchUrls(text: String?): List { if (text.isNullOrEmpty()) { return listOf() } val linkRegex = Regex("""(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*))""") return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList() }