repo.json

force ci

button to force ci

No build yet

branch build is here

Test update

homepage test

NaN for mainpage

test1

fallback

Homepage1

search 1

Add type in search

load part1

load part2

fix sourceIds

fix document + import jsoup

force movie type

debug data str

Enable logs

Log fix

import qulities

custom logging

log1

sourceIds debug 1

force type of sourceIds

array element

fix map sourceIds

parse links

fix json struct

Fix JSOn struct

Update json

json1

json2

json3

json4

json5

data was string instead of list

wrong url

extractor 1

update callback

extractor2

extract

doodre

var

new var

episode season

episodes logging

mixdrop fix

upstream v1

Fix package

Fix log ref

upstream first

regex upstr

fix

conflict

confl2

conf3

conf5

conf6

conf7

fix regex

adding more plugins

Adding code from repositories

Limit build

aniemtv

Second build

Add kotlinx

Enabling two providers

Anywave

Aniwave

Update

more

fix

Cronch

KronchEN

Superstream with secrets

env

perms

perm read

Create test.yml

Update test.yml

Update test.yml

Update test.yml

set env

env23

write implies read

Upgrade
master
Swissky 2024-06-22 00:07:00 +02:00
parent 9859878455
commit 5563a4aa9d
54 changed files with 11192 additions and 52 deletions

View File

@ -6,17 +6,22 @@ concurrency:
cancel-in-progress: true
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
push:
branches:
# choose your default branch
- master
- main
paths-ignore:
- '*.md'
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
environment: env
steps:
- name: Checkout
uses: actions/checkout@master
@ -40,6 +45,35 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Access Secrets
env:
TMDB_API: ${{ secrets.TMDB_API }}
DUMP_API: ${{ secrets.DUMP_API }}
DUMP_KEY: ${{ secrets.DUMP_KEY }}
CRUNCHYROLL_BASIC_TOKEN: ${{ secrets.CRUNCHYROLL_BASIC_TOKEN }}
CRUNCHYROLL_REFRESH_TOKEN: ${{ secrets.CRUNCHYROLL_REFRESH_TOKEN }}
SFMOVIES_API: ${{ secrets.SFMOVIES_API }}
CINEMATV_API: ${{ secrets.CINEMATV_API }}
GHOSTX_API: ${{ secrets.GHOSTX_API }}
SUPERSTREAM_FIRST_API: ${{ secrets.SUPERSTREAM_FIRST_API }}
SUPERSTREAM_SECOND_API: ${{ secrets.SUPERSTREAM_SECOND_API }}
SUPERSTREAM_THIRD_API: ${{ secrets.SUPERSTREAM_THIRD_API }}
SUPERSTREAM_FOURTH_API: ${{ secrets.SUPERSTREAM_FOURTH_API }}
run: |
cd $GITHUB_WORKSPACE/src
echo TMDB_API=$TMDB_API >> local.properties
echo DUMP_API=$DUMP_API >> local.properties
echo DUMP_KEY=$DUMP_KEY >> local.properties
echo CRUNCHYROLL_BASIC_TOKEN=$CRUNCHYROLL_BASIC_TOKEN >> local.properties
echo CRUNCHYROLL_REFRESH_TOKEN=$CRUNCHYROLL_REFRESH_TOKEN >> local.properties
echo SFMOVIES_API=$SFMOVIES_API >> local.properties
echo CINEMATV_API=$CINEMATV_API >> local.properties
echo GHOSTX_API=$GHOSTX_API >> local.properties
echo SUPERSTREAM_FIRST_API=$SUPERSTREAM_FIRST_API >> local.properties
echo SUPERSTREAM_SECOND_API=$SUPERSTREAM_SECOND_API >> local.properties
echo SUPERSTREAM_THIRD_API=$SUPERSTREAM_THIRD_API >> local.properties
echo SUPERSTREAM_FOURTH_API=$SUPERSTREAM_FOURTH_API >> local.properties
- name: Build Plugins
run: |
cd $GITHUB_WORKSPACE/src

5
.gitignore vendored
View File

@ -9,4 +9,7 @@
.externalNativeBuild
.cxx
local.properties
.vscode
.vscode
/NOTES
NOTES.md
scrapping.py

307
9anime/9anime.kt Normal file
View File

@ -0,0 +1,307 @@
package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.Jsoup
import java.util.*
class NineAnimeProvider : MainAPI() {
override var mainUrl = "https://9anime.id"
override var name = "9Anime"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(TvType.Anime)
companion object {
fun getDubStatus(title: String): DubStatus {
return if (title.contains("(dub)", ignoreCase = true)) {
DubStatus.Dubbed
} else {
DubStatus.Subbed
}
}
}
override suspend fun getMainPage(): HomePageResponse {
val items = listOf(
Pair("$mainUrl/ajax/home/widget?name=trending", "Trending"),
Pair("$mainUrl/ajax/home/widget?name=updated_all", "All"),
Pair("$mainUrl/ajax/home/widget?name=updated_sub&page=1", "Recently Updated (SUB)"),
Pair(
"$mainUrl/ajax/home/widget?name=updated_dub&page=1",
"Recently Updated (DUB)(DUB)"
),
Pair(
"$mainUrl/ajax/home/widget?name=updated_chinese&page=1",
"Recently Updated (Chinese)"
),
Pair("$mainUrl/ajax/home/widget?name=random", "Random"),
).apmap { (url, name) ->
val home = Jsoup.parse(
app.get(
url
).mapped<Response>().html
).select("ul.anime-list li").map {
val title = it.selectFirst("a.name").text()
val link = it.selectFirst("a").attr("href")
val poster = it.selectFirst("a.poster img").attr("src")
newAnimeSearchResponse(title, link) {
this.posterUrl = poster
addDubStatus(getDubStatus(title))
}
}
HomePageList(name, home)
}
return HomePageResponse(items)
}
//Credits to https://github.com/jmir1
private val key = "0wMrYU+ixjJ4QdzgfN2HlyIVAt3sBOZnCT9Lm7uFDovkb/EaKpRWhqXS5168ePcG"
private fun getVrf(id: String): String? {
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
return reversed + ue(je(reversed, encode(id) ?: return null)).replace(
"""=+$""".toRegex(),
""
)
}
private fun getLink(url: String): String? {
val i = url.slice(0..5)
val n = url.slice(6..url.lastIndex)
return decode(je(i, ze(n)))
}
private fun ue(input: String): String {
if (input.any { it.code >= 256 }) throw Exception("illegal characters!")
var output = ""
for (i in input.indices step 3) {
val a = intArrayOf(-1, -1, -1, -1)
a[0] = input[i].code shr 2
a[1] = (3 and input[i].code) shl 4
if (input.length > i + 1) {
a[1] = a[1] or (input[i + 1].code shr 4)
a[2] = (15 and input[i + 1].code) shl 2
}
if (input.length > i + 2) {
a[2] = a[2] or (input[i + 2].code shr 6)
a[3] = 63 and input[i + 2].code
}
for (n in a) {
if (n == -1) output += "="
else {
if (n in 0..63) output += key[n]
}
}
}
return output;
}
private fun je(inputOne: String, inputTwo: String): String {
val arr = IntArray(256) { it }
var output = ""
var u = 0
var r: Int
for (a in arr.indices) {
u = (u + arr[a] + inputOne[a % inputOne.length].code) % 256
r = arr[a]
arr[a] = arr[u]
arr[u] = r
}
u = 0
var c = 0
for (f in inputTwo.indices) {
c = (c + f) % 256
u = (u + arr[c]) % 256
r = arr[c]
arr[c] = arr[u]
arr[u] = r
output += (inputTwo[f].code xor arr[(arr[c] + arr[u]) % 256]).toChar()
}
return output
}
private fun ze(input: String): String {
val t = if (input.replace("""[\t\n\f\r]""".toRegex(), "").length % 4 == 0) {
input.replace(Regex("""/==?$/"""), "")
} else input
if (t.length % 4 == 1 || t.contains(Regex("""[^+/0-9A-Za-z]"""))) throw Exception("bad input")
var i: Int
var r = ""
var e = 0
var u = 0
for (o in t.indices) {
e = e shl 6
i = key.indexOf(t[o])
e = e or i
u += 6
if (24 == u) {
r += ((16711680 and e) shr 16).toChar()
r += ((65280 and e) shr 8).toChar()
r += (255 and e).toChar()
e = 0
u = 0
}
}
return if (12 == u) {
e = e shr 4
r + e.toChar()
} else {
if (18 == u) {
e = e shr 2
r += ((65280 and e) shr 8).toChar()
r += (255 and e).toChar()
}
r
}
}
private fun encode(input: String): String? = java.net.URLEncoder.encode(input, "utf-8")
private fun decode(input: String): String? = java.net.URLDecoder.decode(input, "utf-8")
override suspend fun search(query: String): List<SearchResponse> {
val url = "$mainUrl/filter?sort=title%3Aasc&keyword=$query"
return app.get(url).document.select("ul.anime-list li").mapNotNull {
val title = it.selectFirst("a.name").text()
val href =
fixUrlNull(it.selectFirst("a").attr("href"))?.replace(Regex("(\\?ep=(\\d+)\$)"), "")
?: return@mapNotNull null
val image = it.selectFirst("a.poster img").attr("src")
AnimeSearchResponse(
title,
href,
this.name,
TvType.Anime,
image,
null,
if (title.contains("(DUB)") || title.contains("(Dub)")) EnumSet.of(
DubStatus.Dubbed
) else EnumSet.of(DubStatus.Subbed),
)
}
}
data class Response(
@JsonProperty("html") val html: String
)
override suspend fun load(url: String): LoadResponse? {
val validUrl = url.replace("https://9anime.to", mainUrl)
val doc = app.get(validUrl).document
val animeid = doc.selectFirst("div.player-wrapper.watchpage").attr("data-id") ?: return null
val animeidencoded = encode(getVrf(animeid) ?: return null)
val poster = doc.selectFirst("aside.main div.thumb div img").attr("src")
val title = doc.selectFirst(".info .title").text()
val description = doc.selectFirst("div.info p").text().replace("Ver menos", "").trim()
val episodes = Jsoup.parse(
app.get(
"$mainUrl/ajax/anime/servers?ep=1&id=${animeid}&vrf=$animeidencoded&ep=8&episode=&token="
).mapped<Response>().html
)?.select("ul.episodes li a")?.mapNotNull {
val link = it?.attr("href") ?: return@mapNotNull null
val name = "Episode ${it.text()}"
Episode(link, name)
} ?: return null
val recommendations =
doc.select("div.container aside.main section div.body ul.anime-list li")
?.mapNotNull { element ->
val recTitle = element.select("a.name")?.text() ?: return@mapNotNull null
val image = element.select("a.poster img")?.attr("src")
val recUrl = fixUrl(element.select("a").attr("href"))
newAnimeSearchResponse(recTitle, recUrl) {
this.posterUrl = image
addDubStatus(getDubStatus(recTitle))
}
}
val infodoc = doc.selectFirst("div.info .meta .col1").text()
val tvType = if (infodoc.contains("Movie")) TvType.AnimeMovie else TvType.Anime
val status =
if (infodoc.contains("Completed")) ShowStatus.Completed
else if (infodoc.contains("Airing")) ShowStatus.Ongoing
else null
val tags = doc.select("div.info .meta .col1 div:contains(Genre) a").map { it.text() }
return newAnimeLoadResponse(title, validUrl, tvType) {
this.posterUrl = poster
this.plot = description
this.recommendations = recommendations
this.showStatus = status
this.tags = tags
addEpisodes(DubStatus.Subbed, episodes)
}
}
data class Links(
@JsonProperty("url") val url: String
)
data class Servers(
@JsonProperty("28") val mcloud: String?,
@JsonProperty("35") val mp4upload: String?,
@JsonProperty("40") val streamtape: String?,
@JsonProperty("41") val vidstream: String?,
@JsonProperty("43") val videovard: String?
)
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val document = app.get(data).document
val animeid =
document.selectFirst("div.player-wrapper.watchpage").attr("data-id") ?: return false
val animeidencoded = encode(getVrf(animeid) ?: return false)
Jsoup.parse(
app.get(
"$mainUrl/ajax/anime/servers?&id=${animeid}&vrf=$animeidencoded&episode=&token="
).mapped<Response>().html
).select("div.body").map { element ->
val jsonregex = Regex("(\\{.+\\}.*$data)")
val servers = jsonregex.find(element.toString())?.value?.replace(
Regex("(\".*data-base=.*href=\"$data)"),
""
)?.replace("&quot;", "\"") ?: return@map
val jsonservers = parseJson<Servers?>(servers) ?: return@map
listOfNotNull(
jsonservers.vidstream,
jsonservers.mcloud,
jsonservers.mp4upload,
jsonservers.streamtape
).mapNotNull {
try {
val epserver = app.get("$mainUrl/ajax/anime/episode?id=$it").text
(if (epserver.contains("url")) {
parseJson<Links>(epserver)
} else null)?.url?.let { it1 -> getLink(it1.replace("=", "")) }
?.replace("/embed/", "/e/")
} catch (e: Exception) {
logError(e)
null
}
}.apmap { url ->
loadExtractor(
url, data, callback
)
}
}
return true
}
}

41
Aniwave/build.gradle.kts Normal file
View File

@ -0,0 +1,41 @@
// use an integer for version numbers
version = 57
cloudstream {
language = "en"
// All of these properties are optional, you can safely remove them
description = "Watch Aniwave/9anime, I have had reports saying homepage doesn't work the first time but retrying should fix it"
authors = listOf("RowdyRushya, Horis, Stormunblessed, KillerDogeEmpire, Enimax, Chokerman")
/**
* Status int as the following:
* 0: Down
* 1: Ok
* 2: Slow
* 3: Beta only
* */
status = 1 // will be 3 if unspecified
tvTypes = listOf(
"Anime",
"OVA",
)
iconUrl = "https://www.google.com/s2/favicons?domain=aniwave.to&sz=%size%"
requiresResources = true
}
dependencies {
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation("androidx.preference:preference:1.2.1")
}
android {
buildFeatures {
viewBinding = true
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.RowdyAvocado" />

View File

@ -0,0 +1,475 @@
package com.RowdyAvocado
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.Episode
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.HomePageResponse
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.SeasonData
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.addDubStatus
import com.lagradost.cloudstream3.addEpisodes
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.extractors.AnyVidplay
import com.lagradost.cloudstream3.fixUrl
import com.lagradost.cloudstream3.mainPageOf
import com.lagradost.cloudstream3.newAnimeLoadResponse
import com.lagradost.cloudstream3.newAnimeSearchResponse
import com.lagradost.cloudstream3.newEpisode
import com.lagradost.cloudstream3.newHomePageResponse
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.JsUnpacker
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor
import java.net.URLEncoder
import kotlinx.coroutines.delay
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@OptIn(kotlin.ExperimentalStdlibApi::class)
class Aniwave : MainAPI() {
override var mainUrl = AniwavePlugin.currentAniwaveServer
override var name = "Aniwave/9Anime"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedSyncNames = setOf(SyncIdName.Anilist, SyncIdName.MyAnimeList)
override val supportedTypes = setOf(TvType.Anime)
override val hasQuickSearch = true
companion object {
var keys: Pair<String, String>? = null
fun encode(input: String): String =
java.net.URLEncoder.encode(input, "utf-8").replace("+", "%2B")
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
}
private suspend fun getKeys(): Pair<String, String> {
if (keys == null) {
val res =
app.get("https://rowdy-avocado.github.io/multi-keys/").parsedSafe<Keys>()
?: throw Exception("Unable to fetch keys")
keys = res.keys.first() to res.keys.last()
}
return keys!!
}
private fun Element.toSearchResponse(): SearchResponse? {
val title = this.selectFirst(".info .name") ?: return null
val link = title.attr("href").replace(Regex("/ep.*\$"), "")
val poster = this.selectFirst(".poster > a > img")?.attr("src")
val meta = this.selectFirst(".poster > a > .meta > .inner > .left")
val subbedEpisodes = meta?.selectFirst(".sub")?.text()?.toIntOrNull()
val dubbedEpisodes = meta?.selectFirst(".dub")?.text()?.toIntOrNull()
return newAnimeSearchResponse(title.text() ?: return null, link) {
this.posterUrl = poster
addDubStatus(
dubbedEpisodes != null,
subbedEpisodes != null,
dubbedEpisodes,
subbedEpisodes
)
}
}
override val mainPage =
mainPageOf(
"$mainUrl/ajax/home/widget/trending?page=" to "Trending",
"$mainUrl/ajax/home/widget/updated-all?page=" to "All",
"$mainUrl/ajax/home/widget/updated-sub?page=" to "Recently Updated (SUB)",
"$mainUrl/ajax/home/widget/updated-dub?page=" to "Recently Updated (DUB)",
"$mainUrl/ajax/home/widget/updated-china?page=" to "Recently Updated (Chinese)",
"$mainUrl/ajax/home/widget/random?page=" to "Random",
)
override suspend fun quickSearch(query: String): List<SearchResponse> {
delay(1000)
return search(query)
}
override suspend fun search(query: String): List<SearchResponse> {
val url = "$mainUrl/filter?keyword=${query}"
return app.get(url).document.select("#list-items div.inner:has(div.poster)").mapNotNull {
it.toSearchResponse()
}
}
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
val url = request.data + page
val res = app.get(url).parsed<Response>()
if (!res.status.equals(200)) throw ErrorLoadingException("Could not connect to the server")
val home = res.getHtml().select("div.item").mapNotNull { it.toSearchResponse() }
return newHomePageResponse(request.name, home, true)
}
override suspend fun load(url: String): LoadResponse {
val doc = app.get(url).document
val meta = doc.selectFirst("#w-info") ?: throw ErrorLoadingException("Could not find info")
val ratingElement = meta.selectFirst(".brating > #w-rating")
val id = ratingElement?.attr("data-id") ?: throw ErrorLoadingException("Could not find id")
val binfo =
meta.selectFirst(".binfo") ?: throw ErrorLoadingException("Could not find binfo")
val info = binfo.selectFirst(".info") ?: throw ErrorLoadingException("Could not find info")
val poster = binfo.selectFirst(".poster > span > img")?.attr("src")
val backimginfo = doc.selectFirst("#player")?.attr("style")
val backimgRegx = Regex("(http|https).*jpg")
val backposter = backimgRegx.find(backimginfo.toString())?.value ?: poster
val title =
(info.selectFirst(".title") ?: info.selectFirst(".d-title"))?.text()
?: throw ErrorLoadingException("Could not find title")
val vrf = AniwaveUtils.vrfEncrypt(getKeys().first, id)
val episodeListUrl = "$mainUrl/ajax/episode/list/$id?$vrf"
val body =
app.get(episodeListUrl).parsedSafe<Response>()?.getHtml()
?: throw ErrorLoadingException(
"Could not parse json with Vrf=$vrf id=$id url=\n$episodeListUrl"
)
val subEpisodes = ArrayList<Episode>()
val dubEpisodes = ArrayList<Episode>()
val softsubeps = ArrayList<Episode>()
val uncensored = ArrayList<Episode>()
val genres =
doc.select("div.meta:nth-child(1) > div:contains(Genres:) a").mapNotNull {
it.text()
}
val recss =
doc.select("div#watch-second .w-side-section div.body a.item").mapNotNull { rec ->
val href = rec.attr("href")
val rectitle = rec.selectFirst(".name")?.text() ?: ""
val recimg = rec.selectFirst("img")?.attr("src")
newAnimeSearchResponse(rectitle, fixUrl(href)) { this.posterUrl = recimg }
}
val status =
when (doc.selectFirst("div.meta:nth-child(1) > div:contains(Status:) span")?.text()
) {
"Releasing" -> ShowStatus.Ongoing
"Completed" -> ShowStatus.Completed
else -> null
}
val typetwo =
when (doc.selectFirst("div.meta:nth-child(1) > div:contains(Type:) span")?.text()) {
"OVA" -> TvType.OVA
"SPECIAL" -> TvType.OVA
else -> TvType.Anime
}
val duration = doc.selectFirst(".bmeta > div > div:contains(Duration:) > span")?.text()
body.select(".episodes > ul > li > a").apmap { element ->
val ids = element.attr("data-ids").split(",", limit = 3)
val dataDub = element.attr("data-dub").toIntOrNull()
val epNum = element.attr("data-num").toIntOrNull()
val epTitle = element.selectFirst("span.d-title")?.text()
val isUncen = element.attr("data-slug").contains("uncen")
if (ids.size > 0) {
if (isUncen) {
ids.getOrNull(0)?.let { uncen ->
val epdd = "{\"ID\":\"$uncen\",\"type\":\"sub\"}"
uncensored.add(
newEpisode(epdd) {
this.episode = epNum
this.name = epTitle
this.season = -4
}
)
}
} else {
if (ids.size == 1 && dataDub == 1) {
ids.getOrNull(0)?.let { dub ->
val epdd = "{\"ID\":\"$dub\",\"type\":\"dub\"}"
dubEpisodes.add(
newEpisode(epdd) {
this.episode = epNum
this.name = epTitle
this.season = -2
}
)
}
} else {
ids.getOrNull(0)?.let { sub ->
val epdd = "{\"ID\":\"$sub\",\"type\":\"sub\"}"
subEpisodes.add(
newEpisode(epdd) {
this.episode = epNum
this.name = epTitle
this.season = -1
}
)
}
}
}
if (ids.size > 1) {
if (dataDub == 0 || ids.size > 2) {
ids.getOrNull(1)?.let { softsub ->
val epdd = "{\"ID\":\"$softsub\",\"type\":\"softsub\"}"
softsubeps.add(
newEpisode(epdd) {
this.episode = epNum
this.name = epTitle
this.season = -3
}
)
}
} else {
ids.getOrNull(1)?.let { dub ->
val epdd = "{\"ID\":\"$dub\",\"type\":\"dub\"}"
dubEpisodes.add(
newEpisode(epdd) {
this.episode = epNum
this.name = epTitle
this.season = -2
}
)
}
}
if (ids.size > 2) {
ids.getOrNull(2)?.let { dub ->
val epdd = "{\"ID\":\"$dub\",\"type\":\"dub\"}"
dubEpisodes.add(
newEpisode(epdd) {
this.episode = epNum
this.name = epTitle
this.season = -2
}
)
}
}
}
}
}
// season -1 HARDSUBBED
// season -2 Dubbed
// Season -3 SofSubbed
val names =
listOf(
Pair("Sub", -1),
Pair("Dub", -2),
Pair("S-Sub", -3),
Pair("Uncensored", -4),
)
// Reading info from web page to fetch anilistData
val titleRomaji =
(info.selectFirst(".title") ?: info.selectFirst(".d-title"))?.attr("data-jp") ?: ""
val premieredDetails =
info.select(".bmeta > .meta > div")
.find { it.text().contains("Premiered: ", true) }
?.selectFirst("span > a")
?.text()
?.split(" ")
val season = premieredDetails?.get(0).toString()
val year = premieredDetails?.get(1)?.toInt() ?: 0
return newAnimeLoadResponse(title, url, TvType.Anime) {
addEpisodes(DubStatus.Subbed, dubEpisodes)
addEpisodes(DubStatus.Subbed, subEpisodes)
addEpisodes(DubStatus.Subbed, softsubeps)
addEpisodes(DubStatus.Subbed, uncensored)
this.seasonNames = names.map { (name, int) -> SeasonData(int, name) }
plot = info.selectFirst(".synopsis > .shorting > .content")?.text()
this.posterUrl = poster
rating = ratingElement.attr("data-score").toFloat().times(1000f).toInt()
this.backgroundPosterUrl = backposter
this.tags = genres
this.recommendations = recss
this.showStatus = status
if (AniwavePlugin.aniwaveSimklSync)
addAniListId(aniAPICall(AniwaveUtils.aniQuery(titleRomaji, year, season))?.id)
else this.type = typetwo
addDuration(duration)
}
}
private fun serverName(serverID: String?): String? {
val sss =
when (serverID) {
"41" -> "vidplay"
"44" -> "filemoon"
"40" -> "streamtape"
"35" -> "mp4upload"
"28" -> "MyCloud"
else -> null
}
return sss
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val parseData = AppUtils.parseJson<SubDubInfo>(data)
val datavrf = AniwaveUtils.vrfEncrypt(getKeys().first, parseData.ID)
val one = app.get("$mainUrl/ajax/server/list/${parseData.ID}?$datavrf").parsed<Response>()
val two = one.getHtml()
val aas =
two.select("div.servers .type[data-type=${parseData.type}] li").mapNotNull {
val datalinkId = it.attr("data-link-id")
val serverID = it.attr("data-sv-id").toString()
val newSname = serverName(serverID)
Pair(newSname, datalinkId)
}
aas.amap { (sName, sId) ->
try {
val vrf = AniwaveUtils.vrfEncrypt(getKeys().first, sId)
val videncrr = app.get("$mainUrl/ajax/server/$sId?$vrf").parsed<Links>()
val encUrl = videncrr.result?.url ?: return@amap
val asss = AniwaveUtils.vrfDecrypt(getKeys().second, encUrl)
if (sName.equals("filemoon")) {
val res = app.get(asss)
if (res.code == 200) {
val packedJS =
res.document
.selectFirst("script:containsData(function(p,a,c,k,e,d))")
?.data()
.toString()
JsUnpacker(packedJS).unpack().let { unPacked ->
Regex("sources:\\[\\{file:\"(.*?)\"")
.find(unPacked ?: "")
?.groupValues
?.get(1)
?.let { link ->
callback.invoke(
ExtractorLink(
"Filemoon",
"Filemoon",
link,
"",
Qualities.Unknown.value,
link.contains(".m3u8")
)
)
}
}
}
} else if (sName.equals("vidplay")) {
val host = AniwaveUtils.getBaseUrl(asss)
AnyVidplay(host).getUrl(asss, host, subtitleCallback, callback)
} else loadExtractor(asss, subtitleCallback, callback)
} catch (e: Exception) {}
}
return true
}
override suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
val syncId = id.split("/").last()
// formatting the JSON response to search on aniwave site
val anilistData = aniAPICall(AniwaveUtils.aniQuery(name, syncId.toInt()))
val title = anilistData?.title?.romaji ?: anilistData?.title?.english
val year = anilistData?.year
val season = anilistData?.season
val searchUrl =
"$mainUrl/filter?keyword=${title}&year%5B%5D=${year}&season%5B%5D=unknown&season%5B%5D=${season?.lowercase()}&sort=recently_updated"
// searching the anime on aniwave site using advance filter and capturing the url from
// search result
val document = app.get(searchUrl).document
val syncUrl =
document.select("#list-items div.info div.b1 > a")
.find { it.attr("data-jp").equals(title, true) }
?.attr("href")
return fixUrl(syncUrl ?: return null)
}
private suspend fun aniAPICall(query: String): Media? {
// Fetching data using POST method
val url = "https://graphql.anilist.co"
val res =
app.post(
url,
headers =
mapOf(
"Accept" to "application/json",
"Content-Type" to "application/json",
),
data =
mapOf(
"query" to query,
)
)
.parsedSafe<SyncInfo>()
return res?.data?.media
}
// JSON formatter for data fetched from anilistApi
data class SyncTitle(
@JsonProperty("romaji") val romaji: String? = null,
@JsonProperty("english") val english: String? = null,
)
data class Media(
@JsonProperty("title") val title: SyncTitle? = null,
@JsonProperty("id") val id: Int? = null,
@JsonProperty("idMal") val idMal: Int? = null,
@JsonProperty("season") val season: String? = null,
@JsonProperty("seasonYear") val year: Int? = null,
)
data class Response(
@JsonProperty("status") val status: Int,
@JsonProperty("result") val result: String
) {
fun getHtml(): Document {
return Jsoup.parse(result)
}
}
data class AniwaveMediaInfo(
@JsonProperty("result") val result: AniwaveResult? = AniwaveResult()
)
data class AniwaveResult(
@JsonProperty("sources") var sources: ArrayList<AniwaveTracks> = arrayListOf(),
@JsonProperty("tracks") var tracks: ArrayList<AniwaveTracks> = arrayListOf()
)
data class AniwaveTracks(
@JsonProperty("file") var file: String? = null,
@JsonProperty("label") var label: String? = null,
)
data class Data(
@JsonProperty("Media") val media: Media? = null,
)
data class SyncInfo(
@JsonProperty("data") val data: Data? = null,
)
data class Result(@JsonProperty("url") val url: String? = null)
data class Links(@JsonProperty("result") val result: Result? = null)
data class SubDubInfo(
@JsonProperty("ID") val ID: String,
@JsonProperty("type") val type: String
)
data class Keys(@JsonProperty("aniwave") val keys: List<String>)
}

View File

@ -0,0 +1,71 @@
package com.RowdyAvocado
import android.content.Context
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
import com.lagradost.cloudstream3.plugins.Plugin
import com.lagradost.cloudstream3.plugins.PluginManager
enum class ServerList(val link: String) {
TO("https://aniwave.to"),
LI("https://aniwave.li"),
VC("https://aniwave.vc")
}
@CloudstreamPlugin
class AniwavePlugin : Plugin() {
override fun load(context: Context) {
// All providers should be added in this manner. Please don't edit the providers list
// directly.
registerMainAPI(Aniwave())
this.openSettings =
openSettings@{
val manager =
(context.getActivity() as? AppCompatActivity)?.supportFragmentManager
?: return@openSettings
BottomFragment(this).show(manager, "")
}
}
fun reload(context: Context?) {
val pluginData =
PluginManager.getPluginsOnline().find { it.internalName.contains("Aniwave") }
if (pluginData == null) {
PluginManager.hotReloadAllLocalPlugins(context as AppCompatActivity)
} else {
PluginManager.unloadPlugin(pluginData.filePath)
PluginManager.loadAllOnlinePlugins(context!!)
afterPluginsLoadedEvent.invoke(true)
}
}
companion object {
inline fun Handler.postFunction(crossinline function: () -> Unit) {
this.post(
object : Runnable {
override fun run() {
function()
}
}
)
}
var currentAniwaveServer: String
get() = getKey("ANIWAVE_CURRENT_SERVER") ?: ServerList.TO.link
set(value) {
setKey("ANIWAVE_CURRENT_SERVER", value)
}
var aniwaveSimklSync: Boolean
get() = getKey("ANIWAVE_SIMKL_SYNC") ?: false
set(value) {
setKey("ANIWAVE_SIMKL_SYNC", value)
}
}
}

View File

@ -0,0 +1,109 @@
package com.RowdyAvocado
import android.util.Base64
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import java.net.URI
import java.net.URLDecoder
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
@OptIn(kotlin.ExperimentalStdlibApi::class)
object AniwaveUtils {
fun vrfEncrypt(key: String, input: String): String {
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
var vrf = cipher.doFinal(input.toByteArray())
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
vrf = Base64.encode(vrf, Base64.DEFAULT or Base64.NO_WRAP)
vrf = vrfShift(vrf)
// vrf = rot13(vrf)
vrf = vrf.reversed().toByteArray()
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
val stringVrf = vrf.toString(Charsets.UTF_8)
return "vrf=${java.net.URLEncoder.encode(stringVrf, "utf-8")}"
}
fun vrfDecrypt(key: String, input: String): String {
var vrf = input.toByteArray()
vrf = Base64.decode(vrf, Base64.URL_SAFE)
val rc4Key = SecretKeySpec(key.toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
vrf = cipher.doFinal(vrf)
return URLDecoder.decode(vrf.toString(Charsets.UTF_8), "utf-8")
}
private fun rot13(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val byte = vrf[i]
if (byte in 'A'.code..'Z'.code) {
vrf[i] = ((byte - 'A'.code + 13) % 26 + 'A'.code).toByte()
} else if (byte in 'a'.code..'z'.code) {
vrf[i] = ((byte - 'a'.code + 13) % 26 + 'a'.code).toByte()
}
}
return vrf
}
private fun vrfShift(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val shift = arrayOf(-2, -4, -5, 6, 2, -3, 3, 6)[i % 8]
vrf[i] = vrf[i].plus(shift).toByte()
}
return vrf
}
fun aniQuery(name: SyncIdName, id: Int): String {
// creating query for Anilist API using ID
val idType =
when (name) {
SyncIdName.MyAnimeList -> "idMal"
else -> "id"
}
val query =
"""
query {
Media($idType: $id, type: ANIME) {
title {
romaji
english
}
id
idMal
season
seasonYear
}
}
"""
return query
}
fun aniQuery(titleRomaji: String, year: Int, season: String): String {
// creating query for Anilist API using name and other details
val query =
"""
query {
Media(search: "$titleRomaji", season:${season.uppercase()}, seasonYear:$year, type: ANIME) {
title {
romaji
english
}
id
idMal
season
seasonYear
}
}
"""
return query
}
fun getBaseUrl(url: String): String {
return URI(url).let { "${it.scheme}://${it.host}" }
}
}

View File

@ -0,0 +1,117 @@
package com.RowdyAvocado
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Switch
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class BottomFragment(private val plugin: AniwavePlugin) : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val id =
plugin.resources!!.getIdentifier(
"bottom_sheet_layout",
"layout",
BuildConfig.LIBRARY_PACKAGE_NAME
)
val layout = plugin.resources!!.getLayout(id)
val view = inflater.inflate(layout, container, false)
val outlineId =
plugin.resources!!.getIdentifier(
"outline",
"drawable",
BuildConfig.LIBRARY_PACKAGE_NAME
)
// building save button and settings click listener
val saveIconId =
plugin.resources!!.getIdentifier(
"save_icon",
"drawable",
BuildConfig.LIBRARY_PACKAGE_NAME
)
val saveBtn = view.findView<ImageView>("save")
saveBtn.setImageDrawable(plugin.resources!!.getDrawable(saveIconId, null))
saveBtn.background = plugin.resources!!.getDrawable(outlineId, null)
saveBtn.setOnClickListener(
object : OnClickListener {
override fun onClick(btn: View) {
plugin.reload(context)
Toast.makeText(context, "Saved", Toast.LENGTH_SHORT).show()
dismiss()
}
}
)
// building simkl sync switch and settings click listener
val simklSyncSwitch = view.findView<Switch>("simkl_sync")
simklSyncSwitch.isChecked = AniwavePlugin.aniwaveSimklSync
simklSyncSwitch.background = plugin.resources!!.getDrawable(outlineId, null)
simklSyncSwitch.setOnClickListener(
object : OnClickListener {
override fun onClick(btn: View?) {
AniwavePlugin.aniwaveSimklSync = simklSyncSwitch.isChecked
}
}
)
// building server options and settings click listener
val serverGroup = view.findView<RadioGroup>("server_group")
val radioBtnId =
plugin.resources!!.getIdentifier(
"radio_button",
"layout",
BuildConfig.LIBRARY_PACKAGE_NAME
)
ServerList.values().forEach { server ->
val radioBtnLayout = plugin.resources!!.getLayout(radioBtnId)
val radioBtnView = inflater.inflate(radioBtnLayout, container, false)
val radioBtn = radioBtnView.findView<RadioButton>("radio_button")
radioBtn.text = server.link
val newId = View.generateViewId()
radioBtn.id = newId
radioBtn.background = plugin.resources!!.getDrawable(outlineId, null)
radioBtn.setOnClickListener(
object : OnClickListener {
override fun onClick(btn: View?) {
AniwavePlugin.currentAniwaveServer = radioBtn.text.toString()
serverGroup.check(newId)
}
}
)
serverGroup.addView(radioBtnView)
if (AniwavePlugin.currentAniwaveServer.equals(server.link)) serverGroup.check(newId)
}
return view
}
private fun <T : View> View.findView(name: String): T {
val id = plugin.resources!!.getIdentifier(name, "id", BuildConfig.LIBRARY_PACKAGE_NAME)
return this.findViewById(id)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
(dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED
return dialog
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}

View File

@ -0,0 +1,152 @@
package com.RowdyAvocado
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import com.RowdyAvocado.AniwavePlugin.Companion.postFunction
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.utils.Coroutines
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.nicehttp.requestCreator
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
// Credits
// https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsInterceptor.kt
class JsInterceptor(private val serverid: String, private val lang: String) : Interceptor {
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(var payload: String = "") {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
}
}
override fun intercept(chain: Interceptor.Chain): Response {
// val mess = if (serverid == "41") "Vidstream" else if (serverid == "28") "Mcloud" else ""
val request = chain.request()
return runBlocking {
val fixedRequest = resolveWithWebView(request)
return@runBlocking chain.proceed(fixedRequest ?: request)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
fun destroyWebView() {
Coroutines.main {
webView?.stopLoading()
webView?.destroy()
webView = null
println("Destroyed webview")
}
}
// JavaSrcipt gets the Dub or Sub link of vidstream
val jsScript =
"""
(function() {
var click = document.createEvent('MouseEvents');
click.initMouseEvent('click', true, true);
document.querySelector('div[data-type="$lang"] ul li[data-sv-id="$serverid"]').dispatchEvent(click);
})();
"""
val headers =
request.headers
.toMultimap()
.mapValues { it.value.getOrNull(0) ?: "" }
.toMutableMap()
var newRequest: Request? = null
handler.postFunction {
val webview = WebView(context!!)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = USER_AGENT
blockNetworkImage = true
webview.webViewClient =
object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
if (serverid == "41") {
if (!request?.url.toString().contains("vidstream") &&
!request?.url.toString().contains("vizcloud")
)
return null
}
if (serverid == "28") {
if (!request?.url.toString().contains("mcloud")) return null
}
if (request?.url.toString().contains(Regex("list.m3u8|/simple/"))) {
newRequest =
requestCreator(
"GET",
request?.url.toString(),
headers =
mapOf(
"referer" to
"/orp.maertsdiv//:sptth".reversed()
)
)
latch.countDown()
return null
}
return super.shouldInterceptRequest(view, request)
}
override fun onPageFinished(view: WebView?, url: String?) {
view?.evaluateJavascript(jsScript) {}
}
}
webView?.loadUrl(origRequestUrl, headers)
}
}
latch.await(30, TimeUnit.SECONDS)
handler.postFunction {
webView?.stopLoading()
webView?.destroy()
webView = null
// context.let { Toast.makeText(it, "Success!", Toast.LENGTH_SHORT).show()}
}
var loop = 0
val totalTime = 60000L
val delayTime = 100L
while (loop < totalTime / delayTime) {
if (newRequest != null) return newRequest
loop += 1
}
println("Web-view timeout after ${totalTime / 1000}s")
destroyWebView()
return newRequest
}
}

View File

@ -0,0 +1,106 @@
package com.RowdyAvocado
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import com.RowdyAvocado.AniwavePlugin.Companion.postFunction
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
// The following code is extracted from here
// https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/nineanime/src/eu/kanade/tachiyomi/animeextension/en/nineanime/JsVrfInterceptor.kt
// The following code is under the Apache License 2.0
// https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
class JsVrfInterceptor(private val baseUrl: String) {
private val handler by lazy { Handler(Looper.getMainLooper()) }
private val vrfWebView = createWebView()
fun wake() = ""
fun getVrf(query: String): String {
val jscript = getJs(query)
val cdl = CountDownLatch(1)
var vrf = ""
handler.postFunction {
vrfWebView?.evaluateJavascript(jscript) {
vrf = it?.removeSurrounding("\"") ?: ""
cdl.countDown()
}
}
cdl.await(12, TimeUnit.SECONDS)
if (vrf.isBlank()) throw Exception("vrf could not be retrieved")
return vrf
}
@SuppressLint("SetJavaScriptEnabled")
private fun createWebView(): WebView? {
val latch = CountDownLatch(1)
var webView: WebView? = null
handler.postFunction {
val webview = WebView(context!!)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0"
webview.webViewClient =
object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
if (request?.url.toString().contains("$baseUrl/filter")) {
return super.shouldOverrideUrlLoading(view, request)
} else {
// Block the request
return true
}
}
override fun onPageFinished(view: WebView?, url: String?) {
latch.countDown()
}
}
webView?.loadUrl("$baseUrl/filter")
}
}
latch.await()
handler.postFunction { webView?.stopLoading() }
return webView
}
private fun getJs(query: String): String {
return """
(function() {
document.querySelector("form.filters input.form-control").value = '$query';
let inputElemente = document.querySelector('form.filters input.form-control');
let e = document.createEvent('HTMLEvents');
e.initEvent('keyup', true, true);
inputElemente.dispatchEvent(e);
let val = "";
while (val == "") {
let element = document.querySelector('form.filters input[type="hidden"]').value;
if (element) {
val = element;
break;
}
}
document.querySelector("form.filters input.form-control").value = '';
return val;
})();
""".trimIndent()
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape>
<stroke android:width="2dp"
android:color="#FFF" />
<corners
android:bottomLeftRadius="2dp"
android:bottomRightRadius="2dp"
android:topLeftRadius="2dp"
android:topRightRadius="2dp" />
</shape>
</item>
<item android:state_hovered="true">
<shape>
<solid android:color="#99FFFFFF" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@android:color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M21,12.4V7l-4,-4H5C3.89,3 3,3.9 3,5v14c0,1.1 0.89,2 2,2h7.4L21,12.4zM15,15c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3s1.34,-3 3,-3S15,13.34 15,15zM6,6h9v4H6V6zM19.99,16.25l1.77,1.77L16.77,23H15v-1.77L19.99,16.25zM23.25,16.51l-0.85,0.85l-1.77,-1.77l0.85,-0.85c0.2,-0.2 0.51,-0.2 0.71,0l1.06,1.06C23.45,16 23.45,16.32 23.25,16.51z" />
</vector>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutDirection="ltr">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="20dp"
tools:background="@color/cardview_dark_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="20dp">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:text="Aniwave Settings"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<ImageView
android:id="@+id/save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFF"
android:padding="5dp"
android:text="test" />
</LinearLayout>
</LinearLayout>
<ImageView
android:src="@android:drawable/divider_horizontal_dark"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp" />
<Switch
android:id="@+id/simkl_sync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="17sp"
android:padding="5dp"
android:text="Simkl Sync"
android:textStyle="bold" />
<ImageView
android:src="@android:drawable/divider_horizontal_dark"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:text="Aniwave Server"
android:textSize="17sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/server_group"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</RadioGroup>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<RadioButton
android:id="@+id/radio_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingRight="5dp"
android:text="test" />
</LinearLayout>

View File

@ -1,22 +0,0 @@
package com.example
import androidx.appcompat.app.AppCompatActivity
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.SearchResponse
class ExampleProvider(val plugin: TestPlugin) : MainAPI() { // all providers must be an intstance of MainAPI
override var mainUrl = "https://example.com/"
override var name = "Example provider"
override val supportedTypes = setOf(TvType.Movie)
override var lang = "en"
// enable this when your provider has a main page
override val hasMainPage = true
// this function gets called when you search for something
override suspend fun search(query: String): List<SearchResponse> {
return listOf<SearchResponse>()
}
}

26
HiAnime/build.gradle.kts Normal file
View File

@ -0,0 +1,26 @@
// use an integer for version numbers
version = 5
cloudstream {
language = "en"
// All of these properties are optional, you can safely remove them
//description = "Webview is used to load links, reload if necessary"
authors = listOf("Stormunblessed, KillerDogeEmpire")
/**
* Status int as the following:
* 0: Down
* 1: Ok
* 2: Slow
* 3: Beta only
* */
status = 1 // will be 3 if unspecified
tvTypes = listOf(
"Anime",
"OVA",
)
iconUrl = "https://www.google.com/s2/favicons?domain=hianime.to&sz=%size%"
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.stormunblessed"/>

View File

@ -0,0 +1,312 @@
package com.RowdyAvocado
import com.RowdyAvocado.RabbitStream.Companion.extractRabbitStream
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.Actor
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.ActorRole
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.Episode
import com.lagradost.cloudstream3.HomePageResponse
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.addDubStatus
import com.lagradost.cloudstream3.addEpisodes
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.fixUrl
import com.lagradost.cloudstream3.getDurationFromString
import com.lagradost.cloudstream3.mainPageOf
import com.lagradost.cloudstream3.newAnimeLoadResponse
import com.lagradost.cloudstream3.newAnimeSearchResponse
import com.lagradost.cloudstream3.newEpisode
import com.lagradost.cloudstream3.newHomePageResponse
import com.lagradost.cloudstream3.toRatingInt
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.nicehttp.Requests.Companion.await
import java.net.URI
import okhttp3.Interceptor
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
private const val OPTIONS = "OPTIONS"
class HiAnime : MainAPI() {
override var mainUrl = "https://hianime.to"
override var name = "HiAnime"
override val hasQuickSearch = false
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie, TvType.OVA)
val epRegex = Regex("Ep (\\d+)/")
var sid: HashMap<Int, String?> = hashMapOf() // Url hashcode to sid
private fun Element.toSearchResult(): SearchResponse {
val href = fixUrl(this.select("a").attr("href"))
val title = this.select("h3.film-name").text()
val subCount =
this.selectFirst(".film-poster > .tick.ltr > .tick-sub")?.text()?.toIntOrNull()
val dubCount =
this.selectFirst(".film-poster > .tick.ltr > .tick-dub")?.text()?.toIntOrNull()
val posterUrl = fixUrl(this.select("img").attr("data-src"))
val type = getType(this.selectFirst("div.fd-infor > span.fdi-item")?.text() ?: "")
return newAnimeSearchResponse(title, href, type) {
this.posterUrl = posterUrl
addDubStatus(dubCount != null, subCount != null, dubCount, subCount)
}
}
private fun Element.getActorData(): ActorData? {
var actor: Actor? = null
var role: ActorRole? = null
var voiceActor: Actor? = null
val elements = this.select(".per-info")
elements.forEachIndexed { index, actorInfo ->
val name = actorInfo.selectFirst(".pi-name")?.text() ?: return null
val image = actorInfo.selectFirst("a > img")?.attr("data-src") ?: return null
when (index) {
0 -> {
actor = Actor(name, image)
val castType = actorInfo.selectFirst(".pi-cast")?.text() ?: "Main"
role = ActorRole.valueOf(castType)
}
1 -> voiceActor = Actor(name, image)
else -> {}
}
}
return ActorData(actor ?: return null, role, voiceActor = voiceActor)
}
companion object {
fun getType(t: String): TvType {
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
else if (t.contains("Movie")) TvType.AnimeMovie else TvType.Anime
}
fun getStatus(t: String): ShowStatus {
return when (t) {
"Finished Airing" -> ShowStatus.Completed
"Currently Airing" -> ShowStatus.Ongoing
else -> ShowStatus.Completed
}
}
}
override val mainPage =
mainPageOf(
"$mainUrl/recently-updated?page=" to "Latest Episodes",
"$mainUrl/recently-added?page=" to "New On HiAnime",
"$mainUrl/top-airing?page=" to "Top Airing",
"$mainUrl/most-popular?page=" to "Most Popular",
"$mainUrl/most-favorite?page=" to "Most Favorite",
"$mainUrl/completed?page=" to "Latest Completed",
)
override suspend fun search(query: String): List<SearchResponse> {
val link = "$mainUrl/search?keyword=$query"
val res = app.get(link).document
return res.select("div.flw-item").map { it.toSearchResult() }
}
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
val res = app.get("${request.data}$page").document
val items = res.select("div.flw-item").map { it.toSearchResult() }
return newHomePageResponse(request.name, items)
}
override suspend fun load(url: String): LoadResponse {
val document = app.get(url).document
val syncData = tryParseJson<ZoroSyncData>(document.selectFirst("#syncData")?.data())
val title = document.selectFirst(".anisc-detail > .film-name")?.text().toString()
val poster = document.selectFirst(".anisc-poster img")?.attr("src")
val animeId = URI(url).path.split("-").last()
val subCount = document.selectFirst(".anisc-detail .tick-sub")?.text()?.toIntOrNull()
val dubCount = document.selectFirst(".anisc-detail .tick-dub")?.text()?.toIntOrNull()
var dubEpisodes = emptyList<Episode>()
var subEpisodes = emptyList<Episode>()
val epRes =
app.get("$mainUrl/ajax/v2/episode/list/$animeId")
.parsedSafe<Response>()
?.getDocument()
epRes?.select(".ss-list > a[href].ssl-item.ep-item")?.forEachIndexed { index, ep ->
subCount?.let {
if (index < it) {
subEpisodes +=
newEpisode("sub|" + ep.attr("href")) {
name = ep.attr("title")
episode = ep.selectFirst(".ssli-order")?.text()?.toIntOrNull()
}
}
}
dubCount?.let {
if (index < it) {
dubEpisodes +=
newEpisode("dub|" + ep.attr("href")) {
name = ep.attr("title")
episode = ep.selectFirst(".ssli-order")?.text()?.toIntOrNull()
}
}
}
}
val actors =
document.select("div.block-actors-content div.bac-item").mapNotNull {
it.getActorData()
}
val recommendations =
document.select("div.block_area_category div.flw-item").map { it.toSearchResult() }
return newAnimeLoadResponse(title, url, TvType.Anime) {
engName = title
posterUrl = poster
addEpisodes(DubStatus.Subbed, subEpisodes)
addEpisodes(DubStatus.Dubbed, dubEpisodes)
this.recommendations = recommendations
this.actors = actors
addMalId(syncData?.malId?.toIntOrNull())
addAniListId(syncData?.aniListId?.toIntOrNull())
// adding info
document.select(".anisc-info > .item").forEach { info ->
val infoType = info.select("span.item-head").text().removeSuffix(":")
when (infoType) {
"Overview" -> plot = info.selectFirst(".text")?.text()
"Japanese" -> japName = info.selectFirst(".name")?.text()
"Premiered" ->
year =
info.selectFirst(".name")
?.text()
?.substringAfter(" ")
?.toIntOrNull()
"Duration" ->
duration = getDurationFromString(info.selectFirst(".name")?.text())
"Status" -> showStatus = getStatus(info.selectFirst(".name")?.text().toString())
"Genres" -> tags = info.select("a").map { it.text() }
"MAL Score" -> rating = info.selectFirst(".name")?.text().toRatingInt()
else -> {}
}
}
}
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val dubType = data.replace("$mainUrl/", "").split("|").first()
val epId = data.split("|").last().split("=").last()
val servers: List<String> =
app.get("$mainUrl/ajax/v2/episode/servers?episodeId=$epId")
.parsed<Response>()
.getDocument()
.select(".server-item[data-type=$dubType][data-id]")
.map { it.attr("data-id") }
// val extractorData = "https://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=polling"
// Prevent duplicates
servers.distinct().apmap {
val link = "$mainUrl/ajax/v2/episode/sources?id=$it"
val extractorLink = app.get(link).parsed<RapidCloudResponse>().link
val hasLoadedExtractorLink =
loadExtractor(
extractorLink,
"https://rapid-cloud.ru/",
subtitleCallback,
callback
)
if (!hasLoadedExtractorLink) {
extractRabbitStream(
extractorLink,
subtitleCallback,
// Blacklist VidCloud for now
{ videoLink ->
if (!videoLink.url.contains("betterstream")) callback(videoLink)
},
false,
null,
decryptKey = getKey()
) { sourceName -> sourceName }
}
}
return true
}
override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor {
// Needs to be object instead of lambda to make it compile correctly
return object : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val request = chain.request()
if (request.url.toString().endsWith(".ts") &&
request.method != OPTIONS
// No option requests on VidCloud
&&
!request.url.toString().contains("betterstream")
) {
val newRequest =
chain.request()
.newBuilder()
.apply {
sid[extractorLink.url.hashCode()]?.let { sid ->
addHeader("SID", sid)
}
}
.build()
val options = request.newBuilder().method(OPTIONS, request.body).build()
ioSafe { app.baseClient.newCall(options).await() }
return chain.proceed(newRequest)
} else {
return chain.proceed(chain.request())
}
}
}
}
private suspend fun getKey(): String {
return app.get("https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt").text
}
// #region - Data classes
private data class Response(
@JsonProperty("status") val status: Boolean,
@JsonProperty("html") val html: String
) {
fun getDocument(): Document {
return Jsoup.parse(html)
}
}
private data class ZoroSyncData(
@JsonProperty("mal_id") val malId: String?,
@JsonProperty("anilist_id") val aniListId: String?,
)
private data class RapidCloudResponse(@JsonProperty("link") val link: String)
// #endregion - Data classes
}

View File

@ -0,0 +1,12 @@
package com.RowdyAvocado
import android.content.Context
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
import com.lagradost.cloudstream3.plugins.Plugin
@CloudstreamPlugin
class HiAnimeProviderPlugin : Plugin() {
override fun load(context: Context) {
registerMainAPI(HiAnime())
}
}

View File

@ -0,0 +1,871 @@
package com.RowdyAvocado
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.Episode
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.HomePageResponse
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.VPNStatus
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.apmapIndexed
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64DecodeArray
import com.lagradost.cloudstream3.fixUrl
import com.lagradost.cloudstream3.fixUrlNull
import com.lagradost.cloudstream3.getQualityFromString
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.newEpisode
import com.lagradost.cloudstream3.newMovieLoadResponse
import com.lagradost.cloudstream3.newMovieSearchResponse
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
import com.lagradost.cloudstream3.toRatingInt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.nicehttp.NiceResponse
import java.net.URI
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.delay
import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
open class RabbitStream : MainAPI() {
override var mainUrl = "https://sflix.to"
override var name = "Sflix.to"
override val hasQuickSearch = false
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val usesWebView = true
override val supportedTypes =
setOf(
TvType.Movie,
TvType.TvSeries,
)
override val vpnStatus = VPNStatus.None
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
val html = app.get("$mainUrl/home").text
val document = Jsoup.parse(html)
val all = ArrayList<HomePageList>()
val map =
mapOf(
"Trending Movies" to "div#trending-movies",
"Trending TV Shows" to "div#trending-tv",
)
map.forEach {
all.add(
HomePageList(
it.key,
document.select(it.value).select("div.flw-item").map { element ->
element.toSearchResult()
}
)
)
}
document.select("section.block_area.block_area_home.section-id-02").forEach {
val title = it.select("h2.cat-heading").text().trim()
val elements = it.select("div.flw-item").map { element -> element.toSearchResult() }
all.add(HomePageList(title, elements))
}
return HomePageResponse(all)
}
override suspend fun search(query: String): List<SearchResponse> {
val url = "$mainUrl/search/${query.replace(" ", "-")}"
val html = app.get(url).text
val document = Jsoup.parse(html)
return document.select("div.flw-item").map {
val title = it.select("h2.film-name").text()
val href = fixUrl(it.select("a").attr("href"))
val year = it.select("span.fdi-item").text().toIntOrNull()
val image = it.select("img").attr("data-src")
val isMovie = href.contains("/movie/")
val metaInfo = it.select("div.fd-infor > span.fdi-item")
// val rating = metaInfo[0].text()
val quality = getQualityFromString(metaInfo.getOrNull(1)?.text())
if (isMovie) {
newMovieSearchResponse(name = title, url = href, type = TvType.Movie, fix = true) {
posterUrl = image
this.year = year
this.quality = quality
}
} else {
newTvSeriesSearchResponse(
name = title,
url = href,
type = TvType.TvSeries,
fix = true
) {
posterUrl = image
// this.year = year
this.quality = quality
}
}
}
}
override suspend fun load(url: String): LoadResponse {
val document = app.get(url).document
val details = document.select("div.detail_page-watch")
val img = details.select("img.film-poster-img")
val posterUrl = img.attr("src")
val title = img.attr("title") ?: throw ErrorLoadingException("No Title")
/*
val year = Regex("""[Rr]eleased:\s*(\d{4})""").find(
document.select("div.elements").text()
)?.groupValues?.get(1)?.toIntOrNull()
val duration = Regex("""[Dd]uration:\s*(\d*)""").find(
document.select("div.elements").text()
)?.groupValues?.get(1)?.trim()?.plus(" min")*/
var duration = document.selectFirst(".fs-item > .duration")?.text()?.trim()
var year: Int? = null
var tags: List<String>? = null
var cast: List<String>? = null
val youtubeTrailer = document.selectFirst("iframe#iframe-trailer")?.attr("data-src")
val rating =
document.selectFirst(".fs-item > .imdb")
?.text()
?.trim()
?.removePrefix("IMDB:")
?.toRatingInt()
document.select("div.elements > .row > div > .row-line").forEach { element ->
val type = element.select(".type").text() ?: return@forEach
when {
type.contains("Released") -> {
year =
Regex("\\d+")
.find(element.ownText() ?: return@forEach)
?.groupValues
?.firstOrNull()
?.toIntOrNull()
}
type.contains("Genre") -> {
tags = element.select("a").mapNotNull { it.text() }
}
type.contains("Cast") -> {
cast = element.select("a").mapNotNull { it.text() }
}
type.contains("Duration") -> {
duration = duration ?: element.ownText().trim()
}
}
}
val plot = details.select("div.description").text().replace("Overview:", "").trim()
val isMovie = url.contains("/movie/")
// https://sflix.to/movie/free-never-say-never-again-hd-18317 -> 18317
val idRegex = Regex(""".*-(\d+)""")
val dataId = details.attr("data-id")
val id =
if (dataId.isNullOrEmpty())
idRegex.find(url)?.groupValues?.get(1)
?: throw ErrorLoadingException("Unable to get id from '$url'")
else dataId
val recommendations =
document.select("div.film_list-wrap > div.flw-item").mapNotNull { element ->
val titleHeader =
element.select("div.film-detail > .film-name > a")
?: return@mapNotNull null
val recUrl = fixUrlNull(titleHeader.attr("href")) ?: return@mapNotNull null
val recTitle = titleHeader.text() ?: return@mapNotNull null
val poster = element.select("div.film-poster > img").attr("data-src")
newMovieSearchResponse(
name = recTitle,
recUrl,
type =
if (recUrl.contains("/movie/")) TvType.Movie
else TvType.TvSeries,
) { this.posterUrl = poster }
}
if (isMovie) {
// Movies
val episodesUrl = "$mainUrl/ajax/movie/episodes/$id"
val episodes = app.get(episodesUrl).text
// Supported streams, they're identical
val sourceIds =
Jsoup.parse(episodes).select("a").mapNotNull { element ->
var sourceId = element.attr("data-id")
val serverName = element.select("span").text().trim()
if (sourceId.isNullOrEmpty()) sourceId = element.attr("data-linkid")
if (element.select("span").text().trim().isValidServer()) {
if (sourceId.isNullOrEmpty()) {
fixUrlNull(element.attr("href")) to serverName
} else {
"$url.$sourceId".replace("/movie/", "/watch-movie/") to serverName
}
} else {
null
}
}
val comingSoon = sourceIds.isEmpty()
return newMovieLoadResponse(title, url, TvType.Movie, sourceIds) {
this.year = year
this.posterUrl = posterUrl
this.plot = plot
addDuration(duration)
addActors(cast)
this.tags = tags
this.recommendations = recommendations
this.comingSoon = comingSoon
addTrailer(youtubeTrailer)
this.rating = rating
}
} else {
val seasonsDocument = app.get("$mainUrl/ajax/v2/tv/seasons/$id").document
val episodes = arrayListOf<Episode>()
var seasonItems = seasonsDocument.select("div.dropdown-menu.dropdown-menu-model > a")
if (seasonItems.isNullOrEmpty())
seasonItems = seasonsDocument.select("div.dropdown-menu > a.dropdown-item")
seasonItems.apmapIndexed { season, element ->
val seasonId = element.attr("data-id")
if (seasonId.isNullOrBlank()) return@apmapIndexed
var episode = 0
val seasonEpisodes = app.get("$mainUrl/ajax/v2/season/episodes/$seasonId").document
var seasonEpisodesItems =
seasonEpisodes.select("div.flw-item.film_single-item.episode-item.eps-item")
if (seasonEpisodesItems.isNullOrEmpty()) {
seasonEpisodesItems = seasonEpisodes.select("ul > li > a")
}
seasonEpisodesItems.forEach {
val episodeImg = it.select("img")
val episodeTitle = episodeImg.attr("title") ?: it.ownText()
val episodePosterUrl = episodeImg.attr("src")
val episodeData = it.attr("data-id") ?: return@forEach
episode++
val episodeNum =
(it.select("div.episode-number").text() ?: episodeTitle).let { str ->
Regex("""\d+""")
.find(str)
?.groupValues
?.firstOrNull()
?.toIntOrNull()
}
?: episode
episodes.add(
newEpisode(Pair(url, episodeData)) {
this.posterUrl = fixUrlNull(episodePosterUrl)
this.name = episodeTitle?.removePrefix("Episode $episodeNum: ")
this.season = season + 1
this.episode = episodeNum
}
)
}
}
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) {
this.posterUrl = posterUrl
this.year = year
this.plot = plot
addDuration(duration)
addActors(cast)
this.tags = tags
this.recommendations = recommendations
addTrailer(youtubeTrailer)
this.rating = rating
}
}
}
data class Tracks(
@JsonProperty("file") val file: String?,
@JsonProperty("label") val label: String?,
@JsonProperty("kind") val kind: String?
)
data class Sources(
@JsonProperty("file") val file: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String?
)
data class SourceObject(
@JsonProperty("sources") val sources: List<Sources?>? = null,
@JsonProperty("sources_1") val sources1: List<Sources?>? = null,
@JsonProperty("sources_2") val sources2: List<Sources?>? = null,
@JsonProperty("sourcesBackup") val sourcesBackup: List<Sources?>? = null,
@JsonProperty("tracks") val tracks: List<Tracks?>? = null
)
data class SourceObjectEncrypted(
@JsonProperty("sources") val sources: String?,
@JsonProperty("encrypted") val encrypted: Boolean?,
@JsonProperty("sources_1") val sources1: String?,
@JsonProperty("sources_2") val sources2: String?,
@JsonProperty("sourcesBackup") val sourcesBackup: String?,
@JsonProperty("tracks") val tracks: List<Tracks?>?
)
data class IframeJson(
// @JsonProperty("type") val type: String? = null,
@JsonProperty("link") val link: String? = null,
// @JsonProperty("sources") val sources: ArrayList<String> = arrayListOf(),
// @JsonProperty("tracks") val tracks: ArrayList<String> = arrayListOf(),
// @JsonProperty("title") val title: String? = null
)
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val urls =
(tryParseJson<Pair<String, String>>(data)?.let { (prefix, server) ->
val episodesUrl = "$mainUrl/ajax/v2/episode/servers/$server"
// Supported streams, they're identical
app.get(episodesUrl).document.select("a").mapNotNull { element ->
val id = element.attr("data-id") ?: return@mapNotNull null
val serverName = element.select("span").text().trim()
if (element.select("span").text().trim().isValidServer()) {
"$prefix.$id".replace("/tv/", "/watch-tv/") to serverName
} else {
null
}
}
}
?: tryParseJson<List<Pair<String?, String>>>(data))?.distinct()
urls?.apmap { (url, serverName) ->
suspendSafeApiCall {
// Possible without token
// val response = app.get(url)
// val key =
//
// response.document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
// .attr("src").substringAfter("render=")
// val token = getCaptchaToken(mainUrl, key) ?:
// return@suspendSafeApiCall
val serverId = url?.substringAfterLast(".") ?: return@suspendSafeApiCall
val iframeLink =
app.get("${this.mainUrl}/ajax/get_link/$serverId").parsed<IframeJson>().link
?: return@suspendSafeApiCall
// Some smarter ws11 or w10 selection might be required in the future.
// val extractorData =
//
// "https://ws11.rabbitstream.net/socket.io/?EIO=4&transport=polling"
val res =
!loadExtractor(iframeLink, null, subtitleCallback) { extractorLink ->
callback.invoke(
ExtractorLink(
source = serverName,
name = serverName,
url = extractorLink.url,
referer = extractorLink.referer,
quality = extractorLink.quality,
type = extractorLink.type,
headers = extractorLink.headers,
extractorData = extractorLink.extractorData
)
)
}
if (res) {
extractRabbitStream(
iframeLink,
subtitleCallback,
callback,
false,
decryptKey = getKey()
) { it }
}
}
}
return !urls.isNullOrEmpty()
}
// override suspend fun extractorVerifierJob(extractorData: String?) {
// runSflixExtractorVerifierJob(this, extractorData, "https://rabbitstream.net/")
// }
private fun Element.toSearchResult(): SearchResponse {
val inner = this.selectFirst("div.film-poster")
val img = inner!!.select("img")
val title = img.attr("title")
val posterUrl = img.attr("data-src") ?: img.attr("src")
val href = fixUrl(inner.select("a").attr("href"))
val isMovie = href.contains("/movie/")
val otherInfo =
this.selectFirst("div.film-detail > div.fd-infor")?.select("span")?.toList()
?: listOf()
// var rating: Int? = null
var year: Int? = null
var quality: SearchQuality? = null
when (otherInfo.size) {
1 -> {
year = otherInfo[0].text().trim().toIntOrNull()
}
2 -> {
year = otherInfo[0].text().trim().toIntOrNull()
}
3 -> {
// rating = otherInfo[0]?.text()?.toRatingInt()
quality = getQualityFromString(otherInfo[1].text())
year = otherInfo[2].text().trim().toIntOrNull()
}
}
return if (isMovie) {
newMovieSearchResponse(name = title, url = href, type = TvType.Movie, fix = true) {
this.posterUrl = posterUrl
this.year = year
this.quality = quality
}
} else {
newTvSeriesSearchResponse(
name = title,
url = href,
type = TvType.TvSeries,
fix = true
) {
this.posterUrl = posterUrl
// this.year = year
this.quality = quality
}
}
}
companion object {
data class PollingData(
@JsonProperty("sid") val sid: String? = null,
@JsonProperty("upgrades") val upgrades: ArrayList<String> = arrayListOf(),
@JsonProperty("pingInterval") val pingInterval: Int? = null,
@JsonProperty("pingTimeout") val pingTimeout: Int? = null
)
/*
# python code to figure out the time offset based on code if necessary
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
code = "Nxa_-bM"
total = 0
for i, char in enumerate(code[::-1]):
index = chars.index(char)
value = index * 64**i
total += value
print(f"total {total}")
*/
private fun generateTimeStamp(): String {
val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"
var code = ""
var time = unixTimeMS
while (time > 0) {
code += chars[(time % (chars.length)).toInt()]
time /= chars.length
}
return code.reversed()
}
suspend fun getKey(): String? {
return app.get("https://e4.tvembed.cc/e4").text
}
/** Generates a session 1 Get request. */
private suspend fun negotiateNewSid(baseUrl: String): PollingData? {
// Tries multiple times
for (i in 1..5) {
val jsonText =
app.get("$baseUrl&t=${generateTimeStamp()}").text.replaceBefore("{", "")
// println("Negotiated sid $jsonText")
parseJson<PollingData?>(jsonText)?.let {
return it
}
delay(1000L * i)
}
return null
}
/**
* Generates a new session if the request fails
* @return the data and if it is new.
*/
private suspend fun getUpdatedData(
response: NiceResponse,
data: PollingData,
baseUrl: String
): Pair<PollingData, Boolean> {
if (!response.okhttpResponse.isSuccessful) {
return negotiateNewSid(baseUrl)?.let { it to true } ?: (data to false)
}
return data to false
}
private suspend fun initPolling(
extractorData: String,
referer: String
): Pair<PollingData?, String?> {
val headers =
mapOf(
"Referer" to referer // "https://rabbitstream.net/"
)
val data = negotiateNewSid(extractorData) ?: return null to null
app.post(
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
requestBody = "40".toRequestBody(),
headers = headers
)
// This makes the second get request work, and re-connect work.
val reconnectSid =
parseJson<PollingData>(
app.get(
"$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
headers = headers
)
// .also { println("First get
// ${it.text}") }
.text
.replaceBefore("{", "")
)
.sid
// This response is used in the post requests. Same contents in all it seems.
// val authInt =
// app.get(
// "$extractorData&t=${generateTimeStamp()}&sid=${data.sid}",
// timeout = 60,
// headers = headers
// )
// .text
// // .also { println("Second get ${it}") }
// // Dunno if it's actually generated like this, just guessing.
// .toIntOrNull()
// ?.plus(1)
// ?: 3
return data to reconnectSid
}
suspend fun runSflixExtractorVerifierJob(extractorData: String?, referer: String) {
if (extractorData == null) return
val headers =
mapOf(
"Referer" to referer // "https://rabbitstream.net/"
)
lateinit var data: PollingData
var reconnectSid = ""
initPolling(extractorData, referer).also {
data = it.first ?: throw RuntimeException("Data Null")
reconnectSid = it.second ?: throw RuntimeException("ReconnectSid Null")
}
// Prevents them from fucking us over with doing a while(true){} loop
val interval = maxOf(data.pingInterval?.toLong()?.plus(2000) ?: return, 10000L)
var reconnect = false
var newAuth = false
while (true) {
val authData =
when {
newAuth -> "40"
reconnect -> """42["_reconnect", "$reconnectSid"]"""
else -> "3"
}
val url = "${extractorData}&t=${generateTimeStamp()}&sid=${data.sid}"
getUpdatedData(
app.post(url, json = authData, headers = headers),
data,
extractorData
)
.also {
newAuth = it.second
data = it.first
}
// .also { println("Sflix post job ${it.text}") }
val time = measureTimeMillis {
// This acts as a timeout
val getResponse = app.get(url, timeout = interval / 1000, headers = headers)
// .also { println("Sflix get job ${it.text}") }
reconnect = getResponse.text.contains("sid")
}
// Always waits even if the get response is instant, to prevent a while true loop.
if (time < interval - 4000) delay(4000)
}
}
// Only scrape servers with these names
fun String?.isValidServer(): Boolean {
val list = listOf("upcloud", "vidcloud", "streamlare")
return list.contains(this?.lowercase(Locale.ROOT))
}
// For re-use in Zoro
private suspend fun Sources.toExtractorLink(
caller: MainAPI,
name: String,
extractorData: String? = null,
): List<ExtractorLink>? {
return this.file?.let { file ->
// println("FILE::: $file")
val isM3u8 =
URI(this.file).path.endsWith(".m3u8") ||
this.type.equals("hls", ignoreCase = true)
return if (isM3u8) {
suspendSafeApiCall {
M3u8Helper()
.m3u8Generation(
M3u8Helper.M3u8Stream(
this.file,
null,
mapOf("Referer" to "https://mzzcloud.life/")
),
false
)
.map { stream ->
ExtractorLink(
caller.name,
"${caller.name} $name",
stream.streamUrl,
caller.mainUrl,
getQualityFromName(stream.quality?.toString()),
true,
extractorData = extractorData
)
}
}
.takeIf { !it.isNullOrEmpty() }
?: listOf(
// Fallback if m3u8 extractor fails
ExtractorLink(
caller.name,
"${caller.name} $name",
this.file,
caller.mainUrl,
getQualityFromName(this.label),
isM3u8,
extractorData = extractorData
)
)
} else {
listOf(
ExtractorLink(
caller.name,
caller.name,
file,
caller.mainUrl,
getQualityFromName(this.label),
false,
extractorData = extractorData
)
)
}
}
}
private fun Tracks.toSubtitleFile(): SubtitleFile? {
return this.file?.let { SubtitleFile(this.label ?: "Unknown", it) }
}
private fun md5(input: ByteArray): ByteArray {
return MessageDigest.getInstance("MD5").digest(input)
}
private fun generateKey(salt: ByteArray, secret: ByteArray): ByteArray {
var key = md5(secret + salt)
var currentKey = key
while (currentKey.size < 48) {
key = md5(key + secret + salt)
currentKey += key
}
return currentKey
}
private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String {
val cipherData = base64DecodeArray(sourceUrl)
val encrypted = cipherData.copyOfRange(16, cipherData.size)
val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding")
Objects.requireNonNull(aesCBC)
.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(decryptionKey.copyOfRange(0, 32), "AES"),
IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size))
)
val decryptedData = aesCBC!!.doFinal(encrypted)
return String(decryptedData, StandardCharsets.UTF_8)
}
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
return tryParseJson(decrypt(input, key))
}
private fun decrypt(input: String, key: String): String {
return decryptSourceUrl(
generateKey(base64DecodeArray(input).copyOfRange(8, 16), key.toByteArray()),
input
)
}
suspend fun MainAPI.extractRabbitStream(
url: String,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
useSidAuthentication: Boolean,
/** Used for extractorLink name, input: Source name */
extractorData: String? = null,
decryptKey: String? = null,
nameTransformer: (String) -> String,
) = suspendSafeApiCall {
// https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> https://rapid-cloud.ru/embed-6
val mainIframeUrl = url.substringBeforeLast("/")
val mainIframeId =
url.substringAfterLast("/")
.substringBefore(
"?"
) // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> dcPOVRE57YOT
// val iframe = app.get(url, referer = mainUrl)
// val iframeKey =
//
// iframe.document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
// .attr("src").substringAfter("render=")
// val iframeToken = getCaptchaToken(url, iframeKey)
// val number =
// Regex("""recaptchaNumber =
// '(.*?)'""").find(iframe.text)?.groupValues?.get(1)
var sid: String? = null
if (useSidAuthentication && extractorData != null) {
negotiateNewSid(extractorData)?.also { pollingData ->
app.post(
"$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}",
requestBody = "40".toRequestBody(),
timeout = 60
)
val text =
app.get(
"$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}",
timeout = 60
)
.text
.replaceBefore("{", "")
sid = parseJson<PollingData>(text).sid
ioSafe {
app.get("$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}")
}
}
}
val getSourcesUrl =
"${
mainIframeUrl.replace(
"/embed",
"/ajax/embed"
)
}/getSources?id=$mainIframeId${sid?.let { "$&sId=$it" } ?: ""}"
val response =
app.get(
getSourcesUrl,
referer = mainUrl,
headers =
mapOf(
"X-Requested-With" to "XMLHttpRequest",
"Accept" to "*/*",
"Accept-Language" to "en-US,en;q=0.5",
"Connection" to "keep-alive",
"TE" to "trailers"
)
)
val sourceObject =
if (decryptKey != null) {
val encryptedMap = response.parsedSafe<SourceObjectEncrypted>()
val sources = encryptedMap?.sources
if (sources == null || encryptedMap.encrypted == false) {
response.parsedSafe()
} else {
val decrypted = decryptMapped<List<Sources>>(sources, decryptKey)
SourceObject(sources = decrypted, tracks = encryptedMap.tracks)
}
} else {
response.parsedSafe()
}
?: return@suspendSafeApiCall
sourceObject.tracks?.forEach { track ->
track?.toSubtitleFile()?.let { subtitleFile ->
subtitleCallback.invoke(subtitleFile)
}
}
val list =
listOf(
sourceObject.sources to "source 1",
sourceObject.sources1 to "source 2",
sourceObject.sources2 to "source 3",
sourceObject.sourcesBackup to "source backup"
)
list.forEach { subList ->
subList.first?.forEach { source ->
source?.toExtractorLink(
this,
nameTransformer(subList.second),
extractorData,
)
?.forEach {
// Sets Zoro SID used for video loading
// (this as?
// ZoroProvider)?.sid?.set(it.url.hashCode(), sid)
callback(it)
}
}
}
}
}
}

0
Onstream/TODO Normal file
View File

View File

@ -1,27 +1,38 @@
**⚠️ This is currently under development, dont use it yet if you're not comfortable with constantly merging new changes**
# Cloudstream3 Plugin
# `Cloudstream3 Plugin Repo Template`
> Attempt to write some plugins for cloudstream
Template for a [Cloudstream3](https://github.com/recloudstream) plugin repo
Mnemosyne provider
**⚠️ Make sure you check "Include all branches" when using this template**
* Sflix (WIP from scratch) :x:
## Getting started with writing your first plugin
* HiAnime (from Rowdy-Avocado) :white_check_mark:
* Aniwave (from Rowdy-Avocado) :white_check_mark:
* SuperStream (from Hexated) :white_check_mark:
This template includes 1 example plugin.
* ZoroTV (TODO from scratch) :x:
* Onstream (TODO from scratch) :x:
* SoraStream (from Hexated) :x:
1. Open the root build.gradle.kts, read the comments and replace all the placeholders
2. Familiarize yourself with the project structure. Most files are commented
3. Build or deploy your first plugin using:
- Windows: `.\gradlew.bat ExampleProvider:make` or `.\gradlew.bat ExampleProvider:deployWithAdb`
- Linux & Mac: `./gradlew ExampleProvider:make` or `./gradlew ExampleProvider:deployWithAdb`
## License
## Docs
Everything in this repo is released into the public domain. You may use it however you want with no conditions whatsoever
Build and deployment: :ok: (https://shorturl.at/WbYNF)
## Attribution
* https://recloudstream.github.io/csdocs/devs/create-your-own-providers/#3-loading-the-show-page
* https://recloudstream.github.io/csdocs/devs/scraping/starting/
This template as well as the gradle plugin and the whole plugin system is **heavily** based on [Aliucord](https://github.com/Aliucord).
*Go use it, it's a great mobile discord client mod!*
## Debug
```ps1
adb devices
adb connect 192.168.1.X:5555
adb logcat -s mnemo
```
## Notes
* vidcloud / upcloud uses https://rabbitstream.net/
* 9animetv is the same as Aniwave ?

View File

@ -9,9 +9,8 @@ version = -1
cloudstream {
// All of these properties are optional, you can safely remove them
description = "Lorem ipsum"
authors = listOf("Cloudburst")
description = "Sflix"
authors = listOf("Mnemosyne")
/**
* Status int as the following:
@ -21,9 +20,7 @@ cloudstream {
* 3: Beta only
* */
status = 1
tvTypes = listOf("Movie")
requiresResources = true
language = "en"

View File

@ -3,6 +3,8 @@ package com.example
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
import com.lagradost.cloudstream3.plugins.Plugin
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.extractorApis
import android.content.Context
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
@ -14,6 +16,12 @@ class TestPlugin: Plugin() {
override fun load(context: Context) {
activity = context as AppCompatActivity
// All providers should be added in this manner
registerExtractorAPI(DoodRe())
registerExtractorAPI(MixDropCo())
// Force this extractor to be first in the list
addExtractor(Upstream())
registerMainAPI(ExampleProvider(this))
openSettings = { ctx ->
@ -21,4 +29,10 @@ class TestPlugin: Plugin() {
frag.show(activity!!.supportFragmentManager, "Frag")
}
}
private fun addExtractor(element: ExtractorApi) {
element.sourcePlugin = __filename
extractorApis.add(0, element)
}
}

View File

@ -0,0 +1,277 @@
package com.example
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.extractors.DoodLaExtractor
import com.lagradost.cloudstream3.extractors.MixDrop
import com.fasterxml.jackson.annotation.JsonProperty
import org.jsoup.Jsoup
class ExampleProvider(val plugin: TestPlugin) : MainAPI() {
// all providers must be an instance of MainAPI
override var mainUrl = "https://sflix.to"
override var name = "Mnemosyne"
override val supportedTypes = setOf(TvType.Movie, TvType.TvSeries, TvType.Anime)
override var lang = "en"
// enable this when your provider has a main page
override val hasMainPage = true
// this function gets called when you search for something
override suspend fun search(query: String): List<SearchResponse> {
val escaped = query.replace(' ', '-')
val url = "$mainUrl/search/${escaped}"
return app.get(
url,
).document.select(".flw-item").mapNotNull { article ->
val name = article.selectFirst("h2 > a")?.text() ?: ""
val poster = article.selectFirst("img")?.attr("data-src")
val url = article.selectFirst("a.btn")?.attr("href") ?: ""
val type = article.selectFirst("strong")?.text() ?: ""
if (type == "Movie") {
newMovieSearchResponse(name, url) {
posterUrl = poster
}
}
else {
newTvSeriesSearchResponse(name, url) {
posterUrl = poster
}
}
}
}
override val mainPage = mainPageOf(
"tv-show" to "TV Show",
"movie" to "Movie",
"top-imdb" to "Top-IMDB",
)
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
// page: An integer > 0, starts on 1 and counts up, Depends on how much the user has scrolled.
val url = "$mainUrl/${request.data}?page=$page"
var list = mutableListOf<AnimeSearchResponse>()
val res = app.get(url).document
res.select(".flw-item").mapNotNull { article ->
val name = article.selectFirst("h2 > a")?.text() ?: ""
val poster = article.selectFirst("img")?.attr("data-src")
val url = article.selectFirst("a.btn")?.attr("href") ?: ""
list.add(newAnimeSearchResponse(name, url){
this.posterUrl = poster
})
}
return newHomePageResponse(
list = HomePageList(
name = request.name,
list = list,
isHorizontalImages = true
),
hasNext = true
)
}
// start with movie easier than tv series
// this function only displays info about movies and series
override suspend fun load(url: String): LoadResponse {
val document = app.get(url).document
val posterUrl = document.selectFirst("img.film-poster-img")?.attr("src")
val details = document.select("div.detail_page-watch")
val img = details.select("img.film-poster-img")
val title = img.attr("title") ?: throw ErrorLoadingException("No Title")
val plot = details.select("div.description").text().replace("Overview:", "").trim()
val rating = document.selectFirst(".fs-item > .imdb")?.text()?.trim()?.removePrefix("IMDB:")?.toRatingInt()
val isMovie = url.contains("/movie/")
val idRegex = Regex(""".*-(\d+)""")
val dataId = details.attr("data-id")
val id = if (dataId.isNullOrEmpty())
idRegex.find(url)?.groupValues?.get(1)
?: throw ErrorLoadingException("Unable to get id from '$url'")
else dataId
// duration duration = duration ?: element.ownText().trim()
// Casts cast = element.select("a").mapNotNull { it.text() }
// genre
// country
// production
// tags = element.select("a").mapNotNull { it.text() }
if (isMovie){
val episodesUrl = "$mainUrl/ajax/episode/list/$id"
val episodes = app.get(episodesUrl).text
val sourceIds: List<String> = Jsoup.parse(episodes).select("a").mapNotNull { element ->
var sourceId: String? = element.attr("data-id")
if (sourceId.isNullOrEmpty())
sourceId = element.attr("data-linkid")
Log.d("mnemo", "sourceId: $sourceId, type: ${sourceId?.javaClass?.name}")
if (sourceId.isNullOrEmpty()) {
null
}
else{
"$mainUrl/ajax/episode/sources/$sourceId"
}
}
Log.d("mnemo", sourceIds.toString());
val comingSoon = sourceIds.isEmpty()
return newMovieLoadResponse(title, url, TvType.Movie, sourceIds) {
this.posterUrl = posterUrl
this.plot = plot
this.comingSoon = comingSoon
this.rating = rating
// this.year = year
// addDuration(duration)
// addActors(cast)
// this.tags = tags
// this.recommendations = recommendations
// addTrailer(youtubeTrailer)
}
}
else{
// TV series
val seasonsDocument = app.get("$mainUrl/ajax/v2/tv/seasons/$id").document
val episodes = arrayListOf<Episode>()
var seasonItems = seasonsDocument.select("div.dropdown-menu.dropdown-menu-model > a")
if (seasonItems.isNullOrEmpty())
seasonItems = seasonsDocument.select("div.dropdown-menu > a.dropdown-item")
seasonItems.apmapIndexed { season, element ->
val seasonId = element.attr("data-id")
if (seasonId.isNullOrBlank()) return@apmapIndexed
var episode = 0
val seasonEpisodes = app.get("$mainUrl/ajax/v2/season/episodes/$seasonId").document
var seasonEpisodesItems =
seasonEpisodes.select("div.flw-item.film_single-item.episode-item.eps-item")
if (seasonEpisodesItems.isNullOrEmpty()) {
seasonEpisodesItems =
seasonEpisodes.select("ul > li > a")
}
seasonEpisodesItems.forEach {
val episodeImg = it?.select("img")
val episodeTitle = episodeImg?.attr("title") ?: it.ownText()
val episodePosterUrl = episodeImg?.attr("src")
val episodeData = it.attr("data-id") ?: return@forEach
episode++
val episodeNum =
(it.select("div.episode-number").text()
?: episodeTitle).let { str ->
Regex("""\d+""").find(str)?.groupValues?.firstOrNull()
?.toIntOrNull()
} ?: episode
episodes.add(
newEpisode(Pair(url, episodeData)) {
this.posterUrl = fixUrlNull(episodePosterUrl)
this.name = episodeTitle?.removePrefix("Episode $episodeNum: ")
this.season = season + 1
this.episode = episodeNum
}
)
}
}
Log.d("mnemo", episodes.toString());
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) {
this.posterUrl = posterUrl
this.plot = plot
this.rating = rating
// this.year = year
// addDuration(duration)
// addActors(cast)
// this.tags = tags
// this.recommendations = recommendations
// addTrailer(youtubeTrailer)
}
}
}
// this function loads the links (upcloud/vidcloud/doodstream/other)
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
Log.d("mnemo", "LOADLINKS")
Log.d("mnemo", data)
val dataList = data
.replace("[", "")
.replace("]", "")
.replace("\"", "")
.split(",")
val links = dataList.mapNotNull { url ->
var jsonData = app.get(url).parsed<SrcJSON>()
// {"type":"iframe","link":"https://example/e/xxxxx","sources":[],"tracks":[],"title":""}
if (jsonData.link.startsWith("https://dood.watch")){
// Cloudflare bypass
// dood.re need a correct DNS resolver != ISP
var replacedLink = jsonData.link.replace("dood.watch", "dood.re")
Log.d("mnemo", "Handling dood ${replacedLink}")
// Extract the link and display the content
if (!loadExtractor(replacedLink, subtitleCallback, callback)){
Log.d("mnemo", "Couldn't extract dood")
}
}
else if (jsonData.link.startsWith("https://rabbitstream")){
// rabbitstream api player-v2-e4 is not compatible yet
Log.d("mnemo", "Handling rabbit ${jsonData.link}")
if (!loadExtractor(jsonData.link, subtitleCallback, callback)){
Log.d("mnemo", "Couldn't extract rabbit/vidcloud/upcloud")
}
}
else{
Log.d("mnemo", "Handling ${jsonData.link}")
if (!loadExtractor(jsonData.link, subtitleCallback, callback)){
Log.d("mnemo", "Couldn't extract other provider")
}
}
}
return true
}
data class SrcJSON(
@JsonProperty("type") val type: String = "",
@JsonProperty("link") val link: String = "",
@JsonProperty("sources") val sources: ArrayList<String> = arrayListOf(),
@JsonProperty("tracks") val tracks: ArrayList<String> = arrayListOf(),
@JsonProperty("title") val title: String = "",
)
}
class DoodRe : DoodLaExtractor() {
override var mainUrl = "https://dood.re"
}
class MixDropCo : MixDrop(){
override var mainUrl = "https://mixdrop.co"
}

View File

@ -0,0 +1,67 @@
package com.example
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import android.util.Log
class Upstream : ExtractorApi() {
override val name: String = "Upstream"
override val mainUrl: String = "https://upstream.to"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
Log.d("mnemo", "Upstream extractor enabled")
val doc = app.get(url, referer = referer).text
if (doc.isNotBlank()) {
// Find the matches in |file|01097|01|co|upstreamcdn|s18|HTTP
val regex = """\|file\|(\d+)\|(\d+)\|(\w+)\|(\w+)\|(\w+)\|HTTP""".toRegex()
val matchResult = regex.find(doc)
if (matchResult != null) {
val (n2, n1, tld, domain, subdomain) = matchResult.destructured
val fullDomain = "$subdomain.$domain.$tld"
Log.d("mnemo", "n2 = \"$n2\"")
Log.d("mnemo", "n1 = \"$n1\"")
Log.d("mnemo", "domain = \"$fullDomain\"")
var id = "9qx7lhanoezn_n" // master|9qx7lhanoezn_n|hls2
var t = "bYYSztvRHlImhy_PjVqV91W7EoXRu4LXALz76pLJPFI" // sp|10800|bYYSztvRHlImhy_PjVqV91W7EoXRu4LXALz76pLJPFI|m3u8
var e = "10800"
var s = "1719404641" // |data|1719404641|5485070||hide|
var f = "5485070"
var i = "0.0" // &i=0.0&5
var sp = "0" // TODO
// https://s18.upstreamcdn.co/hls2/01/01097/9qx7lhnoezn_n/master.m3u8?t=bYYSztvRHlImhy_PjVqV91W7oXRu4LXALz76pLJPFI&s=1719404641&e=10800&f=5485070&i=0.0&sp=0
val linkUrl = "https://${fullDomain}/hls2/${n1}/${n2}/${id}/master.m3u8?t=${t}&s=${s}&e=${e}&f=${f}&i=${i}&sp=${sp}"
Log.d("mnemo", "Testing ${linkUrl}")
M3u8Helper.generateM3u8(
this.name,
linkUrl,
"$mainUrl/",
headers = mapOf("Origin" to mainUrl)
).forEach(callback)
}
}
else{
Log.d("mnemo", "Got nothing, are you banned ?")
}
}
}

BIN
SoraStream/Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,46 @@
import org.jetbrains.kotlin.konan.properties.Properties
// use an integer for version numbers
version = 228
android {
defaultConfig {
val properties = Properties()
properties.load(project.rootProject.file("local.properties").inputStream())
buildConfigField("String", "TMDB_API", "\"${properties.getProperty("TMDB_API")}\"")
buildConfigField("String", "GHOSTX_API", "\"${properties.getProperty("GHOSTX_API")}\"")
buildConfigField("String", "CINEMATV_API", "\"${properties.getProperty("CINEMATV_API")}\"")
buildConfigField("String", "SFMOVIES_API", "\"${properties.getProperty("SFMOVIES_API")}\"")
buildConfigField("String", "ZSHOW_API", "\"${properties.getProperty("ZSHOW_API")}\"")
buildConfigField("String", "DUMP_API", "\"${properties.getProperty("DUMP_API")}\"")
buildConfigField("String", "DUMP_KEY", "\"${properties.getProperty("DUMP_KEY")}\"")
buildConfigField("String", "CRUNCHYROLL_BASIC_TOKEN", "\"${properties.getProperty("CRUNCHYROLL_BASIC_TOKEN")}\"")
buildConfigField("String", "CRUNCHYROLL_REFRESH_TOKEN", "\"${properties.getProperty("CRUNCHYROLL_REFRESH_TOKEN")}\"")
}
}
cloudstream {
language = "en"
// All of these properties are optional, you can safely remove them
description = "#1 best extention based on MultiAPI"
authors = listOf("Hexated", "Sora")
/**
* Status int as the following:
* 0: Down
* 1: Ok
* 2: Slow
* 3: Beta only
* */
status = 1 // will be 3 if unspecified
tvTypes = listOf(
"AsianDrama",
"TvSeries",
"Anime",
"Movie",
)
iconUrl = "https://cdn.discordapp.com/attachments/1109266606292488297/1193122096159674448/2-modified.png?ex=65ec2a0a&is=65d9b50a&hm=f1e0b0165e71101e5440b47592d9e15727a6c00cdeb3512108067bfbdbef1af7&"
}

103
SoraStream/ci.yml Normal file
View File

@ -0,0 +1,103 @@
name: Build
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency
concurrency:
group: "build"
cancel-in-progress: true
on:
push:
branches:
# choose your default branch
- master
- main
paths-ignore:
- '*.md'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
path: "src"
- name: Checkout builds
uses: actions/checkout@v4
with:
ref: "builds"
path: "builds"
- name: Clean old builds
run: rm $GITHUB_WORKSPACE/builds/*.cs3 || true
- name: Setup JDK 11
uses: actions/setup-java@v4
with:
distribution: "adopt"
java-version: 11
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Access Secrets
env:
TMDB_API: ${{ secrets.TMDB_API }}
DUMP_API: ${{ secrets.DUMP_API }}
DUMP_KEY: ${{ secrets.DUMP_KEY }}
CRUNCHYROLL_BASIC_TOKEN: ${{ secrets.CRUNCHYROLL_BASIC_TOKEN }}
CRUNCHYROLL_REFRESH_TOKEN: ${{ secrets.CRUNCHYROLL_REFRESH_TOKEN }}
ANICHI_API: ${{ secrets.ANICHI_API }}
ANICHI_SERVER: ${{ secrets.ANICHI_SERVER }}
ANICHI_ENDPOINT: ${{ secrets.ANICHI_ENDPOINT }}
ANICHI_APP: ${{ secrets.ANICHI_APP }}
ZSHOW_API: ${{ secrets.ZSHOW_API }}
SFMOVIES_API: ${{ secrets.SFMOVIES_API }}
CINEMATV_API: ${{ secrets.CINEMATV_API }}
GHOSTX_API: ${{ secrets.GHOSTX_API }}
SUPERSTREAM_FIRST_API: ${{ secrets.SUPERSTREAM_FIRST_API }}
SUPERSTREAM_SECOND_API: ${{ secrets.SUPERSTREAM_SECOND_API }}
SUPERSTREAM_THIRD_API: ${{ secrets.SUPERSTREAM_THIRD_API }}
SUPERSTREAM_FOURTH_API: ${{ secrets.SUPERSTREAM_FOURTH_API }}
run: |
cd $GITHUB_WORKSPACE/src
echo TMDB_API=$TMDB_API >> local.properties
echo DUMP_API=$DUMP_API >> local.properties
echo DUMP_KEY=$DUMP_KEY >> local.properties
echo CRUNCHYROLL_BASIC_TOKEN=$CRUNCHYROLL_BASIC_TOKEN >> local.properties
echo CRUNCHYROLL_REFRESH_TOKEN=$CRUNCHYROLL_REFRESH_TOKEN >> local.properties
echo ANICHI_API=$ANICHI_API >> local.properties
echo ANICHI_SERVER=$ANICHI_SERVER >> local.properties
echo ANICHI_ENDPOINT=$ANICHI_ENDPOINT >> local.properties
echo ANICHI_APP=$ANICHI_APP >> local.properties
echo ZSHOW_API=$ZSHOW_API >> local.properties
echo SFMOVIES_API=$SFMOVIES_API >> local.properties
echo CINEMATV_API=$CINEMATV_API >> local.properties
echo GHOSTX_API=$GHOSTX_API >> local.properties
echo SUPERSTREAM_FIRST_API=$SUPERSTREAM_FIRST_API >> local.properties
echo SUPERSTREAM_SECOND_API=$SUPERSTREAM_SECOND_API >> local.properties
echo SUPERSTREAM_THIRD_API=$SUPERSTREAM_THIRD_API >> local.properties
echo SUPERSTREAM_FOURTH_API=$SUPERSTREAM_FOURTH_API >> local.properties
- name: Build Plugins
run: |
cd $GITHUB_WORKSPACE/src
chmod +x gradlew
./gradlew make makePluginsJson
cp **/build/*.cs3 $GITHUB_WORKSPACE/builds
cp build/plugins.json $GITHUB_WORKSPACE/builds
- name: Move Kuramanime
run: |
rm $GITHUB_WORKSPACE/builds/KuramanimeProvider.cs3 || true
cp $GITHUB_WORKSPACE/builds/stored/KuramanimeProvider.cs3 $GITHUB_WORKSPACE/builds
- name: Push builds
run: |
cd $GITHUB_WORKSPACE/builds
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add .
git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
git push --force

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.hexated"/>

View File

@ -0,0 +1,528 @@
package com.hexated
import com.lagradost.cloudstream3.extractors.Filesim
import com.lagradost.cloudstream3.extractors.GMPlayer
import com.lagradost.cloudstream3.extractors.StreamSB
import com.lagradost.cloudstream3.extractors.Voe
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.extractors.Jeniusplay
import com.lagradost.cloudstream3.extractors.PixelDrain
import com.lagradost.cloudstream3.utils.*
import java.math.BigInteger
import java.security.MessageDigest
open class Playm4u : ExtractorApi() {
override val name = "Playm4u"
override val mainUrl = "https://play9str.playm4u.xyz"
override val requiresReferer = true
private val password = "plhq@@@22"
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = app.get(url, referer = referer).document
val script = document.selectFirst("script:containsData(idfile =)")?.data() ?: return
val passScript = document.selectFirst("script:containsData(domain_ref =)")?.data() ?: return
val pass = passScript.substringAfter("CryptoJS.MD5('").substringBefore("')")
val amount = passScript.substringAfter(".toString()), ").substringBefore("));").toInt()
val idFile = "idfile".findIn(script)
val idUser = "idUser".findIn(script)
val domainApi = "DOMAIN_API".findIn(script)
val nameKeyV3 = "NameKeyV3".findIn(script)
val dataEnc = caesarShift(
mahoa(
"Win32|$idUser|$idFile|$referer",
md5(pass)
), amount
).toHex()
val captchaKey =
document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]")
.attr("src").substringAfter("render=")
val token = getCaptchaToken(
url,
captchaKey,
referer = referer
)
val source = app.post(
domainApi, data = mapOf(
"namekey" to nameKeyV3,
"token" to "$token",
"referrer" to "$referer",
"data" to "$dataEnc|${md5(dataEnc + password)}",
), referer = "$mainUrl/"
).parsedSafe<Source>()
callback.invoke(
ExtractorLink(
this.name,
this.name,
source?.data ?: return,
"$mainUrl/",
Qualities.P1080.value,
INFER_TYPE
)
)
subtitleCallback.invoke(
SubtitleFile(
source.sub?.substringBefore("|")?.toLanguage() ?: return,
source.sub.substringAfter("|"),
)
)
}
private fun caesarShift(str: String, amount: Int): String {
var output = ""
val adjustedAmount = if (amount < 0) amount + 26 else amount
for (element in str) {
var c = element
if (c.isLetter()) {
val code = c.code
c = when (code) {
in 65..90 -> ((code - 65 + adjustedAmount) % 26 + 65).toChar()
in 97..122 -> ((code - 97 + adjustedAmount) % 26 + 97).toChar()
else -> c
}
}
output += c
}
return output
}
private fun mahoa(input: String, key: String): String {
val a = CryptoJS.encrypt(key, input)
return a.replace("U2FsdGVkX1", "")
.replace("/", "|a")
.replace("+", "|b")
.replace("=", "|c")
.replace("|", "-z")
}
private fun md5(input: String): String {
val md = MessageDigest.getInstance("MD5")
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
}
private fun String.toHex(): String {
return this.toByteArray().joinToString("") { "%02x".format(it) }
}
private fun String.findIn(data: String): String {
return "$this\\s*=\\s*[\"'](\\S+)[\"'];".toRegex().find(data)?.groupValues?.get(1) ?: ""
}
private fun String.toLanguage(): String {
return if (this == "EN") "English" else this
}
data class Source(
@JsonProperty("data") val data: String? = null,
@JsonProperty("sub") val sub: String? = null,
)
}
open class M4ufree : ExtractorApi() {
override val name = "M4ufree"
override val mainUrl = "https://play.playm4u.xyz"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = session.get(url, referer = referer).document
val script = document.selectFirst("script:containsData(idfile =)")?.data() ?: return
val idFile = "idfile".findIn(script)
val idUser = "idUser".findIn(script)
val video = session.post(
"https://api-plhq.playm4u.xyz/apidatard/$idUser/$idFile",
data = mapOf("referrer" to "$referer"),
headers = mapOf(
"Accept" to "*/*",
"X-Requested-With" to "XMLHttpRequest",
)
).text.let { AppUtils.tryParseJson<Source>(it) }?.data
callback.invoke(
ExtractorLink(
this.name,
this.name,
video ?: return,
referer ?: "",
Qualities.P720.value,
INFER_TYPE
)
)
}
private fun String.findIn(data: String): String? {
return "$this\\s*=\\s*[\"'](\\S+)[\"'];".toRegex().find(data)?.groupValues?.get(1)
}
data class Source(
@JsonProperty("data") val data: String? = null,
)
}
open class VCloud : ExtractorApi() {
override val name: String = "V-Cloud"
override val mainUrl: String = "https://v-cloud.bio"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val res = app.get(url)
val doc = res.document
val changedLink = doc.selectFirst("script:containsData(url =)")?.data()?.let {
val regex = """url\s*=\s*['"](.*)['"];""".toRegex()
val doc2 = app.get(regex.find(it)?.groupValues?.get(1) ?: return).text
regex.find(doc2)?.groupValues?.get(1)?.substringAfter("r=")
}
val header = doc.selectFirst("div.card-header")?.text()
app.get(
base64Decode(changedLink ?: return), cookies = res.cookies, headers = mapOf(
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
)
).document.select("p.text-success ~ a").apmap {
val link = it.attr("href")
if (link.contains("workers.dev") || it.text().contains("[Server : 1]") || link.contains(
"/dl.php?"
)
) {
callback.invoke(
ExtractorLink(
this.name,
this.name,
link,
"",
getIndexQuality(header),
INFER_TYPE
)
)
} else {
val direct = if (link.contains("gofile.io")) app.get(link).url else link
loadExtractor(direct, referer, subtitleCallback, callback)
}
}
}
private fun getIndexQuality(str: String?): Int {
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
?: Qualities.Unknown.value
}
}
open class Streamruby : ExtractorApi() {
override val name = "Streamruby"
override val mainUrl = "https://streamruby.com"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val id = "/e/(\\w+)".toRegex().find(url)?.groupValues?.get(1) ?: return
val response = app.post(
"$mainUrl/dl", data = mapOf(
"op" to "embed",
"file_code" to id,
"auto" to "1",
"referer" to "",
), referer = referer
)
val script = if (!getPacked(response.text).isNullOrEmpty()) {
getAndUnpack(response.text)
} else {
response.document.selectFirst("script:containsData(sources:)")?.data()
}
val m3u8 = Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1)
M3u8Helper.generateM3u8(
name,
m3u8 ?: return,
mainUrl
).forEach(callback)
}
}
open class Uploadever : ExtractorApi() {
override val name = "Uploadever"
override val mainUrl = "https://uploadever.in"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
var res = app.get(url, referer = referer).document
val formUrl = res.select("form").attr("action")
var formData = res.select("form input").associate { it.attr("name") to it.attr("value") }
.filterKeys { it != "go" }
.toMutableMap()
val formReq = app.post(formUrl, data = formData)
res = formReq.document
val captchaKey =
res.select("script[src*=https://www.google.com/recaptcha/api.js?render=]").attr("src")
.substringAfter("render=")
val token = getCaptchaToken(url, captchaKey, referer = "$mainUrl/")
formData = res.select("form#down input").associate { it.attr("name") to it.attr("value") }
.toMutableMap()
formData["adblock_detected"] = "0"
formData["referer"] = url
res = app.post(
formReq.url,
data = formData + mapOf("g-recaptcha-response" to "$token"),
cookies = formReq.cookies
).document
val video = res.select("div.download-button a.btn.btn-dow.recaptchav2").attr("href")
callback.invoke(
ExtractorLink(
this.name,
this.name,
video,
"",
Qualities.Unknown.value,
INFER_TYPE
)
)
}
}
open class Netembed : ExtractorApi() {
override var name: String = "Netembed"
override var mainUrl: String = "https://play.netembed.xyz"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val response = app.get(url, referer = referer)
val script = getAndUnpack(response.text)
val m3u8 = Regex("((https:|http:)//.*\\.m3u8)").find(script)?.groupValues?.getOrNull(1) ?: return
M3u8Helper.generateM3u8(this.name, m3u8, "$mainUrl/").forEach(callback)
}
}
open class Ridoo : ExtractorApi() {
override val name = "Ridoo"
override var mainUrl = "https://ridoo.net"
override val requiresReferer = true
open val defaulQuality = Qualities.P1080.value
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val response = app.get(url, referer = referer)
val script = if (!getPacked(response.text).isNullOrEmpty()) {
getAndUnpack(response.text)
} else {
response.document.selectFirst("script:containsData(sources:)")?.data()
}
val m3u8 = Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1)
val quality = "qualityLabels.*\"(\\d{3,4})[pP]\"".toRegex().find(script)?.groupValues?.get(1)
callback.invoke(
ExtractorLink(
this.name,
this.name,
m3u8 ?: return,
mainUrl,
quality?.toIntOrNull() ?: defaulQuality,
INFER_TYPE
)
)
}
}
open class Gdmirrorbot : ExtractorApi() {
override val name = "Gdmirrorbot"
override val mainUrl = "https://gdmirrorbot.nl"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
app.get(url, referer = referer).document.select("ul#videoLinks li").apmap {
loadExtractor(it.attr("data-link"), "$mainUrl/", subtitleCallback, callback)
}
}
}
open class Streamvid : ExtractorApi() {
override val name = "Streamvid"
override val mainUrl = "https://streamvid.net"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val response = app.get(url, referer = referer)
val script = if (!getPacked(response.text).isNullOrEmpty()) {
getAndUnpack(response.text)
} else {
response.document.selectFirst("script:containsData(sources:)")?.data()
}
val m3u8 =
Regex("src:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1)
M3u8Helper.generateM3u8(
name,
m3u8 ?: return,
mainUrl
).forEach(callback)
}
}
open class Embedrise : ExtractorApi() {
override val name = "Embedrise"
override val mainUrl = "https://embedrise.com"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val res = app.get(url, referer = referer).document
val title = res.select("title").text()
val video = res.select("video#player source").attr("src")
callback.invoke(
ExtractorLink(
this.name,
this.name,
video,
"$mainUrl/",
getIndexQuality(title),
INFER_TYPE
)
)
}
}
class FilemoonNl : Ridoo() {
override val name = "FilemoonNl"
override var mainUrl = "https://filemoon.nl"
override val defaulQuality = Qualities.Unknown.value
}
class Alions : Ridoo() {
override val name = "Alions"
override var mainUrl = "https://alions.pro"
override val defaulQuality = Qualities.Unknown.value
}
class Streamwish : Filesim() {
override val name = "Streamwish"
override var mainUrl = "https://streamwish.to"
}
class UqloadsXyz : Filesim() {
override val name = "Uqloads"
override var mainUrl = "https://uqloads.xyz"
}
class FilelionsTo : Filesim() {
override val name = "Filelions"
override var mainUrl = "https://filelions.to"
}
class Pixeldra : PixelDrain() {
override val mainUrl = "https://pixeldra.in"
}
class TravelR : GMPlayer() {
override val name = "TravelR"
override val mainUrl = "https://travel-russia.xyz"
}
class Mwish : Filesim() {
override val name = "Mwish"
override var mainUrl = "https://mwish.pro"
}
class Animefever : Filesim() {
override val name = "Animefever"
override var mainUrl = "https://animefever.fun"
}
class Multimovies : Ridoo() {
override val name = "Multimovies"
override var mainUrl = "https://multimovies.cloud"
}
class MultimoviesSB : StreamSB() {
override var name = "Multimovies"
override var mainUrl = "https://multimovies.website"
}
class Yipsu : Voe() {
override val name = "Yipsu"
override var mainUrl = "https://yip.su"
}
class Embedwish : Filesim() {
override val name = "Embedwish"
override var mainUrl = "https://embedwish.com"
}
class Flaswish : Ridoo() {
override val name = "Flaswish"
override var mainUrl = "https://flaswish.com"
override val defaulQuality = Qualities.Unknown.value
}
class Comedyshow : Jeniusplay() {
override val mainUrl = "https://comedyshow.to"
override val name = "Comedyshow"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,483 @@
package com.hexated
import com.fasterxml.jackson.annotation.JsonProperty
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
data class CrunchyrollAccessToken(
val accessToken: String? = null,
val tokenType: String? = null,
val bucket: String? = null,
val policy: String? = null,
val signature: String? = null,
val key_pair_id: String? = null,
)
data class FDMovieIFrame(
val link: String,
val quality: String,
val size: String,
val type: String,
)
data class AniIds(var id: Int? = null, var idMal: Int? = null)
data class TmdbDate(
val today: String,
val nextWeek: String,
)
data class AniwaveResponse(
val result: String
) {
fun asJsoup(): Document {
return Jsoup.parse(result)
}
}
data class AniwaveServer(
val result: Result
) {
data class Result(
val url: String
) {
fun decrypt(): String {
return AniwaveUtils.decodeVrf(url)
}
}
}
data class MoflixResponse(
@JsonProperty("title") val title: Episode? = null,
@JsonProperty("episode") val episode: Episode? = null,
) {
data class Episode(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("videos") val videos: ArrayList<Videos>? = arrayListOf(),
) {
data class Videos(
@JsonProperty("name") val name: String? = null,
@JsonProperty("category") val category: String? = null,
@JsonProperty("src") val src: String? = null,
@JsonProperty("quality") val quality: String? = null,
)
}
}
data class AniMedia(
@JsonProperty("id") var id: Int? = null,
@JsonProperty("idMal") var idMal: Int? = null
)
data class AniPage(@JsonProperty("media") var media: java.util.ArrayList<AniMedia> = arrayListOf())
data class AniData(@JsonProperty("Page") var Page: AniPage? = AniPage())
data class AniSearch(@JsonProperty("data") var data: AniData? = AniData())
data class GpressSources(
@JsonProperty("src") val src: String,
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: Int? = null,
@JsonProperty("max") val max: String,
)
data class UHDBackupUrl(
@JsonProperty("url") val url: String? = null,
)
data class ResponseHash(
@JsonProperty("embed_url") val embed_url: String,
@JsonProperty("key") val key: String? = null,
@JsonProperty("type") val type: String? = null,
)
data class KisskhSources(
@JsonProperty("Video") val video: String?,
@JsonProperty("ThirdParty") val thirdParty: String?,
)
data class KisskhSubtitle(
@JsonProperty("src") val src: String?,
@JsonProperty("label") val label: String?,
)
data class KisskhEpisodes(
@JsonProperty("id") val id: Int?,
@JsonProperty("number") val number: Int?,
)
data class KisskhDetail(
@JsonProperty("episodes") val episodes: ArrayList<KisskhEpisodes>? = arrayListOf(),
)
data class KisskhResults(
@JsonProperty("id") val id: Int?,
@JsonProperty("title") val title: String?,
)
data class DriveBotLink(
@JsonProperty("url") val url: String? = null,
)
data class DirectDl(
@JsonProperty("download_url") val download_url: String? = null,
)
data class Safelink(
@JsonProperty("safelink") val safelink: String? = null,
)
data class FDAds(
@JsonProperty("linkr") val linkr: String? = null,
)
data class ZShowEmbed(
@JsonProperty("m") val meta: String? = null,
)
data class WatchsomuchTorrents(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("movieId") val movieId: Int? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null,
)
data class WatchsomuchMovies(
@JsonProperty("torrents") val torrents: ArrayList<WatchsomuchTorrents>? = arrayListOf(),
)
data class WatchsomuchResponses(
@JsonProperty("movie") val movie: WatchsomuchMovies? = null,
)
data class WatchsomuchSubtitles(
@JsonProperty("url") val url: String? = null,
@JsonProperty("label") val label: String? = null,
)
data class WatchsomuchSubResponses(
@JsonProperty("subtitles") val subtitles: ArrayList<WatchsomuchSubtitles>? = arrayListOf(),
)
data class IndexMedia(
@JsonProperty("id") val id: String? = null,
@JsonProperty("driveId") val driveId: String? = null,
@JsonProperty("mimeType") val mimeType: String? = null,
@JsonProperty("size") val size: String? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("modifiedTime") val modifiedTime: String? = null,
)
data class IndexData(
@JsonProperty("files") val files: ArrayList<IndexMedia>? = arrayListOf(),
)
data class IndexSearch(
@JsonProperty("data") val data: IndexData? = null,
)
data class JikanExternal(
@JsonProperty("name") val name: String? = null,
@JsonProperty("url") val url: String? = null,
)
data class JikanData(
@JsonProperty("title") val title: String? = null,
@JsonProperty("external") val external: ArrayList<JikanExternal>? = arrayListOf(),
)
data class JikanResponse(
@JsonProperty("data") val data: JikanData? = null,
)
data class VidsrctoResult(
@JsonProperty("id") val id: String? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("url") val url: String? = null,
)
data class VidsrctoResponse(
@JsonProperty("result") val result: VidsrctoResult? = null,
)
data class VidsrctoSources(
@JsonProperty("result") val result: ArrayList<VidsrctoResult>? = arrayListOf(),
)
data class VidsrctoSubtitles(
@JsonProperty("label") val label: String? = null,
@JsonProperty("file") val file: String? = null,
)
data class AnilistExternalLinks(
@JsonProperty("id") var id: Int? = null,
@JsonProperty("site") var site: String? = null,
@JsonProperty("url") var url: String? = null,
@JsonProperty("type") var type: String? = null,
)
data class AnilistMedia(@JsonProperty("externalLinks") var externalLinks: ArrayList<AnilistExternalLinks> = arrayListOf())
data class AnilistData(@JsonProperty("Media") var Media: AnilistMedia? = AnilistMedia())
data class AnilistResponses(@JsonProperty("data") var data: AnilistData? = AnilistData())
data class CrunchyrollToken(
@JsonProperty("access_token") val accessToken: String? = null,
@JsonProperty("token_type") val tokenType: String? = null,
@JsonProperty("cms") val cms: Cms? = null,
) {
data class Cms(
@JsonProperty("bucket") var bucket: String? = null,
@JsonProperty("policy") var policy: String? = null,
@JsonProperty("signature") var signature: String? = null,
@JsonProperty("key_pair_id") var key_pair_id: String? = null,
)
}
data class CrunchyrollVersions(
@JsonProperty("audio_locale") val audio_locale: String? = null,
@JsonProperty("guid") val guid: String? = null,
)
data class CrunchyrollData(
@JsonProperty("id") val id: String? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("slug_title") val slug_title: String? = null,
@JsonProperty("season_number") val season_number: Int? = null,
@JsonProperty("episode_number") val episode_number: Int? = null,
@JsonProperty("versions") val versions: ArrayList<CrunchyrollVersions>? = null,
@JsonProperty("streams_link") val streams_link: String? = null,
)
data class CrunchyrollResponses(
@JsonProperty("data") val data: ArrayList<CrunchyrollData>? = arrayListOf(),
)
data class CrunchyrollSourcesResponses(
@JsonProperty("streams") val streams: Streams? = Streams(),
@JsonProperty("subtitles") val subtitles: HashMap<String, HashMap<String, String>>? = hashMapOf(),
) {
data class Streams(
@JsonProperty("adaptive_hls") val adaptive_hls: HashMap<String, HashMap<String, String>>? = hashMapOf(),
@JsonProperty("vo_adaptive_hls") val vo_adaptive_hls: HashMap<String, HashMap<String, String>>? = hashMapOf(),
)
}
data class MALSyncSites(
@JsonProperty("Zoro") val zoro: HashMap<String?, HashMap<String, String?>>? = hashMapOf(),
@JsonProperty("9anime") val nineAnime: HashMap<String?, HashMap<String, String?>>? = hashMapOf(),
)
data class MALSyncResponses(
@JsonProperty("Sites") val sites: MALSyncSites? = null,
)
data class HianimeResponses(
@JsonProperty("html") val html: String? = null,
@JsonProperty("link") val link: String? = null,
)
data class MalSyncRes(
@JsonProperty("Sites") val Sites: Map<String, Map<String, Map<String, String>>>? = null,
)
data class GokuData(
@JsonProperty("link") val link: String? = null,
)
data class GokuServer(
@JsonProperty("data") val data: GokuData? = GokuData(),
)
data class AllMovielandEpisodeFolder(
@JsonProperty("title") val title: String? = null,
@JsonProperty("id") val id: String? = null,
@JsonProperty("file") val file: String? = null,
)
data class AllMovielandSeasonFolder(
@JsonProperty("episode") val episode: String? = null,
@JsonProperty("id") val id: String? = null,
@JsonProperty("folder") val folder: ArrayList<AllMovielandEpisodeFolder>? = arrayListOf(),
)
data class AllMovielandServer(
@JsonProperty("title") val title: String? = null,
@JsonProperty("id") val id: String? = null,
@JsonProperty("file") val file: String? = null,
@JsonProperty("folder") val folder: ArrayList<AllMovielandSeasonFolder>? = arrayListOf(),
)
data class AllMovielandPlaylist(
@JsonProperty("file") val file: String? = null,
@JsonProperty("key") val key: String? = null,
@JsonProperty("href") val href: String? = null,
)
data class DumpMedia(
@JsonProperty("id") val id: String? = null,
@JsonProperty("domainType") val domainType: Int? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("releaseTime") val releaseTime: String? = null,
)
data class DumpQuickSearchData(
@JsonProperty("searchResults") val searchResults: ArrayList<DumpMedia>? = arrayListOf(),
)
data class SubtitlingList(
@JsonProperty("languageAbbr") val languageAbbr: String? = null,
@JsonProperty("language") val language: String? = null,
@JsonProperty("subtitlingUrl") val subtitlingUrl: String? = null,
)
data class DefinitionList(
@JsonProperty("code") val code: String? = null,
@JsonProperty("description") val description: String? = null,
)
data class EpisodeVo(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("seriesNo") val seriesNo: Int? = null,
@JsonProperty("definitionList") val definitionList: ArrayList<DefinitionList>? = arrayListOf(),
@JsonProperty("subtitlingList") val subtitlingList: ArrayList<SubtitlingList>? = arrayListOf(),
)
data class DumpMediaDetail(
@JsonProperty("episodeVo") val episodeVo: ArrayList<EpisodeVo>? = arrayListOf(),
)
data class EMovieServer(
@JsonProperty("value") val value: String? = null,
)
data class EMovieSources(
@JsonProperty("file") val file: String? = null,
)
data class EMovieTraks(
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null,
)
data class ShowflixResultsMovies(
@JsonProperty("movieName") val movieName: String? = null,
@JsonProperty("streamwish") val streamwish: String? = null,
@JsonProperty("filelions") val filelions: String? = null,
@JsonProperty("streamruby") val streamruby: String? = null,
)
data class ShowflixResultsSeries(
@JsonProperty("seriesName") val seriesName: String? = null,
@JsonProperty("streamwish") val streamwish: HashMap<String, List<String>>? = hashMapOf(),
@JsonProperty("filelions") val filelions: HashMap<String, List<String>>? = hashMapOf(),
@JsonProperty("streamruby") val streamruby: HashMap<String, List<String>>? = hashMapOf(),
)
data class ShowflixSearchMovies(
@JsonProperty("results") val resultsMovies: ArrayList<ShowflixResultsMovies>? = arrayListOf(),
)
data class ShowflixSearchSeries(
@JsonProperty("results") val resultsSeries: ArrayList<ShowflixResultsSeries>? = arrayListOf(),
)
data class SFMoviesSeriess(
@JsonProperty("title") var title: String? = null,
@JsonProperty("svideos") var svideos: String? = null,
)
data class SFMoviesAttributes(
@JsonProperty("title") var title: String? = null,
@JsonProperty("video") var video: String? = null,
@JsonProperty("releaseDate") var releaseDate: String? = null,
@JsonProperty("seriess") var seriess: ArrayList<ArrayList<SFMoviesSeriess>>? = arrayListOf(),
@JsonProperty("contentId") var contentId: String? = null,
)
data class SFMoviesData(
@JsonProperty("id") var id: Int? = null,
@JsonProperty("attributes") var attributes: SFMoviesAttributes? = SFMoviesAttributes()
)
data class SFMoviesSearch(
@JsonProperty("data") var data: ArrayList<SFMoviesData>? = arrayListOf(),
)
data class RidoContentable(
@JsonProperty("imdbId") var imdbId: String? = null,
@JsonProperty("tmdbId") var tmdbId: Int? = null,
)
data class RidoItems(
@JsonProperty("slug") var slug: String? = null,
@JsonProperty("contentable") var contentable: RidoContentable? = null,
)
data class RidoData(
@JsonProperty("url") var url: String? = null,
@JsonProperty("items") var items: ArrayList<RidoItems>? = arrayListOf(),
)
data class RidoResponses(
@JsonProperty("data") var data: ArrayList<RidoData>? = arrayListOf(),
)
data class RidoSearch(
@JsonProperty("data") var data: RidoData? = null,
)
data class SmashySources(
@JsonProperty("sourceUrls") var sourceUrls: ArrayList<String>? = arrayListOf(),
@JsonProperty("subtitleUrls") var subtitleUrls: String? = null,
)
data class AoneroomResponse(
@JsonProperty("data") val data: Data? = null,
) {
data class Data(
@JsonProperty("items") val items: ArrayList<Items>? = arrayListOf(),
@JsonProperty("list") val list: ArrayList<List>? = arrayListOf(),
) {
data class Items(
@JsonProperty("subjectId") val subjectId: String? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("releaseDate") val releaseDate: String? = null,
)
data class List(
@JsonProperty("resourceLink") val resourceLink: String? = null,
@JsonProperty("extCaptions") val extCaptions: ArrayList<ExtCaptions>? = arrayListOf(),
@JsonProperty("se") val se: Int? = null,
@JsonProperty("ep") val ep: Int? = null,
@JsonProperty("resolution") val resolution: Int? = null,
) {
data class ExtCaptions(
@JsonProperty("lanName") val lanName: String? = null,
@JsonProperty("url") val url: String? = null,
)
}
}
}
data class CinemaTvResponse(
@JsonProperty("streams") val streams: HashMap<String, String>? = null,
@JsonProperty("subtitles") val subtitles: ArrayList<Subtitles>? = arrayListOf(),
) {
data class Subtitles(
@JsonProperty("language") val language: String? = null,
@JsonProperty("file") val file: Any? = null,
)
}
data class NepuSearch(
@JsonProperty("data") val data: ArrayList<Data>? = arrayListOf(),
) {
data class Data(
@JsonProperty("url") val url: String? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("type") val type: String? = null,
)
}

View File

@ -0,0 +1,854 @@
package com.hexated
import com.fasterxml.jackson.annotation.JsonProperty
import com.hexated.SoraExtractor.invoke2embed
import com.hexated.SoraExtractor.invokeAllMovieland
import com.hexated.SoraExtractor.invokeAnimes
import com.hexated.SoraExtractor.invokeAoneroom
import com.hexated.SoraExtractor.invokeFilmxy
import com.hexated.SoraExtractor.invokeKimcartoon
import com.hexated.SoraExtractor.invokeVidSrc
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.metaproviders.TmdbProvider
import com.hexated.SoraExtractor.invokeDahmerMovies
import com.hexated.SoraExtractor.invokeDoomovies
import com.hexated.SoraExtractor.invokeDotmovies
import com.hexated.SoraExtractor.invokeDramaday
import com.hexated.SoraExtractor.invokeDreamfilm
import com.hexated.SoraExtractor.invokeFDMovies
import com.hexated.SoraExtractor.invokeFlixon
import com.hexated.SoraExtractor.invokeGoku
import com.hexated.SoraExtractor.invokeKisskh
import com.hexated.SoraExtractor.invokeLing
import com.hexated.SoraExtractor.invokeM4uhd
import com.hexated.SoraExtractor.invokeNinetv
import com.hexated.SoraExtractor.invokeNowTv
import com.hexated.SoraExtractor.invokeRStream
import com.hexated.SoraExtractor.invokeRidomovies
import com.hexated.SoraExtractor.invokeSmashyStream
import com.hexated.SoraExtractor.invokeDumpStream
import com.hexated.SoraExtractor.invokeEmovies
import com.hexated.SoraExtractor.invokeHdmovies4u
import com.hexated.SoraExtractor.invokeMultimovies
import com.hexated.SoraExtractor.invokeNetmovies
import com.hexated.SoraExtractor.invokeShowflix
import com.hexated.SoraExtractor.invokeTvMovies
import com.hexated.SoraExtractor.invokeUhdmovies
import com.hexated.SoraExtractor.invokeVegamovies
import com.hexated.SoraExtractor.invokeVidsrcto
import com.hexated.SoraExtractor.invokeCinemaTv
import com.hexated.SoraExtractor.invokeMoflix
import com.hexated.SoraExtractor.invokeGhostx
import com.hexated.SoraExtractor.invokeNepu
import com.hexated.SoraExtractor.invokeWatchCartoon
import com.hexated.SoraExtractor.invokeWatchsomuch
import com.hexated.SoraExtractor.invokeZoechip
import com.hexated.SoraExtractor.invokeZshow
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
import com.lagradost.cloudstream3.network.CloudflareKiller
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlin.math.roundToInt
open class SoraStream : TmdbProvider() {
override var name = "SoraStream"
override val hasMainPage = true
override val instantLinkLoading = true
override val useMetaLoadResponse = true
override val hasQuickSearch = true
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
TvType.Anime,
)
val wpRedisInterceptor by lazy { CloudflareKiller() }
/** AUTHOR : Hexated & Sora */
companion object {
/** TOOLS */
private const val tmdbAPI = "https://api.themoviedb.org/3"
const val gdbot = "https://gdtot.pro"
const val anilistAPI = "https://graphql.anilist.co"
const val malsyncAPI = "https://api.malsync.moe"
const val jikanAPI = "https://api.jikan.moe/v4"
private const val apiKey = BuildConfig.TMDB_API
/** ALL SOURCES */
const val twoEmbedAPI = "https://www.2embed.cc"
const val vidSrcAPI = "https://vidsrc.me"
const val dreamfilmAPI = "https://dreamfilmsw.net"
const val noverseAPI = "https://www.nollyverse.com"
const val filmxyAPI = "https://www.filmxy.vip"
const val kimcartoonAPI = "https://kimcartoon.li"
const val hianimeAPI = "https://hianime.to"
const val aniwaveAPI = "https://aniwave.to"
const val crunchyrollAPI = "https://beta-api.crunchyroll.com"
const val kissKhAPI = "https://kisskh.co"
const val lingAPI = "https://ling-online.net"
const val m4uhdAPI = "https://ww1.streamm4u.ws"
const val rStreamAPI = "https://remotestream.cc"
const val flixonAPI = "https://flixon.lol"
const val smashyStreamAPI = "https://embed.smashystream.com"
const val watchSomuchAPI = "https://watchsomuch.tv" // sub only
const val cinemaTvAPI = BuildConfig.CINEMATV_API
const val nineTvAPI = "https://moviesapi.club"
const val nowTvAPI = "https://myfilestorage.xyz"
const val gokuAPI = "https://goku.sx"
const val zshowAPI = BuildConfig.ZSHOW_API
const val ridomoviesAPI = "https://ridomovies.tv"
const val emoviesAPI = "https://emovies.si"
const val multimoviesAPI = "https://multimovies.top"
const val multimovies2API = "https://multimovies.click"
const val netmoviesAPI = "https://netmovies.to"
const val allmovielandAPI = "https://allmovieland.fun"
const val doomoviesAPI = "https://doomovies.net"
const val vidsrctoAPI = "https://vidsrc.to"
const val dramadayAPI = "https://dramaday.me"
const val animetoshoAPI = "https://animetosho.org"
const val showflixAPI = "https://showflix.lol"
const val aoneroomAPI = "https://api3.aoneroom.com"
const val mMoviesAPI = "https://multimovies.uno"
const val watchCartoonAPI = "https://www1.watchcartoononline.bz"
const val moflixAPI = "https://moflix-stream.xyz"
const val zoechipAPI = "https://zoechip.org"
const val nepuAPI = "https://nepu.to"
const val fdMoviesAPI = "https://freedrivemovie.com"
const val uhdmoviesAPI = "https://uhdmovies.asia"
const val hdmovies4uAPI = "https://hdmovies4u.day"
const val vegaMoviesAPI = "https://vegamovies.ong"
const val dotmoviesAPI = "https://luxmovies.biz"
const val tvMoviesAPI = "https://www.tvseriesnmovies.com"
const val dahmerMoviesAPI = "https://odd-bird-1319.zwuhygoaqe.workers.dev"
fun getType(t: String?): TvType {
return when (t) {
"movie" -> TvType.Movie
else -> TvType.TvSeries
}
}
fun getStatus(t: String?): ShowStatus {
return when (t) {
"Returning Series" -> ShowStatus.Ongoing
else -> ShowStatus.Completed
}
}
}
override val mainPage = mainPageOf(
"$tmdbAPI/trending/all/day?api_key=$apiKey&region=US" to "Trending",
"$tmdbAPI/movie/popular?api_key=$apiKey&region=US" to "Popular Movies",
"$tmdbAPI/tv/popular?api_key=$apiKey&region=US&with_original_language=en" to "Popular TV Shows",
"$tmdbAPI/tv/airing_today?api_key=$apiKey&region=US&with_original_language=en" to "Airing Today TV Shows",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=213" to "Netflix",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=1024" to "Amazon",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=2739" to "Disney+",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=453" to "Hulu",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=2552" to "Apple TV+",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=49" to "HBO",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=4330" to "Paramount+",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=3353" to "Peacock",
"$tmdbAPI/movie/top_rated?api_key=$apiKey&region=US" to "Top Rated Movies",
"$tmdbAPI/tv/top_rated?api_key=$apiKey&region=US" to "Top Rated TV Shows",
"$tmdbAPI/movie/upcoming?api_key=$apiKey&region=US" to "Upcoming Movies",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_original_language=ko" to "Korean Shows",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_keywords=210024|222243&sort_by=popularity.desc&air_date.lte=${getDate().today}&air_date.gte=${getDate().today}" to "Airing Today Anime",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_keywords=210024|222243&sort_by=popularity.desc&air_date.lte=${getDate().nextWeek}&air_date.gte=${getDate().today}" to "On The Air Anime",
"$tmdbAPI/discover/tv?api_key=$apiKey&with_keywords=210024|222243" to "Anime",
"$tmdbAPI/discover/movie?api_key=$apiKey&with_keywords=210024|222243" to "Anime Movies",
)
private fun getImageUrl(link: String?): String? {
if (link == null) return null
return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500/$link" else link
}
private fun getOriImageUrl(link: String?): String? {
if (link == null) return null
return if (link.startsWith("/")) "https://image.tmdb.org/t/p/original/$link" else link
}
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
val adultQuery =
if (settingsForProvider.enableAdult) "" else "&without_keywords=190370|13059|226161|195669"
val type = if (request.data.contains("/movie")) "movie" else "tv"
val home = app.get("${request.data}$adultQuery&page=$page")
.parsedSafe<Results>()?.results?.mapNotNull { media ->
media.toSearchResponse(type)
} ?: throw ErrorLoadingException("Invalid Json reponse")
return newHomePageResponse(request.name, home)
}
private fun Media.toSearchResponse(type: String? = null): SearchResponse? {
return newMovieSearchResponse(
title ?: name ?: originalTitle ?: return null,
Data(id = id, type = mediaType ?: type).toJson(),
TvType.Movie,
) {
this.posterUrl = getImageUrl(posterPath)
}
}
override suspend fun quickSearch(query: String): List<SearchResponse>? = search(query)
override suspend fun search(query: String): List<SearchResponse>? {
return app.get("$tmdbAPI/search/multi?api_key=$apiKey&language=en-US&query=$query&page=1&include_adult=${settingsForProvider.enableAdult}")
.parsedSafe<Results>()?.results?.mapNotNull { media ->
media.toSearchResponse()
}
}
override suspend fun load(url: String): LoadResponse? {
val data = parseJson<Data>(url)
val type = getType(data.type)
val append = "alternative_titles,credits,external_ids,keywords,videos,recommendations"
val resUrl = if (type == TvType.Movie) {
"$tmdbAPI/movie/${data.id}?api_key=$apiKey&append_to_response=$append"
} else {
"$tmdbAPI/tv/${data.id}?api_key=$apiKey&append_to_response=$append"
}
val res = app.get(resUrl).parsedSafe<MediaDetail>()
?: throw ErrorLoadingException("Invalid Json Response")
val title = res.title ?: res.name ?: return null
val poster = getOriImageUrl(res.posterPath)
val bgPoster = getOriImageUrl(res.backdropPath)
val orgTitle = res.originalTitle ?: res.originalName ?: return null
val releaseDate = res.releaseDate ?: res.firstAirDate
val year = releaseDate?.split("-")?.first()?.toIntOrNull()
val rating = res.vote_average.toString().toRatingInt()
val genres = res.genres?.mapNotNull { it.name }
val isCartoon = genres?.contains("Animation") ?: false
val isAnime = isCartoon && (res.original_language == "zh" || res.original_language == "ja")
val isAsian = !isAnime && (res.original_language == "zh" || res.original_language == "ko")
val isBollywood = res.production_countries?.any { it.name == "India" } ?: false
val keywords = res.keywords?.results?.mapNotNull { it.name }.orEmpty()
.ifEmpty { res.keywords?.keywords?.mapNotNull { it.name } }
val actors = res.credits?.cast?.mapNotNull { cast ->
ActorData(
Actor(
cast.name ?: cast.originalName
?: return@mapNotNull null, getImageUrl(cast.profilePath)
), roleString = cast.character
)
} ?: return null
val recommendations =
res.recommendations?.results?.mapNotNull { media -> media.toSearchResponse() }
val trailer = res.videos?.results?.map { "https://www.youtube.com/watch?v=${it.key}" }
return if (type == TvType.TvSeries) {
val lastSeason = res.last_episode_to_air?.season_number
val episodes = res.seasons?.mapNotNull { season ->
app.get("$tmdbAPI/${data.type}/${data.id}/season/${season.seasonNumber}?api_key=$apiKey")
.parsedSafe<MediaDetailEpisodes>()?.episodes?.map { eps ->
Episode(
LinkData(
data.id,
res.external_ids?.imdb_id,
res.external_ids?.tvdb_id,
data.type,
eps.seasonNumber,
eps.episodeNumber,
title = title,
year = season.airDate?.split("-")?.first()?.toIntOrNull(),
orgTitle = orgTitle,
isAnime = isAnime,
airedYear = year,
lastSeason = lastSeason,
epsTitle = eps.name,
jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title,
date = season.airDate,
airedDate = res.releaseDate
?: res.firstAirDate,
isAsian = isAsian,
isBollywood = isBollywood,
isCartoon = isCartoon
).toJson(),
name = eps.name + if (isUpcoming(eps.airDate)) " • [UPCOMING]" else "",
season = eps.seasonNumber,
episode = eps.episodeNumber,
posterUrl = getImageUrl(eps.stillPath),
rating = eps.voteAverage?.times(10)?.roundToInt(),
description = eps.overview
).apply {
this.addDate(eps.airDate)
}
}
}?.flatten() ?: listOf()
newTvSeriesLoadResponse(
title,
url,
if (isAnime) TvType.Anime else TvType.TvSeries,
episodes
) {
this.posterUrl = poster
this.backgroundPosterUrl = bgPoster
this.year = year
this.plot = res.overview
this.tags = keywords.takeIf { !it.isNullOrEmpty() } ?: genres
this.rating = rating
this.showStatus = getStatus(res.status)
this.recommendations = recommendations
this.actors = actors
this.contentRating = fetchContentRating(data.id, "US")
addTrailer(trailer)
addTMDbId(data.id.toString())
addImdbId(res.external_ids?.imdb_id)
}
} else {
newMovieLoadResponse(
title,
url,
TvType.Movie,
LinkData(
data.id,
res.external_ids?.imdb_id,
res.external_ids?.tvdb_id,
data.type,
title = title,
year = year,
orgTitle = orgTitle,
isAnime = isAnime,
jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title,
airedDate = res.releaseDate
?: res.firstAirDate,
isAsian = isAsian,
isBollywood = isBollywood
).toJson(),
) {
this.posterUrl = poster
this.backgroundPosterUrl = bgPoster
this.comingSoon = isUpcoming(releaseDate)
this.year = year
this.plot = res.overview
this.duration = res.runtime
this.tags = keywords.takeIf { !it.isNullOrEmpty() } ?: genres
this.rating = rating
this.recommendations = recommendations
this.actors = actors
this.contentRating = fetchContentRating(data.id, "US")
addTrailer(trailer)
addTMDbId(data.id.toString())
addImdbId(res.external_ids?.imdb_id)
}
}
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val res = parseJson<LinkData>(data)
argamap(
{
invokeDumpStream(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
invokeGoku(
res.title,
res.year,
res.season,
res.lastSeason,
res.episode,
subtitleCallback,
callback
)
},
{
invokeVidSrc(res.id, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeAoneroom(
res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (res.isAnime) invokeAnimes(
res.title,
res.epsTitle,
res.date,
res.airedDate,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeDreamfilm(
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
// {
// invokeNoverse(res.title, res.season, res.episode, callback)
// },
{
if (!res.isAnime) invokeFilmxy(
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime && res.isCartoon) invokeKimcartoon(
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime && res.isCartoon) invokeWatchCartoon(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeVidsrcto(
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (res.isAsian || res.isAnime) invokeKisskh(
res.title,
res.season,
res.episode,
res.isAnime,
res.lastSeason,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeGhostx(
res.title,
res.year,
res.season,
res.episode,
callback
)
},
{
if (!res.isAnime) invokeLing(
res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (!res.isAnime) invokeUhdmovies(
res.title,
res.year,
res.season,
res.episode,
callback
)
},
{
if (!res.isAnime) invokeFDMovies(res.title, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeM4uhd(
res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (!res.isAnime) invokeTvMovies(res.title, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeRStream(res.id, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeFlixon(
res.id,
res.imdbId,
res.season,
res.episode,
callback
)
},
{
if (!res.isAnime) invokeSmashyStream(
res.id,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeWatchsomuch(
res.imdbId,
res.season,
res.episode,
subtitleCallback
)
},
{
if (!res.isAnime) invokeNinetv(
res.id,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
invokeDahmerMovies(res.title, res.year, res.season, res.episode, callback)
},
{
invokeCinemaTv(
res.imdbId, res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (!res.isAnime) invokeNowTv(res.id, res.imdbId, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeRidomovies(
res.id,
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeAllMovieland(res.imdbId, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeEmovies(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeVegamovies(
res.title,
res.year,
res.season,
res.lastSeason,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime && res.isBollywood) invokeDotmovies(
res.title,
res.year,
res.season,
res.lastSeason,
res.episode,
subtitleCallback,
callback
)
},
{
if (res.isBollywood) invokeMultimovies(
multimoviesAPI,
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (res.isBollywood) invokeMultimovies(
multimovies2API,
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
invokeNetmovies(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime && res.season == null) invokeDoomovies(
res.title,
subtitleCallback,
callback
)
},
{
if (res.isAsian) invokeDramaday(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invoke2embed(
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeHdmovies4u(
res.title,
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
invokeZshow(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeShowflix(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeMoflix(res.id, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeZoechip(res.title, res.year, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeNepu(
res.title,
res.airedYear ?: res.year,
res.season,
res.episode,
callback
)
}
)
return true
}
data class LinkData(
val id: Int? = null,
val imdbId: String? = null,
val tvdbId: Int? = null,
val type: String? = null,
val season: Int? = null,
val episode: Int? = null,
val aniId: String? = null,
val animeId: String? = null,
val title: String? = null,
val year: Int? = null,
val orgTitle: String? = null,
val isAnime: Boolean = false,
val airedYear: Int? = null,
val lastSeason: Int? = null,
val epsTitle: String? = null,
val jpTitle: String? = null,
val date: String? = null,
val airedDate: String? = null,
val isAsian: Boolean = false,
val isBollywood: Boolean = false,
val isCartoon: Boolean = false,
)
data class Data(
val id: Int? = null,
val type: String? = null,
val aniId: String? = null,
val malId: Int? = null,
)
data class Results(
@JsonProperty("results") val results: ArrayList<Media>? = arrayListOf(),
)
data class Media(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("original_title") val originalTitle: String? = null,
@JsonProperty("media_type") val mediaType: String? = null,
@JsonProperty("poster_path") val posterPath: String? = null,
)
data class Genres(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("name") val name: String? = null,
)
data class Keywords(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("name") val name: String? = null,
)
data class KeywordResults(
@JsonProperty("results") val results: ArrayList<Keywords>? = arrayListOf(),
@JsonProperty("keywords") val keywords: ArrayList<Keywords>? = arrayListOf(),
)
data class Seasons(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("season_number") val seasonNumber: Int? = null,
@JsonProperty("air_date") val airDate: String? = null,
)
data class Cast(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("original_name") val originalName: String? = null,
@JsonProperty("character") val character: String? = null,
@JsonProperty("known_for_department") val knownForDepartment: String? = null,
@JsonProperty("profile_path") val profilePath: String? = null,
)
data class Episodes(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("air_date") val airDate: String? = null,
@JsonProperty("still_path") val stillPath: String? = null,
@JsonProperty("vote_average") val voteAverage: Double? = null,
@JsonProperty("episode_number") val episodeNumber: Int? = null,
@JsonProperty("season_number") val seasonNumber: Int? = null,
)
data class MediaDetailEpisodes(
@JsonProperty("episodes") val episodes: ArrayList<Episodes>? = arrayListOf(),
)
data class Trailers(
@JsonProperty("key") val key: String? = null,
)
data class ResultsTrailer(
@JsonProperty("results") val results: ArrayList<Trailers>? = arrayListOf(),
)
data class AltTitles(
@JsonProperty("iso_3166_1") val iso_3166_1: String? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("type") val type: String? = null,
)
data class ResultsAltTitles(
@JsonProperty("results") val results: ArrayList<AltTitles>? = arrayListOf(),
)
data class ExternalIds(
@JsonProperty("imdb_id") val imdb_id: String? = null,
@JsonProperty("tvdb_id") val tvdb_id: Int? = null,
)
data class Credits(
@JsonProperty("cast") val cast: ArrayList<Cast>? = arrayListOf(),
)
data class ResultsRecommendations(
@JsonProperty("results") val results: ArrayList<Media>? = arrayListOf(),
)
data class LastEpisodeToAir(
@JsonProperty("episode_number") val episode_number: Int? = null,
@JsonProperty("season_number") val season_number: Int? = null,
)
data class ProductionCountries(
@JsonProperty("name") val name: String? = null,
)
data class MediaDetail(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("imdb_id") val imdbId: String? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("original_title") val originalTitle: String? = null,
@JsonProperty("original_name") val originalName: String? = null,
@JsonProperty("poster_path") val posterPath: String? = null,
@JsonProperty("backdrop_path") val backdropPath: String? = null,
@JsonProperty("release_date") val releaseDate: String? = null,
@JsonProperty("first_air_date") val firstAirDate: String? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("runtime") val runtime: Int? = null,
@JsonProperty("vote_average") val vote_average: Any? = null,
@JsonProperty("original_language") val original_language: String? = null,
@JsonProperty("status") val status: String? = null,
@JsonProperty("genres") val genres: ArrayList<Genres>? = arrayListOf(),
@JsonProperty("keywords") val keywords: KeywordResults? = null,
@JsonProperty("last_episode_to_air") val last_episode_to_air: LastEpisodeToAir? = null,
@JsonProperty("seasons") val seasons: ArrayList<Seasons>? = arrayListOf(),
@JsonProperty("videos") val videos: ResultsTrailer? = null,
@JsonProperty("external_ids") val external_ids: ExternalIds? = null,
@JsonProperty("credits") val credits: Credits? = null,
@JsonProperty("recommendations") val recommendations: ResultsRecommendations? = null,
@JsonProperty("alternative_titles") val alternative_titles: ResultsAltTitles? = null,
@JsonProperty("production_countries") val production_countries: ArrayList<ProductionCountries>? = arrayListOf(),
)
}

View File

@ -0,0 +1,347 @@
package com.hexated
import com.hexated.SoraExtractor.invoke2embed
import com.hexated.SoraExtractor.invokeAllMovieland
import com.hexated.SoraExtractor.invokeAnimes
import com.hexated.SoraExtractor.invokeAoneroom
import com.hexated.SoraExtractor.invokeDoomovies
import com.hexated.SoraExtractor.invokeDramaday
import com.hexated.SoraExtractor.invokeDreamfilm
import com.hexated.SoraExtractor.invokeFilmxy
import com.hexated.SoraExtractor.invokeFlixon
import com.hexated.SoraExtractor.invokeGoku
import com.hexated.SoraExtractor.invokeKimcartoon
import com.hexated.SoraExtractor.invokeKisskh
import com.hexated.SoraExtractor.invokeLing
import com.hexated.SoraExtractor.invokeM4uhd
import com.hexated.SoraExtractor.invokeNinetv
import com.hexated.SoraExtractor.invokeNowTv
import com.hexated.SoraExtractor.invokeRStream
import com.hexated.SoraExtractor.invokeRidomovies
import com.hexated.SoraExtractor.invokeSmashyStream
import com.hexated.SoraExtractor.invokeDumpStream
import com.hexated.SoraExtractor.invokeEmovies
import com.hexated.SoraExtractor.invokeMultimovies
import com.hexated.SoraExtractor.invokeNetmovies
import com.hexated.SoraExtractor.invokeShowflix
import com.hexated.SoraExtractor.invokeVidSrc
import com.hexated.SoraExtractor.invokeVidsrcto
import com.hexated.SoraExtractor.invokeCinemaTv
import com.hexated.SoraExtractor.invokeMoflix
import com.hexated.SoraExtractor.invokeGhostx
import com.hexated.SoraExtractor.invokeNepu
import com.hexated.SoraExtractor.invokeWatchCartoon
import com.hexated.SoraExtractor.invokeWatchsomuch
import com.hexated.SoraExtractor.invokeZoechip
import com.hexated.SoraExtractor.invokeZshow
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.argamap
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.ExtractorLink
class SoraStreamLite : SoraStream() {
override var name = "SoraStream-Lite"
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val res = AppUtils.parseJson<LinkData>(data)
argamap(
{
if (!res.isAnime) invokeMoflix(res.id, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeWatchsomuch(
res.imdbId,
res.season,
res.episode,
subtitleCallback
)
},
{
invokeDumpStream(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeNinetv(
res.id,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
invokeGoku(
res.title,
res.year,
res.season,
res.lastSeason,
res.episode,
subtitleCallback,
callback
)
},
{
invokeVidSrc(res.id, res.season, res.episode, callback)
},
{
if (!res.isAnime && res.isCartoon) invokeWatchCartoon(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (res.isAnime) invokeAnimes(
res.title,
res.epsTitle,
res.date,
res.airedDate,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeDreamfilm(
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeFilmxy(
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeGhostx(
res.title,
res.year,
res.season,
res.episode,
callback
)
},
{
if (!res.isAnime && res.isCartoon) invokeKimcartoon(
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeSmashyStream(
res.id,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeVidsrcto(
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (res.isAsian || res.isAnime) invokeKisskh(
res.title,
res.season,
res.episode,
res.isAnime,
res.lastSeason,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeLing(
res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (!res.isAnime) invokeM4uhd(
res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (!res.isAnime) invokeRStream(res.id, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeFlixon(
res.id,
res.imdbId,
res.season,
res.episode,
callback
)
},
{
invokeCinemaTv(
res.imdbId, res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (!res.isAnime) invokeNowTv(res.id, res.imdbId, res.season, res.episode, callback)
},
{
if (!res.isAnime) invokeAoneroom(
res.title, res.airedYear
?: res.year, res.season, res.episode, subtitleCallback, callback
)
},
{
if (!res.isAnime) invokeRidomovies(
res.id,
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeEmovies(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (res.isBollywood) invokeMultimovies(
multimoviesAPI,
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (res.isBollywood) invokeMultimovies(
multimovies2API,
res.title,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
invokeNetmovies(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeAllMovieland(res.imdbId, res.season, res.episode, callback)
},
{
if (!res.isAnime && res.season == null) invokeDoomovies(
res.title,
subtitleCallback,
callback
)
},
{
if (res.isAsian) invokeDramaday(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invoke2embed(
res.imdbId,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
invokeZshow(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeShowflix(
res.title,
res.year,
res.season,
res.episode,
subtitleCallback,
callback
)
},
{
if (!res.isAnime) invokeZoechip(
res.title,
res.year,
res.season,
res.episode,
callback
)
},
{
if (!res.isAnime) invokeNepu(
res.title,
res.airedYear ?: res.year,
res.season,
res.episode,
callback
)
}
)
return true
}
}

View File

@ -0,0 +1,40 @@
package com.hexated
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
import com.lagradost.cloudstream3.plugins.Plugin
import android.content.Context
@CloudstreamPlugin
class SoraStreamPlugin: Plugin() {
override fun load(context: Context) {
// All providers should be added in this manner. Please don't edit the providers list directly.
registerMainAPI(SoraStream())
registerMainAPI(SoraStreamLite())
registerExtractorAPI(Animefever())
registerExtractorAPI(Multimovies())
registerExtractorAPI(MultimoviesSB())
registerExtractorAPI(Yipsu())
registerExtractorAPI(Mwish())
registerExtractorAPI(TravelR())
registerExtractorAPI(Playm4u())
registerExtractorAPI(VCloud())
registerExtractorAPI(Pixeldra())
registerExtractorAPI(M4ufree())
registerExtractorAPI(Streamruby())
registerExtractorAPI(Streamwish())
registerExtractorAPI(FilelionsTo())
registerExtractorAPI(Embedwish())
registerExtractorAPI(UqloadsXyz())
registerExtractorAPI(Uploadever())
registerExtractorAPI(Netembed())
registerExtractorAPI(Flaswish())
registerExtractorAPI(Comedyshow())
registerExtractorAPI(Ridoo())
registerExtractorAPI(Streamvid())
registerExtractorAPI(Embedrise())
registerExtractorAPI(Gdmirrorbot())
registerExtractorAPI(FilemoonNl())
registerExtractorAPI(Alions())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
import org.jetbrains.kotlin.konan.properties.Properties
// use an integer for version numbers
version = 3
android {
defaultConfig {
val properties = Properties()
properties.load(project.rootProject.file("local.properties").inputStream())
buildConfigField("String", "SUPERSTREAM_FIRST_API", "\"${properties.getProperty("SUPERSTREAM_FIRST_API")}\"")
buildConfigField("String", "SUPERSTREAM_SECOND_API", "\"${properties.getProperty("SUPERSTREAM_SECOND_API")}\"")
buildConfigField("String", "SUPERSTREAM_THIRD_API", "\"${properties.getProperty("SUPERSTREAM_THIRD_API")}\"")
buildConfigField("String", "SUPERSTREAM_FOURTH_API", "\"${properties.getProperty("SUPERSTREAM_FOURTH_API")}\"")
}
}
cloudstream {
language = "en"
// All of these properties are optional, you can safely remove them
// description = "Lorem Ipsum"
authors = listOf("Blatzar")
/**
* Status int as the following:
* 0: Down
* 1: Ok
* 2: Slow
* 3: Beta only
* */
status = 1 // will be 3 if unspecified
tvTypes = listOf(
"AsianDrama",
"Anime",
"TvSeries",
"Movie",
)
iconUrl = "https://cdn.discordapp.com/attachments/1109266606292488297/1196694385061003334/icon.png?ex=65efee7e&is=65dd797e&hm=18fa57323826d0cbf3cf5ce7d3f5705de640f2f8d08739d41f95882d2ae0a3e0&"
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.hexated"/>

View File

@ -0,0 +1,243 @@
package com.hexated
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.*
import java.net.URL
object Extractors : Superstream() {
suspend fun invokeInternalSource(
id: Int? = null,
type: Int? = null,
season: Int? = null,
episode: Int? = null,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
) {
fun LinkList.toExtractorLink(): ExtractorLink? {
if (this.path.isNullOrBlank()) return null
return ExtractorLink(
"Internal",
"Internal [${this.size}]",
this.path.replace("\\/", ""),
"",
getQualityFromName(this.quality),
)
}
// No childmode when getting links
// New api does not return video links :(
val query = if (type == ResponseTypes.Movies.value) {
"""{"childmode":"0","uid":"","app_version":"11.5","appid":"$appId","module":"Movie_downloadurl_v3","channel":"Website","mid":"$id","lang":"","expired_date":"${getExpiryDate()}","platform":"android","oss":"1","group":""}"""
} else {
"""{"childmode":"0","app_version":"11.5","module":"TV_downloadurl_v3","channel":"Website","episode":"$episode","expired_date":"${getExpiryDate()}","platform":"android","tid":"$id","oss":"1","uid":"","appid":"$appId","season":"$season","lang":"en","group":""}"""
}
val linkData = queryApiParsed<LinkDataProp>(query, false)
linkData.data?.list?.forEach {
callback.invoke(it.toExtractorLink() ?: return@forEach)
}
// Should really run this query for every link :(
val fid = linkData.data?.list?.firstOrNull { it.fid != null }?.fid
val subtitleQuery = if (type == ResponseTypes.Movies.value) {
"""{"childmode":"0","fid":"$fid","uid":"","app_version":"11.5","appid":"$appId","module":"Movie_srt_list_v2","channel":"Website","mid":"$id","lang":"en","expired_date":"${getExpiryDate()}","platform":"android"}"""
} else {
"""{"childmode":"0","fid":"$fid","app_version":"11.5","module":"TV_srt_list_v2","channel":"Website","episode":"$episode","expired_date":"${getExpiryDate()}","platform":"android","tid":"$id","uid":"","appid":"$appId","season":"$season","lang":"en"}"""
}
val subtitles = queryApiParsed<SubtitleDataProp>(subtitleQuery).data
subtitles?.list?.forEach { subs ->
val sub = subs.subtitles.maxByOrNull { it.support_total ?: 0 }
subtitleCallback.invoke(
SubtitleFile(
sub?.language ?: sub?.lang ?: return@forEach,
sub?.filePath ?: return@forEach
)
)
}
}
suspend fun invokeExternalSource(
mediaId: Int? = null,
type: Int? = null,
season: Int? = null,
episode: Int? = null,
callback: (ExtractorLink) -> Unit,
) {
val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode)
val shareKey = app.get("$fourthAPI/index/share_link?id=${mediaId}&type=$type")
.parsedSafe<ExternalResponse>()?.data?.link?.substringAfterLast("/") ?: return
val headers = mapOf("Accept-Language" to "en")
val shareRes = app.get("$thirdAPI/file/file_share_list?share_key=$shareKey", headers = headers)
.parsedSafe<ExternalResponse>()?.data ?: return
val fids = if (season == null) {
shareRes.file_list
} else {
val parentId = shareRes.file_list?.find { it.file_name.equals("season $season", true) }?.fid
app.get("$thirdAPI/file/file_share_list?share_key=$shareKey&parent_id=$parentId&page=1", headers = headers)
.parsedSafe<ExternalResponse>()?.data?.file_list?.filter {
it.file_name?.contains("s${seasonSlug}e${episodeSlug}", true) == true
}
} ?: return
fids.apmapIndexed { index, fileList ->
val player = app.get("$thirdAPI/file/player?fid=${fileList.fid}&share_key=$shareKey").text
val sources = "sources\\s*=\\s*(.*);".toRegex().find(player)?.groupValues?.get(1)
val qualities = "quality_list\\s*=\\s*(.*);".toRegex().find(player)?.groupValues?.get(1)
listOf(sources, qualities).forEach {
AppUtils.tryParseJson<ArrayList<ExternalSources>>(it)?.forEach org@{ source ->
val format = if (source.type == "video/mp4") ExtractorLinkType.VIDEO else ExtractorLinkType.M3U8
val label = if (format == ExtractorLinkType.M3U8) "Hls" else "Mp4"
if(!(source.label == "AUTO" || format == ExtractorLinkType.VIDEO)) return@org
callback.invoke(
ExtractorLink(
"External",
"External $label [Server ${index + 1}]",
(source.m3u8_url ?: source.file)?.replace("\\/", "/") ?: return@org,
"",
getIndexQuality(if (format == ExtractorLinkType.M3U8) fileList.file_name else source.label),
type = format,
)
)
}
}
}
}
suspend fun invokeWatchsomuch(
imdbId: String? = null,
season: Int? = null,
episode: Int? = null,
subtitleCallback: (SubtitleFile) -> Unit,
) {
val id = imdbId?.removePrefix("tt")
val epsId = app.post(
"$watchSomuchAPI/Watch/ajMovieTorrents.aspx",
data = mapOf(
"index" to "0",
"mid" to "$id",
"wsk" to "30fb68aa-1c71-4b8c-b5d4-4ca9222cfb45",
"lid" to "",
"liu" to ""
), headers = mapOf("X-Requested-With" to "XMLHttpRequest")
).parsedSafe<WatchsomuchResponses>()?.movie?.torrents?.let { eps ->
if (season == null) {
eps.firstOrNull()?.id
} else {
eps.find { it.episode == episode && it.season == season }?.id
}
} ?: return
val (seasonSlug, episodeSlug) = getEpisodeSlug(
season,
episode
)
val subUrl = if (season == null) {
"$watchSomuchAPI/Watch/ajMovieSubtitles.aspx?mid=$id&tid=$epsId&part="
} else {
"$watchSomuchAPI/Watch/ajMovieSubtitles.aspx?mid=$id&tid=$epsId&part=S${seasonSlug}E${episodeSlug}"
}
app.get(subUrl)
.parsedSafe<WatchsomuchSubResponses>()?.subtitles
?.map { sub ->
subtitleCallback.invoke(
SubtitleFile(
sub.label ?: "",
fixUrl(sub.url ?: return@map null, watchSomuchAPI)
)
)
}
}
suspend fun invokeOpenSubs(
imdbId: String? = null,
season: Int? = null,
episode: Int? = null,
subtitleCallback: (SubtitleFile) -> Unit,
) {
val slug = if(season == null) {
"movie/$imdbId"
} else {
"series/$imdbId:$season:$episode"
}
app.get("${openSubAPI}/subtitles/$slug.json", timeout = 120L).parsedSafe<OsResult>()?.subtitles?.map { sub ->
subtitleCallback.invoke(
SubtitleFile(
SubtitleHelper.fromThreeLettersToLanguage(sub.lang ?: "") ?: sub.lang
?: return@map,
sub.url ?: return@map
)
)
}
}
suspend fun invokeVidsrcto(
imdbId: String?,
season: Int?,
episode: Int?,
subtitleCallback: (SubtitleFile) -> Unit,
) {
val url = if (season == null) {
"$vidsrctoAPI/embed/movie/$imdbId"
} else {
"$vidsrctoAPI/embed/tv/$imdbId/$season/$episode"
}
val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return
val subtitles = app.get("$vidsrctoAPI/ajax/embed/episode/$mediaId/subtitles").text
AppUtils.tryParseJson<List<VidsrcSubtitles>>(subtitles)?.map {
subtitleCallback.invoke(
SubtitleFile(
it.label ?: "",
it.file ?: return@map
)
)
}
}
private fun fixUrl(url: String, domain: 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 domain + url
}
return "$domain/$url"
}
}
private fun getIndexQuality(str: String?): Int {
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
?: Qualities.Unknown.value
}
private fun getEpisodeSlug(
season: Int? = null,
episode: Int? = null,
): Pair<String, String> {
return if (season == null && episode == null) {
"" to ""
} else {
(if (season!! < 10) "0$season" else "$season") to (if (episode!! < 10) "0$episode" else "$episode")
}
}
}

View File

@ -0,0 +1,823 @@
package com.hexated
import android.util.Base64
import com.fasterxml.jackson.annotation.JsonProperty
import com.hexated.Extractors.invokeExternalSource
import com.hexated.Extractors.invokeInternalSource
import com.hexated.Extractors.invokeOpenSubs
import com.hexated.Extractors.invokeVidsrcto
import com.hexated.Extractors.invokeWatchsomuch
import com.hexated.Superstream.CipherUtils.getVerify
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.capitalize
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.nicehttp.NiceResponse
import okhttp3.Interceptor
import okhttp3.Response
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import javax.crypto.Cipher
import javax.crypto.Cipher.DECRYPT_MODE
import javax.crypto.Cipher.ENCRYPT_MODE
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.math.roundToInt
open class Superstream : MainAPI() {
private val timeout = 60L
override var name = "SuperStream"
override val hasMainPage = true
override val hasChromecastSupport = true
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
TvType.Anime,
TvType.AnimeMovie,
)
enum class ResponseTypes(val value: Int) {
Series(2),
Movies(1);
fun toTvType(): TvType {
return if (this == Series) TvType.TvSeries else TvType.Movie
}
companion object {
fun getResponseType(value: Int?): ResponseTypes {
return values().firstOrNull { it.value == value } ?: Movies
}
}
}
override val instantLinkLoading = true
private val interceptor = UserAgentInterceptor()
private val headers = mapOf(
"Platform" to "android",
"Accept" to "charset=utf-8",
)
private class UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(
chain.request()
.newBuilder()
.removeHeader("user-agent")
.build()
)
}
}
// Random 32 length string
private fun randomToken(): String {
return (0..31).joinToString("") {
(('0'..'9') + ('a'..'f')).random().toString()
}
}
private val token = randomToken()
private object CipherUtils {
private const val ALGORITHM = "DESede"
private const val TRANSFORMATION = "DESede/CBC/PKCS5Padding"
fun encrypt(str: String, key: String, iv: String): String? {
return try {
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
val bArr = ByteArray(24)
val bytes: ByteArray = key.toByteArray()
var length = if (bytes.size <= 24) bytes.size else 24
System.arraycopy(bytes, 0, bArr, 0, length)
while (length < 24) {
bArr[length] = 0
length++
}
cipher.init(
ENCRYPT_MODE,
SecretKeySpec(bArr, ALGORITHM),
IvParameterSpec(iv.toByteArray())
)
String(Base64.encode(cipher.doFinal(str.toByteArray()), 2), StandardCharsets.UTF_8)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
// Useful for deobfuscation
fun decrypt(str: String, key: String, iv: String): String? {
return try {
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
val bArr = ByteArray(24)
val bytes: ByteArray = key.toByteArray()
var length = if (bytes.size <= 24) bytes.size else 24
System.arraycopy(bytes, 0, bArr, 0, length)
while (length < 24) {
bArr[length] = 0
length++
}
cipher.init(
DECRYPT_MODE,
SecretKeySpec(bArr, ALGORITHM),
IvParameterSpec(iv.toByteArray())
)
val inputStr = Base64.decode(str.toByteArray(), Base64.DEFAULT)
cipher.doFinal(inputStr).decodeToString()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun md5(str: String): String? {
return MD5Util.md5(str)?.let { HexDump.toHexString(it).lowercase() }
}
fun getVerify(str: String?, str2: String, str3: String): String? {
if (str != null) {
return md5(md5(str2) + str3 + str)
}
return null
}
}
private object HexDump {
private val HEX_DIGITS = charArrayOf(
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F'
)
@JvmOverloads
fun toHexString(bArr: ByteArray, i: Int = 0, i2: Int = bArr.size): String {
val cArr = CharArray(i2 * 2)
var i3 = 0
for (i4 in i until i + i2) {
val b = bArr[i4].toInt()
val i5 = i3 + 1
val cArr2 = HEX_DIGITS
cArr[i3] = cArr2[b ushr 4 and 15]
i3 = i5 + 1
cArr[i5] = cArr2[b and 15]
}
return String(cArr)
}
}
private object MD5Util {
fun md5(str: String): ByteArray? {
return md5(str.toByteArray())
}
fun md5(bArr: ByteArray?): ByteArray? {
return try {
val digest = MessageDigest.getInstance("MD5")
digest.update(bArr ?: return null)
digest.digest()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
}
}
}
suspend fun queryApi(query: String, useAlternativeApi: Boolean): NiceResponse {
val encryptedQuery = CipherUtils.encrypt(query, key, iv)!!
val appKeyHash = CipherUtils.md5(appKey)!!
val newBody =
"""{"app_key":"$appKeyHash","verify":"${
getVerify(
encryptedQuery,
appKey,
key
)
}","encrypt_data":"$encryptedQuery"}"""
val base64Body = String(Base64.encode(newBody.toByteArray(), Base64.DEFAULT))
val data = mapOf(
"data" to base64Body,
"appid" to "27",
"platform" to "android",
"version" to appVersionCode,
// Probably best to randomize this
"medium" to "Website&token$token"
)
val url = if (useAlternativeApi) secondAPI else firstAPI
return app.post(
url,
headers = headers,
data = data,
timeout = timeout,
interceptor = interceptor
)
}
suspend inline fun <reified T : Any> queryApiParsed(
query: String,
useAlternativeApi: Boolean = true
): T {
return queryApi(query, useAlternativeApi).parsed()
}
fun getExpiryDate(): Long {
// Current time + 12 hours
return unixTime + 60 * 60 * 12
}
private data class PostJSON(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("poster") val poster: String? = null,
@JsonProperty("poster_2") val poster2: String? = null,
@JsonProperty("box_type") val boxType: Int? = null,
@JsonProperty("imdb_rating") val imdbRating: String? = null,
@JsonProperty("quality_tag") val quality_tag: String? = null,
)
private data class ListJSON(
@JsonProperty("code") val code: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("box_type") val boxType: Int? = null,
@JsonProperty("list") val list: ArrayList<PostJSON> = arrayListOf(),
)
private data class DataJSON(
@JsonProperty("data") val data: ArrayList<ListJSON> = arrayListOf()
)
// We do not want content scanners to notice this scraping going on so we've hidden all constants
// The source has its origins in China so I added some extra security with banned words
// Mayhaps a tiny bit unethical, but this source is just too good :)
// If you are copying this code please use precautions so they do not change their api.
// Free Tibet, The Tienanmen Square protests of 1989
private val iv = base64Decode("d0VpcGhUbiE=")
private val key = base64Decode("MTIzZDZjZWRmNjI2ZHk1NDIzM2FhMXc2")
private val firstAPI = BuildConfig.SUPERSTREAM_FIRST_API
// Another url because the first one sucks at searching
// This one was revealed to me in a dream
private val secondAPI = BuildConfig.SUPERSTREAM_SECOND_API
val thirdAPI = BuildConfig.SUPERSTREAM_THIRD_API
val fourthAPI = BuildConfig.SUPERSTREAM_FOURTH_API
val watchSomuchAPI = "https://watchsomuch.tv"
val openSubAPI = "https://opensubtitles-v3.strem.io"
val vidsrctoAPI = "https://vidsrc.to"
private val appKey = base64Decode("bW92aWVib3g=")
val appId = base64Decode("Y29tLnRkby5zaG93Ym94")
private val appIdSecond = base64Decode("Y29tLm1vdmllYm94cHJvLmFuZHJvaWQ=")
private val appVersion = "11.5"
private val appVersionCode = "129"
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
val hideNsfw = if (settingsForProvider.enableAdult) 0 else 1
val data = queryApiParsed<DataJSON>(
"""{"childmode":"$hideNsfw","app_version":"$appVersion","appid":"$appIdSecond","module":"Home_list_type_v5","channel":"Website","page":"$page","lang":"en","type":"all","pagelimit":"10","expired_date":"${getExpiryDate()}","platform":"android"}
""".trimIndent()
)
// Cut off the first row (featured)
val pages = data.data.let { it.subList(minOf(it.size, 1), it.size) }
.mapNotNull {
var name = it.name
if (name.isNullOrEmpty()) name = "Featured"
val postList = it.list.mapNotNull second@{ post ->
val type = if (post.boxType == 1) TvType.Movie else TvType.TvSeries
newMovieSearchResponse(
name = post.title ?: return@second null,
url = LoadData(post.id ?: return@mapNotNull null, post.boxType).toJson(),
type = type,
fix = false
) {
posterUrl = post.poster ?: post.poster2
quality = getQualityFromString(post.quality_tag ?: "")
}
}
if (postList.isEmpty()) return@mapNotNull null
HomePageList(name, postList)
}
return HomePageResponse(pages, hasNext = !pages.any { it.list.isEmpty() })
}
private data class Data(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("mid") val mid: Int? = null,
@JsonProperty("box_type") val boxType: Int? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("poster_org") val posterOrg: String? = null,
@JsonProperty("poster") val poster: String? = null,
@JsonProperty("cats") val cats: String? = null,
@JsonProperty("year") val year: Int? = null,
@JsonProperty("imdb_rating") val imdbRating: String? = null,
@JsonProperty("quality_tag") val qualityTag: String? = null,
) {
fun toSearchResponse(api: MainAPI): MovieSearchResponse? {
return api.newMovieSearchResponse(
this.title ?: "",
LoadData(
this.id ?: this.mid ?: return null,
this.boxType ?: ResponseTypes.Movies.value
).toJson(),
ResponseTypes.getResponseType(this.boxType).toTvType(),
false
) {
posterUrl = this@Data.posterOrg ?: this@Data.poster
year = this@Data.year
quality = getQualityFromString(this@Data.qualityTag?.replace("-", "") ?: "")
}
}
}
private data class MainDataList(
@JsonProperty("list") val list: ArrayList<Data> = arrayListOf()
)
private data class MainData(
@JsonProperty("data") val data: MainDataList
)
override suspend fun search(query: String): List<SearchResponse> {
val hideNsfw = if (settingsForProvider.enableAdult) 0 else 1
val apiQuery =
// Originally 8 pagelimit
"""{"childmode":"$hideNsfw","app_version":"$appVersion","appid":"$appIdSecond","module":"Search4","channel":"Website","page":"1","lang":"en","type":"all","keyword":"$query","pagelimit":"20","expired_date":"${getExpiryDate()}","platform":"android"}"""
val searchResponse = queryApiParsed<MainData>(apiQuery, true).data.list.mapNotNull {
it.toSearchResponse(this)
}
return searchResponse
}
private data class LoadData(
val id: Int,
val type: Int?
)
private data class MovieData(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("director") val director: String? = null,
@JsonProperty("writer") val writer: String? = null,
@JsonProperty("actors") val actors: String? = null,
@JsonProperty("runtime") val runtime: Int? = null,
@JsonProperty("poster") val poster: String? = null,
@JsonProperty("description") val description: String? = null,
@JsonProperty("cats") val cats: String? = null,
@JsonProperty("year") val year: Int? = null,
@JsonProperty("imdb_id") val imdbId: String? = null,
@JsonProperty("imdb_rating") val imdbRating: String? = null,
@JsonProperty("trailer") val trailer: String? = null,
@JsonProperty("released") val released: String? = null,
@JsonProperty("content_rating") val contentRating: String? = null,
@JsonProperty("tmdb_id") val tmdbId: Int? = null,
@JsonProperty("tomato_meter") val tomatoMeter: Int? = null,
@JsonProperty("poster_org") val posterOrg: String? = null,
@JsonProperty("trailer_url") val trailerUrl: String? = null,
@JsonProperty("imdb_link") val imdbLink: String? = null,
@JsonProperty("box_type") val boxType: Int? = null,
@JsonProperty("recommend") val recommend: List<Data> = listOf(),
)
private data class MovieDataProp(
@JsonProperty("data") val data: MovieData? = MovieData()
)
private data class SeriesDataProp(
@JsonProperty("code") val code: Int? = null,
@JsonProperty("msg") val msg: String? = null,
@JsonProperty("data") val data: SeriesData? = SeriesData()
)
private data class SeriesSeasonProp(
@JsonProperty("code") val code: Int? = null,
@JsonProperty("msg") val msg: String? = null,
@JsonProperty("data") val data: ArrayList<SeriesEpisode>? = arrayListOf()
)
// data class PlayProgress (
//
// @JsonProperty("over" ) val over : Int? = null,
// @JsonProperty("seconds" ) val seconds : Int? = null,
// @JsonProperty("mp4_id" ) val mp4Id : Int? = null,
// @JsonProperty("last_time" ) val lastTime : Int? = null
//
//)
private data class SeriesEpisode(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("tid") val tid: Int? = null,
@JsonProperty("mb_id") val mbId: Int? = null,
@JsonProperty("imdb_id") val imdbId: String? = null,
@JsonProperty("imdb_id_status") val imdbIdStatus: Int? = null,
@JsonProperty("srt_status") val srtStatus: Int? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null,
@JsonProperty("state") val state: Int? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("thumbs") val thumbs: String? = null,
@JsonProperty("thumbs_bak") val thumbsBak: String? = null,
@JsonProperty("thumbs_original") val thumbsOriginal: String? = null,
@JsonProperty("poster_imdb") val posterImdb: Int? = null,
@JsonProperty("synopsis") val synopsis: String? = null,
@JsonProperty("runtime") val runtime: Int? = null,
@JsonProperty("view") val view: Int? = null,
@JsonProperty("download") val download: Int? = null,
@JsonProperty("source_file") val sourceFile: Int? = null,
@JsonProperty("code_file") val codeFile: Int? = null,
@JsonProperty("add_time") val addTime: Int? = null,
@JsonProperty("update_time") val updateTime: Int? = null,
@JsonProperty("released") val released: String? = null,
@JsonProperty("released_timestamp") val releasedTimestamp: Long? = null,
@JsonProperty("audio_lang") val audioLang: String? = null,
@JsonProperty("quality_tag") val qualityTag: String? = null,
@JsonProperty("3d") val _3d: Int? = null,
@JsonProperty("remark") val remark: String? = null,
@JsonProperty("pending") val pending: String? = null,
@JsonProperty("imdb_rating") val imdbRating: String? = null,
@JsonProperty("display") val display: Int? = null,
@JsonProperty("sync") val sync: Int? = null,
@JsonProperty("tomato_meter") val tomatoMeter: Int? = null,
@JsonProperty("tomato_meter_count") val tomatoMeterCount: Int? = null,
@JsonProperty("tomato_audience") val tomatoAudience: Int? = null,
@JsonProperty("tomato_audience_count") val tomatoAudienceCount: Int? = null,
@JsonProperty("thumbs_min") val thumbsMin: String? = null,
@JsonProperty("thumbs_org") val thumbsOrg: String? = null,
@JsonProperty("imdb_link") val imdbLink: String? = null,
// @JsonProperty("quality_tags") val qualityTags: ArrayList<String> = arrayListOf(),
// @JsonProperty("play_progress" ) val playProgress : PlayProgress? = PlayProgress()
)
private data class SeriesLanguage(
@JsonProperty("title") val title: String? = null,
@JsonProperty("lang") val lang: String? = null
)
private data class SeriesData(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("mb_id") val mbId: Int? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("display") val display: Int? = null,
@JsonProperty("state") val state: Int? = null,
@JsonProperty("vip_only") val vipOnly: Int? = null,
@JsonProperty("code_file") val codeFile: Int? = null,
@JsonProperty("director") val director: String? = null,
@JsonProperty("writer") val writer: String? = null,
@JsonProperty("actors") val actors: String? = null,
@JsonProperty("add_time") val addTime: Int? = null,
@JsonProperty("poster") val poster: String? = null,
@JsonProperty("poster_imdb") val posterImdb: Int? = null,
@JsonProperty("banner_mini") val bannerMini: String? = null,
@JsonProperty("description") val description: String? = null,
@JsonProperty("imdb_id") val imdbId: String? = null,
@JsonProperty("cats") val cats: String? = null,
@JsonProperty("year") val year: Int? = null,
@JsonProperty("collect") val collect: Int? = null,
@JsonProperty("view") val view: Int? = null,
@JsonProperty("download") val download: Int? = null,
@JsonProperty("update_time") val updateTime: String? = null,
@JsonProperty("released") val released: String? = null,
@JsonProperty("released_timestamp") val releasedTimestamp: Int? = null,
@JsonProperty("episode_released") val episodeReleased: String? = null,
@JsonProperty("episode_released_timestamp") val episodeReleasedTimestamp: Int? = null,
@JsonProperty("max_season") val maxSeason: Int? = null,
@JsonProperty("max_episode") val maxEpisode: Int? = null,
@JsonProperty("remark") val remark: String? = null,
@JsonProperty("imdb_rating") val imdbRating: String? = null,
@JsonProperty("content_rating") val contentRating: String? = null,
@JsonProperty("tmdb_id") val tmdbId: Int? = null,
@JsonProperty("tomato_url") val tomatoUrl: String? = null,
@JsonProperty("tomato_meter") val tomatoMeter: Int? = null,
@JsonProperty("tomato_meter_count") val tomatoMeterCount: Int? = null,
@JsonProperty("tomato_meter_state") val tomatoMeterState: String? = null,
@JsonProperty("reelgood_url") val reelgoodUrl: String? = null,
@JsonProperty("audience_score") val audienceScore: Int? = null,
@JsonProperty("audience_score_count") val audienceScoreCount: Int? = null,
@JsonProperty("no_tomato_url") val noTomatoUrl: Int? = null,
@JsonProperty("order_year") val orderYear: Int? = null,
@JsonProperty("episodate_id") val episodateId: String? = null,
@JsonProperty("weights_day") val weightsDay: Double? = null,
@JsonProperty("poster_min") val posterMin: String? = null,
@JsonProperty("poster_org") val posterOrg: String? = null,
@JsonProperty("banner_mini_min") val bannerMiniMin: String? = null,
@JsonProperty("banner_mini_org") val bannerMiniOrg: String? = null,
@JsonProperty("trailer_url") val trailerUrl: String? = null,
@JsonProperty("years") val years: ArrayList<Int> = arrayListOf(),
@JsonProperty("season") val season: ArrayList<Int> = arrayListOf(),
@JsonProperty("history") val history: ArrayList<String> = arrayListOf(),
@JsonProperty("imdb_link") val imdbLink: String? = null,
@JsonProperty("episode") val episode: ArrayList<SeriesEpisode> = arrayListOf(),
// @JsonProperty("is_collect") val isCollect: Int? = null,
@JsonProperty("language") val language: ArrayList<SeriesLanguage> = arrayListOf(),
@JsonProperty("box_type") val boxType: Int? = null,
@JsonProperty("year_year") val yearYear: String? = null,
@JsonProperty("season_episode") val seasonEpisode: String? = null
)
override suspend fun load(url: String): LoadResponse {
val loadData = parseJson<LoadData>(url)
// val module = if(type === "TvType.Movie") "Movie_detail" else "*tv series module*"
val isMovie = loadData.type == ResponseTypes.Movies.value
val hideNsfw = if (settingsForProvider.enableAdult) 0 else 1
if (isMovie) { // 1 = Movie
val apiQuery =
"""{"childmode":"$hideNsfw","uid":"","app_version":"$appVersion","appid":"$appIdSecond","module":"Movie_detail","channel":"Website","mid":"${loadData.id}","lang":"en","expired_date":"${getExpiryDate()}","platform":"android","oss":"","group":""}"""
val data = (queryApiParsed<MovieDataProp>(apiQuery)).data
?: throw RuntimeException("API error")
return newMovieLoadResponse(
data.title ?: "",
url,
TvType.Movie,
LinkData(
data.id ?: throw RuntimeException("No movie ID"),
ResponseTypes.Movies.value,
null,
null,
data.id,
data.imdbId
),
) {
this.recommendations =
data.recommend.mapNotNull { it.toSearchResponse(this@Superstream) }
this.posterUrl = data.posterOrg ?: data.poster
this.year = data.year
this.plot = data.description
this.tags = data.cats?.split(",")?.map { it.capitalize() }
this.rating = data.imdbRating?.split("/")?.get(0)?.toIntOrNull()
addTrailer(data.trailerUrl)
this.addImdbId(data.imdbId)
}
} else { // 2 Series
val apiQuery =
"""{"childmode":"$hideNsfw","uid":"","app_version":"$appVersion","appid":"$appIdSecond","module":"TV_detail_1","display_all":"1","channel":"Website","lang":"en","expired_date":"${getExpiryDate()}","platform":"android","tid":"${loadData.id}"}"""
val data = (queryApiParsed<SeriesDataProp>(apiQuery)).data
?: throw RuntimeException("API error")
val episodes = data.season.mapNotNull {
val seasonQuery =
"""{"childmode":"$hideNsfw","app_version":"$appVersion","year":"0","appid":"$appIdSecond","module":"TV_episode","display_all":"1","channel":"Website","season":"$it","lang":"en","expired_date":"${getExpiryDate()}","platform":"android","tid":"${loadData.id}"}"""
(queryApiParsed<SeriesSeasonProp>(seasonQuery)).data
}.flatten()
return newTvSeriesLoadResponse(
data.title ?: "",
url,
TvType.TvSeries,
episodes.mapNotNull {
Episode(
LinkData(
it.tid ?: it.id ?: return@mapNotNull null,
ResponseTypes.Series.value,
it.season,
it.episode,
data.id,
data.imdbId
).toJson(),
it.title,
it.season,
it.episode,
it.thumbs ?: it.thumbsBak ?: it.thumbsMin ?: it.thumbsOriginal
?: it.thumbsOrg,
it.imdbRating?.toDoubleOrNull()?.times(10)?.roundToInt(),
it.synopsis,
it.releasedTimestamp
)
}
) {
this.year = data.year
this.plot = data.description
this.posterUrl = data.posterOrg ?: data.poster
this.rating = data.imdbRating?.split("/")?.get(0)?.toIntOrNull()
this.tags = data.cats?.split(",")?.map { it.capitalize() }
this.addImdbId(data.imdbId)
}
}
}
private data class LinkData(
val id: Int,
val type: Int,
val season: Int?,
val episode: Int?,
val mediaId: Int?,
val imdbId: String?,
)
data class LinkDataProp(
@JsonProperty("code") val code: Int? = null,
@JsonProperty("msg") val msg: String? = null,
@JsonProperty("data") val data: ParsedLinkData? = ParsedLinkData()
)
data class LinkList(
@JsonProperty("path") val path: String? = null,
@JsonProperty("quality") val quality: String? = null,
@JsonProperty("real_quality") val realQuality: String? = null,
@JsonProperty("format") val format: String? = null,
@JsonProperty("size") val size: String? = null,
@JsonProperty("size_bytes") val sizeBytes: Long? = null,
@JsonProperty("count") val count: Int? = null,
@JsonProperty("dateline") val dateline: Long? = null,
@JsonProperty("fid") val fid: Int? = null,
@JsonProperty("mmfid") val mmfid: Int? = null,
@JsonProperty("h265") val h265: Int? = null,
@JsonProperty("hdr") val hdr: Int? = null,
@JsonProperty("filename") val filename: String? = null,
@JsonProperty("original") val original: Int? = null,
@JsonProperty("colorbit") val colorbit: Int? = null,
@JsonProperty("success") val success: Int? = null,
@JsonProperty("timeout") val timeout: Int? = null,
@JsonProperty("vip_link") val vipLink: Int? = null,
@JsonProperty("fps") val fps: Int? = null,
@JsonProperty("bitstream") val bitstream: String? = null,
@JsonProperty("width") val width: Int? = null,
@JsonProperty("height") val height: Int? = null
)
data class ParsedLinkData(
@JsonProperty("seconds") val seconds: Int? = null,
@JsonProperty("quality") val quality: ArrayList<String> = arrayListOf(),
@JsonProperty("list") val list: ArrayList<LinkList> = arrayListOf()
)
data class SubtitleDataProp(
@JsonProperty("code") val code: Int? = null,
@JsonProperty("msg") val msg: String? = null,
@JsonProperty("data") val data: PrivateSubtitleData? = PrivateSubtitleData()
)
data class Subtitles(
@JsonProperty("sid") val sid: Int? = null,
@JsonProperty("mid") val mid: String? = null,
@JsonProperty("file_path") val filePath: String? = null,
@JsonProperty("lang") val lang: String? = null,
@JsonProperty("language") val language: String? = null,
@JsonProperty("delay") val delay: Int? = null,
@JsonProperty("point") val point: String? = null,
@JsonProperty("order") val order: Int? = null,
@JsonProperty("support_total") val support_total: Int? = null,
@JsonProperty("admin_order") val adminOrder: Int? = null,
@JsonProperty("myselect") val myselect: Int? = null,
@JsonProperty("add_time") val addTime: Long? = null,
@JsonProperty("count") val count: Int? = null
)
data class SubtitleList(
@JsonProperty("language") val language: String? = null,
@JsonProperty("subtitles") val subtitles: ArrayList<Subtitles> = arrayListOf()
)
data class PrivateSubtitleData(
@JsonProperty("select") val select: ArrayList<String> = arrayListOf(),
@JsonProperty("list") val list: ArrayList<SubtitleList> = arrayListOf()
)
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val parsed = parseJson<LinkData>(data)
argamap(
{
invokeVidsrcto(
parsed.imdbId,
parsed.season,
parsed.episode,
subtitleCallback
)
},
{
invokeExternalSource(
parsed.mediaId,
parsed.type,
parsed.season,
parsed.episode,
callback
)
},
{
invokeInternalSource(
parsed.id,
parsed.type,
parsed.season,
parsed.episode,
subtitleCallback,
callback
)
},
{
invokeOpenSubs(
parsed.imdbId,
parsed.season,
parsed.episode,
subtitleCallback
)
},
{
invokeWatchsomuch(
parsed.imdbId,
parsed.season,
parsed.episode,
subtitleCallback
)
}
)
return true
}
data class ExternalResponse(
@JsonProperty("data") val data: Data? = null,
) {
data class Data(
@JsonProperty("link") val link: String? = null,
@JsonProperty("file_list") val file_list: ArrayList<FileList>? = arrayListOf(),
) {
data class FileList(
@JsonProperty("fid") val fid: Long? = null,
@JsonProperty("file_name") val file_name: String? = null,
@JsonProperty("oss_fid") val oss_fid: Long? = null,
)
}
}
data class ExternalSources(
@JsonProperty("m3u8_url") val m3u8_url: String? = null,
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null,
@JsonProperty("type") val type: String? = null,
)
data class WatchsomuchTorrents(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("movieId") val movieId: Int? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null,
)
data class WatchsomuchMovies(
@JsonProperty("torrents") val torrents: ArrayList<WatchsomuchTorrents>? = arrayListOf(),
)
data class WatchsomuchResponses(
@JsonProperty("movie") val movie: WatchsomuchMovies? = null,
)
data class WatchsomuchSubtitles(
@JsonProperty("url") val url: String? = null,
@JsonProperty("label") val label: String? = null,
)
data class WatchsomuchSubResponses(
@JsonProperty("subtitles") val subtitles: ArrayList<WatchsomuchSubtitles>? = arrayListOf(),
)
data class OsSubtitles(
@JsonProperty("url") val url: String? = null,
@JsonProperty("lang") val lang: String? = null,
)
data class OsResult(
@JsonProperty("subtitles") val subtitles: ArrayList<OsSubtitles>? = arrayListOf(),
)
data class VidsrcSubtitles(
@JsonProperty("label") val label: String? = null,
@JsonProperty("file") val file: String? = null,
)
}

View File

@ -0,0 +1,14 @@
package com.hexated
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
import com.lagradost.cloudstream3.plugins.Plugin
import android.content.Context
@CloudstreamPlugin
class SuperstreamPlugin: Plugin() {
override fun load(context: Context) {
// All providers should be added in this manner. Please don't edit the providers list directly.
registerMainAPI(Superstream())
}
}

0
ZoroTV/TODO Normal file
View File

366
ZoroTV/zoro.kt Normal file
View File

@ -0,0 +1,366 @@
package com.lagradost.cloudstream3.animeproviders
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.extractRabbitStream
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.runSflixExtractorVerifierJob
import com.lagradost.cloudstream3.network.Requests.Companion.await
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.net.URI
private const val OPTIONS = "OPTIONS"
class ZoroProvider : MainAPI() {
override var mainUrl = "https://zoro.to"
override var name = "Zoro"
override val hasQuickSearch = false
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val usesWebView = true
override val supportedTypes = setOf(
TvType.Anime,
TvType.AnimeMovie,
TvType.OVA
)
companion object {
fun getType(t: String): TvType {
return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
else if (t.contains("Movie")) TvType.AnimeMovie
else TvType.Anime
}
fun getStatus(t: String): ShowStatus {
return when (t) {
"Finished Airing" -> ShowStatus.Completed
"Currently Airing" -> ShowStatus.Ongoing
else -> ShowStatus.Completed
}
}
}
val epRegex = Regex("Ep (\\d+)/")
private fun Element.toSearchResult(): SearchResponse? {
val href = fixUrl(this.select("a").attr("href"))
val title = this.select("h3.film-name").text()
val dubSub = this.select(".film-poster > .tick.ltr").text()
//val episodes = this.selectFirst(".film-poster > .tick-eps")?.text()?.toIntOrNull()
val dubExist = dubSub.contains("dub", ignoreCase = true)
val subExist = dubSub.contains("sub", ignoreCase = true)
val episodes = this.selectFirst(".film-poster > .tick.rtl > .tick-eps")?.text()?.let { eps ->
//println("REGEX:::: $eps")
// current episode / max episode
//Regex("Ep (\\d+)/(\\d+)")
epRegex.find(eps)?.groupValues?.get(1)?.toIntOrNull()
}
if (href.contains("/news/") || title.trim().equals("News", ignoreCase = true)) return null
val posterUrl = fixUrl(this.select("img").attr("data-src"))
val type = getType(this.select("div.fd-infor > span.fdi-item").text())
return newAnimeSearchResponse(title, href, type) {
this.posterUrl = posterUrl
addDubStatus(dubExist, subExist, episodes, episodes)
}
}
override suspend fun getMainPage(): HomePageResponse {
val html = app.get("$mainUrl/home").text
val document = Jsoup.parse(html)
val homePageList = ArrayList<HomePageList>()
document.select("div.anif-block").forEach { block ->
val header = block.select("div.anif-block-header").text().trim()
val animes = block.select("li").mapNotNull {
it.toSearchResult()
}
if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes))
}
document.select("section.block_area.block_area_home").forEach { block ->
val header = block.select("h2.cat-heading").text().trim()
val animes = block.select("div.flw-item").mapNotNull {
it.toSearchResult()
}
if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes))
}
return HomePageResponse(homePageList)
}
private data class Response(
@JsonProperty("status") val status: Boolean,
@JsonProperty("html") val html: String
)
// override suspend fun quickSearch(query: String): List<SearchResponse> {
// val url = "$mainUrl/ajax/search/suggest?keyword=${query}"
// val html = mapper.readValue<Response>(khttp.get(url).text).html
// val document = Jsoup.parse(html)
//
// return document.select("a.nav-item").map {
// val title = it.selectFirst(".film-name")?.text().toString()
// val href = fixUrl(it.attr("href"))
// val year = it.selectFirst(".film-infor > span")?.text()?.split(",")?.get(1)?.trim()?.toIntOrNull()
// val image = it.select("img").attr("data-src")
//
// AnimeSearchResponse(
// title,
// href,
// this.name,
// TvType.TvSeries,
// image,
// year,
// null,
// EnumSet.of(DubStatus.Subbed),
// null,
// null
// )
//
// }
// }
override suspend fun search(query: String): List<SearchResponse> {
val link = "$mainUrl/search?keyword=$query"
val html = app.get(link).text
val document = Jsoup.parse(html)
return document.select(".flw-item").map {
val title = it.selectFirst(".film-detail > .film-name > a")?.attr("title").toString()
val filmPoster = it.selectFirst(".film-poster")
val poster = filmPoster.selectFirst("img")?.attr("data-src")
val episodes = filmPoster.selectFirst("div.rtl > div.tick-eps")?.text()?.let { eps ->
// current episode / max episode
val epRegex = Regex("Ep (\\d+)/")//Regex("Ep (\\d+)/(\\d+)")
epRegex.find(eps)?.groupValues?.get(1)?.toIntOrNull()
}
val dubsub = filmPoster.selectFirst("div.ltr")?.text()
val dubExist = dubsub?.contains("DUB") ?: false
val subExist = dubsub?.contains("SUB") ?: false || dubsub?.contains("RAW") ?: false
val tvType =
getType(it.selectFirst(".film-detail > .fd-infor > .fdi-item")?.text().toString())
val href = fixUrl(it.selectFirst(".film-name a").attr("href"))
newAnimeSearchResponse(title, href, tvType) {
this.posterUrl = poster
addDubStatus(dubExist, subExist, episodes, episodes)
}
}
}
private fun Element?.getActor(): Actor? {
val image =
fixUrlNull(this?.selectFirst(".pi-avatar > img")?.attr("data-src")) ?: return null
val name = this?.selectFirst(".pi-detail > .pi-name")?.text() ?: return null
return Actor(name = name, image = image)
}
data class ZoroSyncData(
@JsonProperty("mal_id") val malId: String?,
@JsonProperty("anilist_id") val aniListId: String?,
)
override suspend fun load(url: String): LoadResponse {
val html = app.get(url).text
val document = Jsoup.parse(html)
val syncData = tryParseJson<ZoroSyncData>(document.selectFirst("#syncData")?.data())
val title = document.selectFirst(".anisc-detail > .film-name")?.text().toString()
val poster = document.selectFirst(".anisc-poster img")?.attr("src")
val tags = document.select(".anisc-info a[href*=\"/genre/\"]").map { it.text() }
var year: Int? = null
var japaneseTitle: String? = null
var status: ShowStatus? = null
for (info in document.select(".anisc-info > .item.item-title")) {
val text = info?.text().toString()
when {
(year != null && japaneseTitle != null && status != null) -> break
text.contains("Premiered") && year == null ->
year =
info.selectFirst(".name")?.text().toString().split(" ").last().toIntOrNull()
text.contains("Japanese") && japaneseTitle == null ->
japaneseTitle = info.selectFirst(".name")?.text().toString()
text.contains("Status") && status == null ->
status = getStatus(info.selectFirst(".name")?.text().toString())
}
}
val description = document.selectFirst(".film-description.m-hide > .text")?.text()
val animeId = URI(url).path.split("-").last()
val episodes = Jsoup.parse(
mapper.readValue<Response>(
app.get(
"$mainUrl/ajax/v2/episode/list/$animeId"
).text
).html
).select(".ss-list > a[href].ssl-item.ep-item").map {
newEpisode(it.attr("href")) {
this.name = it?.attr("title")
this.episode = it.selectFirst(".ssli-order")?.text()?.toIntOrNull()
}
}
val actors = document.select("div.block-actors-content > div.bac-list-wrap > div.bac-item")
?.mapNotNull { head ->
val subItems = head.select(".per-info") ?: return@mapNotNull null
if (subItems.isEmpty()) return@mapNotNull null
var role: ActorRole? = null
val mainActor = subItems.first()?.let {
role = when (it.selectFirst(".pi-detail > .pi-cast")?.text()?.trim()) {
"Supporting" -> ActorRole.Supporting
"Main" -> ActorRole.Main
else -> null
}
it.getActor()
} ?: return@mapNotNull null
val voiceActor = if (subItems.size >= 2) subItems[1]?.getActor() else null
ActorData(actor = mainActor, role = role, voiceActor = voiceActor)
}
val recommendations =
document.select("#main-content > section > .tab-content > div > .film_list-wrap > .flw-item")
.mapNotNull { head ->
val filmPoster = head?.selectFirst(".film-poster")
val epPoster = filmPoster?.selectFirst("img")?.attr("data-src")
val a = head?.selectFirst(".film-detail > .film-name > a")
val epHref = a?.attr("href")
val epTitle = a?.attr("title")
if (epHref == null || epTitle == null || epPoster == null) {
null
} else {
AnimeSearchResponse(
epTitle,
fixUrl(epHref),
this.name,
TvType.Anime,
epPoster,
dubStatus = null
)
}
}
return newAnimeLoadResponse(title, url, TvType.Anime) {
japName = japaneseTitle
engName = title
posterUrl = poster
this.year = year
addEpisodes(DubStatus.Subbed, episodes)
showStatus = status
plot = description
this.tags = tags
this.recommendations = recommendations
this.actors = actors
addMalId(syncData?.malId?.toIntOrNull())
addAniListId(syncData?.aniListId?.toIntOrNull())
}
}
private data class RapidCloudResponse(
@JsonProperty("link") val link: String
)
override suspend fun extractorVerifierJob(extractorData: String?) {
Log.d(this.name, "Starting ${this.name} job!")
runSflixExtractorVerifierJob(this, extractorData, "https://rapid-cloud.ru/")
}
/** Url hashcode to sid */
var sid: HashMap<Int, String?> = hashMapOf()
/**
* Makes an identical Options request before .ts request
* Adds an SID header to the .ts request.
* */
override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor {
return Interceptor { chain ->
val request = chain.request()
if (request.url.toString().endsWith(".ts")
&& request.method != OPTIONS
// No option requests on VidCloud
&& !request.url.toString().contains("betterstream")
) {
val newRequest =
chain.request()
.newBuilder().apply {
sid[extractorLink.url.hashCode()]?.let { sid ->
addHeader("SID", sid)
}
}
.build()
val options = request.newBuilder().method(OPTIONS, request.body).build()
ioSafe { app.baseClient.newCall(options).await() }
return@Interceptor chain.proceed(newRequest)
} else {
return@Interceptor chain.proceed(chain.request())
}
}
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val servers: List<Pair<DubStatus, String>> = Jsoup.parse(
app.get("$mainUrl/ajax/v2/episode/servers?episodeId=" + data.split("=")[1])
.mapped<Response>().html
).select(".server-item[data-type][data-id]").map {
Pair(
if (it.attr("data-type") == "sub") DubStatus.Subbed else DubStatus.Dubbed,
it.attr("data-id")!!
)
}
val extractorData =
"https://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=polling"
// Prevent duplicates
servers.distinctBy { it.second }.apmap {
val link =
"$mainUrl/ajax/v2/episode/sources?id=${it.second}"
val extractorLink = app.get(
link,
).mapped<RapidCloudResponse>().link
val hasLoadedExtractorLink =
loadExtractor(extractorLink, "https://rapid-cloud.ru/", callback)
if (!hasLoadedExtractorLink) {
extractRabbitStream(
extractorLink,
subtitleCallback,
// Blacklist VidCloud for now
{ videoLink -> if (!videoLink.url.contains("betterstream")) callback(videoLink) },
extractorData
) { sourceName ->
sourceName + " - ${it.first}"
}
}
}
return true
}
}

View File

@ -74,10 +74,11 @@ subprojects {
// but you dont need to include any of them if you dont need them
// https://github.com/recloudstream/cloudstream/blob/master/app/build.gradle
implementation(kotlin("stdlib")) // adds standard kotlin features
implementation("com.github.Blatzar:NiceHttp:0.4.4") // http library
implementation("org.jsoup:jsoup:1.16.2") // html parser
implementation("com.github.Blatzar:NiceHttp:0.4.11") // http library
implementation("org.jsoup:jsoup:1.17.2") // html parser
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
}

8
repo.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "mnemosyne",
"description": "Custom extensions for CloudStream",
"manifestVersion": 1,
"pluginLists": [
"https://raw.githubusercontent.com/swisskyrepo/TestPlugins/builds/plugins.json"
]
}

View File

@ -1,9 +1,25 @@
rootProject.name = "CloudstreamPlugins"
// This file sets what projects are included. Every time you add a new project, you must add it
// to the includes below.
// This file sets what projects are included. All new projects should get automatically included unless specified in "disabled" variable.
// Plugins are included like this
val disabled = listOf<String>()
File(rootDir, ".").eachDir { dir ->
if (!disabled.contains(dir.name) && File(dir, "build.gradle.kts").exists()) {
include(dir.name)
}
}
fun File.eachDir(block: (File) -> Unit) {
listFiles()?.filter { it.isDirectory }?.forEach { block(it) }
}
// To only include a single project, comment out the previous lines (except the first one), and include your plugin like so:
/*
include(
"ExampleProvider"
"Sflix",
"HiAnime",
"Anywave"
)
*/