Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View 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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -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",
)
}
}

View file

@ -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)
}
}
}

View file

@ -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"