diff --git a/src/all/javguru/AndroidManifest.xml b/src/all/javguru/AndroidManifest.xml new file mode 100644 index 000000000..ce6b23f5e --- /dev/null +++ b/src/all/javguru/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/all/javguru/build.gradle b/src/all/javguru/build.gradle new file mode 100644 index 000000000..fe11107fc --- /dev/null +++ b/src/all/javguru/build.gradle @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +ext { + extName = 'Jav Guru' + pkgNameSuffix = 'all.javguru' + extClass = '.JavGuru' + extVersionCode = 1 + isNsfw = true +} + +dependencies { + implementation(project(':lib-streamsb-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')) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/javguru/res/mipmap-hdpi/ic_launcher.png b/src/all/javguru/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..bbcb061b6 Binary files /dev/null and b/src/all/javguru/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/javguru/res/mipmap-mdpi/ic_launcher.png b/src/all/javguru/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..0d256b8e1 Binary files /dev/null and b/src/all/javguru/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/javguru/res/mipmap-xhdpi/ic_launcher.png b/src/all/javguru/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..482f71a66 Binary files /dev/null and b/src/all/javguru/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/javguru/res/mipmap-xxhdpi/ic_launcher.png b/src/all/javguru/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d318247a7 Binary files /dev/null and b/src/all/javguru/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/javguru/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/javguru/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4e21545ca Binary files /dev/null and b/src/all/javguru/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/javguru/res/web_hi_res_512.png b/src/all/javguru/res/web_hi_res_512.png new file mode 100644 index 000000000..1e1e735b7 Binary files /dev/null and b/src/all/javguru/res/web_hi_res_512.png differ 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 new file mode 100644 index 000000000..190730a2e --- /dev/null +++ b/src/all/javguru/src/eu/kanade/tachiyomi/animeextension/all/javguru/JavGuru.kt @@ -0,0 +1,335 @@ +package eu.kanade.tachiyomi.animeextension.all.javguru + +import android.util.Base64 +import eu.kanade.tachiyomi.animeextension.all.javguru.extractors.MaxStreamExtractor +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.AnimesPage +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.doodextractor.DoodExtractor +import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor +import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor +import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor +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 okhttp3.Call +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.Request +import okhttp3.Response +import org.jsoup.select.Elements +import rx.Observable +import kotlin.math.min + +class JavGuru : AnimeHttpSource() { + + override val name = "Jav Guru" + + override val baseUrl = "https://jav.guru" + + override val lang = "all" + + override val supportsLatest = true + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(2) + .build() + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + private val streamSbExtractor: StreamSBExtractor by lazy { + StreamSBExtractor(client) + } + + private val streamTapeExtractor: StreamTapeExtractor by lazy { + StreamTapeExtractor(client) + } + + private val doodExtractor: DoodExtractor by lazy { + DoodExtractor(client) + } + + private val mixDropExtractor: MixDropExtractor by lazy { + MixDropExtractor(client) + } + + private val maxStreamExtractor: MaxStreamExtractor by lazy { + MaxStreamExtractor(client) + } + + private lateinit var popularElements: Elements + + override fun fetchPopularAnime(page: Int): Observable { + return if (page == 1) { + client.newCall(popularAnimeRequest(page)) + .asObservableSuccess() + .map(::popularAnimeParse) + } else { + Observable.just(cachedPopularAnimeParse(page)) + } + } + + override fun popularAnimeRequest(page: Int) = + GET("$baseUrl/most-watched-rank/", headers) + + override fun popularAnimeParse(response: Response): AnimesPage { + popularElements = response.asJsoup().select(".tabcontent li") + + return cachedPopularAnimeParse(1) + } + + private fun cachedPopularAnimeParse(page: Int): AnimesPage { + val end = min(page * 20, popularElements.size) + val entries = popularElements.subList((page - 1) * 20, end).map { element -> + SAnime.create().apply { + element.select("a").let { a -> + getIDFromUrl(a)?.let { url = it } + ?: setUrlWithoutDomain(a.attr("href")) + + title = a.text() + thumbnail_url = a.select("img").attr("abs:src") + } + } + } + return AnimesPage(entries, end < popularElements.size) + } + + override fun latestUpdatesRequest(page: Int): Request { + val url = baseUrl + if (page > 1) "/page/$page/" else "" + + return GET(url, headers) + } + + override fun latestUpdatesParse(response: Response): AnimesPage { + val document = response.asJsoup() + + val entries = document.select("div.site-content div.inside-article:not(:contains(nothing))").map { element -> + SAnime.create().apply { + element.select("a").let { a -> + getIDFromUrl(a)?.let { url = it } + ?: setUrlWithoutDomain(a.attr("href")) + } + thumbnail_url = element.select("img").attr("abs:src") + title = element.select("h2 > a").text() + } + } + + val page = document.location() + .substringBeforeLast("/").toHttpUrlOrNull() + ?.pathSegments?.last()?.toIntOrNull() ?: 1 + + val lastPage = document.select("div.wp-pagenavi a") + .last()?.attr("href")?.substringBeforeLast("/") + ?.toHttpUrlOrNull()?.pathSegments?.last()?.toIntOrNull() ?: 1 + + return AnimesPage(entries, page < lastPage) + } + + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + if (query.startsWith(PREFIX_ID)) { + val id = query.substringAfter(PREFIX_ID) + if (id.toIntOrNull() == null) { + return Observable.just(AnimesPage(emptyList(), false)) + } + val url = "/$id/" + val tempAnime = SAnime.create().apply { this.url = url } + return fetchAnimeDetails(tempAnime).map { + val anime = it.apply { this.url = url } + AnimesPage(listOf(anime), false) + } + } else if (query.isNotEmpty()) { + return client.newCall(searchAnimeRequest(page, query, filters)) + .asObservableSuccess() + .map(::searchAnimeParse) + } else { + filters.forEach { filter -> + when (filter) { + is TagFilter, + is CategoryFilter, + -> { + if (filter.state != 0) { + val url = "$baseUrl${filter.toUrlPart()}" + if (page > 1) "page/$page/" else "" + val request = GET(url, headers) + return client.newCall(request) + .asObservableIgnoreCode(404) + .map(::searchAnimeParse) + } + } + is ActressFilter, + is ActorFilter, + is StudioFilter, + is MakerFilter, + -> { + if ((filter.state as String).isNotEmpty()) { + val url = "$baseUrl${filter.toUrlPart()}" + if (page > 1) "page/$page/" else "" + val request = GET(url, headers) + return client.newCall(request) + .asObservableIgnoreCode(404) + .map(::searchAnimeParse) + } + } + else -> { } + } + } + } + + throw Exception("Select at least one Filter") + } + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + if (page > 1) addPathSegments("page/$page/") + addQueryParameter("s", query) + }.build().toString() + + return GET(url, headers) + } + + override fun getFilterList() = getFilters() + + override fun searchAnimeParse(response: Response) = latestUpdatesParse(response) + + override fun animeDetailsParse(response: Response): SAnime { + val document = response.asJsoup() + + 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() + status = SAnime.COMPLETED + description = buildString { + document.selectFirst(".infoleft li:contains(code)")?.text()?.let { append("$it\n") } + document.selectFirst(".infoleft li:contains(director)")?.text()?.let { append("$it\n") } + document.selectFirst(".infoleft li:contains(studio)")?.text()?.let { append("$it\n") } + document.selectFirst(".infoleft li:contains(label)")?.text()?.let { append("$it\n") } + document.selectFirst(".infoleft li:contains(actor)")?.text()?.let { append("$it\n") } + document.selectFirst(".infoleft li:contains(actress)")?.text()?.let { append("$it\n") } + } + } + } + + override fun fetchEpisodeList(anime: SAnime): Observable> { + return Observable.just( + listOf( + SEpisode.create().apply { + url = anime.url + name = "Episode" + }, + ), + ) + } + + override fun videoListParse(response: Response): List