Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 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=".pt.donghuanosekai.DonghuaNoSekaiUrlActivity"
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="donghuanosekai.com"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,7 @@
ext {
extName = 'Donghua no Sekai'
extClass = '.DonghuaNoSekai'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View file

@ -0,0 +1,288 @@
package eu.kanade.tachiyomi.animeextension.pt.donghuanosekai
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.donghuanosekai.extractors.DonghuaNoSekaiExtractor
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.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Donghua no Sekai"
override val baseUrl = "https://donghuanosekai.com"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl)
.add("Origin", baseUrl)
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
override fun popularAnimeSelector() = "div.sidebarContent div.navItensTop li > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.attr("src")
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos?pagina=$page", headers)
override fun latestUpdatesSelector() = "div.boxContent div.itemE > a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.title h3")!!.text()
thumbnail_url = element.selectFirst("div.thumb img")?.attr("src")
}
override fun latestUpdatesNextPageSelector() = "ul.content-pagination > li.next"
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
@Serializable
data class SearchResponseDto(
val results: List<String>,
val page: Int,
val total_page: Int = 1,
)
private val searchToken by lazy {
client.newCall(GET("$baseUrl/donghuas", headers)).execute()
.asJsoup()
.selectFirst("div.menu_filter_box")!!
.attr("data-secury")
}
override fun getFilterList() = DonghuaNoSekaiFilters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = DonghuaNoSekaiFilters.getSearchParameters(filters)
val body = FormBody.Builder().apply {
add("type", "lista")
add("action", "getListFilter")
add("limit", "30")
add("token", searchToken)
add("search", query.ifBlank { "0" })
add("pagina", "$page")
val filterData = baseUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("filter_animation", params.animation)
addQueryParameter("filter_audio", "undefined")
addQueryParameter("filter_letter", params.letter)
addQueryParameter("filter_order", params.orderBy)
addQueryParameter("filter_status", params.status)
addQueryParameter("type_url", "ONA")
}.build().encodedQuery.orEmpty()
val genres = params.genres.joinToString { "\"$it\"" }
val delgenres = params.deleted_genres.joinToString { "\"$it\"" }
add("filters", """{"filter_data": "$filterData", "filter_genre_add": [$genres], "filter_genre_del": [$delgenres]}""")
}.build()
return POST("$baseUrl/wp-admin/admin-ajax.php", body = body, headers = headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
return runCatching {
val data = response.parseAs<SearchResponseDto>()
val animes = data.results.map(Jsoup::parse)
.mapNotNull { it.selectFirst(searchAnimeSelector()) }
.map(::searchAnimeFromElement)
val hasNext = data.total_page > data.page
AnimesPage(animes, hasNext)
}.getOrElse { AnimesPage(emptyList(), false) }
}
override fun searchAnimeSelector() = "div.itemE > a"
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
thumbnail_url = doc.selectFirst("div.poster > img")?.attr("src")
val infos = doc.selectFirst("div.dados")!!
title = infos.selectFirst("h1")!!.text()
genre = infos.select("div.genresL > a").eachText().joinToString()
artist = infos.selectFirst("ul > li:contains(Estúdio)")?.ownText()
author = infos.selectFirst("ul > li:contains(Fansub)")?.ownText()
status = infos.selectFirst("ul > li:contains(Status)")?.ownText().parseStatus()
description = buildString {
doc.select("div.articleContent:has(div:contains(Sinopse)) > div.context > p")
.eachText()
.joinToString("\n\n")
.also(::append)
append("\n")
infos.select("ul.b_flex > li")
.eachText()
.forEach { append("\n$it") }
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
return doc.select(episodeListSelector()).map(::episodeFromElement)
}
override fun episodeListSelector() = "div.episode_list > div.item > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("span.episode")!!.text().also {
name = it
episode_number = it.substringAfterLast(" ").toFloatOrNull() ?: 0F
}
date_upload = element.selectFirst("div.data")?.text().orEmpty().toDate()
}
// ============================ Video Links =============================
private val extractor by lazy { DonghuaNoSekaiExtractor(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
return doc.select("div.slideItem[data-video-url]").parallelCatchingFlatMapBlocking {
client.newCall(GET(it.attr("data-video-url"), headers)).await()
.asJsoup()
.let(extractor::videosFromDocument)
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_VALUES
entryValues = PREF_QUALITY_VALUES
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 fun getRealDoc(document: Document): Document {
return document.selectFirst("div.controles li.list-ep > a")?.let { link ->
client.newCall(GET(link.attr("href")))
.execute()
.asJsoup()
} ?: document
}
private fun String?.parseStatus() = when (this?.run { trim().lowercase() }) {
"completo" -> SAnime.COMPLETED
"em lançamento" -> SAnime.ONGOING
"em pausa" -> SAnime.ON_HIATUS
else -> SAnime.UNKNOWN
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
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 val DATE_FORMATTER by lazy {
SimpleDateFormat("MMMM dd, yyyy", Locale("pt", "BR"))
}
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_VALUES = arrayOf("480p", "720p", "1080p")
}
}

View file

@ -0,0 +1,163 @@
package eu.kanade.tachiyomi.animeextension.pt.donghuanosekai
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object DonghuaNoSekaiFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class TriStateFilterList(name: String, values: List<TriFilterVal>) : AnimeFilter.Group<TriState>(name, values)
class TriFilterVal(name: String) : TriState(name)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (first { it is R } as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.parseTriFilter(
options: Array<Pair<String, String>>,
): List<List<String>> {
return (first { it is R } as TriStateFilterList).state
.filterNot { it.isIgnored() }
.map { filter -> filter.state to options.find { it.first == filter.name }!!.second }
.groupBy { it.first } // group by state
.let { dict ->
val included = dict.get(TriState.STATE_INCLUDE)?.map { it.second }.orEmpty()
val excluded = dict.get(TriState.STATE_EXCLUDE)?.map { it.second }.orEmpty()
listOf(included, excluded)
}
}
class LetterFilter : QueryPartFilter("Primeira letra", DonghuaNoSekaiFiltersData.LETTERS)
class OrderFilter : QueryPartFilter("Ordenar por", DonghuaNoSekaiFiltersData.ORDERS)
class StatusFilter : QueryPartFilter("Status", DonghuaNoSekaiFiltersData.STATUS)
class AnimationFilter : QueryPartFilter("Tipo de animação", DonghuaNoSekaiFiltersData.ANIMATIONS)
class GenresFilter : TriStateFilterList(
"Gêneros",
DonghuaNoSekaiFiltersData.GENRES.map { TriFilterVal(it.first) },
)
val FILTER_LIST get() = AnimeFilterList(
LetterFilter(),
OrderFilter(),
StatusFilter(),
AnimationFilter(),
AnimeFilter.Separator(),
GenresFilter(),
)
data class FilterSearchParams(
val letter: String = "0",
val orderBy: String = "name",
val status: String = "all",
val animation: String = "all",
val genres: List<String> = emptyList(),
val deleted_genres: List<String> = emptyList(),
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val (added, deleted) = filters.parseTriFilter<GenresFilter>(DonghuaNoSekaiFiltersData.GENRES)
return FilterSearchParams(
filters.asQueryPart<LetterFilter>(),
filters.asQueryPart<OrderFilter>(),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<AnimationFilter>(),
added,
deleted,
)
}
private object DonghuaNoSekaiFiltersData {
val EVERY = Pair(" <Selecione> ", "all")
val LETTERS = arrayOf(Pair("Selecione", "0")) + ('A'..'Z').map {
Pair(it.toString(), it.toString().lowercase())
}.toTypedArray()
val ORDERS = arrayOf(
Pair("Nome", "name"),
Pair("Data", "new"),
)
val ANIMATIONS = arrayOf(
EVERY,
Pair("2d", "2d"),
Pair("3d", "3d"),
)
val STATUS = arrayOf(
EVERY,
Pair("Completo", "Completed"),
Pair("Em Breve", "Upcoming"),
Pair("Em Lançamento", "Ongoing"),
Pair("Em Pausado", "Hiatus"),
)
val GENRES = arrayOf(
Pair("Artes Marciais", "54"),
Pair("Aventura", "4"),
Pair("Ação", "2"),
Pair("Boys Love", "208"),
Pair("Carros", "408"),
Pair("Comédia", "16"),
Pair("Corrida", "406"),
Pair("Crime", "392"),
Pair("Cultivo", "5"),
Pair("Demônios", "52"),
Pair("Drama", "26"),
Pair("Ecchi", "104"),
Pair("Esporte", "407"),
Pair("Fantasia", "6"),
Pair("Ficção científica", "94"),
Pair("Guerra", "22"),
Pair("Harém Reverso", "364"),
Pair("Harém", "111"),
Pair("Histórico", "11"),
Pair("Horror", "404"),
Pair("Isekai", "105"),
Pair("Jogo", "98"),
Pair("Josei", "363"),
Pair("Magia", "17"),
Pair("Mecha", "93"),
Pair("Militar", "87"),
Pair("Mistério", "149"),
Pair("Mitologia", "438"),
Pair("Paródia", "443"),
Pair("Politica", "71"),
Pair("Polícia", "223"),
Pair("Psicológico", "285"),
Pair("Reencarnação", "249"),
Pair("Romance", "12"),
Pair("Seinen", "163"),
Pair("Shoujo", "203"),
Pair("Shounen Ai", "176"),
Pair("Shounen", "33"),
Pair("Slice of Life", "106"),
Pair("Sobrenatural", "101"),
Pair("Super Poder", "127"),
Pair("Supernatural", "30"),
Pair("Suspense", "283"),
Pair("Terror", "229"),
Pair("Thriller", "34"),
Pair("Tragédia", "165"),
Pair("Vampiro", "135"),
Pair("Vida Escolar", "18"),
Pair("Violência", "252"),
Pair("Wuxia", "254"),
Pair("Xuanhuan", "256"),
Pair("Yaoi", "173"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.donghuanosekai
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://donghuanosekai.com/<item> intents
* and redirects them to the main Aniyomi process.
*/
class DonghuaNoSekaiUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${DonghuaNoSekai.PREFIX_SEARCH}$item")
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)
}
}

View file

@ -0,0 +1,71 @@
package eu.kanade.tachiyomi.animeextension.pt.donghuanosekai.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
class DonghuaNoSekaiExtractor(
private val client: OkHttpClient,
private val headers: Headers,
) {
fun videosFromDocument(document: Document): List<Video> {
val iframe = document.selectFirst("iframe")
val playerId = document.location().toHttpUrl()
.queryParameter("type")
?.toIntOrNull()?.plus(1) ?: 1
val playerName = "Player $playerId"
return when (iframe) {
null -> {
val source = document.selectFirst("video > source") ?: return emptyList()
val quality = source.attr("size") + "p"
val url = source.attr("src")
listOf(Video(url, "$playerName - $quality", url, headers))
}
else -> {
val iframeUrl = iframe.attr("src")
when {
iframeUrl.contains("nativov2.php") || iframeUrl.contains("/embed2/") -> {
val url = iframeUrl.toHttpUrl().run {
queryParameter("id") ?: queryParameter("v")
} ?: return emptyList()
val quality = url.substringAfter("_").substringBefore("_")
listOf(Video(url, "$playerName - $quality", url, headers))
}
else -> getVideosFromIframeUrl(iframeUrl, playerName)
}
}
}
}
private fun getVideosFromIframeUrl(iframeUrl: String, playerName: String): List<Video> {
return when {
iframeUrl.contains("playerB.php") -> {
client.newCall(GET(iframeUrl, headers)).execute().body.string()
.substringAfter("sources:")
.substringBefore("]")
.split("{")
.drop(1)
.map { line ->
val url = line.substringAfter("file: \"").substringBefore('"')
val quality = line.substringAfter("label: \"")
.substringBefore('"')
.run {
when (this) {
"SD" -> "480p"
"HD" -> "720p"
"FHD", "FULLHD" -> "1080p"
else -> this
}
}
Video(url, "$playerName - $quality", url, headers)
}
}
else -> emptyList()
}
}
}