animerco : update layout selectors and video extractor (#994)

* Update build.gradle

* animerco : update layout selectors and video extractor

big fix as the site changed their layout and video url method
add GdrivePlayerExtractor
This commit is contained in:
Ahmed gamal 2022-11-01 19:06:52 +02:00 committed by GitHub
parent e5898e37b2
commit a5d70d8fc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 190 additions and 52 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'Animerco' extName = 'Animerco'
pkgNameSuffix = 'ar.animerco' pkgNameSuffix = 'ar.animerco'
extClass = '.Animerco' extClass = '.Animerco'
extVersionCode = 16 extVersionCode = 17
libVersion = '13' libVersion = '13'
} }
@ -14,6 +14,7 @@ dependencies {
implementation(project(':lib-streamtape-extractor')) implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-streamsb-extractor')) implementation(project(':lib-streamsb-extractor'))
implementation(project(':lib-dood-extractor')) implementation(project(':lib-dood-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -4,7 +4,7 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.MpforuploadExtractor import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.SharedExtractor import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.SharedExtractor
import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.UQLoadExtractor import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.UQLoadExtractor
import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.VidBomExtractor import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.VidBomExtractor
@ -19,7 +19,9 @@ import eu.kanade.tachiyomi.lib.fembedextractor.FembedExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -52,19 +54,19 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// Popular Anime // Popular Anime
override fun popularAnimeSelector(): String = "div.items article.item" override fun popularAnimeSelector(): String = "div.media-block"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes/page/$page/") // page/$page override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes/page/$page/") // page/$page
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create() val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.data a").attr("href")) anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.thumbnail_url = element.select("div.poster img").attr("src") anime.thumbnail_url = element.select("a").attr("data-src")
anime.title = element.select("div.data a").text() anime.title = element.select("div.info a h3").text()
return anime return anime
} }
override fun popularAnimeNextPageSelector(): String = "i#nextpagination" override fun popularAnimeNextPageSelector(): String = "a.ti-arrow-left-c"
// Episodes // Episodes
@ -73,60 +75,56 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>() val episodeList = mutableListOf<SEpisode>()
val seriesLink1 = document.select("ol[itemscope] li:last-child a").attr("href") // val seriesLink1 = document.select("ol[itemscope] li:last-child a").attr("href")
val seriesLink = document.select("input[name=red]").attr("value") val seriesLink = document.select("link[rel=canonical]").attr("href")
val type = document.select("div.dtsingle").attr("itemtype").substringAfterLast("/") val type = document.select("link[rel=canonical]").attr("href")
if (type.contains("Series")) { if (type.contains("animes")) {
val seasonsHtml = client.newCall( val seasonsHtml = client.newCall(
GET( GET(
seriesLink seriesLink
// headers = Headers.headersOf("Referer", document.location()) // headers = Headers.headersOf("Referer", document.location())
) )
).execute().asJsoup() ).execute().asJsoup()
val seasonsElements = seasonsHtml.select("div.seasontitle a") val seasonsElements = seasonsHtml.select("ul.chapters-list li a.title")
seasonsElements.forEach { seasonsElements.reversed().forEach {
val seasonEpList = parseEpisodesFromSeries(it) val seasonEpList = parseEpisodesFromSeries(it)
episodeList.addAll(seasonEpList) episodeList.addAll(seasonEpList)
} }
} else { } else {
val movieUrl = seriesLink val movieUrl = seriesLink
val episode = SEpisode.create() val episode = SEpisode.create()
episode.name = document.select("div.data h1").text() episode.name = document.select("span.alt-title").text()
episode.episode_number = 1F episode.episode_number = 1F
episode.setUrlWithoutDomain(movieUrl) episode.setUrlWithoutDomain(movieUrl)
episodeList.add(episode) episodeList.add(episode)
} }
return episodeList.reversed() return episodeList
} }
// override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> { private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
// val seasonId = element.attr("abs:href")
val seasonName = element.text() val seasonName = element.text()
// Log.i("seasonname", seasonName)
val episodesUrl = element.attr("abs:href") val episodesUrl = element.attr("abs:href")
val episodesHtml = client.newCall( val episodesHtml = client.newCall(
GET( GET(
episodesUrl, episodesUrl,
) )
).execute().asJsoup() ).execute().asJsoup()
val episodeElements = episodesHtml.select("ul.episodios li") val episodeElements = episodesHtml.select("ul.chapters-list li")
return episodeElements.map { episodeFromElement(it) } return episodeElements.map { episodeFromElement(it) }
} }
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create() val episode = SEpisode.create()
val epNum = getNumberFromEpsString(element.select("div.episodiotitle a").text()) val epNum = getNumberFromEpsString(element.select("a.title h3").text())
episode.episode_number = when { episode.episode_number = when {
(epNum.isNotEmpty()) -> epNum.toFloat() (epNum.isNotEmpty()) -> epNum.toFloat()
else -> 1F else -> 1F
} }
// element.select("td > span.Num").text().toFloat() // element.select("td > span.Num").text().toFloat()
// val SeasonNum = element.ownerDocument().select("div.Title span").text() // val SeasonNum = element.ownerDocument().select("div.Title span").text()
val seasonName = element.ownerDocument().select("span.tagline").text() val seasonName = element.ownerDocument().select("div.media-title h1").text()
episode.name = "$seasonName : " + element.select("div.episodiotitle a").text() episode.name = "$seasonName : " + element.select("a.title h3").text()
episode.setUrlWithoutDomain(element.select("div.episodiotitle a").attr("abs:href")) episode.setUrlWithoutDomain(element.select("a.title").attr("abs:href"))
return episode return episode
} }
@ -136,18 +134,12 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// Video urls // Video urls
override fun videoListRequest(episode: SEpisode): Request {
val document = client.newCall(GET(baseUrl + episode.url)).execute().asJsoup()
val iframe = baseUrl + episode.url
return GET(iframe)
}
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.asJsoup()
return videosFromElement(document) return videosFromElement(document)
} }
override fun videoListSelector() = "div.pframe" // ul#playeroptionsul override fun videoListSelector() = "li.dooplay_player_option" // ul#playeroptionsul
private fun videosFromElement(document: Document): List<Video> { private fun videosFromElement(document: Document): List<Video> {
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
@ -155,7 +147,20 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
for (element in elements) { for (element in elements) {
val location = element.ownerDocument().location() val location = element.ownerDocument().location()
val videoHeaders = Headers.headersOf("Referer", location) val videoHeaders = Headers.headersOf("Referer", location)
val embedUrl = element.select("iframe").attr("src") val qualityy = element.text()
val post = element.attr("data-post")
val num = element.attr("data-nume")
val type = element.attr("data-type")
val pageData = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("nume", num)
.add("post", post)
.add("type", type)
.build()
val ajaxUrl = "https://animerco.com/wp-admin/admin-ajax.php"
val callAjax = client.newCall(POST(ajaxUrl, videoHeaders, pageData)).execute().asJsoup()
val embedUrlT = callAjax.text().substringAfter("embed_url\":\"").substringBefore("\"")
val embedUrl = embedUrlT.replace("\\/", "/")
when { when {
embedUrl.contains("sbembed.com") || embedUrl.contains("sbembed1.com") || embedUrl.contains("sbplay.org") || embedUrl.contains("sbembed.com") || embedUrl.contains("sbembed1.com") || embedUrl.contains("sbplay.org") ||
@ -164,7 +169,10 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
embedUrl.contains("sbplay1.com") || embedUrl.contains("embedsb.com") || embedUrl.contains("watchsb.com") || embedUrl.contains("sbplay1.com") || embedUrl.contains("embedsb.com") || embedUrl.contains("watchsb.com") ||
embedUrl.contains("sbplay2.com") || embedUrl.contains("japopav.tv") || embedUrl.contains("viewsb.com") || embedUrl.contains("sbplay2.com") || embedUrl.contains("japopav.tv") || embedUrl.contains("viewsb.com") ||
embedUrl.contains("sbfast") || embedUrl.contains("sbfull.com") || embedUrl.contains("javplaya.com") || embedUrl.contains("sbfast") || embedUrl.contains("sbfull.com") || embedUrl.contains("javplaya.com") ||
embedUrl.contains("ssbstream.net") || embedUrl.contains("p1ayerjavseen.com") || embedUrl.contains("sbthe.com") embedUrl.contains("ssbstream.net") || embedUrl.contains("p1ayerjavseen.com") || embedUrl.contains("sbthe.com") ||
embedUrl.contains("vidmovie.xyz") || embedUrl.contains("sbspeed.com") || embedUrl.contains("streamsss.net") ||
embedUrl.contains("sblanh.com") || embedUrl.contains("tvmshow.com") || embedUrl.contains("sbanh.com") ||
embedUrl.contains("streamovies.xyz")
-> { -> {
val videos = StreamSBExtractor(client).videosFromUrl(embedUrl, headers) val videos = StreamSBExtractor(client).videosFromUrl(embedUrl, headers)
videoList.addAll(videos) videoList.addAll(videos)
@ -179,7 +187,7 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
videoList.add(video) videoList.add(video)
} }
} }
embedUrl.contains("fembed.com") || embedUrl.contains("fembed") ||
embedUrl.contains("anime789.com") || embedUrl.contains("24hd.club") || embedUrl.contains("fembad.org") || embedUrl.contains("anime789.com") || embedUrl.contains("24hd.club") || embedUrl.contains("fembad.org") ||
embedUrl.contains("vcdn.io") || embedUrl.contains("sharinglink.club") || embedUrl.contains("moviemaniac.org") || embedUrl.contains("vcdn.io") || embedUrl.contains("sharinglink.club") || embedUrl.contains("moviemaniac.org") ||
embedUrl.contains("votrefiles.club") || embedUrl.contains("femoload.xyz") || embedUrl.contains("albavido.xyz") || embedUrl.contains("votrefiles.club") || embedUrl.contains("femoload.xyz") || embedUrl.contains("albavido.xyz") ||
@ -196,7 +204,10 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
embedUrl.contains("youvideos.ru") || embedUrl.contains("streamm4u.club") || // embedUrl.contains("") || embedUrl.contains("youvideos.ru") || embedUrl.contains("streamm4u.club") || // embedUrl.contains("") ||
embedUrl.contains("moviepl.xyz") || embedUrl.contains("asianclub.tv") || // embedUrl.contains("") || embedUrl.contains("moviepl.xyz") || embedUrl.contains("asianclub.tv") || // embedUrl.contains("") ||
embedUrl.contains("vidcloud.fun") || embedUrl.contains("fplayer.info") || // embedUrl.contains("") || embedUrl.contains("vidcloud.fun") || embedUrl.contains("fplayer.info") || // embedUrl.contains("") ||
embedUrl.contains("diasfem.com") || embedUrl.contains("javpoll.com") // embedUrl.contains("") embedUrl.contains("diasfem.com") || embedUrl.contains("javpoll.com") || embedUrl.contains("reeoov.tube") ||
embedUrl.contains("suzihaza.com") || embedUrl.contains("ezsubz.com") || embedUrl.contains("vidsrc.xyz") ||
embedUrl.contains("diampokusy.com") || embedUrl.contains("diampokusy.com") || embedUrl.contains("i18n.pw") ||
embedUrl.contains("vanfem.com") || embedUrl.contains("fembed9hd.com") || embedUrl.contains("votrefilms.xyz") || embedUrl.contains("watchjavnow.xyz")
-> { -> {
val fUrl = embedUrl.replace("\\/", "/") val fUrl = embedUrl.replace("\\/", "/")
@ -204,6 +215,12 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val videos = FembedExtractor(client).videosFromUrl(fUrl) val videos = FembedExtractor(client).videosFromUrl(fUrl)
videoList.addAll(videos) videoList.addAll(videos)
} }
embedUrl.contains("drive.google")
-> {
val embedUrlG = "https://gdriveplayer.to/embed2.php?link=" + embedUrl
val videos = GdrivePlayerExtractor(client).videosFromUrl(embedUrlG)
videoList.addAll(videos)
}
embedUrl.contains("streamtape") -> { embedUrl.contains("streamtape") -> {
val video = StreamTapeExtractor(client).videoFromUrl(embedUrl) val video = StreamTapeExtractor(client).videoFromUrl(embedUrl)
if (video != null) { if (video != null) {
@ -217,13 +234,6 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
videoList.add(video) videoList.add(video)
} }
} }
embedUrl.contains("4shared") -> {
val qualityy = "4shared"
val video = MpforuploadExtractor(client).videoFromUrl(embedUrl, qualityy)
if (video != null) {
videoList.add(video)
}
}
embedUrl.contains("uqload") -> { embedUrl.contains("uqload") -> {
val qualityy = "uqload" val qualityy = "uqload"
val video = UQLoadExtractor(client).videoFromUrl(embedUrl, qualityy) val video = UQLoadExtractor(client).videoFromUrl(embedUrl, qualityy)
@ -280,15 +290,15 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeFromElement(element: Element): SAnime { override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create() val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href")) anime.setUrlWithoutDomain(element.select("a.image").attr("href"))
anime.thumbnail_url = element.select("img").attr("src") anime.thumbnail_url = element.select("a.image").attr("data-src")
anime.title = element.select("img").attr("alt") anime.title = element.select("a.image").attr("alt")
return anime return anime
} }
override fun searchAnimeNextPageSelector(): String = "i#nextpagination" override fun searchAnimeNextPageSelector(): String = "a.ti-arrow-left-c"
override fun searchAnimeSelector(): String = "div.image a" override fun searchAnimeSelector(): String = "div.media-block"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/page/$page/?s=$query") override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/page/$page/?s=$query")
@ -296,11 +306,12 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create() val anime = SAnime.create()
anime.thumbnail_url = document.select("div.poster img").attr("src") anime.thumbnail_url = document.select(".poster").attr("data-src")
anime.title = document.select("div.data h1").text() anime.title = document.select("div.media-title h1").text()
anime.genre = document.select("div.sgeneros a").joinToString(", ") { it.text() } anime.genre = document.select("nav.Nvgnrs a, ul.media-info li:contains(النوع) a").joinToString(", ") { it.text() }
anime.description = document.select("div[itemprop=description] p").text() anime.description = document.select("div.media-story p").text()
anime.author = document.select("div.extra span a").joinToString(", ") { it.text() } anime.author = document.select("ul.media-info li:contains(الشبكات) a").joinToString(", ") { it.text() }
anime.artist = document.select("ul.media-info li:contains(الأستوديو) a").joinToString(", ") { it.text() }
// anime.status = parseStatus(document.select("div.row-line:contains(Status)").text().replace("Status: ", "")) // anime.status = parseStatus(document.select("div.row-line:contains(Status)").text().replace("Status: ", ""))
return anime return anime
} }

View File

@ -0,0 +1,126 @@
package eu.kanade.tachiyomi.animeextension.ar.animerco.extractors
import android.util.Base64
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class GdrivePlayerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url.replace(".me", ".to"))).execute()
.body!!.string()
val eval = JsUnpacker.unpackAndCombine(body)!!.replace("\\", "")
val json = Json.decodeFromString<JsonObject>(REGEX_DATAJSON.getFirst(eval))
val sojson = REGEX_SOJSON.getFirst(eval)
.split(Regex("\\D+"))
.joinToString("") {
Char(it.toInt()).toString()
}
val password = REGEX_PASSWORD.getFirst(sojson).toByteArray()
val decrypted = decryptAES(password, json)!!
val secondEval = JsUnpacker.unpackAndCombine(decrypted)!!.replace("\\", "")
return REGEX_VIDEOURL.findAll(secondEval)
.distinctBy { it.groupValues[2] } // remove duplicates by quality
.map {
val qualityStr = it.groupValues[2]
val quality = "$PLAYER_NAME - ${qualityStr}p"
val videoUrl = "https:" + it.groupValues[1] + "&res=$qualityStr"
Video(videoUrl, quality, videoUrl)
}.toList()
}
private fun decryptAES(password: ByteArray, json: JsonObject): String? {
val salt = json["s"]!!.jsonPrimitive.content
val encodedCiphetext = json["ct"]!!.jsonPrimitive.content
val ciphertext = Base64.decode(encodedCiphetext, Base64.DEFAULT)
val (key, iv) = GenerateKeyAndIv(password, salt.decodeHex())
?: return null
val keySpec = SecretKeySpec(key, "AES")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
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
}
}
private fun Regex.getFirst(item: String): String {
return find(item)?.groups?.elementAt(1)?.value!!
}
// Stolen from AnimixPlay(EN) / GogoCdnExtractor
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
private const val PLAYER_NAME = "GDRIVE"
private val REGEX_DATAJSON = Regex("data='(\\S+?)'")
private val REGEX_PASSWORD = Regex("var pass = \"(\\S+?)\"")
private val REGEX_SOJSON = Regex("null,['|\"](\\w+)['|\"]")
private val REGEX_VIDEOURL = Regex("file\":\"(\\S+?)\".*?res=(\\d+)")
}
}