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
This commit is contained in:
Claudemirovsky 2023-05-23 11:41:57 +00:00 committed by GitHub
parent 48eefa5045
commit 2509be2707
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 537 additions and 778 deletions

View 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"
}

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View 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",
)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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,
)
}

View File

@ -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
}
}

View File

@ -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 {

View File

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

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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
}
}
}