refactor: Convert Kinoking(de) to multisrc(dooplay) (#1665)

This commit is contained in:
Claudemirovsky
2023-06-01 07:55:26 +00:00
committed by GitHub
parent 93519cd8a5
commit 1c41dda68c
22 changed files with 151 additions and 359 deletions

View File

@ -0,0 +1,5 @@
dependencies {
implementation(project(':lib-voe-extractor'))
implementation(project(':lib-streamsb-extractor'))
implementation(project(':lib-dood-extractor'))
}

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,145 @@
package eu.kanade.tachiyomi.animeextension.de.kinoking
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Response
import org.jsoup.nodes.Element
class Kinoking : DooPlay(
"de",
"Kinoking",
"https://kinoking.cc",
) {
companion object {
private const val PREF_HOSTER_KEY = "preferred_hoster"
private const val PREF_HOSTER_TITLE = "Standard-Hoster"
private const val PREF_HOSTER_DEFAULT = "https://dood"
private val PREF_HOSTER_ENTRIES = arrayOf("Doodstream", "StreamSB", "Voe")
private val PREF_HOSTER_VALUES = arrayOf("https://dood", "https://watchsb.com", "https://voe.sx")
private const val PREF_HOSTER_SELECTION_KEY = "hoster_selection"
private const val PREF_HOSTER_SELECTION_TITLE = "Hoster auswählen"
private val PREF_HOSTER_SELECTION_ENTRIES = PREF_HOSTER_ENTRIES
private val PREF_HOSTER_SELECTION_VALUES = arrayOf("dood", "watchsb", "voe")
private val PREF_HOSTER_SELECTION_DEFAULT = PREF_HOSTER_SELECTION_ENTRIES.toSet()
}
override val videoSortPrefKey = PREF_HOSTER_KEY
override val videoSortPrefDefault = PREF_HOSTER_DEFAULT
override val client = network.cloudflareClient
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div#featured-titles div.poster"
// ============================== Episodes ==============================
// Little workaround to show season episode names like the original extension
// TODO: Create a "getEpisodeName(element, seasonName)" function in DooPlay class
override fun episodeFromElement(element: Element, seasonName: String) =
super.episodeFromElement(element, seasonName).apply {
val substring = name.substringBefore(" -")
val newString = substring.replace("Season", "Staffel")
.replace("x", "Folge")
name = name.replace("$substring -", "$newString :")
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "#nextpagination"
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val players = response.use { it.asJsoup().select("li.dooplay_player_option") }
val hosterSelection = preferences.getStringSet(PREF_HOSTER_SELECTION_KEY, PREF_HOSTER_SELECTION_DEFAULT)!!
return players.mapNotNull { player ->
runCatching {
val link = getPlayerUrl(player)
getPlayerVideos(link, player, hosterSelection)
}.getOrDefault(emptyList<Video>())
}.flatten()
}
private fun getPlayerUrl(player: Element): String {
val body = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("post", player.attr("data-post"))
.add("nume", player.attr("data-nume"))
.add("type", player.attr("data-type"))
.build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
.execute()
.use { response ->
response.body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
}
}
private fun getPlayerVideos(link: String, element: Element, hosterSelection: Set<String>): List<Video>? {
return when {
link.contains("https://watchsb") || link.contains("https://viewsb") && hosterSelection.contains("watchsb") -> {
if (element.select("span.flag img").attr("data-src").contains("/en.")) {
val lang = "Englisch"
StreamSBExtractor(client).videosFromUrl(link, headers, suffix = lang)
} else {
val lang = "Deutsch"
StreamSBExtractor(client).videosFromUrl(link, headers, lang)
}
}
link.contains("https://dood.") || link.contains("https://doodstream.") && hosterSelection.contains("dood") -> {
val quality = "Doodstream"
val redirect = !link.contains("https://doodstream")
DoodExtractor(client).videoFromUrl(link, quality, redirect)
?.let(::listOf)
}
link.contains("https://voe.sx") && hosterSelection.contains("voe") == true -> {
val quality = "Voe"
VoeExtractor(client).videoFromUrl(link, quality)
?.let(::listOf)
}
else -> null
}
}
// ============================== Settings ==============================
@Suppress("UNCHECKED_CAST")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val hosterPref = ListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE
entries = PREF_HOSTER_ENTRIES
entryValues = PREF_HOSTER_VALUES
setDefaultValue(PREF_HOSTER_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 subSelection = MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_SELECTION_KEY
title = PREF_HOSTER_SELECTION_TITLE
entries = PREF_HOSTER_SELECTION_ENTRIES
entryValues = PREF_HOSTER_SELECTION_VALUES
setDefaultValue(PREF_HOSTER_SELECTION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
screen.addPreference(hosterPref)
screen.addPreference(subSelection)
}
}

View File

@ -20,6 +20,7 @@ class DooPlayGenerator : ThemeSourceGenerator {
SingleLang("CineVision", "https://cinevisionv3.online", "pt-BR", isNsfw = true, overrideVersionCode = 5),
SingleLang("DonghuaX", "https://donghuax.com", "pt-BR", isNsfw = false),
SingleLang("GoAnimes", "https://goanimes.net", "pt-BR", isNsfw = true),
SingleLang("Kinoking", "https://kinoking.cc", "de", isNsfw = false, overrideVersionCode = 15),
SingleLang("Multimovies", "https://multimovies.tech", "en", isNsfw = false, overrideVersionCode = 6),
SingleLang("pactedanime", "https://pactedanime.com", "en", isNsfw = false, className = "PactedAnime", overrideVersionCode = 4),
SingleLang("Pi Fansubs", "https://pifansubs.org", "pt-BR", isNsfw = true, overrideVersionCode = 16),

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

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

View File

@ -1,88 +0,0 @@
package eu.kanade.tachiyomi.animeextension.de.kinoking
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 CloudflareInterceptor : 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 = request.header("User-Agent")
?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63\""
}
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if (request.url.toString().contains("https://kinoking.cc/")) {
newRequest = GET(request.url.toString(), 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
}
}

View File

@ -1,250 +0,0 @@
package eu.kanade.tachiyomi.animeextension.de.kinoking
import android.app.Application
import android.content.SharedPreferences
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.streamsbextractor.StreamSBExtractor
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 uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.Exception
class Kinoking : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Kinoking"
override val baseUrl = "https://kinoking.cc"
override val lang = "de"
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 = "#featured-titles article.item"
override fun popularAnimeRequest(page: Int): Request {
val interceptor = client.newBuilder().addInterceptor(CloudflareInterceptor()).build()
val headers = interceptor.newCall(GET(baseUrl)).execute().request.headers
return GET(baseUrl, headers = headers)
}
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.data a").attr("href"))
anime.thumbnail_url = element.select("div.poster img[data-src]").attr("data-src")
anime.title = element.select("div.poster img").attr("alt")
return anime
}
override fun popularAnimeNextPageSelector(): String? = null
// episodes
override fun episodeListSelector() = throw Exception("not used")
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val url = document.select("link[rel=canonical]").attr("href")
val newdoc = client.newCall(GET(url, headers = Headers.headersOf("if-modified-since", ""))).execute().asJsoup()
if (newdoc.select("link[rel=canonical]").attr("href").contains("/tvshows/")) {
val episodeElement = newdoc.select("#seasons div.se-c")
episodeElement.forEach {
val episode = parseEpisodesFromSeries(it)
episodeList.addAll(episode)
}
} else {
val episode = SEpisode.create()
episode.name = newdoc.select("div.data h1").text()
episode.episode_number = 1F
episode.setUrlWithoutDomain(newdoc.select("link[rel=canonical]").attr("href"))
episodeList.add(episode)
}
return episodeList.reversed()
}
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
val episodeElements = element.select("div.se-a li")
return episodeElements.map { episodeFromElement(it) }
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.episode_number = element.attr("class").substringAfter("mark-").toFloat()
val staffel = element.select("div.numerando").text().substringBefore(" -")
val folge = element.select("div.numerando").text().substringAfter("- ")
episode.name = "Staffel $staffel Folge $folge : " + element.select("div.episodiotitle a").text()
episode.setUrlWithoutDomain(element.select("div.episodiotitle a").attr("href"))
return episode
}
// Video Extractor
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return videosFromElement(document)
}
private fun videosFromElement(document: Document): List<Video> {
val videoList = mutableListOf<Video>()
val videoelement = document.select("li.dooplay_player_option ")
videoelement.forEach {
val post = it.attr("data-post")
val nume = it.attr("data-nume")
val type = it.attr("data-type")
val videodoc = client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", body = "action=doo_player_ajax&post=$post&nume=$nume&type=$type".toRequestBody("application/x-www-form-urlencoded".toMediaType()))).execute().body.string()
val link = videodoc.substringAfter("\"embed_url\":\"").substringBefore("\",").replace("\\", "")
val hosterSelection = preferences.getStringSet("hoster_selection", setOf("dood", "watchsb", "voe"))
when {
link.contains("https://watchsb") || link.contains("https://viewsb") && hosterSelection?.contains("watchsb") == true -> {
if (it.select("span.flag img").attr("data-src").contains("/en.")) {
val lang = "Englisch"
val video = StreamSBExtractor(client).videosFromUrl(link, headers, suffix = lang)
videoList.addAll(video)
} else {
val lang = "Deutsch"
val video = StreamSBExtractor(client).videosFromUrl(link, headers, lang)
videoList.addAll(video)
}
}
link.contains("https://dood.") || link.contains("https://doodstream.") && hosterSelection?.contains("dood") == true -> {
val quality = "Doodstream"
val redirect = !link.contains("https://doodstream")
val video = DoodExtractor(client).videoFromUrl(link, quality, redirect)
if (video != null) {
videoList.add(video)
}
}
link.contains("https://voe.sx") && hosterSelection?.contains("voe") == true -> {
val quality = "Voe"
val video = VoeExtractor(client).videoFromUrl(link, quality)
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.thumbnail a").attr("href"))
anime.thumbnail_url = element.select("div.thumbnail img").attr("data-src")
anime.title = element.select("div.thumbnail img").attr("alt")
return anime
}
override fun searchAnimeNextPageSelector(): String = "#nextpagination"
override fun searchAnimeSelector(): String = "div.search-page div.result-item"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/page/$page/?s=$query", headers = Headers.headersOf("if-modified-since", ""))
// Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = document.select("div.poster img").attr("data-src")
anime.title = document.select("div.data h1").text()
anime.genre = document.select("div.sgeneros a").joinToString(", ") { it.text() }
anime.description = document.select("div.wp-content p").text()
anime.author = document.select("div.person[itemprop=director] div.name 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", "StreamSB", "Voe")
entryValues = arrayOf("https://dood", "https://watchsb.com", "https://voe.sx")
setDefaultValue("https://dood")
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", "StreamSB", "Voe")
entryValues = arrayOf("dood", "watchsb", "voe")
setDefaultValue(setOf("dood", "watchsb", "voe"))
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
screen.addPreference(hosterPref)
screen.addPreference(subSelection)
}
}