feat(src/pt): New source: Animes Games (#2003)

This commit is contained in:
Claudemirovsky
2023-08-04 05:48:15 -03:00
committed by GitHub
parent 3f9af2b55f
commit 485b97251c
11 changed files with 544 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.net"
android:pathPattern="/animes/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext {
extName = 'Animes Games'
pkgNameSuffix = 'pt.animesgames'
extClass = '.AnimesGames'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 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,282 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgames
import eu.kanade.tachiyomi.animeextension.pt.animesgames.extractors.BloggerExtractor
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.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
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 rx.Observable
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.net"
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 fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/animes/$id"))
.asObservableSuccess()
.map(::searchAnimeByIdParse)
} else {
super.fetchSearchAnime(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()
.use { it.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
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("Not used.")
}
// =========================== 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))")?.let {
it.selectFirst("span[data]")?.text() ?: it.ownText()
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
return getRealDoc(response.use { it.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.use { it.asJsoup() }
val url = doc.selectFirst("div.Link > a")
?.attr("href")
?: return emptyList()
val playerDoc = client.newCall(GET(url, headers)).execute()
.use { it.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()
.use { it.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("Not used.")
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException("Not used.")
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used.")
}
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T {
return use { it.body.string() }.let(json::decodeFromString)
}
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()
.use { req -> req.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,157 @@
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.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.parseTriFilter(
options: Array<Pair<String, String>>,
): List<List<String>> {
return (getFirst<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 {
val included = it.get(TriState.STATE_INCLUDE)?.map { it.second } ?: emptyList<String>()
val excluded = it.get(TriState.STATE_EXCLUDE)?.map { it.second } ?: emptyList<String>()
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.net/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)
}
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgames.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class BloggerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers): List<Video> {
return client.newCall(GET(url, headers)).execute()
.use { it.body.string() }
.takeIf { !it.contains("errorContainer") }
.let { it ?: return emptyList() }
.substringAfter("\"streams\":[")
.substringBefore("]")
.split("},")
.map {
val videoUrl = it.substringAfter("{\"play_url\":\"").substringBefore('"')
val format = it.substringAfter("\"format_id\":").substringBefore("}")
val quality = when (format) {
"18" -> "360p"
"22" -> "720p"
else -> "Unknown"
}
Video(videoUrl, quality, videoUrl, headers = headers)
}
}
}