forked from AlmightyHak/extensions-source
Initial commit
This commit is contained in:
commit
98ed7e8839
2263 changed files with 108711 additions and 0 deletions
17
src/all/animexin/build.gradle
Normal file
17
src/all/animexin/build.gradle
Normal file
|
@ -0,0 +1,17 @@
|
|||
ext {
|
||||
extName = 'AnimeXin'
|
||||
extClass = '.AnimeXin'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animexin.vip'
|
||||
overrideVersionCode = 8
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:dailymotion-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:gdriveplayer-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/all/animexin/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/animexin/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
BIN
src/all/animexin/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/animexin/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
BIN
src/all/animexin/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/animexin/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
BIN
src/all/animexin/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/animexin/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
src/all/animexin/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/animexin/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,91 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.animexin
|
||||
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.VidstreamingExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.YouTubeExtractor
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.dailymotionextractor.DailymotionExtractor
|
||||
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.multisrc.animestream.AnimeStream
|
||||
|
||||
class AnimeXin : AnimeStream(
|
||||
"all",
|
||||
"AnimeXin",
|
||||
"https://animexin.vip",
|
||||
) {
|
||||
override val id = 4620219025406449669
|
||||
|
||||
// ============================ Video Links =============================
|
||||
private val dailymotionExtractor by lazy { DailymotionExtractor(client, headers) }
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val vidstreamingExtractor by lazy { VidstreamingExtractor(client) }
|
||||
private val youTubeExtractor by lazy { YouTubeExtractor(client) }
|
||||
|
||||
override fun getVideoList(url: String, name: String): List<Video> {
|
||||
val prefix = "$name - "
|
||||
return when {
|
||||
url.contains("ok.ru") -> okruExtractor.videosFromUrl(url, prefix)
|
||||
url.contains("dailymotion") -> dailymotionExtractor.videosFromUrl(url, prefix)
|
||||
url.contains("https://dood") -> doodExtractor.videosFromUrl(url, name)
|
||||
url.contains("gdriveplayer") -> {
|
||||
val gdriveHeaders = headersBuilder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.add("Referer", "$baseUrl/")
|
||||
.build()
|
||||
gdrivePlayerExtractor.videosFromUrl(url, name, gdriveHeaders)
|
||||
}
|
||||
url.contains("youtube.com") -> youTubeExtractor.videosFromUrl(url, prefix)
|
||||
url.contains("vidstreaming") -> vidstreamingExtractor.videosFromUrl(url, prefix)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
super.setupPreferenceScreen(screen) // Quality preferences
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_LANG_KEY
|
||||
title = PREF_LANG_TITLE
|
||||
entries = PREF_LANG_VALUES
|
||||
entryValues = PREF_LANG_VALUES
|
||||
setDefaultValue(PREF_LANG_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
|
||||
val language = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
|
||||
return sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.contains(language, true) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_LANG_KEY = "preferred_language"
|
||||
private const val PREF_LANG_TITLE = "Preferred Video Language"
|
||||
private const val PREF_LANG_DEFAULT = "All Sub"
|
||||
private val PREF_LANG_VALUES = arrayOf(
|
||||
"All Sub", "Arabic", "English", "German", "Indonesia", "Italian",
|
||||
"Polish", "Portuguese", "Spanish", "Thai", "Turkish",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.Exception
|
||||
import java.util.Locale
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@ExperimentalSerializationApi
|
||||
class VidstreamingExtractor(private val client: OkHttpClient) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(serverUrl: String, prefix: String): List<Video> {
|
||||
try {
|
||||
val document = client.newCall(GET(serverUrl)).execute().asJsoup()
|
||||
val iv = document.select("div.wrapper")
|
||||
.attr("class").substringAfter("container-")
|
||||
.filter { it.isDigit() }.toByteArray()
|
||||
val secretKey = document.select("body[class]")
|
||||
.attr("class").substringAfter("container-")
|
||||
.filter { it.isDigit() }.toByteArray()
|
||||
val decryptionKey = document.select("div.videocontent")
|
||||
.attr("class").substringAfter("videocontent-")
|
||||
.filter { it.isDigit() }.toByteArray()
|
||||
val encryptAjaxParams = cryptoHandler(
|
||||
document.select("script[data-value]")
|
||||
.attr("data-value"),
|
||||
iv,
|
||||
secretKey,
|
||||
false,
|
||||
).substringAfter("&")
|
||||
|
||||
val httpUrl = serverUrl.toHttpUrl()
|
||||
val host = "https://" + httpUrl.host + "/"
|
||||
val id = httpUrl.queryParameter("id") ?: throw Exception("error getting id")
|
||||
val encryptedId = cryptoHandler(id, iv, secretKey)
|
||||
val token = httpUrl.queryParameter("token")
|
||||
val qualitySuffix = if (token != null) " (Vid-mp4 - Gogostream)" else " (Vid-mp4 - Vidstreaming)"
|
||||
|
||||
val jsonResponse = client.newCall(
|
||||
GET(
|
||||
"${host}encrypt-ajax.php?id=$encryptedId&$encryptAjaxParams&alias=$id",
|
||||
Headers.headersOf(
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
),
|
||||
),
|
||||
).execute().body.string()
|
||||
val data = json.decodeFromString<JsonObject>(jsonResponse)["data"]!!.jsonPrimitive.content
|
||||
val decryptedData = cryptoHandler(data, iv, decryptionKey, false)
|
||||
val videoList = mutableListOf<Video>()
|
||||
val autoList = mutableListOf<Video>()
|
||||
val array = json.decodeFromString<JsonObject>(decryptedData)["source"]!!.jsonArray
|
||||
if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") {
|
||||
val fileURL = array[0].jsonObject["file"].toString().trim('"')
|
||||
val masterPlaylist = client.newCall(GET(fileURL)).execute().body.string()
|
||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
|
||||
.split("#EXT-X-STREAM-INF:").forEach {
|
||||
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",").substringBefore("\n") + "p"
|
||||
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl = fileURL.substringBeforeLast("/") + "/$videoUrl"
|
||||
}
|
||||
videoList.add(Video(videoUrl, prefix + quality + qualitySuffix, videoUrl))
|
||||
}
|
||||
} else {
|
||||
array.forEach {
|
||||
val label = it.jsonObject["label"].toString().lowercase(Locale.ROOT)
|
||||
.trim('"').replace(" ", "")
|
||||
val fileURL = it.jsonObject["file"].toString().trim('"')
|
||||
val videoHeaders = Headers.headersOf("Referer", serverUrl)
|
||||
if (label == "auto") {
|
||||
autoList.add(
|
||||
Video(
|
||||
fileURL,
|
||||
label + qualitySuffix,
|
||||
fileURL,
|
||||
headers = videoHeaders,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
videoList.add(Video(fileURL, label + qualitySuffix, fileURL, headers = videoHeaders))
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList.sortedByDescending {
|
||||
it.quality.substringBefore(qualitySuffix).substringBefore("p").toIntOrNull() ?: -1
|
||||
} + autoList
|
||||
} catch (e: Exception) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cryptoHandler(
|
||||
string: String,
|
||||
iv: ByteArray,
|
||||
secretKeyString: ByteArray,
|
||||
encrypt: Boolean = true,
|
||||
): String {
|
||||
val ivParameterSpec = IvParameterSpec(iv)
|
||||
val secretKey = SecretKeySpec(secretKeyString, "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
return if (!encrypt) {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
|
||||
String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT)))
|
||||
} else {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
|
||||
Base64.encodeToString(cipher.doFinal(string.toByteArray()), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.math.abs
|
||||
|
||||
class YouTubeExtractor(private val client: OkHttpClient) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||
// Ported from https://github.com/dermasmid/scrapetube/blob/master/scrapetube/scrapetube.py
|
||||
// TODO: Make code prettier
|
||||
// GET KEY
|
||||
|
||||
val videoId = url.substringAfter("/embed/")
|
||||
|
||||
val document = client.newCall(GET(url.replace("/embed/", "/watch?v=")))
|
||||
.execute()
|
||||
.asJsoup()
|
||||
|
||||
val ytcfg = document.selectFirst("script:containsData(window.ytcfg=window.ytcfg)")
|
||||
?.data() ?: run {
|
||||
Log.e("YouTubeExtractor", "Failed while trying to fetch the api key >:(")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val clientName = ytcfg.substringAfter("INNERTUBE_CONTEXT_CLIENT_NAME\":", "")
|
||||
.substringBefore(",", "").ifEmpty { "5" }
|
||||
|
||||
val apiKey = ytcfg
|
||||
.substringAfter("innertubeApiKey\":\"", "")
|
||||
.substringBefore('"')
|
||||
|
||||
val playerUrl = "$YOUTUBE_URL/youtubei/v1/player?key=$apiKey&prettyPrint=false"
|
||||
|
||||
val body = """
|
||||
{
|
||||
"context":{
|
||||
"client":{
|
||||
"clientName":"IOS",
|
||||
"clientVersion":"17.33.2",
|
||||
"deviceModel": "iPhone14,3",
|
||||
"userAgent": "com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)",
|
||||
"hl": "en",
|
||||
"timeZone": "UTC",
|
||||
"utcOffsetMinutes": 0
|
||||
}
|
||||
},
|
||||
"videoId":"$videoId",
|
||||
"playbackContext":{
|
||||
"contentPlaybackContext":{
|
||||
"html5Preference":"HTML5_PREF_WANTS"
|
||||
}
|
||||
},
|
||||
"contentCheckOk":true,
|
||||
"racyCheckOk":true
|
||||
}
|
||||
""".trimIndent().toRequestBody("application/json".toMediaType())
|
||||
|
||||
val headers = Headers.Builder().apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
add("Origin", YOUTUBE_URL)
|
||||
add("User-Agent", "com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)")
|
||||
add("X-Youtube-Client-Name", clientName)
|
||||
add("X-Youtube-Client-Version", "17.33.2")
|
||||
}.build()
|
||||
|
||||
val ytResponse = client.newCall(POST(playerUrl, headers, body)).execute()
|
||||
.let { json.decodeFromString<YoutubeResponse>(it.body.string()) }
|
||||
|
||||
val formats = ytResponse.streamingData.adaptiveFormats
|
||||
|
||||
// Get Audio
|
||||
val audioTracks = formats.filter { it.mimeType.startsWith("audio/webm") }
|
||||
.map { Track(it.url, it.audioQuality!! + " (${formatBits(it.averageBitrate!!)}ps)") }
|
||||
|
||||
// Get Subtitles
|
||||
val subs = ytResponse.captions?.renderer?.captionTracks?.map {
|
||||
Track(it.baseUrl, it.label)
|
||||
} ?: emptyList()
|
||||
|
||||
// Get videos, finally
|
||||
return formats.filter { it.mimeType.startsWith("video/mp4") }.map {
|
||||
val codecs = it.mimeType.substringAfter("codecs=\"").substringBefore("\"")
|
||||
Video(
|
||||
it.url,
|
||||
prefix + it.qualityLabel.orEmpty() + " ($codecs)",
|
||||
it.url,
|
||||
subtitleTracks = subs,
|
||||
audioTracks = audioTracks,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun formatBits(size: Long): String {
|
||||
var bits = abs(size)
|
||||
if (bits < 1000) {
|
||||
return "${bits}b"
|
||||
}
|
||||
val iterator = "kMGTPE".iterator()
|
||||
var currentChar = iterator.next()
|
||||
while (bits >= 999950 && iterator.hasNext()) {
|
||||
bits /= 1000
|
||||
currentChar = iterator.next()
|
||||
}
|
||||
return "%.0f%cb".format(bits / 1000.0, currentChar)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class YoutubeResponse(
|
||||
val streamingData: AdaptiveDto,
|
||||
val captions: CaptionsDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AdaptiveDto(val adaptiveFormats: List<TrackDto>)
|
||||
|
||||
@Serializable
|
||||
data class TrackDto(
|
||||
val mimeType: String,
|
||||
val url: String,
|
||||
val averageBitrate: Long? = null,
|
||||
val qualityLabel: String? = null,
|
||||
val audioQuality: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CaptionsDto(
|
||||
@SerialName("playerCaptionsTracklistRenderer")
|
||||
val renderer: CaptionsRendererDto,
|
||||
) {
|
||||
@Serializable
|
||||
data class CaptionsRendererDto(val captionTracks: List<CaptionItem>)
|
||||
|
||||
@Serializable
|
||||
data class CaptionItem(val baseUrl: String, val name: NameDto) {
|
||||
@Serializable
|
||||
data class NameDto(val runs: List<GodDamnitYoutube>)
|
||||
|
||||
@Serializable
|
||||
data class GodDamnitYoutube(val text: String)
|
||||
|
||||
val label by lazy { name.runs.first().text }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val YOUTUBE_URL = "https://www.youtube.com"
|
Loading…
Add table
Add a link
Reference in a new issue