diff --git a/src/all/supjav/AndroidManifest.xml b/src/all/supjav/AndroidManifest.xml new file mode 100644 index 000000000..5a57cd9af --- /dev/null +++ b/src/all/supjav/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/all/supjav/build.gradle b/src/all/supjav/build.gradle new file mode 100644 index 000000000..bc816b44d --- /dev/null +++ b/src/all/supjav/build.gradle @@ -0,0 +1,22 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +ext { + extName = 'SupJav' + pkgNameSuffix = 'all.supjav' + extClass = '.SupJavFactory' + extVersionCode = 1 + libVersion = '13' + containsNsfw = true +} + +dependencies { + implementation(project(":lib-streamtape-extractor")) + implementation(project(":lib-streamwish-extractor")) + implementation(project(":lib-voe-extractor")) + implementation(project(":lib-playlist-utils")) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/supjav/res/mipmap-hdpi/ic_launcher.png b/src/all/supjav/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..eac90571d Binary files /dev/null and b/src/all/supjav/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/supjav/res/mipmap-mdpi/ic_launcher.png b/src/all/supjav/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..86d64f6bf Binary files /dev/null and b/src/all/supjav/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/supjav/res/mipmap-xhdpi/ic_launcher.png b/src/all/supjav/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d6808563c Binary files /dev/null and b/src/all/supjav/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/supjav/res/mipmap-xxhdpi/ic_launcher.png b/src/all/supjav/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..ca673fde0 Binary files /dev/null and b/src/all/supjav/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/supjav/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/supjav/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..0a22b1dc7 Binary files /dev/null and b/src/all/supjav/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/supjav/src/eu/kanade/tachiyomi/animeextension/all/supjav/SupJav.kt b/src/all/supjav/src/eu/kanade/tachiyomi/animeextension/all/supjav/SupJav.kt new file mode 100644 index 000000000..0f6662281 --- /dev/null +++ b/src/all/supjav/src/eu/kanade/tachiyomi/animeextension/all/supjav/SupJav.kt @@ -0,0 +1,259 @@ +package eu.kanade.tachiyomi.animeextension.all.supjav + +import android.app.Application +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.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.ParsedAnimeHttpSource +import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils +import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor +import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor +import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.select.Elements +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SupJav(override val lang: String = "en") : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "SupJav" + + override val baseUrl = "https://supjav.com" + + override val supportsLatest = false + + override val client = network.cloudflareClient + + override fun headersBuilder() = super.headersBuilder() + .set("Referer", "$baseUrl/") + .set("Origin", baseUrl) + + private val langPath = when (lang) { + "en" -> "" + else -> "/$lang" + } + + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET("$baseUrl$langPath/popular/page/$page", headers) + + override fun popularAnimeSelector() = "div.posts > div.post > a" + + override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + + element.selectFirst("img")!!.run { + title = attr("alt") + thumbnail_url = absUrl("data-original").ifBlank { absUrl("src") } + } + } + + override fun popularAnimeNextPageSelector() = "div.pagination li.active:not(:nth-last-child(2))" + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int): Request { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesSelector(): String { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesFromElement(element: Element): SAnime { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesNextPageSelector(): String? { + throw UnsupportedOperationException("Not used.") + } + + // =============================== Search =============================== + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/$id")) + .asObservableSuccess() + .map(::searchAnimeByIdParse) + } else { + super.fetchSearchAnime(page, query, filters) + } + } + + private fun searchAnimeByIdParse(response: Response): AnimesPage { + val details = animeDetailsParse(response.use { it.asJsoup() }) + return AnimesPage(listOf(details), false) + } + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = + GET("$baseUrl$langPath/?s=$query") + + override fun searchAnimeSelector() = popularAnimeSelector() + + override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element) + + override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector() + + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document) = SAnime.create().apply { + val content = document.selectFirst("div.content > div.post-meta")!! + title = content.selectFirst("h2")!!.text() + thumbnail_url = content.selectFirst("img")?.absUrl("src") + + content.selectFirst("div.cats")?.run { + author = select("p:contains(Maker :) > a").textsOrNull() + artist = select("p:contains(Cast :) > a").textsOrNull() + } + genre = content.select("div.tags > a").textsOrNull() + status = SAnime.COMPLETED + } + + private fun Elements.textsOrNull() = eachText().joinToString().takeUnless(String::isEmpty) + + // ============================== Episodes ============================== + override fun fetchEpisodeList(anime: SAnime): Observable> { + val episode = SEpisode.create().apply { + name = "JAV" + episode_number = 1F + url = anime.url + } + + return Observable.just(listOf(episode)) + } + + override fun episodeListSelector(): String { + throw UnsupportedOperationException("Not used.") + } + + override fun episodeFromElement(element: Element): SEpisode { + throw UnsupportedOperationException("Not used.") + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List