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,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.animesgames.AnimesGamesUrlActivity"
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="animesgames.cc"
android:pathPattern="/animes/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,11 @@
ext {
extName = 'Animes Games'
extClass = '.AnimesGames'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:blogger-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,279 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgames
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.lib.bloggerextractor.BloggerExtractor
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 kotlinx.serialization.json.Json
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.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class AnimesGames : ParsedAnimeHttpSource() {
override val name = "Animes Games"
override val baseUrl = "https://animesgames.cc"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl)
.add("Origin", baseUrl)
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = "ul.top10 > li > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.text()
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page")
override fun latestUpdatesSelector() = "div.conteudo section.episodioItem > a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.tituloEP")!!.text()
thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src")
}
override fun latestUpdatesNextPageSelector() = "ol.pagination > a:contains(>)"
// =============================== 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/animes/$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)
}
@Serializable
data class SearchResponseDto(
val results: List<String>,
val page: Int,
val total_page: Int = 1,
)
private val searchToken by lazy {
client.newCall(GET("$baseUrl/lista-de-animes", headers)).execute()
.asJsoup()
.selectFirst("div.menu_filter_box")!!
.attr("data-secury")
}
override fun getFilterList() = AnimesGamesFilters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimesGamesFilters.getSearchParameters(filters)
val body = FormBody.Builder().apply {
add("pagina", "$page")
add("type", "lista")
add("type_url", "anime")
add("limit", "30")
add("token", searchToken)
add("search", query.ifBlank { "0" })
val filterData = baseUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("filter_audio", params.audio)
addQueryParameter("filter_letter", params.letter)
addQueryParameter("filter_order", params.orderBy)
addQueryParameter("filter_sort", "abc")
}.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 searchAnimeParse(response: Response): AnimesPage {
return runCatching {
val data = response.parseAs<SearchResponseDto>()
val animes = data.results.map(Jsoup::parse)
.mapNotNull { it.selectFirst(searchAnimeSelector()) }
.map(::searchAnimeFromElement)
val hasNext = data.total_page > data.page
AnimesPage(animes, hasNext)
}.getOrElse { AnimesPage(emptyList(), false) }
}
override fun searchAnimeSelector() = "section.animeItem > a"
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.tituloAnime")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("src")
}
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
val content = doc.selectFirst("section.conteudoPost")!!
title = content.selectFirst("section > h1")!!.text()
.removePrefix("Assistir ")
.removeSuffix("Temporada Online")
thumbnail_url = content.selectFirst("img")?.attr("data-lazy-src")
description = content.select("section.sinopseEp p").eachText().joinToString("\n")
val infos = content.selectFirst("div.info > ol")!!
author = infos.getInfo("Autor") ?: infos.getInfo("Diretor")
artist = infos.getInfo("Estúdio")
status = when (infos.getInfo("Status")) {
"Completo" -> SAnime.COMPLETED
"Lançamento" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
private fun Element.getInfo(info: String) =
selectFirst("li:has(span:contains($info))")?.run {
selectFirst("span[data]")?.text() ?: ownText()
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
return getRealDoc(response.asJsoup())
.select(episodeListSelector())
.map(::episodeFromElement)
.reversed()
}
override fun episodeListSelector() = "div.listaEp > section.episodioItem > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("div.tituloEP")!!.text().also {
name = it
episode_number = it.substringAfterLast(" ").toFloatOrNull() ?: 1F
}
date_upload = element.selectFirst("span.data")?.text()?.toDate() ?: 0L
}
// ============================ Video Links =============================
private val bloggerExtractor by lazy { BloggerExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val url = doc.selectFirst("div.Link > a")
?.attr("href")
?: return emptyList()
val playerDoc = client.newCall(GET(url, headers)).execute()
.asJsoup()
val iframe = playerDoc.selectFirst("iframe")
return when {
iframe != null -> {
bloggerExtractor.videosFromUrl(iframe.attr("src"), headers)
}
else -> parseDefaultVideo(playerDoc)
}
}
private fun parseDefaultVideo(doc: Document): List<Video> {
val scriptData = doc.selectFirst("script:containsData(jw = {)")
?.data()
?: return emptyList()
val playlistUrl = scriptData.substringAfter("file\":\"")
.substringBefore('"')
.replace("\\", "")
return when {
playlistUrl.endsWith("m3u8") -> {
val separator = "#EXT-X-STREAM-INF:"
client.newCall(GET(playlistUrl, headers)).execute()
.body.string()
.substringAfter(separator)
.split(separator)
.map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl)
}
}
else -> listOf(Video(playlistUrl, "Default", playlistUrl, headers))
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
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("div.linksEP > a:has(li.episodio)")?.let {
client.newCall(GET(it.attr("href"), headers)).execute()
.asJsoup()
} ?: document
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
companion object {
const val PREFIX_SEARCH = "id:"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("dd 'de' MMMM 'de' yyyy", Locale("pt", "BR"))
}
}
}

View file

@ -0,0 +1,153 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgames
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimesGamesFilters {
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 AudioFilter : QueryPartFilter("Audio", AnimesGamesFiltersData.AUDIOS)
class LetterFilter : QueryPartFilter("Primeira letra", AnimesGamesFiltersData.LETTERS)
class OrderFilter : QueryPartFilter("Ordenar por", AnimesGamesFiltersData.ORDERS)
class GenresFilter : TriStateFilterList(
"Gêneros",
AnimesGamesFiltersData.GENRES.map { TriFilterVal(it.first) },
)
val FILTER_LIST get() = AnimeFilterList(
AudioFilter(),
LetterFilter(),
OrderFilter(),
AnimeFilter.Separator(),
GenresFilter(),
)
data class FilterSearchParams(
val audio: String = "0",
val letter: String = "0",
val orderBy: String = "name",
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>(AnimesGamesFiltersData.GENRES)
return FilterSearchParams(
filters.asQueryPart<AudioFilter>(),
filters.asQueryPart<LetterFilter>(),
filters.asQueryPart<OrderFilter>(),
added,
deleted,
)
}
private object AnimesGamesFiltersData {
val AUDIOS = arrayOf(
Pair("Todos", "0"),
Pair("Legendado", "legendado"),
Pair("Dublado", "dublado"),
)
val LETTERS = arrayOf(Pair("Selecione", "0")) + ('A'..'Z').map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val ORDERS = arrayOf(
Pair("Nome", "name"),
Pair("Nota", "new"),
)
val GENRES = arrayOf(
Pair("ASMR", "65"),
Pair("Adaptação de Manga", "49"),
Pair("Animação", "11"),
Pair("Artes Marciais", "8"),
Pair("Aventura", "5"),
Pair("Ação", "7"),
Pair("Bishounen", "45"),
Pair("Boys Love", "67"),
Pair("Comédia Romântica", "44"),
Pair("Comédia", "9"),
Pair("Cotidiano", "56"),
Pair("Demônios", "35"),
Pair("Drama", "20"),
Pair("Ecchi", "31"),
Pair("Escolar", "38"),
Pair("Esporte", "21"),
Pair("Fantasia", "12"),
Pair("Fatia de Vida", "66"),
Pair("Ficção Científica", "23"),
Pair("Game", "58"),
Pair("Harém", "36"),
Pair("Histórico", "33"),
Pair("Infantil", "62"),
Pair("Isekai", "59"),
Pair("Jogos", "14"),
Pair("Magia", "13"),
Pair("Mecha", "42"),
Pair("Militar", "26"),
Pair("Mistério", "24"),
Pair("Mitologia", "72"),
Pair("Musica", "70"),
Pair("Musical", "34"),
Pair("Paródia", "63"),
Pair("Policial", "30"),
Pair("Psicológico", "39"),
Pair("Romance", "15"),
Pair("Ryuri", "41"),
Pair("Samurai", "32"),
Pair("School", "55"),
Pair("Sci-fi", "48"),
Pair("Seinen", "27"),
Pair("Shoujo", "17"),
Pair("Shoujo-ai", "47"),
Pair("Shounen Ai", "57"),
Pair("Shounen", "4"),
Pair("Sitcom", "61"),
Pair("Slice Of Life", "19"),
Pair("Sobrenatural", "18"),
Pair("Super Poder", "6"),
Pair("Suspense", "25"),
Pair("Terror", "22"),
Pair("Thriller", "43"),
Pair("Vampiros", "28"),
Pair("Vida escolar", "16"),
Pair("Yaoi", "64"),
Pair("Yuri", "40"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgames
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://animesgames.cc/animes/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesGamesUrlActivity : 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", "${AnimesGames.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)
}
}