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 app.cash.quickjs.QuickJs
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
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.streamsbextractor.StreamSBExtractor
|
||||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
@ -698,9 +699,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class AutoEmbedExtractor(private val client: OkHttpClient) {
|
class AutoEmbedExtractor(private val client: OkHttpClient) {
|
||||||
@ -708,10 +707,6 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
|||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
fun videosFromUrl(url: String, headers: Headers): List<Video> {
|
fun videosFromUrl(url: String, headers: Headers): List<Video> {
|
||||||
val videoList = mutableListOf<Video>()
|
|
||||||
val serverList = mutableListOf<Server>()
|
|
||||||
val containerList = mutableListOf<Server>()
|
|
||||||
|
|
||||||
val docHeaders = headers.newBuilder()
|
val docHeaders = headers.newBuilder()
|
||||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||||
.add("Host", url.toHttpUrl().host)
|
.add("Host", url.toHttpUrl().host)
|
||||||
@ -721,7 +716,7 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
|||||||
).execute().asJsoup()
|
).execute().asJsoup()
|
||||||
|
|
||||||
// First get all containers
|
// 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 slug = it.attr("href")
|
||||||
val newDocument = client.newCall(
|
val newDocument = client.newCall(
|
||||||
GET("https://${url.toHttpUrl().host}$slug", headers = headers),
|
GET("https://${url.toHttpUrl().host}$slug", headers = headers),
|
||||||
@ -731,72 +726,64 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
|||||||
.substringAfter("replace(\"").substringBefore("\"")
|
.substringAfter("replace(\"").substringBefore("\"")
|
||||||
|
|
||||||
when {
|
when {
|
||||||
container.contains("2embed.to", true) -> {
|
container.contains("2embed.to", true) -> Server(container, "2embed")
|
||||||
containerList.add(Server(container, "2embed"))
|
container.contains("gomostream", true) -> Server(container, "gomostream")
|
||||||
}
|
else -> null
|
||||||
container.contains("gomostream", true) -> {
|
|
||||||
containerList.add(Server(container, "gomostream"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get video servers from containers
|
// Get video servers from containers
|
||||||
serverList.addAll(
|
val serverList = containerList.parallelMap { container ->
|
||||||
containerList.parallelMap { container ->
|
runCatching {
|
||||||
runCatching {
|
when (container.name) {
|
||||||
when (container.name) {
|
"2embed" -> {
|
||||||
"2embed" -> {
|
getTwoEmbedServers(container.url, headers = headers)
|
||||||
getTwoEmbedServers(container.url, container.name, headers = headers)
|
|
||||||
}
|
|
||||||
"gomostream" -> {
|
|
||||||
getGomoStreamServers(container.url, container.name, headers = headers)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
"gomostream" -> {
|
||||||
}.filterNotNull().flatten(),
|
getGomoStreamServers(container.url, headers = headers)
|
||||||
)
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.getOrNull() ?: emptyList()
|
||||||
|
}.flatten()
|
||||||
|
|
||||||
val videoHeaders = headers.newBuilder()
|
val videoHeaders = headers.newBuilder()
|
||||||
.add("Referer", "https://www.2embed.to/")
|
.add("Referer", "https://www.2embed.to/")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
videoList.addAll(
|
return serverList.parallelMap { server ->
|
||||||
serverList.parallelMap { server ->
|
runCatching {
|
||||||
runCatching {
|
val prefix = server.name
|
||||||
val prefix = server.name
|
val videoUrl = server.url
|
||||||
val url = server.url
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
url.contains("streamsb") -> {
|
videoUrl.contains("streamsb") -> {
|
||||||
StreamSBExtractor(client).videosFromUrl(url, headers = headers, prefix = prefix)
|
StreamSBExtractor(client).videosFromUrl(videoUrl, 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
|
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
videoUrl.contains("streamlare") -> {
|
||||||
}.filterNotNull().flatten(),
|
StreamlareExtractor(client).videosFromUrl(videoUrl, prefix = prefix)
|
||||||
)
|
}
|
||||||
|
videoUrl.contains("mixdrop") -> {
|
||||||
return videoList
|
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(
|
data class Server(
|
||||||
@ -809,15 +796,11 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
|||||||
val link: String,
|
val link: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getGomoStreamServers(url: String, name: String, headers: Headers): List<Server> {
|
private fun getGomoStreamServers(url: String, headers: Headers): List<Server> {
|
||||||
val serverList = mutableListOf<Server>()
|
val response = client.newCall(GET(url, headers = headers)).execute()
|
||||||
|
|
||||||
val response = client.newCall(
|
|
||||||
GET(url, headers = headers),
|
|
||||||
).execute()
|
|
||||||
val responseUrl = response.request.url
|
val responseUrl = response.request.url
|
||||||
val document = response.asJsoup()
|
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 token = script.substringAfter("\"_token\": \"").substringBefore("\"")
|
||||||
val tokenCode = script.substringAfter("var tc = '").substringBefore("';")
|
val tokenCode = script.substringAfter("var tc = '").substringBefore("';")
|
||||||
val postUrl = script.substringAfter("\"POST\"").substringAfter("\"").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 functionName = script.substringAfter("'x-token': ").substringBefore("(")
|
||||||
val toExecute = "function $functionName${script.substringAfter("function $functionName")};$functionName(\"$tokenCode\")"
|
val toExecute = "function $functionName${script.substringAfter("function $functionName")};$functionName(\"$tokenCode\")"
|
||||||
|
|
||||||
val quickJs = QuickJs.create()
|
val xToken = QuickJs.create().use { it.evaluate(toExecute).toString() }
|
||||||
val xToken = quickJs.evaluate(toExecute).toString()
|
|
||||||
quickJs.close()
|
|
||||||
|
|
||||||
val postHeaders = Headers.headersOf(
|
val postHeaders = Headers.headersOf(
|
||||||
"Accept", "*/*",
|
"Accept", "*/*",
|
||||||
@ -841,33 +822,29 @@ class AutoEmbedExtractor(private val client: OkHttpClient) {
|
|||||||
"x-token", xToken,
|
"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(
|
val postResponse = client.newCall(
|
||||||
POST(postUrl, headers = postHeaders, body = postBody),
|
POST(postUrl, headers = postHeaders, body = postBody),
|
||||||
).execute().body.string()
|
).execute().body.string()
|
||||||
val list = json.decodeFromString<List<String>>(postResponse)
|
val list = json.decodeFromString<List<String>>(postResponse)
|
||||||
|
|
||||||
serverList.addAll(
|
return list.filter { x -> x != "" }.map {
|
||||||
list.filter { x -> x != "" }.map {
|
Server(it, "[gomostream] ${it.toHttpUrl().host} -")
|
||||||
Server(it, "[gomostream] ${it.toHttpUrl().host} -")
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return serverList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTwoEmbedServers(url: String, name: String, headers: Headers): List<Server> {
|
private fun getTwoEmbedServers(url: String, headers: Headers): List<Server> {
|
||||||
val serverList = mutableListOf<Server>()
|
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||||
val document = client.newCall(
|
|
||||||
GET(url),
|
|
||||||
).execute().asJsoup()
|
|
||||||
|
|
||||||
val captcha = document.selectFirst("script[src*=recaptcha/api.js]")!!
|
val captcha = document.selectFirst("script[src*=recaptcha/api.js]")!!
|
||||||
.attr("src")
|
.attr("src")
|
||||||
.substringAfter("render=")
|
.substringAfter("render=")
|
||||||
|
|
||||||
// Get video host urls
|
// 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(
|
val ajaxHeaders = Headers.headersOf(
|
||||||
"Accept", "*/*",
|
"Accept", "*/*",
|
||||||
"Host", url.toHttpUrl().host,
|
"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),
|
GET("https://www.2embed.to/ajax/embed/play?id=${it.attr("data-id")}&_token=$token", headers = ajaxHeaders),
|
||||||
).execute().body.string()
|
).execute().body.string()
|
||||||
val parsed = json.decodeFromString<Stream>(streamUrl)
|
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
|
// 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? {
|
private fun getCaptchaToken(url: String, key: String): String? {
|
||||||
try {
|
return runCatching {
|
||||||
val domain = Base64.encodeToString(
|
val httpUrl = url.toHttpUrl()
|
||||||
(url.toHttpUrl().scheme + "://" + url.toHttpUrl().host + ":443").encodeToByteArray(),
|
val pureDomain = (httpUrl.scheme + "://" + httpUrl.host + ":443")
|
||||||
0,
|
val domain = Base64.encodeToString(pureDomain.encodeToByteArray(), Base64.DEFAULT)
|
||||||
).replace("\n", "").replace("=", ".")
|
.replace("\n", "")
|
||||||
val vToken = client.newCall(
|
.replace("=", ".")
|
||||||
GET("https://www.google.com/recaptcha/api.js?render=$key"),
|
|
||||||
).execute().body.string().substringAfter("releases/").substringBefore("/")
|
val vToken = client.newCall(GET("https://www.google.com/recaptcha/api.js?render=$key"))
|
||||||
val recapToken = client.newCall(
|
.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"),
|
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")
|
).execute()
|
||||||
if (recapToken != null) {
|
.asJsoup()
|
||||||
val body = FormBody.Builder()
|
.selectFirst("#recaptcha-token")
|
||||||
.add("v", vToken)
|
?.attr("value")
|
||||||
.add("k", key)
|
?.let { recapToken ->
|
||||||
.add("c", recapToken)
|
val body = FormBody.Builder()
|
||||||
.add("co", domain)
|
.add("v", vToken)
|
||||||
.add("sa", "")
|
.add("k", key)
|
||||||
.add("reason", "q")
|
.add("c", recapToken)
|
||||||
.build()
|
.add("co", domain)
|
||||||
return client.newCall(
|
.add("sa", "")
|
||||||
POST("https://www.google.com/recaptcha/api2/reload?k=$key", body = body),
|
.add("reason", "q")
|
||||||
).execute().body.string().substringAfter("rresp\",\"").substringBefore("\"")
|
.build()
|
||||||
}
|
return client.newCall(
|
||||||
} catch (_: Exception) { }
|
POST("https://www.google.com/recaptcha/api2/reload?k=$key", body = body),
|
||||||
return null
|
).execute().body.string().substringAfter("rresp\",\"").substringBefore("\"")
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
// From Dopebox
|
// 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) {
|
class StreamlareExtractor(private val client: OkHttpClient) {
|
||||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||||
val id = url.split("/").last()
|
val id = url.split("/").last()
|
||||||
val videoList = mutableListOf<Video>()
|
|
||||||
val playlist = client.newCall(
|
val playlist = client.newCall(
|
||||||
POST(
|
POST(
|
||||||
"https://slwatch.co/api/video/stream/get",
|
"https://slwatch.co/api/video/stream/get",
|
||||||
@ -20,27 +19,32 @@ class StreamlareExtractor(private val client: OkHttpClient) {
|
|||||||
).execute().body.string()
|
).execute().body.string()
|
||||||
|
|
||||||
val type = playlist.substringAfter("\"type\":\"").substringBefore("\"")
|
val type = playlist.substringAfter("\"type\":\"").substringBefore("\"")
|
||||||
if (type == "hls") {
|
return if (type == "hls") {
|
||||||
val masterPlaylistUrl = playlist.substringAfter("\"file\":\"").substringBefore("\"").replace("\\/", "/")
|
val masterPlaylistUrl = playlist.substringAfter("\"file\":\"").substringBefore("\"").replace("\\/", "/")
|
||||||
val masterPlaylist = client.newCall(GET(masterPlaylistUrl)).execute().body.string()
|
val masterPlaylist = client.newCall(GET(masterPlaylistUrl)).execute().body.string()
|
||||||
|
|
||||||
val separator = "#EXT-X-STREAM-INF"
|
val separator = "#EXT-X-STREAM-INF"
|
||||||
masterPlaylist.substringAfter(separator).split(separator).map {
|
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||||
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
|
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
|
||||||
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
val videoUrl = it.substringAfter("\n").substringBefore("\n").let { urlPart ->
|
||||||
if (!videoUrl.startsWith("http")) videoUrl = "${masterPlaylistUrl.substringBefore("master.m3u8")}$videoUrl"
|
when {
|
||||||
videoList.add(Video(videoUrl, "$prefix $quality (Streamlare)", videoUrl))
|
!urlPart.startsWith("http") ->
|
||||||
|
masterPlaylistUrl.substringBefore("master.m3u8") + urlPart
|
||||||
|
else -> urlPart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Video(videoUrl, "$prefix $quality (Streamlare)", videoUrl)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
playlist.substringAfter("\"label\":\"").split("\"label\":\"").forEach {
|
val separator = "\"label\":\""
|
||||||
val quality = it.substringAfter("\"label\":\"").substringBefore("\",") + " (Sl-mp4)"
|
playlist.substringAfter(separator).split(separator).map {
|
||||||
val token = it.substringAfter("\"file\":\"https:\\/\\/larecontent.com\\/video?token=")
|
val quality = it.substringAfter(separator).substringBefore("\",") + " (Sl-mp4)"
|
||||||
.substringBefore("\",")
|
val apiUrl = it.substringAfter("\"file\":\"").substringBefore("\",")
|
||||||
val response = client.newCall(POST("https://larecontent.com/video?token=$token")).execute()
|
.replace("\\", "")
|
||||||
|
val response = client.newCall(POST(apiUrl)).execute()
|
||||||
val videoUrl = response.request.url.toString()
|
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 baseVersionCode = 1
|
||||||
|
|
||||||
override val sources = listOf(
|
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("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("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("Cinemathek", "https://cinemathek.net", "de", isNsfw = true, overrideVersionCode = 11),
|
||||||
SingleLang("CineVision", "https://cinevisionv3.online", "pt-BR", isNsfw = true, overrideVersionCode = 5),
|
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("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 {
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|