New source: AnimesOnlinex (#873)

* AnimesOnlineX: Create extension base

It needs more extractors, i'll make them soon.

* AnimesOnlineX: add some extractors

* AnimesOnlineX: Fix problems at animeDetails / episodeList*

* AnimesOnlineX: Fix anicdn video extraction

* AnimesOnlineX: get all urls/qualities from .m3u8's

* AnimesOnlineX: Add more qualities and normalize them

* AnimesOnlineX: Fix anime description (again)
This commit is contained in:
Claudemirovsky
2022-09-19 06:25:14 -03:00
committed by GitHub
parent fee171c158
commit 774dc97f0e
14 changed files with 611 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".pt.animesonlinex.AOXUrlActivity"
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="animesonlinex.cc"
android:pathPattern="/animes/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'AnimesOnlineX'
pkgNameSuffix = 'pt.animesonlinex'
extClass = '.AnimesOnlineX'
extVersionCode = 1
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.animeextension.pt.animesonlinex
object AOXConstants {
const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
const val USER_AGENT = "Mozilla/5.0 (Android 11; Mobile; rv:105.0) Gecko/105.0 Firefox/105.0"
const val PREFIX_SEARCH = "slug:"
const val PREFERRED_QUALITY = "preferred_quality"
const val DEFAULT_QUALITY = "720p"
val QUALITY_LIST = arrayOf("480p", "720p", "1080p")
}

View File

@ -0,0 +1,71 @@
package eu.kanade.tachiyomi.animeextension.pt.animesonlinex
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AOXFilters {
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 this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class GenreFilter : QueryPartFilter("Gênero", AOXFiltersData.genres)
val filterList = AnimeFilterList(
AnimeFilter.Header(AOXFiltersData.IGNORE_SEARCH_MSG),
GenreFilter()
)
data class FilterSearchParams(
val genre: String = ""
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
return FilterSearchParams(filters.asQueryPart<GenreFilter>())
}
private object AOXFiltersData {
const val IGNORE_SEARCH_MSG = "NOTA: O filtro por gênero será IGNORADO ao usar a pesquisa por nome."
val genres = arrayOf(
Pair("Artes Marciais", "artes-marciais"),
Pair("Aventura", "aventura"),
Pair("Ação", "acao"),
Pair("Comédia", "comedia"),
Pair("Crime", "crime"),
Pair("Demônio", "demonio"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolar", "escolar"),
Pair("Esporte", "esport"),
Pair("Fantasia", "fantasia"),
Pair("Ficção Cientifica", "ficcao-cientifica"),
Pair("Histórico", "historico"),
Pair("Josei", "josei"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Mistério", "misterio"),
Pair("Romance", "romance"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Shounen", "shounen"),
Pair("Slice of Life", "slice-of-life"),
Pair("Terror", "terror")
)
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.pt.animesonlinex
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://animesonlinex.cc/animes/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class AOXUrlActivity : Activity() {
private val TAG = "AOXUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val searchQuery = AOXConstants.PREFIX_SEARCH + slug
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", searchQuery)
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,346 @@
package eu.kanade.tachiyomi.animeextension.pt.animesonlinex
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animesonlinex.extractors.GenericExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesonlinex.extractors.GuiaNoticiarioBypasser
import eu.kanade.tachiyomi.animeextension.pt.animesonlinex.extractors.QualitiesExtractor
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.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
class AnimesOnlineX : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimesOnlineX"
override val baseUrl = "https://animesonlinex.cc"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.client
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl)
.add("Accept-Language", AOXConstants.ACCEPT_LANGUAGE)
.add("User-Agent", AOXConstants.USER_AGENT)
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "article.w_item_a > a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes/")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
val img = element.selectFirst("img")
val url = element.selectFirst("a")?.attr("href") ?: element.attr("href")
anime.setUrlWithoutDomain(url)
anime.title = img.attr("alt")
anime.thumbnail_url = img.attr("src")
return anime
}
override fun popularAnimeNextPageSelector() = throw Exception("not used")
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
return AnimesPage(animes, false)
}
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "ul.episodios > li"
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val epList = doc.select(episodeListSelector())
if (epList.size < 1) {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(response.request.url.toString())
episode.episode_number = 1F
episode.name = "Filme"
return listOf(episode)
}
return epList.reversed().map { episodeFromElement(it) }
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val origName = element.selectFirst("div.numerando").text()
episode.episode_number = origName.substringAfter("- ")
.replace("-", "")
.toFloat() + if ("Dub" in origName) 0.5F else 0F
episode.name = "Temp " + origName.replace(" - ", ": Ep ")
episode.setUrlWithoutDomain(element.selectFirst("a").attr("href"))
episode.date_upload = element.selectFirst("span.date")
?.text()
?.toDate() ?: 0L
return episode
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val urls = document.select("div.source-box:not(#source-player-trailer) div.pframe a")
.map { it.attr("href") }
val resolutions = document.select("ul#playeroptionsul > li:not(#player-option-trailer)")
.map {
val player = it.selectFirst("span.title").text()
val expectedQuality = it.selectFirst("span.resol")
.text()
.replace("HD", "720p")
"$player - $expectedQuality"
}
val videoList = mutableListOf<Video>()
urls.forEachIndexed { index, it ->
val url = GuiaNoticiarioBypasser(client, headers).fromUrl(it)
val videos = runCatching { getPlayerVideos(url, resolutions.get(index)) }
.getOrNull() ?: emptyList<Video>()
videoList.addAll(videos)
}
return videoList
}
private fun getPlayerVideos(url: String, qualityStr: String): List<Video> {
return when {
"/vplayer/?source" in url || "embed.redecine.org" in url -> {
val videoUrl = url.getParam("source") ?: url.getParam("url")!!
if (".m3u8" in videoUrl) {
QualitiesExtractor(client, headers)
.getVideoList(videoUrl, qualityStr)
} else {
listOf(Video(videoUrl, qualityStr, videoUrl, headers))
}
}
"/firestream/?" in url || "doramasonline.org" in url ||
"anicdn.org" in url || "animeshd.org" in url ->
GenericExtractor(client, headers).getVideoList(url, qualityStr)
else -> emptyList<Video>()
}
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
private fun searchAnimeBySlugParse(response: Response, slug: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/animes/$slug"
return AnimesPage(listOf(details), false)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val url = response.request.url.toString()
val document = response.asJsoup()
val animes = when {
"/generos/" in url -> {
document.select(latestUpdatesSelector()).map { element ->
popularAnimeFromElement(element)
}
}
else -> {
document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
}
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(AOXConstants.PREFIX_SEARCH)) {
val slug = query.removePrefix(AOXConstants.PREFIX_SEARCH)
client.newCall(GET("$baseUrl/animes/$slug", headers))
.asObservableSuccess()
.map { response ->
searchAnimeBySlugParse(response, slug)
}
} else {
val params = AOXFilters.getSearchParameters(filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
}
override fun getFilterList(): AnimeFilterList = AOXFilters.filterList
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: AOXFilters.FilterSearchParams): Request {
return when {
query.isBlank() -> {
val genre = filters.genre
var url = "$baseUrl/generos/$genre"
if (page > 1) url += "/page/$page"
GET(url, headers)
}
else -> GET("$baseUrl/page/$page/?s=$query", headers)
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
anime.title = element.text()
return anime
}
override fun searchAnimeNextPageSelector(): String = latestUpdatesNextPageSelector()
override fun searchAnimeSelector(): String = "div.result-item div.details div.title a"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val doc = getRealDoc(document)
val sheader = doc.selectFirst("div.sheader")
val img = sheader.selectFirst("div.poster > img")
anime.thumbnail_url = img.attr("src")
val name = sheader.selectFirst("div.data > h1").text()
anime.title = name
val status = sheader.selectFirst("div.alert")?.text()
anime.status = parseStatus(status)
anime.genre = sheader.select("div.data > div.sgeneros > a")
.joinToString(", ") { it.text() }
val info = doc.selectFirst("div#info")
var description = info.select("div.wp-content > p")
.joinToString("\n") { it.text() }
.substringBefore("Assistir $name") + "\n"
status?.let { description += "\n$it" }
info.getInfo("Título")?.let { description += "$it" }
info.getInfo("Ano")?.let { description += "$it" }
info.getInfo("Temporadas")?.let { description += "$it" }
info.getInfo("Episódios")?.let { description += "$it" }
anime.description = description
return anime
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "div.resppages > a > span.fa-chevron-right"
override fun latestUpdatesSelector(): String = "div.content article > div.poster"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/episodio/page/$page", headers)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = AOXConstants.PREFERRED_QUALITY
title = "Qualidade preferida"
entries = AOXConstants.QUALITY_LIST
entryValues = AOXConstants.QUALITY_LIST
setDefaultValue(AOXConstants.DEFAULT_QUALITY)
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()
}
}
screen.addPreference(videoQualityPref)
}
// ============================= Utilities ==============================
private val animeMenuSelector = "div.pag_episodes div.item a[href] i.fa-bars"
private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst(animeMenuSelector)
if (menu != null) {
val originalUrl = menu.parent().attr("href")
val req = client.newCall(GET(originalUrl, headers)).execute()
return req.asJsoup()
} else {
return document
}
}
private fun parseStatus(status: String?): Int {
return when (status) {
null -> SAnime.COMPLETED
else -> SAnime.ONGOING
}
}
private fun Element.getInfo(substring: String): String? {
val target = this.selectFirst("div.custom_fields:contains($substring)")
?: return null
val key = target.selectFirst("b").text()
val value = target.selectFirst("span").text()
return "\n$key: $value"
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
private fun String.getParam(param: String): String? {
return Uri.parse(this).getQueryParameter(param)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(AOXConstants.PREFERRED_QUALITY, AOXConstants.DEFAULT_QUALITY)!!
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (quality in video.quality) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy", Locale.ENGLISH)
}
}
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.animeextension.pt.animesonlinex.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class GenericExtractor(
private val client: OkHttpClient,
private val headers: Headers
) {
fun getVideoList(url: String, qualityStr: String): List<Video> {
val resHeaders = headers.newBuilder()
.set("Referer", "https://guianoticiario.net/")
.build()
val response = client.newCall(GET(url, resHeaders)).execute()
val body = response.body!!.string()
val item = if ("/firestream/" in url) "play_url" else "file"
val REGEX_URL = Regex("${item}\":\"(.*?)\"")
val videoUrl = REGEX_URL.find(body)!!.groupValues.get(1)
val videoHeaders = when {
"anicdn" in url -> {
headers.newBuilder()
.set("Referer", "https://anicdn.org/")
.build()
}
else -> headers
}
return listOf(Video(videoUrl, qualityStr, videoUrl, videoHeaders))
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.pt.animesonlinex.extractors
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class GuiaNoticiarioBypasser(
private val client: OkHttpClient,
private val headers: Headers
) {
private val REGEX_LINK = Regex("link\\.href = \"(\\S+?)\"")
fun fromUrl(url: String): String {
val firstBody = client.newCall(GET(url, headers)).execute()
.body!!.string()
var next = REGEX_LINK.find(firstBody)!!.groupValues.get(1)
var currentPage = client.newCall(GET(next, headers)).execute()
var iframeUrl = ""
while (iframeUrl == "") {
val currentBody = currentPage.body!!.string()
val currentDoc = currentPage.asJsoup(currentBody)
val possibleIframe = currentDoc.selectFirst("iframe")
if (REGEX_LINK.containsMatchIn(currentBody)) {
// Just to get necessary cookies when needed
if (possibleIframe != null) {
client.newCall(GET(possibleIframe.attr("src"), headers)).execute()
}
val newHeaders = headers.newBuilder()
.set("Referer", next)
.build()
next = REGEX_LINK.find(currentBody)!!.groupValues.get(1)
currentPage = client.newCall(GET(next, newHeaders)).execute()
} else {
iframeUrl = possibleIframe.attr("src")
}
}
return iframeUrl
}
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.animeextension.pt.animesonlinex.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class QualitiesExtractor(
private val client: OkHttpClient,
private val headers: Headers
) {
fun getVideoList(url: String, qualityStr: String): List<Video> {
val playlistBody = client.newCall(GET(url, headers)).execute()
.body!!.string()
val separator = "#EXT-X-STREAM-INF:"
val playerName = qualityStr.substringBefore(" -")
return playlistBody.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore("\n")
.substringBefore(",") + "p"
val path = it.substringAfter("\n").substringBefore("\n")
val videoUrl = if (!path.startsWith("https:")) {
url.substringBeforeLast("/") + "/$path"
} else path
Video(videoUrl, "$playerName - $quality", videoUrl, headers)
}
}
}