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,7 @@
ext {
extName = 'Anitube'
extClass = '.Anitube'
extVersionCode = 13
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,236 @@
package eu.kanade.tachiyomi.animeextension.pt.anitube
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
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.util.asJsoup
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Anitube"
override val baseUrl = "https://www.anitube.vip"
override val lang = "pt-BR"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl)
.add("Accept-Language", ACCEPT_LANGUAGE)
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime/page/$page", headers)
override fun popularAnimeSelector() = "div.lista_de_animes div.ani_loop_item_img > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!!
title = img.attr("title")
thumbnail_url = img.attr("src")
}
/**
* Translation of this abomination:
* First it tries to get the `a.current` element IF its not the second-to-last,
* and then gets the next `a` element (only useful for the `episodeListParser`).
*
* If the first selector fails, then it tries to match a `div.pagination`
* element that does not have any `a.current` element inside it,
* and also doesn't have just three elements (previous - current - next),
* and finally gets the last `a` element("next" button, only useful to `episodeListParser`).
*
* I hate the antichrist.
*/
override fun popularAnimeNextPageSelector() =
"div.pagination > a.current:not(:nth-last-child(2)) + a, " +
"div.pagination:not(:has(.current)):not(:has(a:first-child + a + a:last-child)) > a:last-child"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers)
override fun latestUpdatesSelector() = "div.threeItensPerContent > div.epi_loop_item > a"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun getFilterList(): AnimeFilterList = AnitubeFilters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = if (query.isBlank()) {
val params = AnitubeFilters.getSearchParameters(filters)
val season = params.season
val genre = params.genre
val year = params.year
val char = params.initialChar
when {
season.isNotBlank() -> "$baseUrl/temporada/$season/$year"
genre.isNotBlank() -> "$baseUrl/genero/$genre/page/$page/${char.replace("todos", "")}"
else -> "$baseUrl/anime/page/$page/letra/$char"
}
} else {
"$baseUrl/busca.php?s=$query&submit=Buscar"
}
return GET(url, headers)
}
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
val content = doc.selectFirst("div.anime_container_content")!!
val infos = content.selectFirst("div.anime_infos")!!
title = doc.selectFirst("div.anime_container_titulo")!!.text()
thumbnail_url = content.selectFirst("img")?.attr("src")
genre = infos.getInfo("Gêneros")
author = infos.getInfo("Autor")
artist = infos.getInfo("Estúdio")
status = parseStatus(infos.getInfo("Status"))
val infoItems = listOf("Ano", "Direção", "Episódios", "Temporada", "Título Alternativo")
description = buildString {
append(doc.selectFirst("div.sinopse_container_content")!!.text() + "\n")
infoItems.forEach { item ->
infos.getInfo(item)?.also { append("\n$item: $it") }
}
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "div.animepag_episodios_item > a"
override fun episodeListParse(response: Response) = buildList {
var doc = getRealDoc(response.asJsoup())
do {
if (isNotEmpty()) {
val path = doc.selectFirst(popularAnimeNextPageSelector())!!.attr("href")
doc = client.newCall(GET(baseUrl + path, headers)).execute().asJsoup()
}
doc.select(episodeListSelector())
.map(::episodeFromElement)
.also(::addAll)
} while (doc.selectFirst(popularAnimeNextPageSelector()) != null)
reverse()
}
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
episode_number = element.selectFirst("div.animepag_episodios_item_views")!!
.text()
.substringAfter(" ")
.toFloatOrNull() ?: 0F
name = element.selectFirst("div.animepag_episodios_item_nome")!!.text()
date_upload = element.selectFirst("div.animepag_episodios_item_date")!!
.text()
.toDate()
}
// ============================ Video Links =============================
override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response, headers)
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = 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()
}
}.let(screen::addPreference)
}
// ============================= Utilities ==============================
private fun getRealDoc(document: Document): Document {
if (!document.location().contains("/video/")) {
return document
}
return document.selectFirst("div.controles_ep > a[href]:has(i.spr.listaEP)")
?.let {
val path = it.attr("href")
client.newCall(GET(baseUrl + path, headers)).execute().asJsoup()
} ?: document
}
private fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()) {
"Completo" -> SAnime.COMPLETED
"Em Progresso" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
private fun Element.getInfo(key: String): String? {
val element = selectFirst("div.anime_info:has(b:contains($key))")
val genres = element?.select("a")
val text = if (genres?.size == 0) {
element.ownText()
} else {
genres?.eachText()?.joinToString()
}
return text?.ifEmpty { null }
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareByDescending { it.quality.equals(quality) },
)
}
private fun String.toDate(): Long {
return runCatching {
DATE_FORMATTER.parse(this)?.time
}.getOrNull() ?: 0L
}
companion object {
private val DATE_FORMATTER by lazy { SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH) }
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "HD"
private val PREF_QUALITY_ENTRIES = arrayOf("SD", "HD", "FULLHD")
}
}

View file

@ -0,0 +1,135 @@
package eu.kanade.tachiyomi.animeextension.pt.anitube
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnitubeFilters {
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
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (first { it is R } as QueryPartFilter).toQueryPart()
}
class GenreFilter : QueryPartFilter("Gênero", AnitubeFiltersData.GENRES)
class CharacterFilter : QueryPartFilter("Inicia com", AnitubeFiltersData.INITIAL_CHARS)
class YearFilter : QueryPartFilter("Ano", AnitubeFiltersData.YEARS)
class SeasonFilter : QueryPartFilter("Temporada", AnitubeFiltersData.SEASONS)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header(AnitubeFiltersData.IGNORE_SEARCH_MSG),
GenreFilter(),
CharacterFilter(),
AnimeFilter.Header(AnitubeFiltersData.IGNORE_SEASON_MSG),
SeasonFilter(),
YearFilter(),
)
data class FilterSearchParams(
val genre: String = "",
val season: String = "",
val year: String = "2024",
val initialChar: String = "todos",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
return FilterSearchParams(
filters.asQueryPart<GenreFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<CharacterFilter>(),
)
}
private object AnitubeFiltersData {
const val IGNORE_SEARCH_MSG = "NOTA: Os filtros abaixos são IGNORADOS durante a pesquisa."
const val IGNORE_SEASON_MSG = "Nota: o filtro de temporada IGNORA o filtro de gênero/letra."
val EVERY = Pair("Qualquer um", "")
val SEASONS = arrayOf(
EVERY,
Pair("Outono", "outono"),
Pair("Inverno", "inverno"),
Pair("Primavera", "primavera"),
Pair("Verão", "verao"),
)
val YEARS = (2024 downTo 1979).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val INITIAL_CHARS = arrayOf(
Pair("Qualquer letra", "todos"),
) + ('A'..'Z').map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val GENRES = arrayOf(
EVERY,
Pair("Ação", "acao"),
Pair("Artes marciais", "artes-marciais"),
Pair("Aventura", "aventura"),
Pair("CGI", "cgi"),
Pair("Comédia", "comedia"),
Pair("Demencia", "demencia"),
Pair("Demônios", "demonios"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolar", "escolar"),
Pair("Espaço", "espaco"),
Pair("Esporte", "esporte"),
Pair("Fantasia", "fantasia"),
Pair("Ficção Científica", "ficcao-cientifica"),
Pair("Gore", "gore"),
Pair("Gourmet", "gourmet"),
Pair("Harém", "harem"),
Pair("Harém Reverso", "harem-reverso"),
Pair("Hentai", "hentai"),
Pair("Histórico", "historico"),
Pair("Idol", "idol"),
Pair("Isekai", "isekai"),
Pair("Jogos", "jogos"),
Pair("Josei", "josei"),
Pair("Kodomo", "kodomo"),
Pair("Live Action", "live-action"),
Pair("Magia", "magia"),
Pair("Mahou Shoujo", "mahou-shoujo"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Mistério", "misterio"),
Pair("Mundo Virtual", "mundo-virtual"),
Pair("Musical", "musical"),
Pair("Paródia", "parodia"),
Pair("Policial", "policial"),
Pair("Pós-Apocalíptico", "pos-apocaliptico"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo-ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Shounen-ai", "shounen-ai"),
Pair("Slice of life", "slice-of-life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoder", "superpoder"),
Pair("Suspense", "suspense"),
Pair("Terror", "terror"),
Pair("Thriller", "thriller"),
Pair("Tokusatsu", "tokusatsu"),
Pair("Tragédia", "tragedia"),
Pair("Vampiros", "vampiros"),
Pair("Vida Escolar", "vida-escolar"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
}
}

View file

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.Response
object AnitubeExtractor {
fun getVideoList(response: Response, headers: Headers): List<Video> {
val doc = response.asJsoup()
val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
.attr("content")
.replace("cdn1", "cdn3")
val type = serverUrl.split("/").get(3)
val qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null)
val paths = listOf("appsd", "apphd").let {
if (type.endsWith("2")) {
it.map { path -> path + "2" }
} else {
it
}
} + listOf("appfullhd")
return qualities.mapIndexed { index, quality ->
val path = paths[index]
val url = serverUrl.replace(type, path)
Video(url, quality, url, headers = headers)
}.reversed()
}
}