From 50da08725a0fe8cd7db3b69831f4ad8c90d19251 Mon Sep 17 00:00:00 2001 From: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:12:13 +0500 Subject: [PATCH] feat(src/all): MissAV & JavGuru, Get hd covers (#2305) --- lib/javcoverfetcher/build.gradle.kts | 17 ++ .../lib/javcoverfetcher/JavCoverFetcher.kt | 153 ++++++++++++++++++ src/all/javguru/build.gradle | 2 + .../animeextension/all/javguru/JavGuru.kt | 69 ++++++-- .../javguru/extractors/EmTurboExtractor.kt | 7 +- .../javguru/extractors/MaxStreamExtractor.kt | 7 +- src/all/missav/build.gradle | 1 + .../animeextension/all/missav/MissAV.kt | 14 +- 8 files changed, 249 insertions(+), 21 deletions(-) create mode 100644 lib/javcoverfetcher/build.gradle.kts create mode 100644 lib/javcoverfetcher/src/main/java/eu/kanade/tachiyomi/lib/javcoverfetcher/JavCoverFetcher.kt diff --git a/lib/javcoverfetcher/build.gradle.kts b/lib/javcoverfetcher/build.gradle.kts new file mode 100644 index 000000000..a6d657dd8 --- /dev/null +++ b/lib/javcoverfetcher/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("com.android.library") + kotlin("android") +} + +android { + compileSdk = AndroidConfig.compileSdk + namespace = "eu.kanade.tachiyomi.lib.javcoverfetcher" + + defaultConfig { + minSdk = AndroidConfig.minSdk + } +} + +dependencies { + compileOnly(libs.bundles.common) +} diff --git a/lib/javcoverfetcher/src/main/java/eu/kanade/tachiyomi/lib/javcoverfetcher/JavCoverFetcher.kt b/lib/javcoverfetcher/src/main/java/eu/kanade/tachiyomi/lib/javcoverfetcher/JavCoverFetcher.kt new file mode 100644 index 000000000..de0f0e297 --- /dev/null +++ b/lib/javcoverfetcher/src/main/java/eu/kanade/tachiyomi/lib/javcoverfetcher/JavCoverFetcher.kt @@ -0,0 +1,153 @@ +package eu.kanade.tachiyomi.lib.javcoverfetcher + +import android.content.SharedPreferences +import android.util.Log +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.FormBody +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.internal.commonEmptyHeaders +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat + +object JavCoverFetcher { + + private val CLIENT by lazy { + Injekt.get().cloudflareClient.newBuilder() + .addInterceptor(::amazonAgeVerifyIntercept) + .build() + } + + private val HEADERS by lazy { + commonEmptyHeaders.newBuilder() + .set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36") + .build() + } + + private fun amazonAgeVerifyIntercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + if (!request.url.host.contains("amazon.co.jp") || !response.request.url.pathSegments.contains("black-curtain")) { + return response + } + + val document = response.use { it.asJsoup() } + val targetUrl = document.selectFirst("#black-curtain-yes-button a")?.attr("abs:href") + ?: throw IOException("Failed to bypass Amazon Age Gate") + + val newRequest = request.newBuilder().apply { + url(targetUrl) + }.build() + + return chain.proceed(newRequest) + } + + /** + * Get HD Jav Cover from Amazon + * + * @param jpTitle title of jav in japanese + */ + fun getCoverByTitle(jpTitle: String): String? { + return runCatching { + val amazonUrl = getDDGSearchResult(jpTitle) + ?: return@runCatching null + + getHDCoverFromAmazonUrl(amazonUrl) + }.getOrElse { + Log.e("JavCoverFetcher", it.stackTraceToString()) + null + } + } + + /** + * Get HD Jav Cover from Amazon + * + * @param javId standard JAV code e.g PRIN-006 + */ + fun getCoverById(javId: String): String? { + return runCatching { + val jpTitle = getJPTitleFromID(javId) + ?: return@runCatching null + + val amazonUrl = getDDGSearchResult(jpTitle) + ?: return@runCatching null + + getHDCoverFromAmazonUrl(amazonUrl) + }.getOrElse { + Log.e("JavCoverFetcher", it.stackTraceToString()) + null + } + } + + private fun getJPTitleFromID(javId: String): String? { + val url = "https://www.javlibrary.com/ja/vl_searchbyid.php?keyword=$javId" + + val request = GET(url, HEADERS) + + val response = CLIENT.newCall(request).execute() + + var document = response.use { it.asJsoup() } + + // possibly multiple results or none + if (response.request.url.pathSegments.contains("vl_searchbyid.php")) { + val targetUrl = document.selectFirst(".videos a[href*=\"?v=\"]")?.attr("abs:href") + ?: return null + + document = CLIENT.newCall(GET(targetUrl, HEADERS)).execute().use { it.asJsoup() } + } + + val dirtyTitle = document.selectFirst(".post-title")?.text() + + val id = document.select("#video_info tr > td:contains(品番) + td").text() + + return dirtyTitle?.substringAfter(id)?.trim() + } + + private fun getDDGSearchResult(jpTitle: String): String? { + val url = "https://lite.duckduckgo.com/lite" + + val form = FormBody.Builder() + .add("q", "site:amazon.co.jp inurl:/dp/$jpTitle") + .build() + + val request = POST(url, HEADERS, form) + + val response = CLIENT.newCall(request).execute() + + val document = response.use { it.asJsoup() } + + return document.selectFirst("a.result-link")?.attr("href") + } + + private fun getHDCoverFromAmazonUrl(amazonUrl: String): String? { + val request = GET(amazonUrl, HEADERS) + + val response = CLIENT.newCall(request).execute() + + val document = response.use { it.asJsoup() } + + val smallImage = document.selectFirst("#landingImage")?.attr("src") + + return smallImage?.replace(Regex("""(\._\w+_\.jpg)"""), ".jpg") + } + + fun addPreferenceToScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = "JavCoverFetcherPref" + title = "Fetch HD covers from Amazon" + summary = "Attempts to fetch HD covers from Amazon.\nMay result in incorrect cover." + setDefaultValue(false) + }.also(screen::addPreference) + } + + val SharedPreferences.fetchHDCovers + get() = getBoolean("JavCoverFetcherPref", false) + +} diff --git a/src/all/javguru/build.gradle b/src/all/javguru/build.gradle index fc2ed44a2..af5287fe7 100644 --- a/src/all/javguru/build.gradle +++ b/src/all/javguru/build.gradle @@ -13,11 +13,13 @@ ext { } dependencies { + implementation(project(':lib-streamwish-extractor')) implementation(project(':lib-streamtape-extractor')) implementation(project(':lib-dood-extractor')) implementation(project(':lib-mixdrop-extractor')) implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1" implementation(project(':lib-playlist-utils')) + implementation(project(':lib-javcoverfetcher')) } apply from: "$rootDir/common.gradle" diff --git a/src/all/javguru/src/eu/kanade/tachiyomi/animeextension/all/javguru/JavGuru.kt b/src/all/javguru/src/eu/kanade/tachiyomi/animeextension/all/javguru/JavGuru.kt index 921463216..8eb4d641b 100644 --- a/src/all/javguru/src/eu/kanade/tachiyomi/animeextension/all/javguru/JavGuru.kt +++ b/src/all/javguru/src/eu/kanade/tachiyomi/animeextension/all/javguru/JavGuru.kt @@ -1,8 +1,12 @@ package eu.kanade.tachiyomi.animeextension.all.javguru +import android.app.Application import android.util.Base64 +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.animeextension.all.javguru.extractors.EmTurboExtractor import eu.kanade.tachiyomi.animeextension.all.javguru.extractors.MaxStreamExtractor +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.SAnime @@ -10,12 +14,14 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor +import eu.kanade.tachiyomi.lib.javcoverfetcher.JavCoverFetcher +import eu.kanade.tachiyomi.lib.javcoverfetcher.JavCoverFetcher.fetchHDCovers import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor +import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.util.asJsoup import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -28,9 +34,11 @@ import okhttp3.Request import okhttp3.Response import org.jsoup.select.Elements import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import kotlin.math.min -class JavGuru : AnimeHttpSource() { +class JavGuru : AnimeHttpSource(), ConfigurableAnimeSource { override val name = "Jav Guru" @@ -40,16 +48,15 @@ class JavGuru : AnimeHttpSource() { override val supportsLatest = true - override val client = network.cloudflareClient.newBuilder() - .rateLimit(2) - .build() + override val client = network.cloudflareClient private val noRedirectClient = client.newBuilder() .followRedirects(false) .build() - override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") + private val preference by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } private lateinit var popularElements: Elements @@ -186,9 +193,11 @@ class JavGuru : AnimeHttpSource() { override fun animeDetailsParse(response: Response): SAnime { val document = response.asJsoup() + val javId = document.selectFirst(".infoleft li:contains(code)")?.ownText() + val siteCover = document.select(".large-screenshot img").attr("abs:src") + return SAnime.create().apply { title = document.select(".titl").text() - thumbnail_url = document.select(".large-screenshot img").attr("abs:src") genre = document.select(".infoleft a[rel*=tag]").joinToString { it.text() } author = document.selectFirst(".infoleft li:contains(studio) a")?.text() artist = document.selectFirst(".infoleft li:contains(label) a")?.text() @@ -201,6 +210,11 @@ class JavGuru : AnimeHttpSource() { document.selectFirst(".infoleft li:contains(actor)")?.text()?.let { append("$it\n") } document.selectFirst(".infoleft li:contains(actress)")?.text()?.let { append("$it\n") } } + thumbnail_url = if (preference.fetchHDCovers) { + javId?.let { JavCoverFetcher.getCoverById(it) } ?: siteCover + } else { + siteCover + } } } @@ -257,7 +271,7 @@ class JavGuru : AnimeHttpSource() { .build() val redirectUrl = noRedirectClient.newCall(GET(olidUrl, newHeaders)) - .execute().header("location") + .execute().use { it.header("location") } ?: return null if (redirectUrl.toHttpUrlOrNull() == null) { @@ -267,18 +281,22 @@ class JavGuru : AnimeHttpSource() { return redirectUrl } + private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) } private val streamTapeExtractor by lazy { StreamTapeExtractor(client) } private val doodExtractor by lazy { DoodExtractor(client) } private val mixDropExtractor by lazy { MixDropExtractor(client) } - private val maxStreamExtractor by lazy { MaxStreamExtractor(client) } - private val emTurboExtractor by lazy { EmTurboExtractor(client) } + private val maxStreamExtractor by lazy { MaxStreamExtractor(client, headers) } + private val emTurboExtractor by lazy { EmTurboExtractor(client, headers) } private fun getVideos(hosterUrl: String): List