add german extension: Anime-Loads (#1073)

This commit is contained in:
LuftVerbot
2022-12-14 00:05:52 +01:00
committed by GitHub
parent 64c7677847
commit f7c99ff786
20 changed files with 619 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -0,0 +1,19 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Anime-Loads (experimental)'
pkgNameSuffix = 'de.animeloads'
extClass = '.Anime-Loads'
extVersionCode = 1
libVersion = '13'
}
dependencies {
implementation(project(':lib-voe-extractor'))
implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-dood-extractor'))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,437 @@
package eu.kanade.tachiyomi.animeextension.de.animeloads
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.Exception
class `Anime-Loads` : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Anime-Loads (experimental)"
override val baseUrl = "https://www.anime-loads.org"
override val lang = "de"
override val supportsLatest = false
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(DdosGuardInterceptor(network.client))
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector(): String = "div.row div.col-sm-6 div.panel-body"
override fun popularAnimeRequest(page: Int): Request {
val interceptor = client.newBuilder().addInterceptor(RedirectInterceptor()).build()
interceptor.newCall(GET(baseUrl)).execute().headers
return GET("$baseUrl/anime-series/page/$page")
}
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.row a.cover-img").attr("href"))
anime.thumbnail_url = element.select("div.row a.cover-img img").attr("src")
anime.title = element.select("div.row h4.title-list a").text()
return anime
}
override fun popularAnimeNextPageSelector(): String = "i.glyphicon-forward"
// episodes
override fun episodeListSelector() = throw Exception("not used")
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val episode = SEpisode.create()
val series = document.select("a[title=\"Anime Serien\"]")
if (series.attr("title").contains("Anime Serien")) {
val eplist = document.select("#streams_episodes_1 div.list-group")
val url = document.select("meta[property=\"og:url\"]").attr("content")
val ep = parseEpisodesFromSeries(eplist, url)
episodeList.addAll(ep)
} else {
episode.name = document.select("div.page-header > h1").attr("title")
episode.episode_number = 1F
episode.setUrlWithoutDomain(document.select("meta[property=\"og:url\"]").attr("content"))
episodeList.add(episode)
}
return episodeList.reversed()
}
private fun parseEpisodesFromSeries(element: Elements, url: String): List<SEpisode> {
val episodeElement = element.select("a.list-group-item")
return episodeElement.map { episodeFromElement(it, url) }
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not Used")
private fun episodeFromElement(element: Element, url: String): SEpisode {
val episode = SEpisode.create()
val id = element.attr("aria-controls")
episode.setUrlWithoutDomain("$url#$id")
episode.name = "Ep." + element.select("span:nth-child(1)").text()
episode.episode_number = element.select("span strong").text().toFloat()
return episode
}
// Video Extractor
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val url = response.request.url.toString()
val id = url
.substringAfter("#streams")
return videosFromElement(document, id, url)
}
private fun videosFromElement(document: Document, id: String, url: String): List<Video> {
val videoList = mutableListOf<Video>()
val hosterSelection = preferences.getStringSet("hoster_selection", setOf("dood", "voe", "stape"))
val element = document.select("div#streams$id")
val enc = element.attr("data-enc")
val capfiles = client.newCall(
POST(
"$baseUrl/files/captcha",
body = "cID=0&rT=1".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
headers = Headers.headersOf(
"X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#streams$id", ""), "Accept", "application/json, text/javascript, */*; q=0.01", "cache-control", "max-age=15"
)
)
).execute().asJsoup()
val hashes = capfiles.toString().substringAfter("[").substringBefore("]").split(",")
val hashlist = mutableListOf<String>()
val pnglist = mutableListOf<String>()
var max = "1"
var min = "99999"
hashes.forEach {
val hash = it.replace("<body>", "")
.replace("[", "")
.replace("\"", "").replace("]", "")
.replace("</body>", "").replace("%20", "")
val png = client.newCall(
GET(
"$baseUrl/files/captcha?cid=0&hash=$hash",
headers = Headers.headersOf(
"Referer", url.replace("#streams$id", ""),
"Accept", "image/avif,image/webp,*/*", "cache-control", "max-age=15"
)
)
).execute().body?.byteString()
val size = png.toString()
.substringAfter("[size=").substringBefore(" hex")
pnglist.add("$size | $hash")
hashlist.add(size)
for (num in hashlist) {
if (max < num) {
max = num
}
}
for (num in hashlist) {
if (min > num) {
min = num
}
}
}
var int = 0
pnglist.forEach { diffit ->
if (int == 0) {
if (diffit.substringBefore(" |").toInt() != max.toInt() && diffit.substringBefore(" |").toInt() != min.toInt()) {
int = 1
val hash = diffit.substringBefore(" |").toInt()
val diffmax = max.toInt() - hash
val diffmin = hash - min.toInt()
if (diffmax > diffmin) {
pnglist.forEach { itmax ->
if (max.toInt() == itmax.substringBefore(" |").toInt()) {
val maxhash = itmax.substringAfter("| ")
network.cloudflareClient.newCall(
POST(
"$baseUrl/files/captcha",
body = "cID=0&pC=$maxhash&rT=2".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
headers = Headers.headersOf(
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#streams$id", ""), "Accept", "*/*", "cache-control", "max-age=15"
)
)
).execute()
val maxdoc = client.newCall(
POST(
"$baseUrl/ajax/captcha",
body = "enc=${enc.replace("=", "%3D")}&response=captcha&captcha-idhf=0&captcha-hf=$maxhash".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
headers = Headers.headersOf(
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#streams$id", ""),
"Accept", "application/json, text/javascript, */*; q=0.01", "cache-control", "max-age=15"
)
)
).execute().asJsoup().toString()
if (maxdoc.substringAfter("\"code\":\"").substringBefore("\",").contains("error")) {
throw Exception("Captcha bypass failed! Clear Cookies & Webview data. Or wait some time.")
} else {
val links = maxdoc.substringAfter("\"content\":[").substringBefore("</body>").split("{\"links\":")
links.forEach {
if (it.contains("link")) {
val hoster = it.substringAfter("\"hoster\":\"").substringBefore("\",\"")
val linkpart = it.substringAfter("\"link\":\"").substringBefore("\"}]")
val leaveurl = client.newCall(GET("$baseUrl/leave/$linkpart")).execute().request.url.toString()
val neexurl = client.newCall(GET(leaveurl)).execute().request.url.toString()
val neexdoc = client.newCall(GET(leaveurl)).execute().asJsoup()
val nextlink = neexdoc.select("div#continue a").attr("href")
val anipart = nextlink.substringAfter("$baseUrl/leave/")
Thread.sleep(10000)
client.newCall(GET(nextlink, headers = Headers.headersOf("referer", neexurl))).execute().asJsoup()
when {
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
val link = client.newCall(GET("$baseUrl/leave/$anipart")).execute().request.url.toString()
val video = try {
VoeExtractor(client).videoFromUrl(link)
} catch (e: Exception) {
null
}
if (video != null) {
videoList.add(video)
}
}
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
val link = client.newCall(GET("$baseUrl/leave/$anipart")).execute().request.url.toString()
val video = try {
StreamTapeExtractor(client).videoFromUrl(link)
} catch (e: Exception) {
null
}
if (video != null) {
videoList.add(video)
}
}
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
val link = client.newCall(GET("$baseUrl/leave/$anipart")).execute().request.url.toString()
val video = try {
DoodExtractor(client).videoFromUrl(link)
} catch (e: Exception) {
null
}
if (video != null) {
videoList.add(video)
}
}
}
}
}
}
}
}
} else {
pnglist.forEach { itmin ->
if (min.toInt() == itmin.substringBefore(" |").toInt()) {
val minhash = itmin.substringAfter("| ")
network.cloudflareClient.newCall(
POST(
"$baseUrl/files/captcha",
body = "cID=0&pC=$minhash&rT=2".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
headers = Headers.headersOf(
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#streams$id", ""), "Accept", "*/*"
)
)
).execute()
val mindoc = client.newCall(
POST(
"$baseUrl/ajax/captcha",
body = "enc=${enc.replace("=", "%3D")}&response=captcha&captcha-idhf=0&captcha-hf=$minhash".toRequestBody("application/x-www-form-urlencoded".toMediaType()),
headers = Headers.headersOf(
"Origin", baseUrl, "X-Requested-With", "XMLHttpRequest", "Referer", url.replace("#streams$id", ""),
"Accept", "application/json, text/javascript, */*; q=0.01"
)
)
).execute().asJsoup().toString()
if (mindoc.substringAfter("\"code\":\"").substringBefore("\",").contains("error")) {
throw Exception("Captcha bypass failed! Clear Cookies & Webview data. Or wait some time.")
} else {
val links = mindoc.substringAfter("\"content\":[").substringBefore("</body>").split("{\"links\":")
links.forEach {
if (it.contains("link")) {
val hoster = it.substringAfter("\"hoster\":\"").substringBefore("\",\"")
val linkpart = it.substringAfter("\"link\":\"").substringBefore("\"}]")
val leaveurl = client.newCall(GET("$baseUrl/leave/$linkpart")).execute().request.url.toString()
val neexurl = client.newCall(GET(leaveurl)).execute().request.url.toString()
val neexdoc = client.newCall(GET(leaveurl)).execute().asJsoup()
val nextlink = neexdoc.select("div#continue a").attr("href")
val anipart = nextlink.substringAfter("$baseUrl/leave/")
Thread.sleep(10000)
client.newCall(GET(nextlink, headers = Headers.headersOf("referer", neexurl))).execute().asJsoup()
when {
hoster.contains("voesx") && hosterSelection?.contains("voe") == true -> {
val link = client.newCall(GET("$baseUrl/leave/$anipart")).execute().request.url.toString()
val video = try {
VoeExtractor(client).videoFromUrl(link)
} catch (e: Exception) {
null
}
if (video != null) {
videoList.add(video)
}
}
hoster.contains("streamtapecom") && hosterSelection?.contains("stape") == true -> {
val link = client.newCall(GET("$baseUrl/leave/$anipart")).execute().request.url.toString()
val video = try {
StreamTapeExtractor(client).videoFromUrl(link)
} catch (e: Exception) {
null
}
if (video != null) {
videoList.add(video)
}
}
hoster.contains("doodstream") && hosterSelection?.contains("dood") == true -> {
val link = client.newCall(GET("$baseUrl/leave/$anipart")).execute().request.url.toString()
val video = try {
DoodExtractor(client).videoFromUrl(link)
} catch (e: Exception) {
null
}
if (video != null) {
videoList.add(video)
}
}
}
}
}
}
}
}
}
}
}
}
return videoList.reversed()
}
override fun List<Video>.sort(): List<Video> {
val hoster = preferences.getString("preferred_hoster", null)
if (hoster != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(hoster)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// Search
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.row a.cover-img").attr("href"))
anime.thumbnail_url = element.select("div.row a.cover-img img").attr("src")
anime.title = element.select("div.row h4.title-list a").text()
return anime
}
override fun searchAnimeNextPageSelector(): String = "i.glyphicon-forward"
override fun searchAnimeSelector(): String = "div.row div.col-sm-6 div.panel-body"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/search/page/$page?q=$query")
// Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = document.select("#description img.img-responsive").attr("src")
anime.title = document.select("div.page-header > h1").attr("title")
anime.genre = document.select("#description div.label-group a.label.label-info").joinToString(", ") { it.text() }
anime.description = document.select("div.pt20").not("strong").text()
anime.author = document.select("div.col-md-6.text-left p:nth-child(3) a").joinToString(", ") { it.text() }
anime.status = SAnime.COMPLETED
return anime
}
// Latest
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesSelector(): String = throw Exception("Not used")
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val hosterPref = ListPreference(screen.context).apply {
key = "preferred_hoster"
title = "Standard-Hoster"
entries = arrayOf("Doodstream", "Voe", "MIXdrop")
entryValues = arrayOf("https://dood", "https://voe.sx", "https://streamtape.com")
setDefaultValue("https://voe.sx")
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 subSelection = MultiSelectListPreference(screen.context).apply {
key = "hoster_selection"
title = "Hoster auswählen"
entries = arrayOf("Doodstream", "Voe", "Streamtape")
entryValues = arrayOf("dood", "voe", "stape")
setDefaultValue(setOf("dood", "voe", "stape"))
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
screen.addPreference(hosterPref)
screen.addPreference(subSelection)
}
}

View File

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.animeextension.de.animeloads
import android.util.Log
import android.webkit.CookieManager
import eu.kanade.tachiyomi.network.GET
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
class DdosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
private val cookieManager by lazy { CookieManager.getInstance() }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
// Check if DDos-GUARD is on
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
return response
}
response.close()
val cookies = cookieManager.getCookie(originalRequest.url.toString())
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
} else {
emptyList()
}
Log.i("newCookie", "OldCookies: $oldCookie")
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
if (!ddg2Cookie?.value.isNullOrEmpty()) {
return chain.proceed(originalRequest)
}
val newCookie = getNewCookie(originalRequest.url) ?: return chain.proceed(originalRequest)
val newCookieHeader = buildString {
(oldCookie + newCookie).forEachIndexed { index, cookie ->
if (index > 0) append("; ")
append(cookie.name).append('=').append(cookie.value)
}
}
return chain.proceed(originalRequest.newBuilder().addHeader("cookie", newCookieHeader).build())
}
fun getNewCookie(url: HttpUrl): Cookie? {
val cookies = cookieManager.getCookie(url.toString())
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
} else {
emptyList()
}
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
if (!ddg2Cookie?.value.isNullOrEmpty()) {
return ddg2Cookie
}
val wellKnown = client.newCall(GET("https://check.ddos-guard.net/check.js"))
.execute().body!!.string()
.substringAfter("'", "")
.substringBefore("'", "")
val checkUrl = "${url.scheme}://${url.host + wellKnown}"
return client.newCall(GET(checkUrl)).execute().header("set-cookie")?.let {
Cookie.parse(url, it)
}
}
companion object {
private val ERROR_CODES = listOf(403)
private val SERVER_CHECK = listOf("ddos-guard")
}
}

View File

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.animeextension.de.animeloads
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class RedirectInterceptor : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("bruh")
return chain.proceed(newRequest)
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
// We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
var newRequest: Request? = null
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0"
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if (request.url.toString().contains("/bootstrap/3.3.2/css/bootstrap.min.css")) {
newRequest = GET(origRequestUrl, request.requestHeaders.toHeaders())
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, headers)
}
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
// around 4 seconds but it can take more due to slow networks or server issues.
latch.await(12, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return newRequest
}
}