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.animesdigital.AnimesDigitalUrlActivity"
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="animesdigital.org"
android:pathPattern="/anime/a/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,11 @@
ext {
extName = 'Animes Digital'
extClass = '.AnimesDigital'
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:unpacker"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -0,0 +1,298 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors.ProtectorExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors.ScriptExtractor
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.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
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
class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Animes Digital"
override val baseUrl = "https://animesdigital.org"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = latestUpdatesSelector()
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page")
override fun latestUpdatesSelector() = "div.b_flex:nth-child(2) > div.itemE > a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")!!.let {
it.attr("data-lazy-src").ifEmpty { it.attr("src") }
}
title = element.selectFirst("span.title_anime")!!.text()
}
override fun latestUpdatesNextPageSelector() = "ul > li.next"
// =============================== Search ===============================
override fun getFilterList() = AnimesDigitalFilters.FILTER_LIST
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/a/$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)
}
private val searchToken by lazy {
client.newCall(GET("$baseUrl/animes-legendado")).execute().asJsoup()
.selectFirst("div.menu_filter_box")!!
.attr("data-secury")
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimesDigitalFilters.getSearchParameters(filters)
val body = FormBody.Builder().apply {
add("type", "lista")
add("limit", "30")
add("token", searchToken)
if (query.isNotEmpty()) {
add("search", query)
}
add("pagina", "$page")
val filterData = baseUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("type_url", params.type)
addQueryParameter("filter_audio", params.audio)
addQueryParameter("filter_letter", params.initialLetter)
addQueryParameter("filter_order", "name")
}.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/func/listanime", body = body, headers = headers)
}
override fun searchAnimeSelector() = "div.itemA > a"
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeParse(response: Response): AnimesPage {
return runCatching {
val data = response.parseAs<SearchResponseDto>()
val animes = data.results.map(Jsoup::parseBodyFragment)
.mapNotNull { it.selectFirst(searchAnimeSelector()) }
.map(::searchAnimeFromElement)
val hasNext = data.total_page > data.page
AnimesPage(animes, hasNext)
}.getOrElse { AnimesPage(emptyList(), false) }
}
@Serializable
data class SearchResponseDto(
val results: List<String>,
val page: Int,
val total_page: Int,
)
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("data-lazy-src")
status = when (doc.selectFirst("div.clw > div.playon")?.text()) {
"Em Lançamento" -> SAnime.ONGOING
"Completo" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
with(doc.selectFirst("div.crw > div.dados")!!) {
artist = getInfo("Estúdio")
author = getInfo("Autor") ?: getInfo("Diretor")
title = selectFirst("h1")!!.text()
genre = select("div.genre a").eachText().joinToString()
description = selectFirst("div.sinopse")?.text()
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val pagination = doc.selectFirst("ul.content-pagination")
return if (pagination != null) {
val episodes = mutableListOf<SEpisode>()
episodes += doc.select(episodeListSelector()).map(::episodeFromElement)
val lastPage = doc.selectFirst("ul.content-pagination > li:nth-last-child(2) > span")!!.text().toInt()
for (i in 2..lastPage) {
val request = GET(doc.location() + "/page/$i", headers)
val res = client.newCall(request).execute()
val pageDoc = res.asJsoup()
episodes += pageDoc.select(episodeListSelector()).map(::episodeFromElement)
}
episodes
} else {
doc.select(episodeListSelector()).map(::episodeFromElement)
}
}
override fun episodeListSelector() = "div.item_ep > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val epname = element.selectFirst("div.episode")!!.text()
episode_number = epname.substringAfterLast(" ").toFloatOrNull() ?: 1F
name = buildString {
append(epname)
element.selectFirst("div.sub_title")?.text()?.also {
if (!it.contains("Ainda não tem um titulo oficial")) {
append(" - ", it)
}
}
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val player = response.asJsoup().selectFirst("div#player")!!
return player.select("div.tab-video").flatMap { div ->
val noComment = div.outerHtml().replace("<!--", "").replace("-->", "")
val newDoc = Jsoup.parseBodyFragment(noComment)
newDoc.select(videoListSelector()).ifEmpty { newDoc.select("a") }.flatMap { element ->
runCatching {
videosFromElement(element)
}.onFailure { it.printStackTrace() }.getOrElse { emptyList() }
}
}
}
private val protectorExtractor by lazy { ProtectorExtractor(client) }
private fun videosFromElement(element: Element): List<Video> {
return when (element.tagName()) {
"iframe" -> {
val url = element.absUrl("data-lazy-src").ifEmpty { element.absUrl("src") }
client.newCall(GET(url, headers)).execute()
.asJsoup()
.select(videoListSelector())
.flatMap(::videosFromElement)
}
"script" -> ScriptExtractor.videosFromScript(element.data(), headers)
"a" -> protectorExtractor.videosFromUrl(element.attr("href"))
else -> emptyList()
}
}
private val scriptSelectors = listOf("eval", "player.src", "this.src", "sources:")
.joinToString { "script:containsData($it):not(:containsData(/bg.mp4))" }
override fun videoListSelector() = "iframe, $scriptSelectors"
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_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 ==============================
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()
}
private fun getRealDoc(document: Document): Document {
return document.selectFirst("div.subitem > a:contains(menu)")?.let { link ->
client.newCall(GET(link.attr("href")))
.execute()
.asJsoup()
} ?: document
}
private fun Element.getInfo(key: String): String? {
return selectFirst("div.info:has(span:containsOwn($key))")?.run {
ownText()
.trim()
.takeUnless { it.isBlank() || it == "?" }
}
}
companion object {
const val PREFIX_SEARCH = "id:"
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_ENTRIES = arrayOf("360p", "480p", "720p")
}
}

View file

@ -0,0 +1,235 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimesDigitalFilters {
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 InitialLetterFilter : QueryPartFilter("Primeira letra", AnimesDigitalFiltersData.INITIAL_LETTER)
class AudioFilter : QueryPartFilter("Língua/Áudio", AnimesDigitalFiltersData.AUDIOS)
class TypeFilter : QueryPartFilter("Tipo", AnimesDigitalFiltersData.TYPES)
class GenresFilter : TriStateFilterList(
"Gêneros",
AnimesDigitalFiltersData.GENRES.map { TriFilterVal(it.first) },
)
val FILTER_LIST: AnimeFilterList
get() = AnimeFilterList(
InitialLetterFilter(),
AudioFilter(),
TypeFilter(),
AnimeFilter.Separator(),
GenresFilter(),
)
data class FilterSearchParams(
val initialLetter: String = "0",
val audio: String = "0",
val type: String = "Anime",
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>(AnimesDigitalFiltersData.GENRES)
return FilterSearchParams(
filters.asQueryPart<InitialLetterFilter>(),
filters.asQueryPart<AudioFilter>(),
filters.asQueryPart<TypeFilter>(),
added,
deleted,
)
}
private object AnimesDigitalFiltersData {
val INITIAL_LETTER = arrayOf(Pair("Selecione", "0")) + ('A'..'Z').map {
Pair(it.toString(), it.toString().lowercase())
}.toTypedArray()
val AUDIOS = arrayOf(
Pair("Todos", "0"),
Pair("Legendado", "legendado"),
Pair("Dublado", "dublado"),
)
val TYPES = arrayOf(
Pair("Animes", "Anime"),
Pair("Desenhos", "Desenho"),
Pair("Doramas", "Dorama"),
Pair("Tokusatsus", "Tokusatsus"),
)
val GENRES = arrayOf(
Pair("Ação", "10"),
Pair("Adaptação de Manga", "58"),
Pair("Adolescente", "149"),
Pair("Adventure", "100"),
Pair("Amadurecimento", "207"),
Pair("Animação", "45"),
Pair("Aniplex", "201"),
Pair("Artes Marciais", "13"),
Pair("Aventura", "11"),
Pair("Baseball", "96"),
Pair("Bishounen", "36"),
Pair("Bolos", "194"),
Pair("Boys Love", "205"),
Pair("Cartas", "83"),
Pair("Clubes", "110"),
Pair("Clubs", "185"),
Pair("Comédia", "17"),
Pair("Cotidiano", "118"),
Pair("Cozinha", "195"),
Pair("Crianças", "79"),
Pair("Culinária", "172"),
Pair("Cyberpunk Sci-Fi", "128"),
Pair("Dark Fantasy", "141"),
Pair("Demência", "105"),
Pair("Demônio", "77"),
Pair("Deusas", "152"),
Pair("Dorama", "182"),
Pair("Drama", "19"),
Pair("Dramas Coreanos", "183"),
Pair("Ecchi", "26"),
Pair("Elfos", "188"),
Pair("Escolar", "40"),
Pair("Espacial", "103"),
Pair("Espaço", "108"),
Pair("Espionagem", "150"),
Pair("Esporte", "29"),
Pair("Esportes", "52"),
Pair("eSports", "180"),
Pair("Família", "121"),
Pair("Fantasia", "25"),
Pair("Fantasia científica", "192"),
Pair("Fatia de Vida", "146"),
Pair("Ficção", "98"),
Pair("Ficção Científica", "27"),
Pair("Ficção de aventura", "161"),
Pair("Filme de super-herói", "46"),
Pair("Fuji TV.", "202"),
Pair("Futebol", "111"),
Pair("Game", "47"),
Pair("Gourmet", "209"),
Pair("Harém?", "20"),
Pair("Historia", "122"),
Pair("História de super-herói", "162"),
Pair("Histórico", "54"),
Pair("Horror", "78"),
Pair("Horror e Mistério", "198"),
Pair("Idol", "211"),
Pair("Infantil", "112"),
Pair("Insanidade", "197"),
Pair("Isekai", "51"),
Pair("Jogo", "48"),
Pair("Jogos", "38"),
Pair("Josei", "84"),
Pair("Juujin", "153"),
Pair("Kodomo", "39"),
Pair("Light novel", "99"),
Pair("Live Action", "179"),
Pair("Lolicon", "191"),
Pair("Luta", "189"),
Pair("Magia", "33"),
Pair("Magica", "61"),
Pair("Mahou Shoujo", "157"),
Pair("Mangá", "117"),
Pair("Mecha", "37"),
Pair("Mechas", "143"),
Pair("Medieval", "144"),
Pair("Melodrama", "184"),
Pair("Militar", "55"),
Pair("Mistério", "31"),
Pair("Música", "50"),
Pair("Novel", "107"),
Pair("Nudez", "181"),
Pair("Paródia", "72"),
Pair("Pastelão", "147"),
Pair("Piratas", "217"),
Pair("Policial", "60"),
Pair("Programa de TV japoneses", "139"),
Pair("Programa infantis", "175"),
Pair("Programas e séries brasileiras", "176"),
Pair("Programas Infantis", "178"),
Pair("Psicológico", "71"),
Pair("Realidade Virtual", "164"),
Pair("Robô", "145"),
Pair("Romance", "21"),
Pair("Samurai", "57"),
Pair("sci-fi", "49"),
Pair("Seinen", "23"),
Pair("Série baseado em mangás", "138"),
Pair("Série baseado em quadrinhos", "87"),
Pair("Séries", "65"),
Pair("Shonen", "127"),
Pair("Shoujo", "32"),
Pair("Shoujo Mahou", "216"),
Pair("Shoujo-ai", "59"),
Pair("Shounen", "14"),
Pair("Shounen-ai", "95"),
Pair("Slice Of Life", "35"),
Pair("Sobrenatural", "16"),
Pair("Sports", "186"),
Pair("Steampunk", "80"),
Pair("Super Heróis", "148"),
Pair("Super Poderes", "12"),
Pair("Superaventura", "170"),
Pair("Superhero fiction", "173"),
Pair("Supernatural", "86"),
Pair("Superpoderes", "41"),
Pair("Suspense", "15"),
Pair("Terror", "90"),
Pair("Thriller", "94"),
Pair("TMS Entertainment", "104"),
Pair("Tokusatsu", "171"),
Pair("Tragédia", "85"),
Pair("Vampiro.", "106"),
Pair("Vida Colegial", "196"),
Pair("Vida Cotidiana", "142"),
Pair("Vida de trabalho", "206"),
Pair("Vida Diaria", "97"),
Pair("Vida Escolar", "22"),
Pair("Violência.", "56"),
Pair("Violentos", "167"),
Pair("Visual Novel", "129"),
Pair("White Fox", "109"),
Pair("WIT", "213"),
Pair("Yaoi", "115"),
Pair("Yuri", "34"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital
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://animesdigital.org//anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesDigitalUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 2) {
val item = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimesDigital.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,22 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
private const val HOST = "https://sabornutritivo.com"
class ProtectorExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val fixedUrl = if (!url.startsWith("https")) "https:$url" else url
val token = fixedUrl.toHttpUrl().queryParameter("token")!!
val headers = Headers.headersOf("cookie", "token=$token;")
val doc = client.newCall(GET("$HOST/social.php", headers)).execute().asJsoup()
val videoHeaders = Headers.headersOf("referer", doc.location())
val iframeUrl = doc.selectFirst("iframe")!!.attr("src").trim()
return listOf(Video(iframeUrl, "Animes Digital", iframeUrl, videoHeaders))
}
}

View file

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import okhttp3.Headers
object ScriptExtractor {
fun videosFromScript(scriptData: String, headers: Headers): List<Video> {
val script = when {
"eval(function" in scriptData -> Unpacker.unpack(scriptData)
else -> scriptData
}.ifEmpty { null }?.replace("\\", "") ?: return emptyList()
return script.substringAfter("sources:").substringAfter(".src(")
.substringBefore(")")
.substringAfter("[")
.substringBefore("]")
.split("{")
.drop(1)
.map {
val quality = it.substringAfter("label", "")
.substringAfterKey()
.trim()
.ifEmpty { "Animes Digital" }
val url = it.substringAfter("file").substringAfter("src")
.substringAfterKey()
.trim()
Video(url, quality, url, headers)
}
}
private fun String.substringAfterKey() = substringAfter(':')
.substringAfter('"')
.substringBefore('"')
.substringAfter("'")
.substringBefore("'")
}