refactor: Make extensions use GdrivePlayer lib (#1909)
This commit is contained in:
@ -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'))
|
||||||
|
@ -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()
|
||||||
@ -162,7 +162,7 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
val finalUrl = VIDBOM_REGEX.find(url)!!.groupValues[0]
|
val finalUrl = VIDBOM_REGEX.find(url)!!.groupValues[0]
|
||||||
VidBomExtractor(client).videosFromUrl("https://www.$finalUrl.html")
|
VidBomExtractor(client).videosFromUrl("https://www.$finalUrl.html")
|
||||||
}
|
}
|
||||||
STREAMWISH_REGEX.containsMatchIn(url) -> {
|
STREAMWISH_REGEX.containsMatchIn(url) -> {
|
||||||
val headers = headers.newBuilder()
|
val headers = headers.newBuilder()
|
||||||
.set("Referer", url)
|
.set("Referer", url)
|
||||||
.set("Accept-Encoding", "gzip, deflate, br")
|
.set("Accept-Encoding", "gzip, deflate, br")
|
||||||
@ -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"
|
||||||
|
|
||||||
@ -380,7 +381,7 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
Type("Special", "special1"),
|
Type("Special", "special1"),
|
||||||
Type("TV", "tv2"),
|
Type("TV", "tv2"),
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getStatusList() = listOf(
|
private fun getStatusList() = listOf(
|
||||||
Status("أختر", ""),
|
Status("أختر", ""),
|
||||||
@ -388,7 +389,7 @@ class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
Status("مكتمل", "complete"),
|
Status("مكتمل", "complete"),
|
||||||
Status("يعرض الان", "%d9%8a%d8%b9%d8%b1%d8%b6-%d8%a7%d9%84%d8%a7%d9%86-1"),
|
Status("يعرض الان", "%d9%8a%d8%b9%d8%b1%d8%b6-%d8%a7%d9%84%d8%a7%d9%86-1"),
|
||||||
|
|
||||||
)
|
)
|
||||||
companion object {
|
companion object {
|
||||||
private val VIDBOM_REGEX = Regex("(?:v[aie]d[bp][aoe]?m|myvii?d|segavid|v[aei]{1,2}dshar[er]?)\\.(?:com|net|org|xyz)(?::\\d+)?/(?:embed[/-])?([A-Za-z0-9]+)")
|
private val VIDBOM_REGEX = Regex("(?:v[aie]d[bp][aoe]?m|myvii?d|segavid|v[aei]{1,2}dshar[er]?)\\.(?:com|net|org|xyz)(?::\\d+)?/(?:embed[/-])?([A-Za-z0-9]+)")
|
||||||
private val STREAMSB_REGEX = Regex("(?:view|watch|embed(?:tv)?|tube|player|cloudemb|japopav|javplaya|p1ayerjavseen|gomovizplay|stream(?:ovies)?|vidmovie|javside|aintahalu|finaltayibin|yahlusubh|taeyabathuna|)?s{0,2}b?(?:embed\\d?|play\\d?|video|fast|full|streams{0,3}|the|speed|l?anh|tvmshow|longvu|arslanrocky|chill|rity|hight|brisk|face|lvturbo|net|one|asian|ani|rapid|sonic|lona)?\\.(?:com|net|org|one|tv|xyz|fun|pro|sbs)")
|
private val STREAMSB_REGEX = Regex("(?:view|watch|embed(?:tv)?|tube|player|cloudemb|japopav|javplaya|p1ayerjavseen|gomovizplay|stream(?:ovies)?|vidmovie|javside|aintahalu|finaltayibin|yahlusubh|taeyabathuna|)?s{0,2}b?(?:embed\\d?|play\\d?|video|fast|full|streams{0,3}|the|speed|l?anh|tvmshow|longvu|arslanrocky|chill|rity|hight|brisk|face|lvturbo|net|one|asian|ani|rapid|sonic|lona)?\\.(?:com|net|org|one|tv|xyz|fun|pro|sbs)")
|
||||||
|
@ -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+)")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
@ -19,7 +19,7 @@ class StreamWishExtractor(private val client: OkHttpClient) {
|
|||||||
val qRegex = Regex("\".*?\":\\s*\"(.*?)\"").findAll(qualities)
|
val qRegex = Regex("\".*?\":\\s*\"(.*?)\"").findAll(qualities)
|
||||||
return qRegex.mapIndexed { index, matchResult ->
|
return qRegex.mapIndexed { index, matchResult ->
|
||||||
val src = streamLink.groupValues[1] + "_" + streamQuality[index] + "/index-v1-a1" + streamLink.groupValues[3]
|
val src = streamLink.groupValues[1] + "_" + streamQuality[index] + "/index-v1-a1" + streamLink.groupValues[3]
|
||||||
val quality = "Mirror: " + matchResult.groupValues[1]
|
val quality = "Mirror: " + matchResult.groupValues[1]
|
||||||
Video(src, quality, src, headers)
|
Video(src, quality, src, headers)
|
||||||
}.toList()
|
}.toList()
|
||||||
}
|
}
|
||||||
|
@ -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'))
|
||||||
|
@ -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") -> {
|
||||||
|
@ -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+)")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 ===============================
|
||||||
|
@ -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
|
||||||
|
@ -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>,
|
||||||
)
|
)
|
||||||
|
@ -163,13 +163,13 @@ class Tuktukcinema : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
DoodExtractor(client).videoFromUrl("https://www.$finalUrl", "Dood mirror", false)?.let(::listOf)
|
DoodExtractor(client).videoFromUrl("https://www.$finalUrl", "Dood mirror", false)?.let(::listOf)
|
||||||
}
|
}
|
||||||
url.contains("uqload") -> {
|
url.contains("uqload") -> {
|
||||||
UQLoadExtractor(client).videoFromUrl(url, "Uqload mirror")?.let(::listOf)
|
UQLoadExtractor(client).videoFromUrl(url, "Uqload mirror")?.let(::listOf)
|
||||||
}
|
}
|
||||||
url.contains("tape") -> {
|
url.contains("tape") -> {
|
||||||
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]+)")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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+)")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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+)")
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user