feat(multisrc/dooplay): Convert Multimovies(en) to multisrc (#1639)
* feat(multisrc/dooplay): Convert Multimovies(en) to multisrc * feat: Add MultimoviesCloud extractor * refactor: Use shared-libs when possible * refactor: General refactoration * chore: Bump version * fix: Fix NPE in episodes list page
8
multisrc/overrides/dooplay/multimovies/additional.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
dependencies {
|
||||
implementation(project(':lib-streamsb-extractor'))
|
||||
implementation(project(':lib-voe-extractor'))
|
||||
implementation(project(':lib-dood-extractor'))
|
||||
implementation(project(':lib-mixdrop-extractor'))
|
||||
implementation(project(':lib-cryptoaes'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
196
multisrc/overrides/dooplay/multimovies/src/Multimovies.kt
Normal file
@ -0,0 +1,196 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.multimovies
|
||||
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.en.multimovies.extractors.AutoEmbedExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.en.multimovies.extractors.MultimoviesCloudExtractor
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class Multimovies : DooPlay(
|
||||
"en",
|
||||
"Multimovies",
|
||||
"https://multimovies.tech",
|
||||
) {
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/genre/anime-series/page/$page/")
|
||||
|
||||
override fun popularAnimeSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override val seasonListSelector = "div#seasons > div:not(:contains(no episodes this season))"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val doc = response.use { getRealAnimeDoc(it.asJsoup()) }
|
||||
val seasonList = doc.select(seasonListSelector)
|
||||
return if ("/movies/" in doc.location()) {
|
||||
SEpisode.create().apply {
|
||||
setUrlWithoutDomain(doc.location())
|
||||
episode_number = 1F
|
||||
name = episodeMovieText
|
||||
}.let(::listOf)
|
||||
} else if (seasonList.size < 1) {
|
||||
throw Exception("The source provides ZERO episodes.")
|
||||
} else {
|
||||
seasonList.flatMap(::getSeasonEpisodes).reversed()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
override val prefQualityEntries = arrayOf("1080", "720", "480", "360", "240")
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val players = document.select("ul#playeroptionsul li")
|
||||
return players.flatMap(::getPlayerVideos)
|
||||
}
|
||||
|
||||
private fun getPlayerVideos(player: Element): List<Video> {
|
||||
if (player.attr("data-nume") == "trailer") return emptyList()
|
||||
val url = getPlayerUrl(player)
|
||||
val streamSbServers = listOf(
|
||||
"sbembed.com", "sbembed1.com", "sbplay.org",
|
||||
"sbvideo.net", "streamsb.net", "sbplay.one",
|
||||
"cloudemb.com", "playersb.com", "tubesb.com",
|
||||
"sbplay1.com", "embedsb.com", "watchsb.com",
|
||||
"sbplay2.com", "japopav.tv", "viewsb.com",
|
||||
"sbfast", "sbfull.com", "javplaya.com",
|
||||
"ssbstream.net", "p1ayerjavseen.com", "sbthe.com",
|
||||
"sbchill.com", "sblongvu.com", "sbanh.com",
|
||||
"sblanh.com", "sbhight.com", "sbbrisk.com",
|
||||
"sbspeed.com", "multimovies.website",
|
||||
)
|
||||
return when {
|
||||
streamSbServers.any { it in url } ->
|
||||
StreamSBExtractor(client).videosFromUrl(url, headers = headers, prefix = "[multimovies]")
|
||||
|
||||
url.contains("multimovies.cloud") ->
|
||||
MultimoviesCloudExtractor(client).videosFromUrl(url)
|
||||
url.contains("autoembed.to") || url.contains("2embed.to") -> {
|
||||
val newHeaders = headers.newBuilder()
|
||||
.set("Referer", url)
|
||||
.build()
|
||||
AutoEmbedExtractor(client).videosFromUrl(url, headers = newHeaders)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPlayerUrl(player: Element): String {
|
||||
val body = FormBody.Builder()
|
||||
.add("action", "doo_player_ajax")
|
||||
.add("post", player.attr("data-post"))
|
||||
.add("nume", player.attr("data-nume"))
|
||||
.add("type", player.attr("data-type"))
|
||||
.build()
|
||||
|
||||
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
|
||||
.execute()
|
||||
.use { response ->
|
||||
response.body.string()
|
||||
.substringAfter("\"embed_url\":\"")
|
||||
.substringBefore("\",")
|
||||
.replace("\\", "")
|
||||
.let { url ->
|
||||
when {
|
||||
url.lowercase().contains("iframe") -> {
|
||||
url.substringAfter("=\"")
|
||||
.substringBefore("\" ")
|
||||
}
|
||||
else -> url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/page/$page/?s=$query", headers)
|
||||
} else {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.getFirst<GenreFilter>()
|
||||
val streamingFilter = filterList.getFirst<StreamingFilter>()
|
||||
val ficUniFilter = filterList.getFirst<FicUniFilter>()
|
||||
val channelFilter = filterList.getFirst<ChannelFilter>()
|
||||
|
||||
when {
|
||||
genreFilter.state != 0 -> GET("$baseUrl/genre/$genreFilter/page/$page", headers)
|
||||
streamingFilter.state != 0 -> GET("$baseUrl/genre/$streamingFilter/page/$page", headers)
|
||||
ficUniFilter.state != 0 -> GET("$baseUrl/genre/$ficUniFilter/page/$page", headers)
|
||||
channelFilter.state != 0 -> GET("$baseUrl/genre/$channelFilter/page/$page", headers)
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override fun getFilterList() = getMultimoviesFilterList()
|
||||
override val fetchGenres = false
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector() = "div.pagination > *:last-child:not(span):not(.current)"
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoServerPref = ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = PREF_SERVER_TITLE
|
||||
entries = PREF_SERVER_ENTRIES
|
||||
entryValues = PREF_SERVER_VALUES
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(videoServerPref)
|
||||
super.setupPreferenceScreen(screen) // quality pref
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return first { it is R } as R
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_TITLE = "Preferred Server"
|
||||
private const val PREF_SERVER_DEFAULT = "multimovies"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf(
|
||||
"multimovies",
|
||||
"2Embed Vidcloud",
|
||||
"2Embed Voe",
|
||||
"2Embed Streamlare",
|
||||
"2Embed MixDrop",
|
||||
"Gomo Dood",
|
||||
)
|
||||
private val PREF_SERVER_VALUES = arrayOf(
|
||||
"multimovies",
|
||||
"[2embed] server vidcloud",
|
||||
"[2embed] server voe",
|
||||
"[2embed] server streamlare",
|
||||
"[2embed] server mixdrop",
|
||||
"[gomostream] dood",
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.multimovies
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
internal fun getMultimoviesFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("NOTE: Ignored if using text search!"),
|
||||
AnimeFilter.Separator(),
|
||||
GenreFilter(getGenreList()),
|
||||
StreamingFilter(getStreamingList()),
|
||||
FicUniFilter(getFictionalUniverseList()),
|
||||
ChannelFilter(getChannelList()),
|
||||
)
|
||||
|
||||
internal class GenreFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Genres", vals)
|
||||
internal class StreamingFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Streaming service", vals)
|
||||
internal class FicUniFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Fictional universe", vals)
|
||||
internal class ChannelFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Channel", vals)
|
||||
|
||||
internal fun getGenreList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Action", "action"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Animation", "animation"),
|
||||
Pair("Crime", "crime"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Family", "family"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Mystery", "mystery"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Thriller", "thriller"),
|
||||
Pair("Science Fiction", "science-fiction"),
|
||||
)
|
||||
|
||||
internal fun getStreamingList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Amazon Prime", "amazon-prime"),
|
||||
Pair("Disney Hotstar", "disney-hotstar"),
|
||||
Pair("K-drama", "k-drama"),
|
||||
Pair("Netflix", "netflix"),
|
||||
Pair("Sony Liv", "sony-liv"),
|
||||
)
|
||||
|
||||
internal fun getFictionalUniverseList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("DC Universe", "dc-universe"),
|
||||
Pair("Fast and Furious movies", "multimovies-com-fast-and-furious-movies"),
|
||||
Pair("Harry Potter movies", "multimovies-com-harry-potter-movies"),
|
||||
Pair("Marvel Collection", "marvel-collection"),
|
||||
Pair("Mission Impossible", "mission-impossible-collection"),
|
||||
Pair("Pirates of the Caribbean Collection", "pirates-of-the-caribbean-collection"),
|
||||
Pair("Resident Evil", "resident-evil"),
|
||||
Pair("Transformers Collection", "transformers-collection"),
|
||||
Pair("Wrong Turn", "wrong-turn"),
|
||||
Pair("X-Men Collection", "x-men-collection"),
|
||||
)
|
||||
|
||||
internal fun getChannelList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Hindi Dub Anime", "anime-hindi"),
|
||||
Pair("Anime Series", "anime-series"),
|
||||
Pair("Anime Movies", "anime-movies"),
|
||||
Pair("Cartoon Network", "cartoon-network"),
|
||||
Pair("Disney Channel", "disney-channel"),
|
||||
Pair("Disney XD", "disney-xd"),
|
||||
Pair("Hungama", "hungama"),
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
override fun toString() = vals[state].second
|
||||
}
|
@ -683,6 +683,7 @@ import android.util.Base64
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@ -698,9 +699,7 @@ import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
@ -708,10 +707,6 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, headers: Headers): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val serverList = mutableListOf<Server>()
|
||||
val containerList = mutableListOf<Server>()
|
||||
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.add("Host", url.toHttpUrl().host)
|
||||
@ -721,7 +716,7 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
).execute().asJsoup()
|
||||
|
||||
// First get all containers
|
||||
document.select("div > a[id*=server]").forEach {
|
||||
val containerList = document.select("div > a[id*=server]").mapNotNull {
|
||||
val slug = it.attr("href")
|
||||
val newDocument = client.newCall(
|
||||
GET("https://${url.toHttpUrl().host}$slug", headers = headers),
|
||||
@ -731,72 +726,64 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
.substringAfter("replace(\"").substringBefore("\"")
|
||||
|
||||
when {
|
||||
container.contains("2embed.to", true) -> {
|
||||
containerList.add(Server(container, "2embed"))
|
||||
}
|
||||
container.contains("gomostream", true) -> {
|
||||
containerList.add(Server(container, "gomostream"))
|
||||
}
|
||||
container.contains("2embed.to", true) -> Server(container, "2embed")
|
||||
container.contains("gomostream", true) -> Server(container, "gomostream")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
// Get video servers from containers
|
||||
serverList.addAll(
|
||||
containerList.parallelMap { container ->
|
||||
runCatching {
|
||||
when (container.name) {
|
||||
"2embed" -> {
|
||||
getTwoEmbedServers(container.url, container.name, headers = headers)
|
||||
}
|
||||
"gomostream" -> {
|
||||
getGomoStreamServers(container.url, container.name, headers = headers)
|
||||
}
|
||||
else -> null
|
||||
val serverList = containerList.parallelMap { container ->
|
||||
runCatching {
|
||||
when (container.name) {
|
||||
"2embed" -> {
|
||||
getTwoEmbedServers(container.url, headers = headers)
|
||||
}
|
||||
}.getOrNull()
|
||||
}.filterNotNull().flatten(),
|
||||
)
|
||||
"gomostream" -> {
|
||||
getGomoStreamServers(container.url, headers = headers)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.getOrNull() ?: emptyList()
|
||||
}.flatten()
|
||||
|
||||
val videoHeaders = headers.newBuilder()
|
||||
.add("Referer", "https://www.2embed.to/")
|
||||
.build()
|
||||
|
||||
videoList.addAll(
|
||||
serverList.parallelMap { server ->
|
||||
runCatching {
|
||||
val prefix = server.name
|
||||
val url = server.url
|
||||
return serverList.parallelMap { server ->
|
||||
runCatching {
|
||||
val prefix = server.name
|
||||
val videoUrl = server.url
|
||||
|
||||
when {
|
||||
url.contains("streamsb") -> {
|
||||
StreamSBExtractor(client).videosFromUrl(url, headers = headers, prefix = prefix)
|
||||
}
|
||||
url.contains("streamlare") -> {
|
||||
StreamlareExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
}
|
||||
url.contains("mixdrop") -> {
|
||||
MixDropExtractor(client).videoFromUrl(url, prefix = prefix)
|
||||
}
|
||||
url.contains("https://voe") -> {
|
||||
VoeExtractor(client).videoFromUrl(url, server.name)?.let { listOf(it) }
|
||||
}
|
||||
url.contains("rabbitstream") -> {
|
||||
RabbitStreamExtractor(client).videosFromUrl(url, headers = videoHeaders, prefix = prefix)
|
||||
}
|
||||
url.contains("mixdrop") -> {
|
||||
MixDropExtractor(client).videoFromUrl(url, prefix = prefix)
|
||||
}
|
||||
url.contains("https://dood") -> {
|
||||
val video = DoodExtractor(client).videoFromUrl(url, server.name, false)
|
||||
video?.let { listOf(it) }
|
||||
}
|
||||
else -> null
|
||||
when {
|
||||
videoUrl.contains("streamsb") -> {
|
||||
StreamSBExtractor(client).videosFromUrl(videoUrl, headers = headers, prefix = prefix)
|
||||
}
|
||||
}.getOrNull()
|
||||
}.filterNotNull().flatten(),
|
||||
)
|
||||
|
||||
return videoList
|
||||
videoUrl.contains("streamlare") -> {
|
||||
StreamlareExtractor(client).videosFromUrl(videoUrl, prefix = prefix)
|
||||
}
|
||||
videoUrl.contains("mixdrop") -> {
|
||||
MixDropExtractor(client).videoFromUrl(videoUrl, prefix = prefix)
|
||||
}
|
||||
videoUrl.contains("https://voe") -> {
|
||||
VoeExtractor(client).videoFromUrl(videoUrl, server.name)
|
||||
?.let(::listOf)
|
||||
}
|
||||
videoUrl.contains("rabbitstream") -> {
|
||||
RabbitStreamExtractor(client).videosFromUrl(videoUrl, headers = videoHeaders, prefix = prefix)
|
||||
}
|
||||
videoUrl.contains("mixdrop") -> {
|
||||
MixDropExtractor(client).videoFromUrl(videoUrl, prefix = prefix)
|
||||
}
|
||||
videoUrl.contains("https://dood") -> {
|
||||
DoodExtractor(client).videoFromUrl(videoUrl, server.name, false)
|
||||
?.let(::listOf)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.getOrNull() ?: emptyList()
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
data class Server(
|
||||
@ -809,15 +796,11 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
val link: String,
|
||||
)
|
||||
|
||||
private fun getGomoStreamServers(url: String, name: String, headers: Headers): List<Server> {
|
||||
val serverList = mutableListOf<Server>()
|
||||
|
||||
val response = client.newCall(
|
||||
GET(url, headers = headers),
|
||||
).execute()
|
||||
private fun getGomoStreamServers(url: String, headers: Headers): List<Server> {
|
||||
val response = client.newCall(GET(url, headers = headers)).execute()
|
||||
val responseUrl = response.request.url
|
||||
val document = response.asJsoup()
|
||||
val script = document.selectFirst("script:containsData(_token)")?.data() ?: return serverList
|
||||
val script = document.selectFirst("script:containsData(_token)")?.data() ?: return emptyList()
|
||||
val token = script.substringAfter("\"_token\": \"").substringBefore("\"")
|
||||
val tokenCode = script.substringAfter("var tc = '").substringBefore("';")
|
||||
val postUrl = script.substringAfter("\"POST\"").substringAfter("\"").substringBefore("\"")
|
||||
@ -826,9 +809,7 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
val functionName = script.substringAfter("'x-token': ").substringBefore("(")
|
||||
val toExecute = "function $functionName${script.substringAfter("function $functionName")};$functionName(\"$tokenCode\")"
|
||||
|
||||
val quickJs = QuickJs.create()
|
||||
val xToken = quickJs.evaluate(toExecute).toString()
|
||||
quickJs.close()
|
||||
val xToken = QuickJs.create().use { it.evaluate(toExecute).toString() }
|
||||
|
||||
val postHeaders = Headers.headersOf(
|
||||
"Accept", "*/*",
|
||||
@ -841,33 +822,29 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
"x-token", xToken,
|
||||
)
|
||||
|
||||
val postBody = "tokenCode=$tokenCode&_token=$token".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
val postBody = FormBody.Builder()
|
||||
.add("tokenCode", tokenCode)
|
||||
.add("_token", token)
|
||||
.build()
|
||||
val postResponse = client.newCall(
|
||||
POST(postUrl, headers = postHeaders, body = postBody),
|
||||
).execute().body.string()
|
||||
val list = json.decodeFromString<List<String>>(postResponse)
|
||||
|
||||
serverList.addAll(
|
||||
list.filter { x -> x != "" }.map {
|
||||
Server(it, "[gomostream] ${it.toHttpUrl().host} -")
|
||||
},
|
||||
)
|
||||
|
||||
return serverList
|
||||
return list.filter { x -> x != "" }.map {
|
||||
Server(it, "[gomostream] ${it.toHttpUrl().host} -")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTwoEmbedServers(url: String, name: String, headers: Headers): List<Server> {
|
||||
val serverList = mutableListOf<Server>()
|
||||
val document = client.newCall(
|
||||
GET(url),
|
||||
).execute().asJsoup()
|
||||
private fun getTwoEmbedServers(url: String, headers: Headers): List<Server> {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
|
||||
val captcha = document.selectFirst("script[src*=recaptcha/api.js]")!!
|
||||
.attr("src")
|
||||
.substringAfter("render=")
|
||||
|
||||
// Get video host urls
|
||||
document.select("div.dropdown-menu > a[data-id]").map {
|
||||
return document.select("div.dropdown-menu > a[data-id]").map {
|
||||
val ajaxHeaders = Headers.headersOf(
|
||||
"Accept", "*/*",
|
||||
"Host", url.toHttpUrl().host,
|
||||
@ -881,42 +858,45 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||
GET("https://www.2embed.to/ajax/embed/play?id=${it.attr("data-id")}&_token=$token", headers = ajaxHeaders),
|
||||
).execute().body.string()
|
||||
val parsed = json.decodeFromString<Stream>(streamUrl)
|
||||
serverList.add(
|
||||
Server(parsed.link, "[2embed] ${it.text()} - "),
|
||||
)
|
||||
Server(parsed.link, "[2embed] ${it.text()} - ")
|
||||
}
|
||||
|
||||
return serverList
|
||||
}
|
||||
|
||||
// https://github.com/recloudstream/cloudstream/blob/7b47f93190fb2b106da44150c4431178eb3995dc/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt#L123
|
||||
private fun getCaptchaToken(url: String, key: String): String? {
|
||||
try {
|
||||
val domain = Base64.encodeToString(
|
||||
(url.toHttpUrl().scheme + "://" + url.toHttpUrl().host + ":443").encodeToByteArray(),
|
||||
0,
|
||||
).replace("\n", "").replace("=", ".")
|
||||
val vToken = client.newCall(
|
||||
GET("https://www.google.com/recaptcha/api.js?render=$key"),
|
||||
).execute().body.string().substringAfter("releases/").substringBefore("/")
|
||||
val recapToken = client.newCall(
|
||||
return runCatching {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
val pureDomain = (httpUrl.scheme + "://" + httpUrl.host + ":443")
|
||||
val domain = Base64.encodeToString(pureDomain.encodeToByteArray(), Base64.DEFAULT)
|
||||
.replace("\n", "")
|
||||
.replace("=", ".")
|
||||
|
||||
val vToken = client.newCall(GET("https://www.google.com/recaptcha/api.js?render=$key"))
|
||||
.execute()
|
||||
.body.string()
|
||||
.substringAfter("releases/")
|
||||
.substringBefore("/")
|
||||
|
||||
client.newCall(
|
||||
GET("https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=cs3&k=$key&co=$domain&v=$vToken"),
|
||||
).execute().asJsoup().selectFirst("#recaptcha-token")?.attr("value")
|
||||
if (recapToken != null) {
|
||||
val body = FormBody.Builder()
|
||||
.add("v", vToken)
|
||||
.add("k", key)
|
||||
.add("c", recapToken)
|
||||
.add("co", domain)
|
||||
.add("sa", "")
|
||||
.add("reason", "q")
|
||||
.build()
|
||||
return client.newCall(
|
||||
POST("https://www.google.com/recaptcha/api2/reload?k=$key", body = body),
|
||||
).execute().body.string().substringAfter("rresp\",\"").substringBefore("\"")
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return null
|
||||
).execute()
|
||||
.asJsoup()
|
||||
.selectFirst("#recaptcha-token")
|
||||
?.attr("value")
|
||||
?.let { recapToken ->
|
||||
val body = FormBody.Builder()
|
||||
.add("v", vToken)
|
||||
.add("k", key)
|
||||
.add("c", recapToken)
|
||||
.add("co", domain)
|
||||
.add("sa", "")
|
||||
.add("reason", "q")
|
||||
.build()
|
||||
return client.newCall(
|
||||
POST("https://www.google.com/recaptcha/api2/reload?k=$key", body = body),
|
||||
).execute().body.string().substringAfter("rresp\",\"").substringBefore("\"")
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
// From Dopebox
|
@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.multimovies.extractors
|
||||
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
// Based on FilmPalast(de)'s StreamHideVidExtractor
|
||||
class MultimoviesCloudExtractor(private val client: OkHttpClient) {
|
||||
// from nineanime / ask4movie FilemoonExtractor
|
||||
private val subtitleRegex = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
|
||||
|
||||
fun videosFromUrl(url: String): List<Video> {
|
||||
val page = client.newCall(GET(url)).execute().body.string()
|
||||
val unpacked = JsUnpacker.unpackAndCombine(page) ?: return emptyList()
|
||||
val playlistUrl = unpacked.substringAfter("sources:")
|
||||
.substringAfter("file:\"")
|
||||
.substringBefore('"')
|
||||
|
||||
val playlistData = client.newCall(GET(playlistUrl)).execute().body.string()
|
||||
|
||||
val subs = subtitleRegex.findAll(playlistData).map {
|
||||
val subUrl = fixUrl(it.groupValues[2], playlistUrl)
|
||||
Track(subUrl, it.groupValues[1])
|
||||
}.toList()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
return playlistData.substringAfter(separator).split(separator).map {
|
||||
val resolution = it.substringAfter("RESOLUTION=")
|
||||
.substringBefore("\n")
|
||||
.substringAfter("x")
|
||||
.substringBefore(",") + "p"
|
||||
|
||||
val urlPart = it.substringAfter("\n").substringBefore("\n")
|
||||
val videoUrl = fixUrl(urlPart, playlistUrl)
|
||||
Video(videoUrl, "[multimovies cloud] - $resolution", videoUrl, subtitleTracks = subs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixUrl(urlPart: String, playlistUrl: String) =
|
||||
when {
|
||||
!urlPart.startsWith("https:") -> playlistUrl.substringBeforeLast("/") + "/$urlPart"
|
||||
else -> urlPart
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.multimovies.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES.decrypt
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class RabbitStreamExtractor(private val client: OkHttpClient) {
|
||||
|
||||
// Prevent (automatic) caching the .JS file for different episodes, because it
|
||||
// changes everytime, and a cached old .js will have a invalid AES password,
|
||||
// invalidating the decryption algorithm.
|
||||
// We cache it manually when initializing the class.
|
||||
private val newClient = client.newBuilder()
|
||||
.cache(null)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
val host = httpUrl.host
|
||||
val id = httpUrl.pathSegments.last()
|
||||
val embed = httpUrl.pathSegments.first()
|
||||
|
||||
val newHeaders = headers.newBuilder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.add("Host", host)
|
||||
.build()
|
||||
|
||||
val jsonBody = client.newCall(
|
||||
GET("https://rabbitstream.net/ajax/$embed/getSources?id=$id", headers = newHeaders),
|
||||
).execute().body.string()
|
||||
val parsed = json.decodeFromString<Source>(jsonBody)
|
||||
|
||||
val key = newClient.newCall(
|
||||
GET("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"),
|
||||
).execute().body.string()
|
||||
|
||||
val decrypted = decrypt(parsed.sources, key).ifEmpty { return emptyList() }
|
||||
val subtitleList = parsed.tracks.map {
|
||||
Track(it.file, it.label)
|
||||
}
|
||||
|
||||
val files = json.decodeFromString<List<File>>(decrypted)
|
||||
return files.flatMap { jsonFile ->
|
||||
val videoHeaders = Headers.headersOf(
|
||||
"Accept",
|
||||
"*/*",
|
||||
"Origin",
|
||||
"https://$host",
|
||||
"Referer",
|
||||
"https://$host/",
|
||||
"User-Agent",
|
||||
headers["User-Agent"] ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
|
||||
)
|
||||
|
||||
val masterPlaylist = client.newCall(
|
||||
GET(jsonFile.file, headers = videoHeaders),
|
||||
).execute().body.string()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||
val quality = prefix + it.substringAfter("RESOLUTION=")
|
||||
.substringBefore("\n")
|
||||
.substringAfter("x")
|
||||
.substringBefore(",") + "p"
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
Video(videoUrl, quality, videoUrl, headers = videoHeaders, subtitleTracks = subtitleList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Source(
|
||||
val sources: String,
|
||||
val tracks: List<Sub>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Sub(
|
||||
val file: String,
|
||||
val label: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class File(
|
||||
val file: String,
|
||||
)
|
||||
}
|
@ -10,7 +10,6 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
class StreamlareExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||
val id = url.split("/").last()
|
||||
val videoList = mutableListOf<Video>()
|
||||
val playlist = client.newCall(
|
||||
POST(
|
||||
"https://slwatch.co/api/video/stream/get",
|
||||
@ -20,27 +19,32 @@ class StreamlareExtractor(private val client: OkHttpClient) {
|
||||
).execute().body.string()
|
||||
|
||||
val type = playlist.substringAfter("\"type\":\"").substringBefore("\"")
|
||||
if (type == "hls") {
|
||||
return if (type == "hls") {
|
||||
val masterPlaylistUrl = playlist.substringAfter("\"file\":\"").substringBefore("\"").replace("\\/", "/")
|
||||
val masterPlaylist = client.newCall(GET(masterPlaylistUrl)).execute().body.string()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
|
||||
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
if (!videoUrl.startsWith("http")) videoUrl = "${masterPlaylistUrl.substringBefore("master.m3u8")}$videoUrl"
|
||||
videoList.add(Video(videoUrl, "$prefix $quality (Streamlare)", videoUrl))
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { urlPart ->
|
||||
when {
|
||||
!urlPart.startsWith("http") ->
|
||||
masterPlaylistUrl.substringBefore("master.m3u8") + urlPart
|
||||
else -> urlPart
|
||||
}
|
||||
}
|
||||
Video(videoUrl, "$prefix $quality (Streamlare)", videoUrl)
|
||||
}
|
||||
} else {
|
||||
playlist.substringAfter("\"label\":\"").split("\"label\":\"").forEach {
|
||||
val quality = it.substringAfter("\"label\":\"").substringBefore("\",") + " (Sl-mp4)"
|
||||
val token = it.substringAfter("\"file\":\"https:\\/\\/larecontent.com\\/video?token=")
|
||||
.substringBefore("\",")
|
||||
val response = client.newCall(POST("https://larecontent.com/video?token=$token")).execute()
|
||||
val separator = "\"label\":\""
|
||||
playlist.substringAfter(separator).split(separator).map {
|
||||
val quality = it.substringAfter(separator).substringBefore("\",") + " (Sl-mp4)"
|
||||
val apiUrl = it.substringAfter("\"file\":\"").substringBefore("\",")
|
||||
.replace("\\", "")
|
||||
val response = client.newCall(POST(apiUrl)).execute()
|
||||
val videoUrl = response.request.url.toString()
|
||||
videoList.add(Video(videoUrl, "$prefix $quality (Streamlare)", videoUrl))
|
||||
Video(videoUrl, "$prefix $quality (Streamlare)", videoUrl)
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
}
|
@ -11,17 +11,18 @@ class DooPlayGenerator : ThemeSourceGenerator {
|
||||
override val baseVersionCode = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Animes House", "https://animeshouse.net", "pt-BR", isNsfw = false, overrideVersionCode = 4),
|
||||
SingleLang("AnimeOnline360", "https://animeonline360.me", "en", isNsfw = false),
|
||||
SingleLang("AnimeOnline.Ninja", "https://www1.animeonline.ninja", "es", className = "AnimeOnlineNinja", isNsfw = false, overrideVersionCode = 26),
|
||||
SingleLang("AnimesFox BR", "https://animesfoxbr.com", "pt-BR", isNsfw = false, overrideVersionCode = 1),
|
||||
SingleLang("AnimePlayer", "https://animeplayer.com.br", "pt-BR", isNsfw = true),
|
||||
SingleLang("AnimesFox BR", "https://animesfoxbr.com", "pt-BR", isNsfw = false, overrideVersionCode = 1),
|
||||
SingleLang("Animes House", "https://animeshouse.net", "pt-BR", isNsfw = false, overrideVersionCode = 4),
|
||||
SingleLang("Cinemathek", "https://cinemathek.net", "de", isNsfw = true, overrideVersionCode = 11),
|
||||
SingleLang("CineVision", "https://cinevisionv3.online", "pt-BR", isNsfw = true, overrideVersionCode = 5),
|
||||
SingleLang("GoAnimes", "https://goanimes.net", "pt-BR", isNsfw = true),
|
||||
SingleLang("pactedanime", "https://pactedanime.com", "en", isNsfw = false, className = "PactedAnime", overrideVersionCode = 4),
|
||||
SingleLang("AnimeOnline360", "https://animeonline360.me", "en", isNsfw = false),
|
||||
SingleLang("Pi Fansubs", "https://pifansubs.org", "pt-BR", isNsfw = true, overrideVersionCode = 16),
|
||||
SingleLang("DonghuaX", "https://donghuax.com", "pt-BR", isNsfw = false),
|
||||
SingleLang("GoAnimes", "https://goanimes.net", "pt-BR", isNsfw = true),
|
||||
SingleLang("Multimovies", "https://multimovies.tech", "en", isNsfw = false, overrideVersionCode = 6),
|
||||
SingleLang("pactedanime", "https://pactedanime.com", "en", isNsfw = false, className = "PactedAnime", overrideVersionCode = 4),
|
||||
SingleLang("Pi Fansubs", "https://pifansubs.org", "pt-BR", isNsfw = true, overrideVersionCode = 16),
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
@ -1,20 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Multimovies'
|
||||
pkgNameSuffix = 'en.multimovies'
|
||||
extClass = '.Multimovies'
|
||||
extVersionCode = 6
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib-streamsb-extractor'))
|
||||
implementation(project(':lib-voe-extractor'))
|
||||
implementation(project(':lib-dood-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 67 KiB |
@ -1,429 +0,0 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.multimovies
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.en.multimovies.extractors.AutoEmbedExtractor
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.Exception
|
||||
|
||||
class Multimovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Multimovies"
|
||||
|
||||
override val baseUrl = "https://multimovies.tech"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.content > div.items > article.item"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
return GET("$baseUrl/genre/anime-series/page/$page/")
|
||||
}
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
|
||||
anime.setUrlWithoutDomain(element.selectFirst("div h3 a")!!.attr("href").toHttpUrl().encodedPath)
|
||||
anime.title = element.selectFirst("div h3 a")!!.text()
|
||||
anime.thumbnail_url = element.selectFirst("div.poster img")!!.attr("src")
|
||||
if (!anime.thumbnail_url.toString().startsWith("https://")) {
|
||||
anime.thumbnail_url = element.select("div.poster img").attr("data-src")
|
||||
}
|
||||
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.pagination span.current ~ a"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw Exception("Not used")
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val animes = if (response.request.url.encodedPath.startsWith("/genre/")) {
|
||||
document.select(searchGenreAnimeSelector()).map { element ->
|
||||
searchGenreAnimeFromElement(element)
|
||||
}
|
||||
} else {
|
||||
document.select(searchAnimeSelector()).map { element ->
|
||||
searchAnimeFromElement(element)
|
||||
}
|
||||
}
|
||||
|
||||
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return AnimesPage(animes, hasNextPage)
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = "div.search-page > div.result-item"
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
|
||||
anime.setUrlWithoutDomain(element.select("div.details > div.title a").attr("href").toHttpUrl().encodedPath)
|
||||
anime.title = element.select("div.details > div.title a").text()
|
||||
anime.thumbnail_url = element.select("div.image img").attr("src")
|
||||
if (!anime.thumbnail_url.toString().startsWith("https://")) {
|
||||
anime.thumbnail_url = element.select("div.poster img").attr("data-src")
|
||||
}
|
||||
|
||||
return anime
|
||||
}
|
||||
|
||||
private fun searchGenreAnimeSelector(): String = "div.items > article.item"
|
||||
|
||||
private fun searchGenreAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
|
||||
anime.setUrlWithoutDomain(element.select("div.data h3 a").attr("href").toHttpUrl().encodedPath)
|
||||
anime.title = element.select("div.data h3 a").text()
|
||||
anime.thumbnail_url = element.select("div.poster img").attr("src")
|
||||
if (!anime.thumbnail_url.toString().startsWith("https://")) {
|
||||
anime.thumbnail_url = element.select("div.poster img").attr("data-src")
|
||||
}
|
||||
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "div.pagination span.current ~ a"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/page/$page/?s=$query", headers)
|
||||
} else {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val streamingFilter = filterList.find { it is StreamingFilter } as StreamingFilter
|
||||
val ficUniFilter = filterList.find { it is FicUniFilter } as FicUniFilter
|
||||
val channelFilter = filterList.find { it is ChannelFilter } as ChannelFilter
|
||||
|
||||
when {
|
||||
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page", headers)
|
||||
streamingFilter.state != 0 -> GET("$baseUrl/genre/${streamingFilter.toUriPart()}/page/$page", headers)
|
||||
ficUniFilter.state != 0 -> GET("$baseUrl/genre/${ficUniFilter.toUriPart()}/page/$page", headers)
|
||||
channelFilter.state != 0 -> GET("$baseUrl/genre/${channelFilter.toUriPart()}/page/$page", headers)
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("NOTE: Ignored if using text search!"),
|
||||
AnimeFilter.Separator(),
|
||||
GenreFilter(getGenreList()),
|
||||
StreamingFilter(getStreamingList()),
|
||||
FicUniFilter(getFictionalUniverseList()),
|
||||
ChannelFilter(getChannelList()),
|
||||
)
|
||||
|
||||
private class GenreFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Genres", vals)
|
||||
private class StreamingFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Streaming service", vals)
|
||||
private class FicUniFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Fictional universe", vals)
|
||||
private class ChannelFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Channel", vals)
|
||||
|
||||
private fun getGenreList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Action", "action"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Animation", "animation"),
|
||||
Pair("Anime Movies", "anime-movies"),
|
||||
Pair("Anime Series", "anime-series"),
|
||||
Pair("Crime", "crime"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Family", "family"),
|
||||
Pair("History", "history"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Science Fiction", "science-fiction"),
|
||||
Pair("Thriller", "thriller"),
|
||||
)
|
||||
|
||||
private fun getStreamingList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Amazon Prime", "amazone-prime"),
|
||||
Pair("Apple TV +", "apple-tv"),
|
||||
Pair("Disney+Hotstar", "disneyhotstar"),
|
||||
Pair("HBO MAX", "hbo-max"),
|
||||
Pair("Hulu", "hulu"),
|
||||
Pair("Netflix", "netflix"),
|
||||
Pair("Sony Liv", "sony-liv"),
|
||||
Pair("Voot", "voot"),
|
||||
)
|
||||
|
||||
private fun getFictionalUniverseList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("DC Universe", "dc-universe"),
|
||||
Pair("Fast and Furious", "fast-and-furious"),
|
||||
Pair("Harry Potter", "harry"),
|
||||
Pair("Jurassic Park", "jurassic-park"),
|
||||
Pair("Marvel Cinematic", "marvel"),
|
||||
Pair("Matrix", "matrix"),
|
||||
Pair("Mission Impossible", "mission-impossible"),
|
||||
Pair("Pirates of the Caribbean", "pirates"),
|
||||
Pair("Resident Evil", "resident-evil"),
|
||||
Pair("Star Wars", "star-wars"),
|
||||
Pair("Terminator", "terminator"),
|
||||
Pair("Transformers", "transformer"),
|
||||
Pair("Wrong Turn", "wrong-turn"),
|
||||
Pair("X-Men", "xmen"),
|
||||
)
|
||||
|
||||
private fun getChannelList() = arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Cartoon Network", "cartoon-network"),
|
||||
Pair("Disney Channel", "disney"),
|
||||
Pair("Disney XD", "disney-xd"),
|
||||
Pair("Hungama", "hungama"),
|
||||
Pair("Nick", "nick"),
|
||||
Pair("Pogo", "pogo"),
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = document.select("div.sheader > div.data > h1").text()
|
||||
anime.genre = document.select("div.sgeneros a").eachText().joinToString(separator = ", ")
|
||||
anime.description = document.selectFirst("div#info p")!!.text()
|
||||
return anime
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListSelector() = throw Exception("Not used")
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
|
||||
if (response.request.url.encodedPath.startsWith("/movies/")) {
|
||||
val episode = SEpisode.create()
|
||||
|
||||
episode.name = document.select("div.data > h1").text()
|
||||
episode.episode_number = 1F
|
||||
episode.setUrlWithoutDomain(response.request.url.encodedPath)
|
||||
episodeList.add(episode)
|
||||
} else {
|
||||
var counter = 1
|
||||
for (season in document.select("div#seasons > div")) {
|
||||
val seasonList = mutableListOf<SEpisode>()
|
||||
for (ep in season.select("ul > li")) {
|
||||
if (ep.childrenSize() > 0) {
|
||||
val episode = SEpisode.create()
|
||||
|
||||
episode.name = "Season ${ep.selectFirst("div.numerando")!!.ownText().substringAfter("E")} - ${ep.selectFirst("a[href]")!!.ownText()}"
|
||||
episode.episode_number = counter.toFloat()
|
||||
episode.setUrlWithoutDomain(ep.selectFirst("a[href]")!!.attr("href").toHttpUrl().encodedPath)
|
||||
|
||||
if (ep.selectFirst("p:contains(Filler)") != null) episode.scanlator = "Filler Episode"
|
||||
|
||||
seasonList.add(episode)
|
||||
|
||||
counter++
|
||||
}
|
||||
}
|
||||
episodeList.addAll(seasonList)
|
||||
}
|
||||
}
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used")
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
val reqUrL = response.request.url
|
||||
|
||||
document.select("ul.ajax_mode > li").forEach {
|
||||
if (it.attr("data-nume") == "trailer") return@forEach
|
||||
|
||||
val postHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Conent-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", reqUrL.host)
|
||||
.add("Origin", "https://${reqUrL.host}")
|
||||
.add("Referer", reqUrL.toString())
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val postData = "action=doo_player_ajax&post=${it.attr("data-post")}&nume=${it.attr("data-nume")}&type=${it.attr("data-type")}".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
val embedded = client.newCall(
|
||||
POST("$baseUrl/wp-admin/admin-ajax.php", body = postData, headers = postHeaders),
|
||||
).execute().body.string()
|
||||
|
||||
val parsed = json.decodeFromString<Embed>(embedded)
|
||||
val url = if (parsed.embed_url.contains("<iframe", true)) {
|
||||
Jsoup.parse(parsed.embed_url).selectFirst("iframe")!!.attr("src")
|
||||
} else {
|
||||
parsed.embed_url
|
||||
}
|
||||
|
||||
when {
|
||||
url.contains("sbembed.com") || url.contains("sbembed1.com") || url.contains("sbplay.org") ||
|
||||
url.contains("sbvideo.net") || url.contains("streamsb.net") || url.contains("sbplay.one") ||
|
||||
url.contains("cloudemb.com") || url.contains("playersb.com") || url.contains("tubesb.com") ||
|
||||
url.contains("sbplay1.com") || url.contains("embedsb.com") || url.contains("watchsb.com") ||
|
||||
url.contains("sbplay2.com") || url.contains("japopav.tv") || url.contains("viewsb.com") ||
|
||||
url.contains("sbfast") || url.contains("sbfull.com") || url.contains("javplaya.com") ||
|
||||
url.contains("ssbstream.net") || url.contains("p1ayerjavseen.com") || url.contains("sbthe.com") ||
|
||||
url.contains("sbchill.com") || url.contains("sblongvu.com") || url.contains("sbanh.com") ||
|
||||
url.contains("sblanh.com") || url.contains("sbhight.com") || url.contains("sbbrisk.com") ||
|
||||
url.contains("sbspeed.com") || url.contains("multimovies.website") -> {
|
||||
videoList.addAll(
|
||||
StreamSBExtractor(client).videosFromUrl(url, headers = headers, prefix = "[multimovies]"),
|
||||
)
|
||||
}
|
||||
url.contains("autoembed.to") || url.contains("2embed.to") -> {
|
||||
val newHeaders = headers.newBuilder()
|
||||
.add("Referer", url).build()
|
||||
videoList.addAll(
|
||||
AutoEmbedExtractor(client).videosFromUrl(url, headers = newHeaders),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw Exception("Not used")
|
||||
|
||||
override fun videoFromElement(element: Element) = throw Exception("Not used")
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw Exception("Not used")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
@Serializable
|
||||
data class Embed(
|
||||
val embed_url: String,
|
||||
)
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||
val server = preferences.getString("preferred_server", "multimovies")!!
|
||||
val qualityRegex = """(\d+)p""".toRegex()
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ qualityRegex.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "240")
|
||||
setDefaultValue("1080")
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
val serverPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_server"
|
||||
title = "Preferred Server"
|
||||
entries = arrayOf(
|
||||
"multimovies",
|
||||
"2Embed Vidcloud",
|
||||
"2Embed Voe",
|
||||
"2Embed Streamlare",
|
||||
"2Embed MixDrop",
|
||||
"Gomo Dood",
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
"multimovies",
|
||||
"[2embed] server vidcloud",
|
||||
"[2embed] server voe",
|
||||
"[2embed] server streamlare",
|
||||
"[2embed] server mixdrop",
|
||||
"[gomostream] dood",
|
||||
)
|
||||
setDefaultValue("multimovies")
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(serverPref)
|
||||
screen.addPreference(videoQualityPref)
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.multimovies.extractors
|
||||
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class MixDropExtractor(private val client: OkHttpClient) {
|
||||
fun videoFromUrl(url: String, lang: String = "", prefix: String = ""): List<Video> {
|
||||
val doc = client.newCall(GET(url)).execute().asJsoup()
|
||||
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
|
||||
?.data()
|
||||
?.let { JsUnpacker.unpackAndCombine(it) }
|
||||
?: return emptyList<Video>()
|
||||
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
|
||||
.substringBefore("\"")
|
||||
val quality = ("MixDrop").let {
|
||||
if (lang.isNotBlank()) {
|
||||
"$it($lang)"
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
val referer = Headers.headersOf("Referer", "https://mixdrop.co/")
|
||||
return listOf(Video(url, prefix + quality, videoUrl, headers = referer))
|
||||
}
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.multimovies.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.DigestException
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class RabbitStreamExtractor(private val client: OkHttpClient) {
|
||||
|
||||
// Prevent (automatic) caching the .JS file for different episodes, because it
|
||||
// changes everytime, and a cached old .js will have a invalid AES password,
|
||||
// invalidating the decryption algorithm.
|
||||
// We cache it manually when initializing the class.
|
||||
private val newClient = client.newBuilder()
|
||||
.cache(null)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val id = url.toHttpUrl().pathSegments.last()
|
||||
val embed = url.toHttpUrl().pathSegments.first()
|
||||
|
||||
val newHeaders = headers.newBuilder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.add("Host", url.toHttpUrl().host)
|
||||
.build()
|
||||
|
||||
val jsonBody = client.newCall(
|
||||
GET("https://rabbitstream.net/ajax/$embed/getSources?id=$id", headers = newHeaders),
|
||||
).execute().body.string()
|
||||
val parsed = json.decodeFromString<Source>(jsonBody)
|
||||
|
||||
val key = newClient.newCall(
|
||||
GET("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"),
|
||||
).execute().body.string()
|
||||
|
||||
val decrypted = decrypt(parsed.sources, key) ?: return videoList
|
||||
val subtitleList = parsed.tracks.map {
|
||||
Track(it.file, it.label)
|
||||
}
|
||||
|
||||
val files = json.decodeFromString<List<File>>(decrypted)
|
||||
files.forEach {
|
||||
val videoHeaders = Headers.headersOf(
|
||||
"Accept",
|
||||
"*/*",
|
||||
"Origin",
|
||||
"https://${url.toHttpUrl().host}",
|
||||
"Referer",
|
||||
"https://${url.toHttpUrl().host}/",
|
||||
"User-Agent",
|
||||
headers["User-Agent"] ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
|
||||
)
|
||||
|
||||
val masterPlaylist = client.newCall(
|
||||
GET(it.file, headers = videoHeaders),
|
||||
).execute().body.string()
|
||||
|
||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
|
||||
.forEach { res ->
|
||||
val quality = prefix + res.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p "
|
||||
val videoUrl = res.substringAfter("\n").substringBefore("\n")
|
||||
videoList.add(
|
||||
Video(videoUrl, quality, videoUrl, headers = videoHeaders, subtitleTracks = subtitleList),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Source(
|
||||
val sources: String,
|
||||
val tracks: List<Sub>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Sub(
|
||||
val file: String,
|
||||
val label: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class File(
|
||||
val file: String,
|
||||
)
|
||||
|
||||
// Stolen from zoro extension
|
||||
private fun decrypt(encodedData: String, remoteKey: String): String? {
|
||||
val saltedData = Base64.decode(encodedData, Base64.DEFAULT)
|
||||
val salt = saltedData.copyOfRange(8, 16)
|
||||
val ciphertext = saltedData.copyOfRange(16, saltedData.size)
|
||||
val password = remoteKey.toByteArray()
|
||||
val (key, iv) = generateKeyAndIv(password, salt) ?: return null
|
||||
val keySpec = SecretKeySpec(key, "AES")
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
|
||||
val decryptedData = String(cipher.doFinal(ciphertext))
|
||||
return decryptedData
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/41434590/8166854
|
||||
private fun generateKeyAndIv(
|
||||
password: ByteArray,
|
||||
salt: ByteArray,
|
||||
hashAlgorithm: String = "MD5",
|
||||
keyLength: Int = 32,
|
||||
ivLength: Int = 16,
|
||||
iterations: Int = 1,
|
||||
): List<ByteArray>? {
|
||||
val md = MessageDigest.getInstance(hashAlgorithm)
|
||||
val digestLength = md.getDigestLength()
|
||||
val targetKeySize = keyLength + ivLength
|
||||
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
||||
var generatedData = ByteArray(requiredLength)
|
||||
var generatedLength = 0
|
||||
|
||||
try {
|
||||
md.reset()
|
||||
|
||||
while (generatedLength < targetKeySize) {
|
||||
if (generatedLength > 0) {
|
||||
md.update(
|
||||
generatedData,
|
||||
generatedLength - digestLength,
|
||||
digestLength,
|
||||
)
|
||||
}
|
||||
|
||||
md.update(password)
|
||||
md.update(salt, 0, 8)
|
||||
md.digest(generatedData, generatedLength, digestLength)
|
||||
|
||||
for (i in 1 until iterations) {
|
||||
md.update(generatedData, generatedLength, digestLength)
|
||||
md.digest(generatedData, generatedLength, digestLength)
|
||||
}
|
||||
|
||||
generatedLength += digestLength
|
||||
}
|
||||
val result = listOf(
|
||||
generatedData.copyOfRange(0, keyLength),
|
||||
generatedData.copyOfRange(keyLength, targetKeySize),
|
||||
)
|
||||
return result
|
||||
} catch (e: DigestException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|