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,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,7 @@
ext {
extName = '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,260 @@
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.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
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 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 popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = "article.top10_animes_item > 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/page/$page/")
override fun latestUpdatesSelector() = "article.main_content_article > a"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "div.paginacao > a.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/anime/$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)
}
override fun getFilterList() = AniDongFilters.FILTER_LIST
private val nonce by lazy {
client.newCall(GET("$baseUrl/?js_global=1&ver=6.2.2")).execute()
.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 searchAnimeParse(response: Response): AnimesPage {
val searchData: SearchResultDto = response.body.string()
.takeIf { it.trim() != "402" }
?.let(json::decodeFromString)
?: return AnimesPage(emptyList(), 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 searchAnimeSelector(): String {
throw UnsupportedOperationException()
}
override fun searchAnimeFromElement(element: Element): SAnime {
throw UnsupportedOperationException()
}
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== 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()?.also {
append("Nomes alternativos: $it\n")
}
doc.selectFirst("div[itemprop=description]")?.text()?.also {
append("\n$it")
}
}
}
// ============================== Episodes ==============================
override fun episodeListSelector(): String {
throw UnsupportedOperationException()
}
override fun episodeFromElement(element: Element): SEpisode {
throw UnsupportedOperationException()
}
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", apiHeaders, body)).execute()
.body.string()
val data = json.decodeFromString<EpisodeListDto>(res)
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}"
}
// ============================ 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()
.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()
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================= 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,124 @@
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
.asSequence()
.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())
}
}