add new experimental source zoro.to

This commit is contained in:
jmir1
2021-10-03 18:34:09 +02:00
parent 1b094dfd90
commit 37c534acda
5 changed files with 407 additions and 0 deletions

View File

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

12
src/en/zoro/build.gradle Normal file
View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'zoro.to (experimental)'
pkgNameSuffix = 'en.zoro'
extClass = '.Zoro'
extVersionCode = 1
libVersion = '12'
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,101 @@
package eu.kanade.tachiyomi.animeextension.en.zoro
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class GetSourcesInterceptor(private val getSources: String, private val client: OkHttpClient) : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
private val initWebView by lazy {
WebSettings.getDefaultUserAgent(context)
}
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
initWebView
val request = chain.request()
try {
val newRequest = resolveWithWebView(request)
return chain.proceed(newRequest ?: request)
} catch (e: Exception) {
throw IOException(e)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
Log.i("bruh", "hello")
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 (Linux; Android) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.109 Safari/537.36 CrKey/1.54.248666"
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
val url = request.url.toString()
if (url.contains(getSources)) {
val newHeaders = request.requestHeaders.toHeaders()
newRequest = GET(url, newHeaders)
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return newRequest
}
companion object {
const val TIMEOUT_SEC: Long = 10
}
}

View File

@ -0,0 +1,88 @@
package eu.kanade.tachiyomi.animeextension.en.zoro;
public class JSONUtil {
public static String escape(String input) {
StringBuilder output = new StringBuilder();
for(int i=0; i<input.length(); i++) {
char ch = input.charAt(i);
int chx = (int) ch;
// let's not put any nulls in our strings
assert(chx != 0);
if(ch == '\n') {
output.append("\\n");
} else if(ch == '\t') {
output.append("\\t");
} else if(ch == '\r') {
output.append("\\r");
} else if(ch == '\\') {
output.append("\\\\");
} else if(ch == '"') {
output.append("\\\"");
} else if(ch == '\b') {
output.append("\\b");
} else if(ch == '\f') {
output.append("\\f");
} else if(chx >= 0x10000) {
assert false : "Java stores as u16, so it should never give us a character that's bigger than 2 bytes. It literally can't.";
} else if(chx > 127) {
output.append(String.format("\\u%04x", chx));
} else {
output.append(ch);
}
}
return output.toString();
}
public static String unescape(String input) {
StringBuilder builder = new StringBuilder();
int i = 0;
while (i < input.length()) {
char delimiter = input.charAt(i); i++; // consume letter or backslash
if(delimiter == '\\' && i < input.length()) {
// consume first after backslash
char ch = input.charAt(i); i++;
if(ch == '\\' || ch == '/' || ch == '"' || ch == '\'') {
builder.append(ch);
}
else if(ch == 'n') builder.append('\n');
else if(ch == 'r') builder.append('\r');
else if(ch == 't') builder.append('\t');
else if(ch == 'b') builder.append('\b');
else if(ch == 'f') builder.append('\f');
else if(ch == 'u') {
StringBuilder hex = new StringBuilder();
// expect 4 digits
if (i+4 > input.length()) {
throw new RuntimeException("Not enough unicode digits! ");
}
for (char x : input.substring(i, i + 4).toCharArray()) {
if(!Character.isLetterOrDigit(x)) {
throw new RuntimeException("Bad character in unicode escape.");
}
hex.append(Character.toLowerCase(x));
}
i+=4; // consume those four digits.
int code = Integer.parseInt(hex.toString(), 16);
builder.append((char) code);
} else {
throw new RuntimeException("Illegal escape sequence: \\"+ch);
}
} else { // it's not a backslash, or it's the last character.
builder.append(delimiter);
}
}
return builder.toString();
}
}

View File

@ -0,0 +1,204 @@
package eu.kanade.tachiyomi.animeextension.en.zoro
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
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.network.GET
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
class Zoro : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "zoro.to (experimental)"
override val baseUrl = "https://zoro.to"
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector(): String = "div.flw-item"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/most-popular?page=$page")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = element.select("div.film-poster > img").attr("data-src")
anime.setUrlWithoutDomain(baseUrl + element.select("div.film-detail a").attr("href"))
anime.title = element.select("div.film-detail a").attr("data-jname")
anime.description = element.select("div.film-detail div.description").firstOrNull()?.text()
return anime
}
override fun popularAnimeNextPageSelector(): String = "li.page-item a[title=Next]"
override fun episodeListSelector() = "ul#episode_page li a"
override fun episodeListRequest(anime: SAnime): Request {
val id = anime.url.substringAfterLast("-")
val referer = Headers.headersOf("Referer", baseUrl + anime.url)
return GET("$baseUrl/ajax/v2/episode/list/$id", referer)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val data = response.body!!.string().substringAfter("\"html\":\"").substringBefore("<script>")
val unescapedData = JSONUtil.unescape(data)
Log.i("bruuh", unescapedData)
val episodeList = mutableListOf<SEpisode>()
val document = Jsoup.parse(unescapedData)
val aList = document.select("a.ep-item")
for (a in aList) {
val episode = SEpisode.create()
episode.episode_number = a.attr("data-number").toFloat()
episode.name = "Episode ${a.attr("data-number")}: ${a.attr("title")}"
episode.url = a.attr("href")
episode.date_upload = System.currentTimeMillis()
episodeList.add(episode)
}
return episodeList.reversed()
}
override fun episodeFromElement(element: Element) = throw Exception("not used")
override fun videoListRequest(episode: SEpisode): Request {
val id = episode.url.substringAfterLast("?ep=")
val referer = Headers.headersOf("Referer", baseUrl + episode.url)
return GET("$baseUrl/ajax/v2/episode/servers?episodeId=$id", referer)
}
override fun videoListParse(response: Response): List<Video> {
val body = response.body!!.string()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val data = body.substringAfter("\"html\":\"").substringBefore("<script>")
val unescapedData = JSONUtil.unescape(data)
Log.i("bruh", unescapedData)
val serversHtml = Jsoup.parse(unescapedData)
val videoList = mutableListOf<Video>()
serversHtml.select("div.server-item").forEach {
val id = it.attr("data-id")
val quality = it.attr("data-type")
val video = getVideoFromServer(
client.newCall(GET("$baseUrl/ajax/v2/episode/sources?id=$id", episodeReferer)).execute(),
quality
)
if (video != null) videoList.add(video)
}
return videoList
}
private fun getVideoFromServer(response: Response, quality: String): Video? {
val body = response.body!!.string()
val url = body.substringAfter("\"link\":\"").substringBefore("\"").toHttpUrl()
val id = url.pathSegments.last()
val getSources = url.toString().substringBefore("embed-6") + "ajax/embed-6/getSources?id=$id&_token="
val referer = Headers.headersOf("Referer", baseUrl)
val recaptchaClient = client.newBuilder().addInterceptor(GetSourcesInterceptor(getSources, client)).build()
val lol = recaptchaClient.newCall(GET("$url&autoPlay=1", referer)).execute().body!!.string()
Log.i("bruh", lol)
if (!lol.contains("{\"sources\":[{\"file\":\"")) return null
val videoUrl = lol.substringAfter("{\"sources\":[{\"file\":\"").substringBefore("\"")
return Video(videoUrl, quality, videoUrl, null)
}
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")
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/search?keyword=$query&page=$page")
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("div.anisc-detail h2").attr("data-jname")
anime.description = document.select("div.anisc-info > div.item-title > div.text").text()
anime.author = document.select("div.item-title:contains(Studios:) a").text()
anime.status = parseStatus(document.select("div.item-title:contains(Status:) span.name").text())
anime.genre = document.select("div.item-title:contains(Genres:) a").joinToString { it.text() }
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Currently Airing" -> SAnime.ONGOING
"Finished Airing" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/top-airing")
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("sub", "dub")
entryValues = arrayOf("sub", "dub")
setDefaultValue("dub")
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(videoQualityPref)
}
}