refactor: Make extensions use GdrivePlayer lib (#1909)

This commit is contained in:
Claudemirovsky
2023-07-16 11:19:37 +00:00
committed by GitHub
parent 6a62b77765
commit f9ab9febf5
18 changed files with 52 additions and 582 deletions

View File

@ -10,6 +10,7 @@ ext {
} }
dependencies { dependencies {
implementation(project(':lib-gdriveplayer-extractor'))
implementation(project(':lib-streamsb-extractor')) implementation(project(':lib-streamsb-extractor'))
implementation(project(':lib-dood-extractor')) implementation(project(':lib-dood-extractor'))
implementation(project(':lib-voe-extractor')) implementation(project(':lib-voe-extractor'))

View File

@ -6,10 +6,9 @@ import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.SharedExtractor import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.SharedExtractor
import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.VidYardExtractor
import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.StreamWishExtractor import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.StreamWishExtractor
import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.VidYardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -18,6 +17,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
@ -135,7 +135,7 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
url.contains("drive.google") -> { url.contains("drive.google") -> {
val embedUrlG = "https://gdriveplayer.to/embed2.php?link=$url" val embedUrlG = "https://gdriveplayer.to/embed2.php?link=$url"
GdrivePlayerExtractor(client).videosFromUrl(embedUrlG) GdrivePlayerExtractor(client).videosFromUrl(embedUrlG, "GdrivePlayer", headers = headers)
} }
url.contains("vidyard") -> { url.contains("vidyard") -> {
val headers = headers.newBuilder() val headers = headers.newBuilder()
@ -178,6 +178,7 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
else -> null else -> null
} ?: emptyList() } ?: emptyList()
} }
// override fun videoListSelector() = "script:containsData(m3u8)" // override fun videoListSelector() = "script:containsData(m3u8)"
override fun videoListSelector() = "li[data-i] a" override fun videoListSelector() = "li[data-i] a"

View File

@ -1,125 +0,0 @@
package eu.kanade.tachiyomi.animeextension.ar.anime4up.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.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+)")
}
}

View File

@ -11,7 +11,7 @@ class StreamWishExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers): List<Video> { fun videosFromUrl(url: String, headers: Headers): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup() val doc = client.newCall(GET(url)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(sources)")!!.data() val script = doc.selectFirst("script:containsData(sources)")!!.data()
val scriptData = if(script.contains("eval")) JsUnpacker.unpackAndCombine(script)!! else script val scriptData = if (script.contains("eval")) JsUnpacker.unpackAndCombine(script)!! else script
val m3u8 = Regex("sources:\\s*\\[\\{\\s*\\t*file:\\s*[\"']([^\"']+)").find(scriptData)!!.groupValues[1] val m3u8 = Regex("sources:\\s*\\[\\{\\s*\\t*file:\\s*[\"']([^\"']+)").find(scriptData)!!.groupValues[1]
val streamLink = Regex("(.*)_,(.*),\\.urlset/master(.*)").find(m3u8)!! val streamLink = Regex("(.*)_,(.*),\\.urlset/master(.*)").find(m3u8)!!
val streamQuality = streamLink.groupValues[2].split(",").reversed() val streamQuality = streamLink.groupValues[2].split(",").reversed()

View File

@ -10,6 +10,7 @@ ext {
} }
dependencies { dependencies {
implementation(project(':lib-gdriveplayer-extractor'))
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'))

View File

@ -4,7 +4,6 @@ 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.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
@ -15,6 +14,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
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
@ -189,7 +189,7 @@ class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
embedUrl.contains("drive.google") embedUrl.contains("drive.google")
-> { -> {
val embedUrlG = "https://gdriveplayer.to/embed2.php?link=" + embedUrl val embedUrlG = "https://gdriveplayer.to/embed2.php?link=" + embedUrl
val videos = GdrivePlayerExtractor(client).videosFromUrl(embedUrlG) val videos = GdrivePlayerExtractor(client).videosFromUrl(embedUrlG, "GdrivePlayer", headers = headers)
videoList.addAll(videos) videoList.addAll(videos)
} }
embedUrl.contains("streamtape") -> { embedUrl.contains("streamtape") -> {

View File

@ -1,125 +0,0 @@
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.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+)")
}
}

View File

@ -27,7 +27,7 @@ import okhttp3.Response
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ArabAnime: ConfigurableAnimeSource, AnimeHttpSource() { class ArabAnime : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "ArabAnime" override val name = "ArabAnime"
@ -81,20 +81,20 @@ class ArabAnime: ConfigurableAnimeSource, AnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val watchData = response.asJsoup().select("div#datawatch").text().decodeBase64() val watchData = response.asJsoup().select("div#datawatch").text().decodeBase64()
val serversJson = json.decodeFromString<Episode>(watchData) val serversJson = json.decodeFromString<Episode>(watchData)
val selectServer =serversJson.ep_info[0].stream_servers[0].decodeBase64() val selectServer = serversJson.ep_info[0].stream_servers[0].decodeBase64()
val watchPage = client.newCall(GET(selectServer)).execute().asJsoup() val watchPage = client.newCall(GET(selectServer)).execute().asJsoup()
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
watchPage.select("option").forEach { it -> watchPage.select("option").forEach { it ->
val link = it.attr("data-src").decodeBase64() val link = it.attr("data-src").decodeBase64()
if (link.contains("www.arabanime.net/embed")){ if (link.contains("www.arabanime.net/embed")) {
val sources = client.newCall(GET(link)).execute().asJsoup().select("source") val sources = client.newCall(GET(link)).execute().asJsoup().select("source")
sources.forEach { source -> sources.forEach { source ->
if(!source.attr("src").contains("static")){ if (!source.attr("src").contains("static")) {
val quality = source.attr("label").let {q -> val quality = source.attr("label").let { q ->
if(q.contains("p")) q else q + "p" if (q.contains("p")) q else q + "p"
} }
videoList.add( videoList.add(
Video(source.attr("src"), "${it.text()}: $quality" ,source.attr("src")) Video(source.attr("src"), "${it.text()}: $quality", source.attr("src")),
) )
} }
} }
@ -132,7 +132,7 @@ class ArabAnime: ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
return if(response.body.contentType() == "application/json".toMediaType()){ return if (response.body.contentType() == "application/json".toMediaType()) {
popularAnimeParse(response) popularAnimeParse(response)
} else { } else {
val searchResult = response.asJsoup().select("div.show") val searchResult = response.asJsoup().select("div.show")
@ -178,7 +178,7 @@ class ArabAnime: ConfigurableAnimeSource, AnimeHttpSource() {
val animeList = latestEpisodes.map { val animeList = latestEpisodes.map {
SAnime.create().apply { SAnime.create().apply {
val url = it.select("a.as-info").attr("href") val url = it.select("a.as-info").attr("href")
.replace("watch","show").substringBeforeLast("/") .replace("watch", "show").substringBeforeLast("/")
setUrlWithoutDomain(url) setUrlWithoutDomain(url)
title = it.select("a.as-info").text() title = it.select("a.as-info").text()
thumbnail_url = it.select("img").attr("src") thumbnail_url = it.select("img").attr("src")
@ -210,20 +210,20 @@ class ArabAnime: ConfigurableAnimeSource, AnimeHttpSource() {
CatUnit("اختر", ""), CatUnit("اختر", ""),
CatUnit("التقييم", "2"), CatUnit("التقييم", "2"),
CatUnit("اخر الانميات المضافة", "1"), CatUnit("اخر الانميات المضافة", "1"),
CatUnit("الابجدية", "0") CatUnit("الابجدية", "0"),
) )
private fun getTypeFilterList() = listOf( private fun getTypeFilterList() = listOf(
CatUnit("اختر", ""), CatUnit("اختر", ""),
CatUnit("الكل", ""), CatUnit("الكل", ""),
CatUnit("فيلم", "0"), CatUnit("فيلم", "0"),
CatUnit("انمى", "1") CatUnit("انمى", "1"),
) )
private fun getStatFilterList() = listOf( private fun getStatFilterList() = listOf(
CatUnit("اختر", ""), CatUnit("اختر", ""),
CatUnit("الكل", ""), CatUnit("الكل", ""),
CatUnit("مستمر", "1"), CatUnit("مستمر", "1"),
CatUnit("مكتمل", "0") CatUnit("مكتمل", "0"),
) )
// =============================== Preferences =============================== // =============================== Preferences ===============================

View File

@ -5,14 +5,16 @@ import java.io.ByteArrayOutputStream
fun String.decodeBase64(): String = String(this.toByteArray().decodeBase64()) fun String.decodeBase64(): String = String(this.toByteArray().decodeBase64())
fun ByteArray.decodeBase64(): ByteArray { fun ByteArray.decodeBase64(): ByteArray {
val table = intArrayOf(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, val table = intArrayOf(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1,
-1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
)
val output = ByteArrayOutputStream() val output = ByteArrayOutputStream()
var position = 0 var position = 0

View File

@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
data class PopularAnimeResponse( data class PopularAnimeResponse(
val Shows: List<String>, val Shows: List<String>,
val current_page: Int, val current_page: Int,
val last_page: Int val last_page: Int,
) )
@Serializable @Serializable
@ -18,20 +18,23 @@ data class AnimeItem(
val anime_score: String, val anime_score: String,
val anime_slug: String, val anime_slug: String,
val anime_type: String, val anime_type: String,
val info_src: String val info_src: String,
) )
@Serializable @Serializable
data class ShowItem( data class ShowItem(
val EPS: List<EPS>, val EPS: List<EPS>,
val show: List<Show> val show: List<Show>,
) )
@Serializable @Serializable
data class EPS( data class EPS(
val episode_name: String, val episode_name: String,
val episode_number: Int, val episode_number: Int,
@SerialName("info-src") @SerialName("info-src")
val `info-src`: String val `info-src`: String,
) )
@Serializable @Serializable
data class Show( data class Show(
val anime_cover_image_url: String, val anime_cover_image_url: String,
@ -45,13 +48,15 @@ data class Show(
val anime_status: String, val anime_status: String,
val anime_type: String, val anime_type: String,
val show_episode_count: Int, val show_episode_count: Int,
val wallpapaer: String val wallpapaer: String,
) )
@Serializable @Serializable
data class Episode( data class Episode(
val ep_info: List<EpInfo> val ep_info: List<EpInfo>,
) )
@Serializable @Serializable
data class EpInfo( data class EpInfo(
val stream_servers: List<String> val stream_servers: List<String>,
) )

View File

@ -169,7 +169,7 @@ class Tuktukcinema : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
StreamTapeExtractor(client).videoFromUrl(url)?.let(::listOf) StreamTapeExtractor(client).videoFromUrl(url)?.let(::listOf)
} }
url.contains("upstream", ignoreCase = true) -> { url.contains("upstream", ignoreCase = true) -> {
UpStreamExtractor(client).videoFromUrl(url.replace("//","//www.")) UpStreamExtractor(client).videoFromUrl(url.replace("//", "//www."))
} }
else -> null else -> null
} ?: emptyList() } ?: emptyList()
@ -338,6 +338,5 @@ class Tuktukcinema : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
companion object { companion object {
private val VIDBOM_REGEX = Regex("(?:v[aie]d[bp][aoe]?m|myvii?d|govad|segavid|v[aei]{1,2}dshar[er]?)\\.(?:com|net|org|xyz)(?::\\d+)?/(?:embed[/-])?([A-Za-z0-9]+).html") private val VIDBOM_REGEX = Regex("(?:v[aie]d[bp][aoe]?m|myvii?d|govad|segavid|v[aei]{1,2}dshar[er]?)\\.(?:com|net|org|xyz)(?::\\d+)?/(?:embed[/-])?([A-Za-z0-9]+).html")
private val DOOD_REGEX = Regex("(do*d(?:stream)?\\.(?:com?|watch|to|s[ho]|cx|la|w[sf]|pm|re|yt|stream))/[de]/([0-9a-zA-Z]+)") private val DOOD_REGEX = Regex("(do*d(?:stream)?\\.(?:com?|watch|to|s[ho]|cx|la|w[sf]|pm|re|yt|stream))/[de]/([0-9a-zA-Z]+)")
} }
} }

View File

@ -10,7 +10,7 @@ class UpStreamExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String): List<Video> { fun videoFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup() val doc = client.newCall(GET(url)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(sources)")?.data() ?: return emptyList() val script = doc.selectFirst("script:containsData(sources)")?.data() ?: return emptyList()
val scriptData = if("eval" in script) JsUnpacker.unpackAndCombine(script)!! else script val scriptData = if ("eval" in script) JsUnpacker.unpackAndCombine(script)!! else script
val m3u8 = Regex("sources:\\s*\\[\\{\\s*\\t*file:\\s*[\"']([^\"']+)").find(scriptData)!!.groupValues[1] val m3u8 = Regex("sources:\\s*\\[\\{\\s*\\t*file:\\s*[\"']([^\"']+)").find(scriptData)!!.groupValues[1]
return Video(m3u8, "Upstream", m3u8).let(::listOf) return Video(m3u8, "Upstream", m3u8).let(::listOf)
} }

View File

@ -11,6 +11,7 @@ ext {
} }
dependencies { dependencies {
implementation(project(':lib-gdriveplayer-extractor'))
implementation(project(':lib-okru-extractor')) implementation(project(':lib-okru-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1" implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
} }

View File

@ -5,7 +5,6 @@ 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.en.myanime.extractors.DailymotionExtractor import eu.kanade.tachiyomi.animeextension.en.myanime.extractors.DailymotionExtractor
import eu.kanade.tachiyomi.animeextension.en.myanime.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.en.myanime.extractors.YouTubeExtractor import eu.kanade.tachiyomi.animeextension.en.myanime.extractors.YouTubeExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter import eu.kanade.tachiyomi.animesource.model.AnimeFilter
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@ -216,7 +216,8 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
YouTubeExtractor(client).videosFromUrl(url, "YouTube - ") YouTubeExtractor(client).videosFromUrl(url, "YouTube - ")
} }
url.contains("gdriveplayer") -> { url.contains("gdriveplayer") -> {
GdrivePlayerExtractor(client).videosFromUrl(url, name = "Gdriveplayer") val newHeaders = headersBuilder().add("Referer", baseUrl).build()
GdrivePlayerExtractor(client).videosFromUrl(url, name = "Gdriveplayer", headers = newHeaders)
} }
else -> null else -> null
} }

View File

@ -1,155 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.myanime.extractors
import android.util.Base64
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
import org.jsoup.Jsoup
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, name: String): List<Video> {
val headers = Headers.headersOf(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Host",
"gdriveplayer.to",
"Referer",
"https://myanime.live/",
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
)
val body = client.newCall(GET(url.replace(".us", ".to"), headers = headers)).execute()
.body.string()
val subtitleUrl = Jsoup.parse(body).selectFirst("div:contains(\\.srt)")
val subtitleList = mutableListOf<Track>()
if (subtitleUrl != null) {
try {
subtitleList.add(
Track(
"https://gdriveplayer.to/?subtitle=" + subtitleUrl.text(),
"Subtitles",
),
)
} catch (a: Exception) { }
}
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 - $name"
val videoUrl = "https:" + it.groupValues[1] + "&res=$qualityStr"
try {
Video(videoUrl, quality, videoUrl, subtitleTracks = subtitleList)
} catch (a: Exception) {
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+)")
}
}

View File

@ -11,6 +11,7 @@ ext {
} }
dependencies { dependencies {
implementation(project(':lib-gdriveplayer-extractor'))
implementation(project(':lib-yourupload-extractor')) implementation(project(':lib-yourupload-extractor'))
implementation(project(':lib-okru-extractor')) implementation(project(':lib-okru-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1" implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"

View File

@ -6,7 +6,6 @@ import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.id.neonime.extractors.BloggerExtractor import eu.kanade.tachiyomi.animeextension.id.neonime.extractors.BloggerExtractor
import eu.kanade.tachiyomi.animeextension.id.neonime.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.id.neonime.extractors.LinkBoxExtractor import eu.kanade.tachiyomi.animeextension.id.neonime.extractors.LinkBoxExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -15,6 +14,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -254,7 +254,8 @@ class NeoNime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
when { when {
iframeUrl.contains("gdriveplayer.to") -> { iframeUrl.contains("gdriveplayer.to") -> {
videoList.addAll(GdrivePlayerExtractor(client).videosFromUrl(iframeUrl, it.text())) val newHeaders = headersBuilder().add("Referer", baseUrl).build()
videoList.addAll(GdrivePlayerExtractor(client).videosFromUrl(iframeUrl, it.text(), headers = newHeaders))
} }
} }
} }

View File

@ -1,138 +0,0 @@
package eu.kanade.tachiyomi.animeextension.id.neonime.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.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
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, name: String): List<Video> {
val headers = Headers.headersOf(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Host",
"gdriveplayer.to",
"Referer",
"https://neonime.fun/",
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
)
val body = client.newCall(GET(url.replace(".me", ".to"), headers = headers)).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 - $name"
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+)")
}
}