feat(src/all): MissAV & JavGuru, Get hd covers (#2305)

This commit is contained in:
AwkwardPeak7 2023-10-02 21:12:13 +05:00 committed by GitHub
parent 1a4c98f704
commit 50da08725a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 249 additions and 21 deletions

View File

@ -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)
}

View File

@ -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<NetworkHelper>().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)
}

View File

@ -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"

View File

@ -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<Application>().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<Video> {
return runCatching {
when {
hosterUrl.contains("javplaya") -> {
streamWishExtractor.videosFromUrl(hosterUrl)
}
hosterUrl.contains("streamtape") -> {
streamTapeExtractor.videoFromUrl(hosterUrl)
?.let(::listOf) ?: emptyList()
streamTapeExtractor.videoFromUrl(hosterUrl).let(::listOfNotNull)
}
hosterUrl.contains("dood") -> {
@ -304,6 +322,14 @@ class JavGuru : AnimeHttpSource() {
}.getOrDefault(emptyList())
}
override fun List<Video>.sort(): List<Video> {
val quality = preference.getString(PREF_QUALITY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
@ -336,6 +362,19 @@ class JavGuru : AnimeHttpSource() {
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY
title = PREF_QUALITY_TITLE
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
JavCoverFetcher.addPreferenceToScreen(screen)
}
companion object {
const val PREFIX_ID = "id:"
@ -347,6 +386,10 @@ class JavGuru : AnimeHttpSource() {
"mixdrop",
"mixdroop",
)
private const val PREF_QUALITY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720"
}
override fun episodeListParse(response: Response): List<SEpisode> {

View File

@ -4,15 +4,16 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
class EmTurboExtractor(private val client: OkHttpClient) {
class EmTurboExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistExtractor by lazy { PlaylistUtils(client) }
private val playlistExtractor by lazy { PlaylistUtils(client, headers) }
fun getVideos(url: String): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val document = client.newCall(GET(url, headers)).execute().asJsoup()
val script = document.selectFirst("script:containsData(urlplay)")
?.data()

View File

@ -5,15 +5,16 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
class MaxStreamExtractor(private val client: OkHttpClient) {
class MaxStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playListUtils by lazy { PlaylistUtils(client) }
private val playListUtils by lazy { PlaylistUtils(client, headers) }
fun videoFromUrl(url: String): List<Video> {
val document = client.newCall(GET(url)).execute().asJsoup()
val document = client.newCall(GET(url, headers)).execute().asJsoup()
val script = document.selectFirst("script:containsData(function(p,a,c,k,e,d))")
?.data()

View File

@ -14,6 +14,7 @@ ext {
dependencies {
implementation(project(':lib-unpacker'))
implementation(project(':lib-playlist-utils'))
implementation(project(':lib-javcoverfetcher'))
}
apply from: "$rootDir/common.gradle"

View File

@ -10,6 +10,8 @@ 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.AnimeHttpSource
import eu.kanade.tachiyomi.lib.javcoverfetcher.JavCoverFetcher
import eu.kanade.tachiyomi.lib.javcoverfetcher.JavCoverFetcher.fetchHDCovers
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
@ -98,6 +100,9 @@ class MissAV : AnimeHttpSource(), ConfigurableAnimeSource {
override fun animeDetailsParse(response: Response): SAnime {
val document = response.use { it.asJsoup() }
val jpTitle = document.select("div.text-secondary span:contains(title) + span").text()
val siteCover = document.selectFirst("video.player")?.attr("abs:data-poster")
return SAnime.create().apply {
title = document.selectFirst("h1.text-base")!!.text()
genre = document.getInfo("/genres/")
@ -107,8 +112,6 @@ class MissAV : AnimeHttpSource(), ConfigurableAnimeSource {
).joinToString()
artist = document.getInfo("/actresses/")
status = SAnime.COMPLETED
thumbnail_url = document.selectFirst("video.player")?.attr("abs:data-poster")
description = buildString {
document.selectFirst("div.mb-1")?.text()?.also { append("$it\n") }
@ -119,6 +122,11 @@ class MissAV : AnimeHttpSource(), ConfigurableAnimeSource {
.eachText()
.forEach { append("\n$it") }
}
thumbnail_url = if (preferences.fetchHDCovers) {
JavCoverFetcher.getCoverByTitle(jpTitle) ?: siteCover
} else {
siteCover
}
}
}
@ -169,6 +177,8 @@ class MissAV : AnimeHttpSource(), ConfigurableAnimeSource {
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
JavCoverFetcher.addPreferenceToScreen(screen)
}
override fun episodeListParse(response: Response): List<SEpisode> {