feat(src/pt): New source: AniDong (#1782)

This commit is contained in:
Claudemirovsky
2023-06-26 06:46:10 +00:00
committed by GitHub
parent c2d9e2498d
commit 65bdbd5a05
11 changed files with 512 additions and 0 deletions

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.anidong.AniDongUrlActivity"
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="anidong.net"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext {
extName = 'AniDong'
pkgNameSuffix = 'pt.anidong'
extClass = '.AniDong'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,258 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.EpisodeListDto
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.SearchResultDto
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.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
class AniDong : ParsedAnimeHttpSource() {
override val name = "AniDong"
override val baseUrl = "https://anidong.net"
override val lang = "pt-BR"
override val supportsLatest = true
private val json: Json by injectLazy()
private val apiHeaders by lazy {
headersBuilder() // sets user-agent
.add("Referer", baseUrl)
.add("x-requested-with", "XMLHttpRequest")
.build()
}
// ============================== Popular ===============================
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
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = "article.top10_animes_item > a"
// ============================== Episodes ==============================
override fun episodeFromElement(element: Element): SEpisode {
throw UnsupportedOperationException("Not used.")
}
override fun episodeListSelector(): String {
throw UnsupportedOperationException("Not used.")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val id = doc.selectFirst("link[rel=shortlink]")!!.attr("href").substringAfter("=")
val body = FormBody.Builder()
.add("action", "show_videos")
.add("anime_id", id)
.build()
val res = client.newCall(POST("$baseUrl/api", headers = apiHeaders, body = body)).execute()
val data = json.decodeFromString<EpisodeListDto>(res.body.string())
return buildList {
data.episodes.forEach { add(episodeFromObject(it, "Episódio")) }
data.movies.forEach { add(episodeFromObject(it, "Filme")) }
data.ovas.forEach { add(episodeFromObject(it, "OVA")) }
sortByDescending { it.episode_number }
}
}
private fun episodeFromObject(episode: EpisodeDto, prefix: String) = SEpisode.create().apply {
setUrlWithoutDomain(episode.epi_url)
episode_number = episode.epi_num.toFloatOrNull() ?: 0F
name = "$prefix ${episode.epi_num}"
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
val infos = doc.selectFirst("div.anime_infos")!!
setUrlWithoutDomain(doc.location())
title = infos.selectFirst("div > h3")!!.ownText()
thumbnail_url = infos.selectFirst("img")!!.attr("src")
genre = infos.select("div[itemprop=genre] a").eachText().joinToString()
artist = infos.selectFirst("div[itemprop=productionCompany]")!!.text()
status = doc.selectFirst("div:contains(Status) span")?.text().let {
when {
it == null -> SAnime.UNKNOWN
it == "Completo" -> SAnime.COMPLETED
it.contains("Lançamento") -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
description = buildString {
infos.selectFirst("div.anime_name + div.anime_info")?.text()?.let {
append("Nomes alternativos: $it\n")
}
doc.selectFirst("div[itemprop=description]")?.text()?.let {
append("\n$it")
}
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
return doc.select("div.player_option").flatMap {
val url = it.attr("data-playerlink")
val playerName = it.text().trim()
videosFromUrl(url, playerName)
}
}
private fun videosFromUrl(url: String, playerName: String): List<Video> {
val scriptData = client.newCall(GET(url, apiHeaders)).execute()
.use { it.asJsoup() }
.selectFirst("script:containsData(sources)")
?.data() ?: return emptyList()
return scriptData.substringAfter("sources: [").substringBefore("]")
.split("{")
.drop(1)
.map {
val videoUrl = it.substringAfter("file: \"").substringBefore('"')
val label = it.substringAfter("label: \"", "Unknown").substringBefore('"')
val quality = "$playerName - $label"
Video(videoUrl, quality, videoUrl, headers = apiHeaders)
}
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException("Not used.")
}
override fun videoListSelector(): String {
throw UnsupportedOperationException("Not used.")
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used.")
}
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element): SAnime {
throw UnsupportedOperationException("Not used.")
}
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException("Not used.")
}
override fun searchAnimeParse(response: Response): AnimesPage {
val searchData: SearchResultDto = response.use { it.body.string() }
.takeIf { it.trim() != "402" }
?.let(json::decodeFromString)
?: return AnimesPage(emptyList<SAnime>(), false)
val animes = searchData.animes.map {
SAnime.create().apply {
setUrlWithoutDomain(it.url)
title = it.title
thumbnail_url = it.thumbnail_url
}
}
val hasNextPage = searchData.pages > 1 && searchData.animes.size == 10
return AnimesPage(animes, hasNextPage)
}
override fun getFilterList() = AniDongFilters.FILTER_LIST
private val nonce by lazy {
client.newCall(GET("$baseUrl/?js_global=1&ver=6.2.2")).execute()
.use { it.body.string() }
.substringAfter("search_nonce")
.substringAfter("'")
.substringBefore("'")
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AniDongFilters.getSearchParameters(filters)
val body = FormBody.Builder()
.add("letra", "")
.add("action", "show_animes_ajax")
.add("nome", query)
.add("status", params.status)
.add("formato", params.format)
.add("search_nonce", nonce)
.add("paged", page.toString())
.apply {
params.genres.forEach { add("generos[]", it) }
}.build()
return POST("$baseUrl/wp-admin/admin-ajax.php", headers = apiHeaders, body = body)
}
override fun searchAnimeSelector(): String {
throw UnsupportedOperationException("Not used.")
}
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/anime/$id"))
.asObservableSuccess()
.map(::searchAnimeByIdParse)
} else {
super.fetchSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
// =============================== Latest ===============================
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "div.paginacao > a.next"
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page/")
override fun latestUpdatesSelector() = "article.main_content_article > a"
// ============================= Utilities ==============================
private fun getRealDoc(document: Document): Document {
if (!document.location().contains("/video/")) return document
return document.selectFirst(".episodioControleItem:has(i.ri-grid-fill)")?.let {
client.newCall(GET(it.attr("href"), headers)).execute().asJsoup()
} ?: document
}
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View File

@ -0,0 +1,123 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniDongFilters {
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 CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
.toList()
}
class StatusFilter : QueryPartFilter("Status", AniDongFiltersData.STATUS_LIST)
class FormatFilter : QueryPartFilter("Formato", AniDongFiltersData.FORMAT_LIST)
class GenresFilter : CheckBoxFilterList("Gêneros", AniDongFiltersData.GENRES_LIST)
val FILTER_LIST get() = AnimeFilterList(
StatusFilter(),
FormatFilter(),
GenresFilter(),
)
data class FilterSearchParams(
val status: String = "",
val format: String = "",
val genres: List<String> = emptyList(),
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<FormatFilter>(),
filters.parseCheckbox<GenresFilter>(AniDongFiltersData.GENRES_LIST),
)
}
private object AniDongFiltersData {
private val SELECT = Pair("<Selecione>", "")
val STATUS_LIST = arrayOf(
SELECT,
Pair("Lançamento", "Lançamento"),
Pair("Completo", "Completo"),
)
val FORMAT_LIST = arrayOf(
SELECT,
Pair("Donghua", "Anime"),
Pair("Filme", "Filme"),
)
val GENRES_LIST = arrayOf(
Pair("Artes Marciais", "9"),
Pair("Aventura", "6"),
Pair("Ação", "2"),
Pair("Boys Love", "43"),
Pair("Comédia", "15"),
Pair("Corrida", "94"),
Pair("Cultivo", "12"),
Pair("Demônios", "18"),
Pair("Detetive", "24"),
Pair("Drama", "16"),
Pair("Escolar", "77"),
Pair("Espaço", "54"),
Pair("Esporte", "95"),
Pair("Fantasia", "7"),
Pair("Guerra", "26"),
Pair("Harém", "17"),
Pair("Histórico", "8"),
Pair("Horror", "44"),
Pair("Isekai", "72"),
Pair("Jogo", "25"),
Pair("Mecha", "40"),
Pair("Militar", "21"),
Pair("Mistério", "3"),
Pair("Mitolgia", "96"),
Pair("Mitologia", "19"),
Pair("O Melhor Donghua", "91"),
Pair("Polícia", "57"),
Pair("Política", "63"),
Pair("Psicológico", "33"),
Pair("Reencarnação", "30"),
Pair("Romance", "11"),
Pair("Sci-Fi", "39"),
Pair("Slice of Life", "84"),
Pair("Sobrenatural", "4"),
Pair("Super Poder", "67"),
Pair("Suspense", "32"),
Pair("Tragédia", "58"),
Pair("Vampiro", "82"),
)
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong
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://anidong.net/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AniDongUrlActivity : 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", "${AniDong.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,53 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable
data class SearchResultDto(
val animes: List<AnimeDto>,
@SerialName("total_pages")
val pages: Int,
)
@Serializable
data class AnimeDto(
@SerialName("anime_capa")
val thumbnail_url: String,
@SerialName("anime_permalink")
val url: String,
@SerialName("anime_title")
val title: String,
)
@Serializable
data class EpisodeListDto(
@Serializable(with = EpisodeListSerializer::class)
@SerialName("episodios")
val episodes: List<EpisodeDto>,
@Serializable(with = EpisodeListSerializer::class)
@SerialName("filmes")
val movies: List<EpisodeDto>,
@Serializable(with = EpisodeListSerializer::class)
val ovas: List<EpisodeDto>,
)
@Serializable
data class EpisodeDto(
val epi_num: String,
val epi_url: String,
)
object EpisodeListSerializer :
JsonTransformingSerializer<List<EpisodeDto>>(ListSerializer(EpisodeDto.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement =
when (element) {
is JsonObject -> JsonArray(element.values.toList())
else -> JsonArray(emptyList())
}
}