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 Upgrademaster
parent
9859878455
commit
5563a4aa9d
|
@ -6,17 +6,22 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
# choose your default branch
|
|
||||||
- master
|
- master
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '*.md'
|
- '*.md'
|
||||||
|
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment: env
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@master
|
||||||
|
@ -40,6 +45,35 @@ jobs:
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v2
|
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
|
- name: Build Plugins
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/src
|
cd $GITHUB_WORKSPACE/src
|
||||||
|
|
|
@ -9,4 +9,7 @@
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
.vscode
|
.vscode
|
||||||
|
/NOTES
|
||||||
|
NOTES.md
|
||||||
|
scrapping.py
|
|
@ -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(""", "\"") ?: 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.RowdyAvocado" />
|
|
@ -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>)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}" }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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%"
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.stormunblessed"/>
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
README.md
45
README.md
|
@ -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:
|
||||||
|
|
||||||
|
* HiAnime (from Rowdy-Avocado) :white_check_mark:
|
||||||
## Getting started with writing your first plugin
|
* 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 ?
|
|
@ -9,9 +9,8 @@ version = -1
|
||||||
|
|
||||||
cloudstream {
|
cloudstream {
|
||||||
// All of these properties are optional, you can safely remove them
|
// All of these properties are optional, you can safely remove them
|
||||||
|
description = "Sflix"
|
||||||
description = "Lorem ipsum"
|
authors = listOf("Mnemosyne")
|
||||||
authors = listOf("Cloudburst")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status int as the following:
|
* Status int as the following:
|
||||||
|
@ -21,9 +20,7 @@ cloudstream {
|
||||||
* 3: Beta only
|
* 3: Beta only
|
||||||
* */
|
* */
|
||||||
status = 1
|
status = 1
|
||||||
|
|
||||||
tvTypes = listOf("Movie")
|
tvTypes = listOf("Movie")
|
||||||
|
|
||||||
requiresResources = true
|
requiresResources = true
|
||||||
language = "en"
|
language = "en"
|
||||||
|
|
|
@ -3,6 +3,8 @@ package com.example
|
||||||
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
import com.lagradost.cloudstream3.plugins.Plugin
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
import com.lagradost.cloudstream3.APIHolder
|
import com.lagradost.cloudstream3.APIHolder
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
@ -14,6 +16,12 @@ class TestPlugin: Plugin() {
|
||||||
override fun load(context: Context) {
|
override fun load(context: Context) {
|
||||||
activity = context as AppCompatActivity
|
activity = context as AppCompatActivity
|
||||||
// All providers should be added in this manner
|
// 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))
|
registerMainAPI(ExampleProvider(this))
|
||||||
|
|
||||||
openSettings = { ctx ->
|
openSettings = { ctx ->
|
||||||
|
@ -21,4 +29,10 @@ class TestPlugin: Plugin() {
|
||||||
frag.show(activity!!.supportFragmentManager, "Frag")
|
frag.show(activity!!.supportFragmentManager, "Frag")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun addExtractor(element: ExtractorApi) {
|
||||||
|
element.sourcePlugin = __filename
|
||||||
|
extractorApis.add(0, element)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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 ?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
|
@ -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&"
|
||||||
|
}
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.hexated"/>
|
|
@ -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
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -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®ion=US" to "Trending",
|
||||||
|
"$tmdbAPI/movie/popular?api_key=$apiKey®ion=US" to "Popular Movies",
|
||||||
|
"$tmdbAPI/tv/popular?api_key=$apiKey®ion=US&with_original_language=en" to "Popular TV Shows",
|
||||||
|
"$tmdbAPI/tv/airing_today?api_key=$apiKey®ion=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®ion=US" to "Top Rated Movies",
|
||||||
|
"$tmdbAPI/tv/top_rated?api_key=$apiKey®ion=US" to "Top Rated TV Shows",
|
||||||
|
"$tmdbAPI/movie/upcoming?api_key=$apiKey®ion=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(),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
@ -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&"
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.hexated"/>
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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,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
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,10 +74,11 @@ subprojects {
|
||||||
// but you dont need to include any of them if you dont need them
|
// but you dont need to include any of them if you dont need them
|
||||||
// https://github.com/recloudstream/cloudstream/blob/master/app/build.gradle
|
// https://github.com/recloudstream/cloudstream/blob/master/app/build.gradle
|
||||||
implementation(kotlin("stdlib")) // adds standard kotlin features
|
implementation(kotlin("stdlib")) // adds standard kotlin features
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.4") // http library
|
implementation("com.github.Blatzar:NiceHttp:0.4.11") // http library
|
||||||
implementation("org.jsoup:jsoup:1.16.2") // html parser
|
implementation("org.jsoup:jsoup:1.17.2") // html parser
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.0")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.0")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0")
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.16.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "mnemosyne",
|
||||||
|
"description": "Custom extensions for CloudStream",
|
||||||
|
"manifestVersion": 1,
|
||||||
|
"pluginLists": [
|
||||||
|
"https://raw.githubusercontent.com/swisskyrepo/TestPlugins/builds/plugins.json"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,9 +1,25 @@
|
||||||
rootProject.name = "CloudstreamPlugins"
|
rootProject.name = "CloudstreamPlugins"
|
||||||
|
|
||||||
// This file sets what projects are included. Every time you add a new project, you must add it
|
// This file sets what projects are included. All new projects should get automatically included unless specified in "disabled" variable.
|
||||||
// to the includes below.
|
|
||||||
|
|
||||||
// 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(
|
include(
|
||||||
"ExampleProvider"
|
"Sflix",
|
||||||
|
"HiAnime",
|
||||||
|
"Anywave"
|
||||||
)
|
)
|
||||||
|
*/
|
Loading…
Reference in New Issue