feat(src/all): New source: SupJav (#2614)

This commit is contained in:
Claudemirovsky 2023-12-06 08:48:22 -03:00 committed by GitHub
parent 82c0ff0c47
commit 6639a45005
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 355 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.supjav.SupJavUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="supjav.com"
android:pathPattern="/..*\\.html"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -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<Application>().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<AnimesPage> {
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<List<SEpisode>> {
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<Video> {
val doc = response.use { it.asJsoup() }
val players = doc.select("div.btnst > a").toList()
.filter { it.text() in SUPPORTED_PLAYERS }
.map { it.text() to it.attr("data-link").reversed() }
return players.parallelCatchingFlatMap(::videosFromPlayer)
}
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val protectorHeaders by lazy {
super.headersBuilder().set("referer", "$PROTECTOR_URL/").build()
}
private val noRedirectClient by lazy {
client.newBuilder().followRedirects(false).build()
}
private fun videosFromPlayer(player: Pair<String, String>): List<Video> {
val (hoster, id) = player
val url = noRedirectClient.newCall(GET("$PROTECTOR_URL/supjav.php?c=$id", protectorHeaders)).execute()
.use { it.headers["location"] }
?: return emptyList()
return when (hoster) {
"ST" -> streamtapeExtractor.videosFromUrl(url)
"VOE" -> voeExtractor.videosFromUrl(url)
"FST" -> streamwishExtractor.videosFromUrl(url)
"TV" -> {
val body = client.newCall(GET(url)).execute().use { it.body.string() }
val playlistUrl = body.substringAfter("var urlPlay = '", "")
.substringBefore("';")
.takeUnless(String::isEmpty)
?: return emptyList()
playlistUtils.extractFromHls(playlistUrl, url, videoNameGen = { "TV - $it" })
.distinctBy { it.videoUrl }
}
else -> emptyList()
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException("Not used.")
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException("Not used.")
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used.")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_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()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
runBlocking {
map {
async(Dispatchers.Default) {
runCatching { f(it) }.getOrElse { emptyList() }
}
}.awaitAll().flatten()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val PROTECTOR_URL = "https://lk1.supremejav.com/supjav.php"
private val SUPPORTED_PLAYERS = setOf("TV", "FST", "VOE", "ST")
private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Preferred video quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
}
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.animeextension.all.supjav
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
class SupJavFactory : AnimeSourceFactory {
override fun createSources() = listOf(
SupJav("en"),
SupJav("ja"),
SupJav("zh"),
)
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.all.supjav
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://supjav.com/<language>/<item> intents
* and redirects them to the main Aniyomi process.
*/
class SupJavUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments ?: return
if (pathSegments.isNotEmpty()) {
val path = pathSegments.joinToString("/")
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${SupJav.PREFIX_SEARCH}$path")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}