forked from AlmightyHak/extensions-source
Initial commit
This commit is contained in:
commit
98ed7e8839
2263 changed files with 108711 additions and 0 deletions
23
src/pt/anidong/AndroidManifest.xml
Normal file
23
src/pt/anidong/AndroidManifest.xml
Normal 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>
|
7
src/pt/anidong/build.gradle
Normal file
7
src/pt/anidong/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AniDong'
|
||||
extClass = '.AniDong'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/pt/anidong/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/pt/anidong/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
src/pt/anidong/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/pt/anidong/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
src/pt/anidong/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/pt/anidong/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
src/pt/anidong/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/pt/anidong/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
src/pt/anidong/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/pt/anidong/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
|
@ -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:"
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue