feat(src/pt): New source: Animes Órion (#1809)

This commit is contained in:
Claudemirovsky
2023-07-01 18:20:36 +00:00
committed by GitHub
parent d6c1a6a2af
commit 74d8fb20f5
11 changed files with 481 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.animesorion.AnimesOrionUrlActivity"
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="animesorion.com"
android:pathPattern="/anime/..*"
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)
}
ext {
extName = 'Animes Órion'
pkgNameSuffix = 'pt.animesorion'
extClass = '.AnimesOrion'
extVersionCode = 1
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,195 @@
package eu.kanade.tachiyomi.animeextension.pt.animesorion
import eu.kanade.tachiyomi.animeextension.pt.animesorion.extractors.LinkfunBypasser
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.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class AnimesOrion : ParsedAnimeHttpSource() {
override val name = "Animes Órion"
override val baseUrl = "https://animesorion.com"
override val lang = "pt-BR"
override val supportsLatest = true
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
override fun popularAnimeSelector() = "div.tab-content-block article > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("h2.ttl")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("src")
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lista-de-episodios?page=$page")
override fun latestUpdatesSelector() = "div.mv-list > article"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
title = element.selectFirst("h2")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("src")
}
override fun latestUpdatesNextPageSelector() = "nav.pagination > a.next"
// =============================== 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/anime/$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)
}
override fun getFilterList() = AnimesOrionFilters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimesOrionFilters.getSearchParameters(filters)
val url = "$baseUrl/animes".toHttpUrl().newBuilder().apply {
addQueryParameter("q", query)
addQueryParameter("page", page.toString())
addQueryParameter("tipo", params.type)
addQueryParameter("genero", params.genre)
addQueryParameter("status", params.status)
addQueryParameter("letra", params.letter)
addQueryParameter("audio", params.audio)
addQueryParameter("ano", params.year)
addQueryParameter("temporada", params.season)
}.build().toString()
return GET(url, headers)
}
override fun searchAnimeSelector() = latestUpdatesSelector()
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
thumbnail_url = doc.selectFirst("img.lnk-blk")!!.attr("src")
val infos = doc.selectFirst("header.hd > div.rght")!!
title = infos.selectFirst("h2.title")!!.text()
genre = infos.select(">a").eachText().joinToString()
status = parseStatus(infos.selectFirst("span.tag + a")?.text())
description = buildString {
infos.selectFirst("h2.ttl")?.text()
?.takeIf(String::isNotBlank)
?.let { append("Títulos alternativos: $it\n\n") }
doc.select("div.entry > p").eachText().forEach {
append("$it\n")
}
}
}
private fun parseStatus(status: String?): Int {
return when (status?.trim()) {
"Em Lançamento" -> SAnime.ONGOING
"Finalizado" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "article.epsd > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val num = element.selectFirst("span.ttl")!!.text()
episode_number = num.toFloatOrNull() ?: 1F
name = "Episódio $num"
scanlator = element.selectFirst("span.pdx")?.text() ?: "Leg"
}
override fun episodeListParse(response: Response) =
super.episodeListParse(response)
.sortedWith(
compareBy(
{ it.scanlator != "Leg" }, // Dub first
{ it.episode_number },
),
).reversed()
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val url = doc.selectFirst("div.rvwf > a")!!.attr("href")
val bypasser = LinkfunBypasser(client)
return client.newCall(GET(url, headers))
.execute()
.use(bypasser::getIframeResponse)
.use(::extractVideoFromResponse)
.let(::listOf)
}
private fun extractVideoFromResponse(response: Response): Video {
val decodedBody = LinkfunBypasser.decodeAtob(response.body.string())
val url = decodedBody
.substringAfter("sources")
.substringAfter("file: \"")
.substringBefore('"')
val videoHeaders = Headers.headersOf("Referer", response.request.url.toString())
return Video(url, "default", url, videoHeaders)
}
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 fun getRealDoc(document: Document): Document {
if (!document.location().contains("/episodio/")) return document
return document.selectFirst("div.epsdsnv > a:has(i.fa-indent)")?.let {
client.newCall(GET(baseUrl + it.attr("href"), headers)).execute().asJsoup()
} ?: document
}
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View File

@ -0,0 +1,155 @@
package eu.kanade.tachiyomi.animeextension.pt.animesorion
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimesOrionFilters {
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 TypeFilter : QueryPartFilter("Tipo", AnimesOrionFiltersData.TYPES)
class GenreFilter : QueryPartFilter("Gênero", AnimesOrionFiltersData.GENRES)
class StatusFilter : QueryPartFilter("Status", AnimesOrionFiltersData.STATUS)
class LetterFilter : QueryPartFilter("Letra", AnimesOrionFiltersData.LETTERS)
class AudioFilter : QueryPartFilter("Áudio", AnimesOrionFiltersData.AUDIOS)
class YearFilter : QueryPartFilter("Ano", AnimesOrionFiltersData.YEARS)
class SeasonFilter : QueryPartFilter("Temporada", AnimesOrionFiltersData.SEASONS)
val FILTER_LIST get() = AnimeFilterList(
TypeFilter(),
GenreFilter(),
StatusFilter(),
LetterFilter(),
AudioFilter(),
YearFilter(),
SeasonFilter(),
)
data class FilterSearchParams(
val type: String = "todos",
val genre: String = "todos",
val status: String = "todos",
val letter: String = "todas",
val audio: String = "todos",
val year: String = "todos",
val season: String = "todas",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<GenreFilter>(),
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<LetterFilter>(),
filters.asQueryPart<AudioFilter>(),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<SeasonFilter>(),
)
}
private object AnimesOrionFiltersData {
val EVERY = Pair("<Escolha>", "todos")
val EVERY_F = Pair("<Escolha>", "todas")
val TYPES = arrayOf(
EVERY,
Pair("Especial", "especial"),
Pair("Filme", "filme"),
Pair("ONA", "ona"),
Pair("OVA", "ova"),
Pair("Série de TV", "serie"),
)
val GENRES = arrayOf(
EVERY,
Pair("Artes Maciais", "artes_maciais"),
Pair("Aventura", "aventura"),
Pair("Ação", "acao"),
Pair("Carros", "carros"),
Pair("Comédia", "comedia"),
Pair("Demência", "demencia"),
Pair("Demônios", "demonios"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Erótico", "erotico"),
Pair("Escolar", "escolar"),
Pair("Espaço", "espaco"),
Pair("Esporte", "esporte"),
Pair("Fantasia", "fantasia"),
Pair("Ficção Científica", "ficcao_cientifica"),
Pair("Gourmet", "gourmet"),
Pair("Harem", "harem"),
Pair("Hentai", "hentai"),
Pair("Histórico", "historico"),
Pair("Infantil", "infantil"),
Pair("Jogos", "jogos"),
Pair("Josei", "josei"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Mistério", "misterio"),
Pair("Música", "musica"),
Pair("Paródia", "parodia"),
Pair("Polícia", "policia"),
Pair("Psicológico", "psicologico"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Seinen", "seinen"),
Pair("Shoujo Ai", "shoujo_ai"),
Pair("Shoujo", "shoujo"),
Pair("Shounen Ai", "shounen_ai"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Super Poder", "super_poder"),
Pair("Terror", "terror"),
Pair("Thriller", "thriller"),
Pair("Vampiro", "vampiro"),
Pair("Vida Diária", "vida_diaria"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
val STATUS = arrayOf(
EVERY,
Pair("Em lançamento", "lancamento"),
Pair("Finalizado", "finalizado"),
)
val LETTERS = arrayOf(EVERY_F) + ('a'..'z').map {
Pair(it.toString().uppercase(), it.toString())
}.toTypedArray()
val AUDIOS = arrayOf(
EVERY,
Pair("Dublado", "dublado"),
Pair("Legendado", "legendado"),
)
val YEARS = arrayOf(EVERY) + (2023 downTo 1962).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val SEASONS = arrayOf(
EVERY_F,
Pair("Inverno", "inverno"),
Pair("Outono", "outono"),
Pair("Primavera", "primavera"),
Pair("Verão", "verao"),
)
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animesorion
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://animesorion.com/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesOrionUrlActivity : 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", "${AnimesOrion.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,54 @@
package eu.kanade.tachiyomi.animeextension.pt.animesorion.extractors
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Response
class LinkfunBypasser(private val client: OkHttpClient) {
fun getIframeResponse(response: Response): Response {
return response.use { page ->
val document = page.asJsoup(decodeAtob(page.body.string()))
val newHeaders = Headers.headersOf("Referer", response.request.url.toString())
val iframe = document.selectFirst("iframe")
if (iframe != null) {
client.newCall(GET(iframe.attr("src"), newHeaders))
.execute()
} else {
val formBody = FormBody.Builder().apply {
document.select("input[name]").forEach {
add(it.attr("name"), it.attr("value"))
}
}.build()
val formUrl = document.selectFirst("form")!!.attr("action")
client.newCall(POST(formUrl, newHeaders, formBody))
.execute()
.use(::getIframeResponse)
}
}
}
companion object {
fun decodeAtob(html: String): String {
val atobContent = html.substringAfter("atob(\"").substringBefore("\"));")
val hexAtob = atobContent.replace("\\x", "").decodeHex()
val decoded = Base64.decode(hexAtob, Base64.DEFAULT)
return String(decoded)
}
// Stolen from AnimixPlay(EN) / GogoCdnExtractor
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
}
}