feat(multisrc): New theme: AnimeStream (#1653)
@ -0,0 +1,6 @@
|
||||
dependencies {
|
||||
implementation(project(':lib-okru-extractor'))
|
||||
implementation(project(':lib-gdriveplayer-extractor'))
|
||||
implementation(project(':lib-streamsb-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 21 KiB |
110
multisrc/overrides/animestream/animexin/src/AnimeXin.kt
Normal file
@ -0,0 +1,110 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.animexin
|
||||
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DailymotionExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.DoodExtractor
|
||||
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.gdriveplayerextractor.GdrivePlayerExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||
|
||||
class AnimeXin : AnimeStream(
|
||||
"all",
|
||||
"AnimeXin",
|
||||
"https://animexin.vip",
|
||||
) {
|
||||
override val id = 4620219025406449669
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun getVideoList(url: String, name: String): List<Video> {
|
||||
val streamSbDomains = listOf(
|
||||
"sbhight", "sbrity", "sbembed.com", "sbembed1.com", "sbplay.org",
|
||||
"sbvideo.net", "streamsb.net", "sbplay.one", "cloudemb.com",
|
||||
"playersb.com", "tubesb.com", "sbplay1.com", "embedsb.com",
|
||||
"watchsb.com", "sbplay2.com", "japopav.tv", "viewsb.com",
|
||||
"sbfast", "sbfull.com", "javplaya.com", "ssbstream.net",
|
||||
"p1ayerjavseen.com", "sbthe.com", "vidmovie.xyz", "sbspeed.com",
|
||||
"streamsss.net", "sblanh.com", "tvmshow.com", "sbanh.com",
|
||||
"streamovies.xyz",
|
||||
)
|
||||
val prefix = "$name - "
|
||||
return when {
|
||||
url.contains("ok.ru") -> {
|
||||
OkruExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
}
|
||||
streamSbDomains.any { it in url } -> {
|
||||
StreamSBExtractor(client).videosFromUrl(url, headers, prefix = prefix)
|
||||
}
|
||||
url.contains("dailymotion") -> {
|
||||
DailymotionExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
}
|
||||
url.contains("https://dood") -> {
|
||||
DoodExtractor(client).videosFromUrl(url, quality = 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("Host", "gdriveplayer.to")
|
||||
.add("Referer", "$baseUrl/")
|
||||
.build()
|
||||
GdrivePlayerExtractor(client).videosFromUrl(url, name = name, headers = gdriveHeaders)
|
||||
}
|
||||
url.contains("youtube.com") -> {
|
||||
YouTubeExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
}
|
||||
url.contains("vidstreaming") -> {
|
||||
VidstreamingExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
super.setupPreferenceScreen(screen) // Quality preferences
|
||||
val videoLangPref = 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()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(videoLangPref)
|
||||
}
|
||||
|
||||
// ============================= 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,91 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@Serializable
|
||||
data class DailyQuality(
|
||||
val qualities: Auto,
|
||||
val subtitles: Subtitle? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class Auto(
|
||||
val auto: List<Item>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Item(
|
||||
val type: String,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Subtitle(
|
||||
val data: Map<String, SubtitleObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class SubtitleObject(
|
||||
val label: String,
|
||||
val urls: List<String>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DailymotionExtractor(private val client: OkHttpClient) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val htmlString = client.newCall(GET(url)).execute().body.string()
|
||||
|
||||
val internalData = htmlString.substringAfter("\"dmInternalData\":").substringBefore("</script>")
|
||||
val ts = internalData.substringAfter("\"ts\":").substringBefore(",")
|
||||
val v1st = internalData.substringAfter("\"v1st\":\"").substringBefore("\",")
|
||||
|
||||
val jsonUrl = "https://www.dailymotion.com/player/metadata/video/${url.toHttpUrl().encodedPath}?locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
|
||||
val parsed = json.decodeFromString<DailyQuality>(
|
||||
client.newCall(GET(jsonUrl))
|
||||
.execute().body.string(),
|
||||
)
|
||||
|
||||
val subtitleList = mutableListOf<Track>()
|
||||
if (parsed.subtitles != null) {
|
||||
try {
|
||||
subtitleList.addAll(
|
||||
parsed.subtitles.data.map { k ->
|
||||
Track(
|
||||
k.value.urls.first(),
|
||||
k.value.label,
|
||||
)
|
||||
},
|
||||
)
|
||||
} catch (a: Exception) { }
|
||||
}
|
||||
|
||||
val masterUrl = parsed.qualities.auto.first().url
|
||||
|
||||
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",NAME") + "p"
|
||||
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
|
||||
try {
|
||||
videoList.add(Video(videoUrl, prefix + quality, videoUrl, subtitleTracks = subtitleList))
|
||||
} catch (a: Exception) {
|
||||
videoList.add(Video(videoUrl, prefix + quality, videoUrl))
|
||||
}
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class DoodExtractor(private val client: OkHttpClient) {
|
||||
|
||||
fun videoFromUrl(
|
||||
url: String,
|
||||
quality: String? = null,
|
||||
redirect: Boolean = true,
|
||||
): Video? {
|
||||
val newQuality = quality ?: "Doodstream" + if (redirect) " mirror" else ""
|
||||
|
||||
return try {
|
||||
val response = client.newCall(GET(url)).execute()
|
||||
val newUrl = if (redirect) response.request.url.toString() else url
|
||||
|
||||
val doodTld = newUrl.substringAfter("https://dood.").substringBefore("/")
|
||||
val content = response.body.string()
|
||||
|
||||
val subtitleList = mutableListOf<Track>()
|
||||
val subtitleRegex = """src:'//(srt[^']*?)',\s*label:'([^']*?)'""".toRegex()
|
||||
try {
|
||||
subtitleList.addAll(
|
||||
subtitleRegex.findAll(content).map {
|
||||
Track(
|
||||
"https://" + it.groupValues[1],
|
||||
it.groupValues[2],
|
||||
)
|
||||
},
|
||||
)
|
||||
} catch (a: Exception) { }
|
||||
|
||||
if (!content.contains("'/pass_md5/")) return null
|
||||
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
|
||||
val token = md5.substringAfterLast("/")
|
||||
val randomString = getRandomString()
|
||||
val expiry = System.currentTimeMillis()
|
||||
val videoUrlStart = client.newCall(
|
||||
GET(
|
||||
"https://dood.$doodTld/pass_md5/$md5",
|
||||
Headers.headersOf("referer", newUrl),
|
||||
),
|
||||
).execute().body.string()
|
||||
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
|
||||
try {
|
||||
Video(newUrl, newQuality, videoUrl, headers = doodHeaders(doodTld), subtitleTracks = subtitleList)
|
||||
} catch (a: Exception) {
|
||||
Video(newUrl, newQuality, videoUrl, headers = doodHeaders(doodTld))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun videosFromUrl(
|
||||
url: String,
|
||||
quality: String? = null,
|
||||
redirect: Boolean = true,
|
||||
): List<Video> {
|
||||
val video = videoFromUrl(url, quality, redirect)
|
||||
return video?.let { listOf(it) } ?: emptyList<Video>()
|
||||
}
|
||||
|
||||
private fun getRandomString(length: Int = 10): String {
|
||||
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||
return (1..length)
|
||||
.map { allowedChars.random() }
|
||||
.joinToString("")
|
||||
}
|
||||
|
||||
private fun doodHeaders(tld: String) = Headers.Builder().apply {
|
||||
add("User-Agent", "Aniyomi")
|
||||
add("Referer", "https://dood.$tld/")
|
||||
}.build()
|
||||
}
|
@ -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,182 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.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 kotlinx.serialization.json.long
|
||||
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
|
||||
|
||||
var ytcfgString = ""
|
||||
val videoId = url.substringAfter("/embed/")
|
||||
|
||||
val document = client.newCall(
|
||||
GET(url.replace("/embed/", "/watch?v=")),
|
||||
).execute().asJsoup()
|
||||
|
||||
for (element in document.select("script")) {
|
||||
val scriptData = element.data()
|
||||
if (scriptData.startsWith("(function() {window.ytplayer={};")) {
|
||||
ytcfgString = scriptData
|
||||
}
|
||||
}
|
||||
|
||||
val apiKey = getKey(ytcfgString, "innertubeApiKey")
|
||||
|
||||
val playerUrl = "https://www.youtube.com/youtubei/v1/player?key=$apiKey&prettyPrint=false"
|
||||
|
||||
val body = """
|
||||
{
|
||||
"context":{
|
||||
"client":{
|
||||
"clientName":"ANDROID",
|
||||
"clientVersion":"17.31.35",
|
||||
"androidSdkVersion":30,
|
||||
"userAgent":"com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip",
|
||||
"hl":"en",
|
||||
"timeZone":"UTC",
|
||||
"utcOffsetMinutes":0
|
||||
}
|
||||
},
|
||||
"videoId":"$videoId",
|
||||
"params":"8AEB",
|
||||
"playbackContext":{
|
||||
"contentPlaybackContext":{
|
||||
"html5Preference":"HTML5_PREF_WANTS"
|
||||
}
|
||||
},
|
||||
"contentCheckOk":true,
|
||||
"racyCheckOk":true
|
||||
}
|
||||
""".trimIndent().toRequestBody("application/json".toMediaType())
|
||||
|
||||
val headers = Headers.headersOf(
|
||||
"X-YouTube-Client-Name", "3",
|
||||
"X-YouTube-Client-Version", "17.31.35",
|
||||
"Origin", "https://www.youtube.com",
|
||||
"User-Agent", "com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip",
|
||||
"content-type", "application/json",
|
||||
)
|
||||
|
||||
val postResponse = client.newCall(
|
||||
POST(playerUrl, headers = headers, body = body),
|
||||
).execute()
|
||||
|
||||
val responseObject = json.decodeFromString<JsonObject>(postResponse.body.string())
|
||||
val videoList = mutableListOf<Video>()
|
||||
|
||||
val formats = responseObject["streamingData"]!!
|
||||
.jsonObject["adaptiveFormats"]!!
|
||||
.jsonArray
|
||||
|
||||
val audioTracks = mutableListOf<Track>()
|
||||
val subtitleTracks = mutableListOf<Track>()
|
||||
|
||||
// Get Audio
|
||||
for (format in formats) {
|
||||
if (format.jsonObject["mimeType"]!!.jsonPrimitive.content.startsWith("audio/webm")) {
|
||||
try {
|
||||
audioTracks.add(
|
||||
Track(
|
||||
format.jsonObject["url"]!!.jsonPrimitive.content,
|
||||
format.jsonObject["audioQuality"]!!.jsonPrimitive.content +
|
||||
" (${formatBits(format.jsonObject["averageBitrate"]!!.jsonPrimitive.long)}ps)",
|
||||
),
|
||||
)
|
||||
} catch (a: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
// Get Subtitles
|
||||
if (responseObject.containsKey("captions")) {
|
||||
val captionTracks = responseObject["captions"]!!
|
||||
.jsonObject["playerCaptionsTracklistRenderer"]!!
|
||||
.jsonObject["captionTracks"]!!
|
||||
.jsonArray
|
||||
|
||||
for (caption in captionTracks) {
|
||||
val captionJson = caption.jsonObject
|
||||
try {
|
||||
subtitleTracks.add(
|
||||
Track(
|
||||
// TODO: Would replacing srv3 with vtt work for every video?
|
||||
captionJson["baseUrl"]!!.jsonPrimitive.content.replace("srv3", "vtt"),
|
||||
captionJson["name"]!!.jsonObject["runs"]!!.jsonArray[0].jsonObject["text"]!!.jsonPrimitive.content,
|
||||
),
|
||||
)
|
||||
} catch (a: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
// List formats
|
||||
for (format in formats) {
|
||||
val mimeType = format.jsonObject["mimeType"]!!.jsonPrimitive.content
|
||||
if (mimeType.startsWith("video/mp4")) {
|
||||
videoList.add(
|
||||
try {
|
||||
Video(
|
||||
format.jsonObject["url"]!!.jsonPrimitive.content,
|
||||
prefix + format.jsonObject["qualityLabel"]!!.jsonPrimitive.content +
|
||||
" (${mimeType.substringAfter("codecs=\"").substringBefore("\"")})",
|
||||
format.jsonObject["url"]!!.jsonPrimitive.content,
|
||||
audioTracks = audioTracks,
|
||||
subtitleTracks = subtitleTracks,
|
||||
)
|
||||
} catch (a: Exception) {
|
||||
Video(
|
||||
format.jsonObject["url"]!!.jsonPrimitive.content,
|
||||
prefix + format.jsonObject["qualityLabel"]!!.jsonPrimitive.content +
|
||||
" (${mimeType.substringAfter("codecs=\"").substringBefore("\"")})",
|
||||
format.jsonObject["url"]!!.jsonPrimitive.content,
|
||||
)
|
||||
},
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
fun getKey(string: String, key: String): String {
|
||||
var pattern = Regex("\"$key\":\"(.*?)\"")
|
||||
return pattern.find(string)?.groupValues?.get(1) ?: ""
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
23
multisrc/overrides/animestream/default/AndroidManifest.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.multisrc.animestream.AnimeStreamUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
3
multisrc/overrides/animestream/lmanime/additional.gradle
Normal file
@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
implementation(project(":lib-okru-extractor"))
|
||||
}
|
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 13 KiB |
113
multisrc/overrides/animestream/lmanime/src/LMAnime.kt
Normal file
@ -0,0 +1,113 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.lmanime
|
||||
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.all.lmanime.extractors.DailymotionExtractor
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Response
|
||||
|
||||
class LMAnime : AnimeStream(
|
||||
"all",
|
||||
"LMAnime",
|
||||
"https://lmanime.com",
|
||||
) {
|
||||
// ============================ Video Links =============================
|
||||
override val prefQualityValues = arrayOf("144p", "288p", "480p", "720p", "1080p")
|
||||
override val prefQualityEntries = prefQualityValues
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val items = response.asJsoup().select(videoListSelector())
|
||||
val allowed = preferences.getStringSet(PREF_ALLOWED_LANGS_KEY, PREF_ALLOWED_LANGS_DEFAULT)!!
|
||||
return items
|
||||
.filter { element ->
|
||||
val text = element.text()
|
||||
allowed.any { it in text }
|
||||
}.parallelMap {
|
||||
val language = it.text().substringBefore(" ")
|
||||
val url = getHosterUrl(it)
|
||||
getVideoList(url, language)
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
override fun getVideoList(url: String, name: String): List<Video> {
|
||||
val prefix = "$name -"
|
||||
return when {
|
||||
"ok.ru" in url ->
|
||||
OkruExtractor(client).videosFromUrl(url, prefix)
|
||||
"dailymotion.com" in url ->
|
||||
DailymotionExtractor(client).videosFromUrl(url, "Dailymotion ($name)")
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
super.setupPreferenceScreen(screen) // Quality preferences
|
||||
val langPref = ListPreference(screen.context).apply {
|
||||
key = PREF_LANG_KEY
|
||||
title = PREF_LANG_TITLE
|
||||
entries = PREF_LANG_ENTRIES
|
||||
entryValues = PREF_LANG_ENTRIES
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
val allowedPref = MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_ALLOWED_LANGS_KEY
|
||||
title = PREF_ALLOWED_LANGS_TITLE
|
||||
entries = PREF_ALLOWED_LANGS_ENTRIES
|
||||
entryValues = PREF_ALLOWED_LANGS_ENTRIES
|
||||
setDefaultValue(PREF_ALLOWED_LANGS_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(langPref)
|
||||
screen.addPreference(allowedPref)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
|
||||
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.contains(lang, true) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_LANG_KEY = "pref_language"
|
||||
private const val PREF_LANG_TITLE = "Preferred language"
|
||||
private const val PREF_LANG_DEFAULT = "English"
|
||||
private val PREF_LANG_ENTRIES = arrayOf(
|
||||
"English",
|
||||
"Español",
|
||||
"Indonesian",
|
||||
"Portugués",
|
||||
"Türkçe",
|
||||
"العَرَبِيَّة",
|
||||
"ไทย",
|
||||
)
|
||||
|
||||
private const val PREF_ALLOWED_LANGS_KEY = "pref_allowed_languages"
|
||||
private const val PREF_ALLOWED_LANGS_TITLE = "Allowed languages to fetch videos"
|
||||
private val PREF_ALLOWED_LANGS_ENTRIES = PREF_LANG_ENTRIES
|
||||
private val PREF_ALLOWED_LANGS_DEFAULT = PREF_ALLOWED_LANGS_ENTRIES.toSet()
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.lmanime.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@Serializable
|
||||
data class DailyQuality(
|
||||
val qualities: Auto,
|
||||
) {
|
||||
@Serializable
|
||||
data class Auto(
|
||||
val auto: List<Video>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Video(
|
||||
val type: String,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DailymotionExtractor(private val client: OkHttpClient) {
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||
val id = url.substringBefore("?").substringAfterLast("/")
|
||||
val jsonUrl = "https://www.dailymotion.com/player/metadata/video/$id"
|
||||
val jsonRequest = client.newCall(GET(jsonUrl)).execute().body.string()
|
||||
val parsed = json.decodeFromString<DailyQuality>(jsonRequest)
|
||||
|
||||
val masterUrl = parsed.qualities.auto.first().url
|
||||
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
return masterPlaylist.substringAfter(separator).split(separator).map {
|
||||
val resolution = it.substringAfter("RESOLUTION=")
|
||||
.substringAfter("x")
|
||||
.substringBefore(",NAME") + "p"
|
||||
val quality = "$prefix $resolution"
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
|
||||
Video(videoUrl, quality, videoUrl)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
implementation(project(":lib-unpacker"))
|
||||
}
|
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 18 KiB |
26
multisrc/overrides/animestream/rinecloud/src/RineCloud.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.rinecloud
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.pt.rinecloud.extractors.RineCloudExtractor
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||
|
||||
class RineCloud : AnimeStream(
|
||||
"pt-BR",
|
||||
"RineCloud",
|
||||
"https://rine.cloud",
|
||||
) {
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
override val prefQualityEntries = prefQualityValues
|
||||
|
||||
override fun getVideoList(url: String, name: String): List<Video> {
|
||||
return when {
|
||||
"rine.cloud" in url -> {
|
||||
RineCloudExtractor(client).videosFromUrl(url, headers)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.rinecloud.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class RineCloudExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, headers: Headers): List<Video> {
|
||||
val playerDoc = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||
val scriptData = playerDoc.selectFirst("script:containsData(JuicyCodes.Run)")
|
||||
?.data()
|
||||
?: return emptyList()
|
||||
|
||||
val decodedData = scriptData.substringAfter("(").substringBefore(")")
|
||||
.split("+\"")
|
||||
.joinToString("") { it.replace("\"", "") }
|
||||
.let { Base64.decode(it, Base64.DEFAULT) }
|
||||
.let(::String)
|
||||
|
||||
val unpackedJs = Unpacker.unpack(decodedData).ifEmpty { return emptyList() }
|
||||
|
||||
val masterPlaylistUrl = unpackedJs.substringAfter("sources:[")
|
||||
.substringAfter("file\":\"")
|
||||
.substringBefore('"')
|
||||
|
||||
val playlistData = client.newCall(GET(masterPlaylistUrl, headers)).execute()
|
||||
.body.string()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF:"
|
||||
return playlistData.substringAfter(separator).split(separator).map {
|
||||
val quality = it.substringAfter("RESOLUTION=")
|
||||
.substringAfter("x")
|
||||
.substringBefore("\n")
|
||||
.substringBefore(",") + "p"
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
Video(videoUrl, "RineCloud - $quality", videoUrl, headers = headers)
|
||||
}
|
||||
}
|
||||
}
|