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

View file

@ -0,0 +1,7 @@
ext {
extName = 'AniDong'
extClass = '.AniDong'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,260 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.EpisodeListDto
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.SearchResultDto
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.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
class AniDong : ParsedAnimeHttpSource() {
override val name = "AniDong"
override val baseUrl = "https://anidong.net"
override val lang = "pt-BR"
override val supportsLatest = true
private val json: Json by injectLazy()
private val apiHeaders by lazy {
headersBuilder() // sets user-agent
.add("Referer", baseUrl)
.add("x-requested-with", "XMLHttpRequest")
.build()
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = "article.top10_animes_item > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.attr("src")
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page/")
override fun latestUpdatesSelector() = "article.main_content_article > a"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "div.paginacao > a.next"
// =============================== 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/anime/$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)
}
override fun getFilterList() = AniDongFilters.FILTER_LIST
private val nonce by lazy {
client.newCall(GET("$baseUrl/?js_global=1&ver=6.2.2")).execute()
.body.string()
.substringAfter("search_nonce")
.substringAfter("'")
.substringBefore("'")
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AniDongFilters.getSearchParameters(filters)
val body = FormBody.Builder()
.add("letra", "")
.add("action", "show_animes_ajax")
.add("nome", query)
.add("status", params.status)
.add("formato", params.format)
.add("search_nonce", nonce)
.add("paged", page.toString())
.apply {
params.genres.forEach { add("generos[]", it) }
}.build()
return POST("$baseUrl/wp-admin/admin-ajax.php", headers = apiHeaders, body = body)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val searchData: SearchResultDto = response.body.string()
.takeIf { it.trim() != "402" }
?.let(json::decodeFromString)
?: return AnimesPage(emptyList(), false)
val animes = searchData.animes.map {
SAnime.create().apply {
setUrlWithoutDomain(it.url)
title = it.title
thumbnail_url = it.thumbnail_url
}
}
val hasNextPage = searchData.pages > 1 && searchData.animes.size == 10
return AnimesPage(animes, hasNextPage)
}
override fun searchAnimeSelector(): String {
throw UnsupportedOperationException()
}
override fun searchAnimeFromElement(element: Element): SAnime {
throw UnsupportedOperationException()
}
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
val infos = doc.selectFirst("div.anime_infos")!!
setUrlWithoutDomain(doc.location())
title = infos.selectFirst("div > h3")!!.ownText()
thumbnail_url = infos.selectFirst("img")?.attr("src")
genre = infos.select("div[itemprop=genre] a").eachText().joinToString()
artist = infos.selectFirst("div[itemprop=productionCompany]")?.text()
status = doc.selectFirst("div:contains(Status) span")?.text().let {
when {
it == null -> SAnime.UNKNOWN
it == "Completo" -> SAnime.COMPLETED
it.contains("Lançamento") -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
description = buildString {
infos.selectFirst("div.anime_name + div.anime_info")?.text()?.also {
append("Nomes alternativos: $it\n")
}
doc.selectFirst("div[itemprop=description]")?.text()?.also {
append("\n$it")
}
}
}
// ============================== Episodes ==============================
override fun episodeListSelector(): String {
throw UnsupportedOperationException()
}
override fun episodeFromElement(element: Element): SEpisode {
throw UnsupportedOperationException()
}
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val id = doc.selectFirst("link[rel=shortlink]")!!.attr("href").substringAfter("=")
val body = FormBody.Builder()
.add("action", "show_videos")
.add("anime_id", id)
.build()
val res = client.newCall(POST("$baseUrl/api", apiHeaders, body)).execute()
.body.string()
val data = json.decodeFromString<EpisodeListDto>(res)
return buildList {
data.episodes.forEach { add(episodeFromObject(it, "Episódio")) }
data.movies.forEach { add(episodeFromObject(it, "Filme")) }
data.ovas.forEach { add(episodeFromObject(it, "OVA")) }
sortByDescending { it.episode_number }
}
}
private fun episodeFromObject(episode: EpisodeDto, prefix: String) = SEpisode.create().apply {
setUrlWithoutDomain(episode.epi_url)
episode_number = episode.epi_num.toFloatOrNull() ?: 0F
name = "$prefix ${episode.epi_num}"
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
return doc.select("div.player_option").flatMap {
val url = it.attr("data-playerlink")
val playerName = it.text().trim()
videosFromUrl(url, playerName)
}
}
private fun videosFromUrl(url: String, playerName: String): List<Video> {
val scriptData = client.newCall(GET(url, apiHeaders)).execute()
.asJsoup()
.selectFirst("script:containsData(sources)")
?.data() ?: return emptyList()
return scriptData.substringAfter("sources: [").substringBefore("]")
.split("{")
.drop(1)
.map {
val videoUrl = it.substringAfter("file: \"").substringBefore('"')
val label = it.substringAfter("label: \"", "Unknown").substringBefore('"')
val quality = "$playerName - $label"
Video(videoUrl, quality, videoUrl, headers = apiHeaders)
}
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoListSelector(): String {
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(".episodioControleItem:has(i.ri-grid-fill)")?.let {
client.newCall(GET(it.attr("href"), headers)).execute()
.asJsoup()
} ?: document
}
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View file

@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniDongFilters {
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 CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
.toList()
}
class StatusFilter : QueryPartFilter("Status", AniDongFiltersData.STATUS_LIST)
class FormatFilter : QueryPartFilter("Formato", AniDongFiltersData.FORMAT_LIST)
class GenresFilter : CheckBoxFilterList("Gêneros", AniDongFiltersData.GENRES_LIST)
val FILTER_LIST get() = AnimeFilterList(
StatusFilter(),
FormatFilter(),
GenresFilter(),
)
data class FilterSearchParams(
val status: String = "",
val format: String = "",
val genres: List<String> = emptyList(),
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<StatusFilter>(),
filters.asQueryPart<FormatFilter>(),
filters.parseCheckbox<GenresFilter>(AniDongFiltersData.GENRES_LIST),
)
}
private object AniDongFiltersData {
private val SELECT = Pair("<Selecione>", "")
val STATUS_LIST = arrayOf(
SELECT,
Pair("Lançamento", "Lançamento"),
Pair("Completo", "Completo"),
)
val FORMAT_LIST = arrayOf(
SELECT,
Pair("Donghua", "Anime"),
Pair("Filme", "Filme"),
)
val GENRES_LIST = arrayOf(
Pair("Artes Marciais", "9"),
Pair("Aventura", "6"),
Pair("Ação", "2"),
Pair("Boys Love", "43"),
Pair("Comédia", "15"),
Pair("Corrida", "94"),
Pair("Cultivo", "12"),
Pair("Demônios", "18"),
Pair("Detetive", "24"),
Pair("Drama", "16"),
Pair("Escolar", "77"),
Pair("Espaço", "54"),
Pair("Esporte", "95"),
Pair("Fantasia", "7"),
Pair("Guerra", "26"),
Pair("Harém", "17"),
Pair("Histórico", "8"),
Pair("Horror", "44"),
Pair("Isekai", "72"),
Pair("Jogo", "25"),
Pair("Mecha", "40"),
Pair("Militar", "21"),
Pair("Mistério", "3"),
Pair("Mitolgia", "96"),
Pair("Mitologia", "19"),
Pair("O Melhor Donghua", "91"),
Pair("Polícia", "57"),
Pair("Política", "63"),
Pair("Psicológico", "33"),
Pair("Reencarnação", "30"),
Pair("Romance", "11"),
Pair("Sci-Fi", "39"),
Pair("Slice of Life", "84"),
Pair("Sobrenatural", "4"),
Pair("Super Poder", "67"),
Pair("Suspense", "32"),
Pair("Tragédia", "58"),
Pair("Vampiro", "82"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong
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://anidong.net/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AniDongUrlActivity : 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", "${AniDong.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,53 @@
package eu.kanade.tachiyomi.animeextension.pt.anidong.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable
data class SearchResultDto(
val animes: List<AnimeDto>,
@SerialName("total_pages")
val pages: Int,
)
@Serializable
data class AnimeDto(
@SerialName("anime_capa")
val thumbnail_url: String,
@SerialName("anime_permalink")
val url: String,
@SerialName("anime_title")
val title: String,
)
@Serializable
data class EpisodeListDto(
@Serializable(with = EpisodeListSerializer::class)
@SerialName("episodios")
val episodes: List<EpisodeDto>,
@Serializable(with = EpisodeListSerializer::class)
@SerialName("filmes")
val movies: List<EpisodeDto>,
@Serializable(with = EpisodeListSerializer::class)
val ovas: List<EpisodeDto>,
)
@Serializable
data class EpisodeDto(
val epi_num: String,
val epi_url: String,
)
object EpisodeListSerializer :
JsonTransformingSerializer<List<EpisodeDto>>(ListSerializer(EpisodeDto.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement =
when (element) {
is JsonObject -> JsonArray(element.values.toList())
else -> JsonArray(emptyList())
}
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.animefire.AFUrlActivity"
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="animefire.plus"
android:pathPattern="/animes/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,7 @@
ext {
extName = 'Anime Fire'
extClass = '.AnimeFire'
extVersionCode = 6
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,92 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AFFilters {
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", AFFiltersData.GENRES)
class SeasonFilter : QueryPartFilter("Temporada", AFFiltersData.SEASONS)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header(AFFiltersData.IGNORE_SEARCH_MSG),
SeasonFilter(),
AnimeFilter.Header(AFFiltersData.IGNORE_SEASON_MSG),
GenreFilter(),
)
data class FilterSearchParams(
val genre: String = "",
val season: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
return FilterSearchParams(
filters.asQueryPart<GenreFilter>(),
filters.asQueryPart<SeasonFilter>(),
)
}
private object AFFiltersData {
const val IGNORE_SEARCH_MSG = "NOTA: Os filtros abaixos são IGNORADOS durante a pesquisa."
const val IGNORE_SEASON_MSG = "NOTA: O filtro de gêneros IGNORA o de temporadas."
val EVERY = Pair("Qualquer um", "")
val SEASONS = arrayOf(
EVERY,
Pair("Outono", "outono"),
Pair("Inverno", "inverno"),
Pair("Primavera", "primavera"),
Pair("Verão", "verao"),
)
val GENRES = arrayOf(
Pair("Ação", "acao"),
Pair("Artes Marciais", "artes-marciais"),
Pair("Aventura", "aventura"),
Pair("Comédia", "comedia"),
Pair("Demônios", "demonios"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Espaço", "espaco"),
Pair("Esporte", "esporte"),
Pair("Fantasia", "fantasia"),
Pair("Ficção Científica", "ficcao-cientifica"),
Pair("Harém", "harem"),
Pair("Horror", "horror"),
Pair("Jogos", "jogos"),
Pair("Josei", "josei"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Mistério", "misterio"),
Pair("Musical", "musical"),
Pair("Paródia", "parodia"),
Pair("Psicológico", "psicologico"),
Pair("Romance", "romance"),
Pair("Seinen", "seinen"),
Pair("Shoujo-ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Slice of Life", "slice-of-life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoder", "superpoder"),
Pair("Suspense", "suspense"),
Pair("Vampiros", "vampiros"),
Pair("Vida Escolar", "vida-escolar"),
)
}
}

View file

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire
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://animefire.net/animes/<id> intents
* and redirects them to the main Aniyomi process.
*/
class AFUrlActivity : Activity() {
private val tag = "AFUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
val searchQuery = AnimeFire.PREFIX_SEARCH + id
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,208 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animefire.extractors.AnimeFireExtractor
import eu.kanade.tachiyomi.animeextension.pt.animefire.extractors.IframeExtractor
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.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
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 uy.kohesive.injekt.injectLazy
class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Anime Fire"
override val baseUrl = "https://animefire.plus"
override val lang = "pt-BR"
override val supportsLatest = true
private val json: Json by injectLazy()
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/top-animes/$page")
override fun popularAnimeSelector() = latestUpdatesSelector()
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/home/$page")
override fun latestUpdatesSelector() = "article.cardUltimosEps > a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
val url = element.attr("href")
// get anime url from episode url
when (url.substringAfterLast("/").toIntOrNull()) {
null -> setUrlWithoutDomain(url)
else -> {
val substr = url.substringBeforeLast("/")
setUrlWithoutDomain("$substr-todos-os-episodios")
}
}
title = element.selectFirst("h3.animeTitle")!!.text()
thumbnail_url = element.selectFirst("img")?.attr("data-src")
}
override fun latestUpdatesNextPageSelector() = "ul.pagination img.seta-right"
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) {
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)
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AFFilters.getSearchParameters(filters)
if (query.isBlank()) {
return when {
params.season.isNotBlank() -> GET("$baseUrl/temporada/${params.season}/$page")
else -> GET("$baseUrl/genero/${params.genre}/$page")
}
}
val fixedQuery = query.trim().replace(" ", "-").lowercase()
return GET("$baseUrl/pesquisar/$fixedQuery/$page")
}
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 content = document.selectFirst("div.divDivAnimeInfo")!!
val names = content.selectFirst("div.div_anime_names")!!
val infos = content.selectFirst("div.divAnimePageInfo")!!
setUrlWithoutDomain(document.location())
thumbnail_url = content.selectFirst("div.sub_animepage_img > img")?.attr("data-src")
title = names.selectFirst("h1")!!.text()
genre = infos.select("a.spanGeneros").eachText().joinToString()
author = infos.getInfo("Estúdios")
status = parseStatus(infos.getInfo("Status"))
description = buildString {
content.selectFirst("div.divSinopse > span")?.also {
append(it.text() + "\n")
}
names.selectFirst("h6")?.also { append("\nNome alternativo: ${it.text()}") }
infos.getInfo("Dia de")?.also { append("\nDia de lançamento: $it") }
infos.getInfo("Áudio")?.also { append("\nTipo: $it") }
infos.getInfo("Ano")?.also { append("\nAno: $it") }
infos.getInfo("Episódios")?.also { append("\nEpisódios: $it") }
infos.getInfo("Temporada")?.also { append("\nTemporada: $it") }
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeListSelector(): String = "div.div_video_list > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val url = element.attr("href")
setUrlWithoutDomain(url)
name = element.text()
episode_number = url.substringAfterLast("/").toFloatOrNull() ?: 0F
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoElement = document.selectFirst("video#my-video")
return if (videoElement != null) {
AnimeFireExtractor(client, json).videoListFromElement(videoElement, headers)
} else {
IframeExtractor(client).videoListFromDocument(document, 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_VALUES
entryValues = PREF_QUALITY_VALUES
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()
}
}.also(screen::addPreference)
}
override fun getFilterList(): AnimeFilterList = AFFilters.FILTER_LIST
// ============================= Utilities ==============================
private fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()) {
"Completo" -> SAnime.COMPLETED
"Em lançamento" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
private fun Element.getInfo(key: String): String? {
return selectFirst("div.animeInfo:contains($key) span")?.text()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
companion object {
const val PREFIX_SEARCH = "id:"
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 = "720p"
private val PREF_QUALITY_VALUES = arrayOf("360p", "720p")
}
}

View file

@ -0,0 +1,18 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class AFResponseDto(
@SerialName("data")
val videos: List<VideoDto>,
)
@Serializable
data class VideoDto(
@SerialName("src")
val url: String,
@SerialName("label")
val quality: String,
)

View file

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire.extractors
import eu.kanade.tachiyomi.animeextension.pt.animefire.dto.AFResponseDto
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient
import org.jsoup.nodes.Element
class AnimeFireExtractor(private val client: OkHttpClient, private val json: Json) {
fun videoListFromElement(videoElement: Element, headers: Headers): List<Video> {
val jsonUrl = videoElement.attr("data-video-src")
val response = client.newCall(GET(jsonUrl)).execute()
.body.string()
val responseDto = json.decodeFromString<AFResponseDto>(response)
return responseDto.videos.map {
val url = it.url.replace("\\", "")
Video(url, it.quality, url, headers = headers)
}
}
}

View file

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
class IframeExtractor(private val client: OkHttpClient) {
fun videoListFromDocument(doc: Document, headers: Headers): List<Video> {
val iframeElement = doc.selectFirst("div#div_video iframe")!!
val iframeUrl = iframeElement.attr("src")
val response = client.newCall(GET(iframeUrl, headers)).execute()
.body.string()
val url = response.substringAfter("play_url")
.substringAfter(":\"")
.substringBefore("\"")
val video = Video(url, "Default", url, headers = headers)
return listOf(video)
}
}

View file

@ -0,0 +1,14 @@
ext {
extName = 'AnimePlayer'
extClass = '.AnimePlayer'
themePkg = 'dooplay'
baseUrl = 'https://animeplayer.com.br'
overrideVersionCode = 3
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:blogger-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -0,0 +1,112 @@
package eu.kanade.tachiyomi.animeextension.pt.animeplayer
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.lib.bloggerextractor.BloggerExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class AnimePlayer : DooPlay(
"pt-BR",
"AnimePlayer",
"https://animeplayer.com.br",
) {
// ============================== Popular ===============================
override fun popularAnimeSelector() = "div#archive-content article div.poster"
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/")
override fun popularAnimeNextPageSelector() = "a > i#nextpagination"
// =============================== Latest ===============================
override val latestUpdatesPath = "episodios"
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealAnimeDoc(document)
val content = doc.selectFirst("div#contenedor > div.data")!!
doc.selectFirst("div.sheader div.poster > img")!!.let {
thumbnail_url = it.getImageUrl()
title = it.attr("alt").ifEmpty {
content.selectFirst("div.data > h1")!!.text()
}
}
genre = content.select("div.sgeneros > a")
.eachText()
.joinToString()
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealAnimeDoc(response.asJsoup())
val seasonList = doc.select(seasonListSelector)
return if (seasonList.size < 1) {
SEpisode.create().apply {
setUrlWithoutDomain(doc.location())
episode_number = 1F
name = episodeMovieText
}.let(::listOf)
} else {
seasonList.flatMap(::getSeasonEpisodes)
}
}
override fun getSeasonEpisodes(season: Element): List<SEpisode> {
val seasonName = season.selectFirst("span.title")!!.text()
return season.select(episodeListSelector()).mapNotNull { element ->
try {
episodeFromElement(element, seasonName)
} catch (e: Throwable) {
e.printStackTrace()
null
}
}
}
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element, seasonName: String) = SEpisode.create().apply {
val epNum = element.selectFirst("div.episodiotitle p")!!.text()
.trim()
.let(episodeNumberRegex::find)
?.groupValues
?.last() ?: "0"
val href = element.selectFirst("a[href]")!!
episode_number = epNum.toFloatOrNull() ?: 0F
name = "$seasonName x Episódio $epNum"
setUrlWithoutDomain(href.absUrl("href"))
}
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("360p", "720p")
override val prefQualityEntries = prefQualityValues
private val bloggerExtractor by lazy { BloggerExtractor(client) }
override fun videoListParse(response: Response): List<Video> {
val playerUrl = response.asJsoup()
.selectFirst("div.playex iframe")
?.absUrl("src")
?.toHttpUrlOrNull()
?: return emptyList()
val url = playerUrl.queryParameter("link") ?: playerUrl.toString()
return bloggerExtractor.videosFromUrl(url, headers)
}
// ============================== Filters ===============================
override fun genresListSelector() = "ul.genres a"
// ============================= Utilities ==============================
override val animeMenuSelector = "div.pag_episodes div.item a[href] i.icon-bars"
}

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.animescx.AnimesCXUrlActivity"
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="animescx.com.br"
android:pathPattern="/..*/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,12 @@
ext {
extName = 'Animes CX'
extClass = '.AnimesCX'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:googledrive-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,270 @@
package eu.kanade.tachiyomi.animeextension.pt.animescx
import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
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.lib.googledriveextractor.GoogleDriveExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
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.builtins.serializer
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArrayBuilder
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.putJsonArray
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 uy.kohesive.injekt.injectLazy
class AnimesCX : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Animes CX"
override val baseUrl = "https://animescx.com.br"
override val lang = "pt-BR"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/doramas-legendados/page/$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val doc = response.asJsoup()
val animes = doc.select(popularAnimeSelector()).map(::popularAnimeFromElement)
return AnimesPage(animes, doc.hasNextPage())
}
override fun popularAnimeSelector() = "div.listaAnimes_Riverlab_Container > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.infolistaAnimes_RiverLab")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
override fun popularAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/doramas-em-lancamento/page/$page", headers)
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesSelector(): String {
throw UnsupportedOperationException()
}
override fun latestUpdatesFromElement(element: Element): SAnime {
throw UnsupportedOperationException()
}
override fun latestUpdatesNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path", headers))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
.apply { setUrlWithoutDomain(response.request.url.toString()) }
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl/page/$page/?s=$query", headers)
override fun searchAnimeSelector() = "article.rl_episodios:has(.rl_AnimeIndexImg)"
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
with(element.selectFirst("a")!!) {
setUrlWithoutDomain(attr("href"))
title = text()
}
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
override fun searchAnimeNextPageSelector() = "a.next.page-numbers"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val infos = document.selectFirst("div.rl_anime_metadados")!!
thumbnail_url = infos.selectFirst("img")?.absUrl("src")
title = infos.selectFirst(".rl_nome_anime")!!.text()
genre = infos.getInfo("Gêneros").replace(";", ",")
status = when (infos.getInfo("Status")) {
"Completo" -> SAnime.COMPLETED
"Lançando", "Sendo Legendado!" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
description = infos.getInfo("Sinopse")
}
private fun Element.getInfo(text: String) =
selectFirst(".rl_anime_meta:contains($text)")?.ownText().orEmpty()
// ============================== Episodes ==============================
override fun episodeListSelector() = ".rl_anime_episodios > article.rl_episodios"
override fun episodeListParse(response: Response) = buildList {
var doc = response.asJsoup()
do {
if (isNotEmpty()) {
val url = doc.selectFirst("a.rl_anime_pagination:contains()")!!.absUrl("href")
doc = client.newCall(GET(url, headers)).execute().asJsoup()
}
doc.select(episodeListSelector())
.map(::episodeFromElement)
.also(::addAll)
} while (doc.hasNextPage())
reverse()
}
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val num = element.selectFirst("header")!!.text().substringAfterLast(' ')
episode_number = num.toFloatOrNull() ?: 0F
name = "Episódio $num"
scanlator = element.selectFirst("div.rl_episodios_info:contains(Fansub)")?.ownText()
url = json.encodeToString(
buildJsonObject {
element.select("div.rl_episodios_opcnome[onclick]").forEach {
putJsonArray(it.text(), { getVideoHosts(it.attr("onclick"), element) })
}
},
)
}
private fun JsonArrayBuilder.getVideoHosts(onclick: String, element: Element) {
val itemId = onclick.substringAfterLast("rlToggle('").substringBefore("'")
element.select("#$itemId a.rl_episodios_link").toList()
.filter { it.text() != "Mega" }
.forEach { el ->
val urlId = el.attr("href").substringAfter("id=")
val url = String(Base64.decode(urlId, Base64.DEFAULT)).reversed()
add(json.encodeToJsonElement(VideoHost.serializer(), VideoHost(el.text(), url)))
}
}
@Serializable
class VideoHost(val name: String, val url: String)
// ============================ Video Links =============================
private val gdriveExtractor by lazy { GoogleDriveExtractor(client, headers) }
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val data = episode.url.parseAs<Map<String, List<VideoHost>>>()
return data.flatMap { (quality, items) ->
items.flatMap {
when (it.name) {
"MediaFire" -> {
val doc = client.newCall(GET(it.url, headers)).await().asJsoup()
val url = doc.selectFirst("a#downloadButton")?.attr("href")
url?.let { listOf(Video(url, "Mediafire - $quality", url, headers)) }.orEmpty()
}
"Google Drive" -> {
GDRIVE_REGEX.find(it.url)?.groupValues?.get(0)
?.let { gdriveExtractor.videosFromUrl(it, "GDrive - $quality") }
.orEmpty()
}
else -> emptyList()
}
}
}.sort()
}
override fun videoListParse(response: Response): List<Video> {
throw UnsupportedOperationException()
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
// ============================== 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"
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun String.getPage() = substringAfterLast("/page/").substringBefore("/")
private fun Document.hasNextPage() =
selectFirst("a.rl_anime_pagination:last-child")
?.let { it.attr("href").getPage() != location().getPage() }
?: false
companion object {
const val PREFIX_SEARCH = "id:"
private val GDRIVE_REGEX = Regex("""[\w-]{28,}""")
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "FULL HD"
private val PREF_QUALITY_ENTRIES = arrayOf("MP4", "SD", "HD", "FULL HD")
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animescx
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://animescx.com.br/<type>/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesCXUrlActivity : 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 path = pathSegments.joinToString("/")
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimesCX.PREFIX_SEARCH}$path")
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,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.animesdigital.AnimesDigitalUrlActivity"
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="animesdigital.org"
android:pathPattern="/anime/a/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,11 @@
ext {
extName = 'Animes Digital'
extClass = '.AnimesDigital'
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:unpacker"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -0,0 +1,298 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors.ProtectorExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors.ScriptExtractor
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.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
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.Injekt
import uy.kohesive.injekt.api.get
class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Animes Digital"
override val baseUrl = "https://animesdigital.org"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = latestUpdatesSelector()
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page")
override fun latestUpdatesSelector() = "div.b_flex:nth-child(2) > div.itemE > a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")!!.let {
it.attr("data-lazy-src").ifEmpty { it.attr("src") }
}
title = element.selectFirst("span.title_anime")!!.text()
}
override fun latestUpdatesNextPageSelector() = "ul > li.next"
// =============================== Search ===============================
override fun getFilterList() = AnimesDigitalFilters.FILTER_LIST
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/anime/a/$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)
}
private val searchToken by lazy {
client.newCall(GET("$baseUrl/animes-legendado")).execute().asJsoup()
.selectFirst("div.menu_filter_box")!!
.attr("data-secury")
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimesDigitalFilters.getSearchParameters(filters)
val body = FormBody.Builder().apply {
add("type", "lista")
add("limit", "30")
add("token", searchToken)
if (query.isNotEmpty()) {
add("search", query)
}
add("pagina", "$page")
val filterData = baseUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("type_url", params.type)
addQueryParameter("filter_audio", params.audio)
addQueryParameter("filter_letter", params.initialLetter)
addQueryParameter("filter_order", "name")
}.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 searchAnimeSelector() = "div.itemA > a"
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeParse(response: Response): AnimesPage {
return runCatching {
val data = response.parseAs<SearchResponseDto>()
val animes = data.results.map(Jsoup::parseBodyFragment)
.mapNotNull { it.selectFirst(searchAnimeSelector()) }
.map(::searchAnimeFromElement)
val hasNext = data.total_page > data.page
AnimesPage(animes, hasNext)
}.getOrElse { AnimesPage(emptyList(), false) }
}
@Serializable
data class SearchResponseDto(
val results: List<String>,
val page: Int,
val total_page: Int,
)
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
thumbnail_url = doc.selectFirst("div.poster > img")?.attr("data-lazy-src")
status = when (doc.selectFirst("div.clw > div.playon")?.text()) {
"Em Lançamento" -> SAnime.ONGOING
"Completo" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
with(doc.selectFirst("div.crw > div.dados")!!) {
artist = getInfo("Estúdio")
author = getInfo("Autor") ?: getInfo("Diretor")
title = selectFirst("h1")!!.text()
genre = select("div.genre a").eachText().joinToString()
description = selectFirst("div.sinopse")?.text()
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val pagination = doc.selectFirst("ul.content-pagination")
return if (pagination != null) {
val episodes = mutableListOf<SEpisode>()
episodes += doc.select(episodeListSelector()).map(::episodeFromElement)
val lastPage = doc.selectFirst("ul.content-pagination > li:nth-last-child(2) > span")!!.text().toInt()
for (i in 2..lastPage) {
val request = GET(doc.location() + "/page/$i", headers)
val res = client.newCall(request).execute()
val pageDoc = res.asJsoup()
episodes += pageDoc.select(episodeListSelector()).map(::episodeFromElement)
}
episodes
} else {
doc.select(episodeListSelector()).map(::episodeFromElement)
}
}
override fun episodeListSelector() = "div.item_ep > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
val epname = element.selectFirst("div.episode")!!.text()
episode_number = epname.substringAfterLast(" ").toFloatOrNull() ?: 1F
name = buildString {
append(epname)
element.selectFirst("div.sub_title")?.text()?.also {
if (!it.contains("Ainda não tem um titulo oficial")) {
append(" - ", it)
}
}
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val player = response.asJsoup().selectFirst("div#player")!!
return player.select("div.tab-video").flatMap { div ->
val noComment = div.outerHtml().replace("<!--", "").replace("-->", "")
val newDoc = Jsoup.parseBodyFragment(noComment)
newDoc.select(videoListSelector()).ifEmpty { newDoc.select("a") }.flatMap { element ->
runCatching {
videosFromElement(element)
}.onFailure { it.printStackTrace() }.getOrElse { emptyList() }
}
}
}
private val protectorExtractor by lazy { ProtectorExtractor(client) }
private fun videosFromElement(element: Element): List<Video> {
return when (element.tagName()) {
"iframe" -> {
val url = element.absUrl("data-lazy-src").ifEmpty { element.absUrl("src") }
client.newCall(GET(url, headers)).execute()
.asJsoup()
.select(videoListSelector())
.flatMap(::videosFromElement)
}
"script" -> ScriptExtractor.videosFromScript(element.data(), headers)
"a" -> protectorExtractor.videosFromUrl(element.attr("href"))
else -> emptyList()
}
}
private val scriptSelectors = listOf("eval", "player.src", "this.src", "sources:")
.joinToString { "script:containsData($it):not(:containsData(/bg.mp4))" }
override fun videoListSelector() = "iframe, $scriptSelectors"
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
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()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun getRealDoc(document: Document): Document {
return document.selectFirst("div.subitem > a:contains(menu)")?.let { link ->
client.newCall(GET(link.attr("href")))
.execute()
.asJsoup()
} ?: document
}
private fun Element.getInfo(key: String): String? {
return selectFirst("div.info:has(span:containsOwn($key))")?.run {
ownText()
.trim()
.takeUnless { it.isBlank() || it == "?" }
}
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("360p", "480p", "720p")
}
}

View file

@ -0,0 +1,235 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimesDigitalFilters {
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 InitialLetterFilter : QueryPartFilter("Primeira letra", AnimesDigitalFiltersData.INITIAL_LETTER)
class AudioFilter : QueryPartFilter("Língua/Áudio", AnimesDigitalFiltersData.AUDIOS)
class TypeFilter : QueryPartFilter("Tipo", AnimesDigitalFiltersData.TYPES)
class GenresFilter : TriStateFilterList(
"Gêneros",
AnimesDigitalFiltersData.GENRES.map { TriFilterVal(it.first) },
)
val FILTER_LIST: AnimeFilterList
get() = AnimeFilterList(
InitialLetterFilter(),
AudioFilter(),
TypeFilter(),
AnimeFilter.Separator(),
GenresFilter(),
)
data class FilterSearchParams(
val initialLetter: String = "0",
val audio: String = "0",
val type: String = "Anime",
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>(AnimesDigitalFiltersData.GENRES)
return FilterSearchParams(
filters.asQueryPart<InitialLetterFilter>(),
filters.asQueryPart<AudioFilter>(),
filters.asQueryPart<TypeFilter>(),
added,
deleted,
)
}
private object AnimesDigitalFiltersData {
val INITIAL_LETTER = arrayOf(Pair("Selecione", "0")) + ('A'..'Z').map {
Pair(it.toString(), it.toString().lowercase())
}.toTypedArray()
val AUDIOS = arrayOf(
Pair("Todos", "0"),
Pair("Legendado", "legendado"),
Pair("Dublado", "dublado"),
)
val TYPES = arrayOf(
Pair("Animes", "Anime"),
Pair("Desenhos", "Desenho"),
Pair("Doramas", "Dorama"),
Pair("Tokusatsus", "Tokusatsus"),
)
val GENRES = arrayOf(
Pair("Ação", "10"),
Pair("Adaptação de Manga", "58"),
Pair("Adolescente", "149"),
Pair("Adventure", "100"),
Pair("Amadurecimento", "207"),
Pair("Animação", "45"),
Pair("Aniplex", "201"),
Pair("Artes Marciais", "13"),
Pair("Aventura", "11"),
Pair("Baseball", "96"),
Pair("Bishounen", "36"),
Pair("Bolos", "194"),
Pair("Boys Love", "205"),
Pair("Cartas", "83"),
Pair("Clubes", "110"),
Pair("Clubs", "185"),
Pair("Comédia", "17"),
Pair("Cotidiano", "118"),
Pair("Cozinha", "195"),
Pair("Crianças", "79"),
Pair("Culinária", "172"),
Pair("Cyberpunk Sci-Fi", "128"),
Pair("Dark Fantasy", "141"),
Pair("Demência", "105"),
Pair("Demônio", "77"),
Pair("Deusas", "152"),
Pair("Dorama", "182"),
Pair("Drama", "19"),
Pair("Dramas Coreanos", "183"),
Pair("Ecchi", "26"),
Pair("Elfos", "188"),
Pair("Escolar", "40"),
Pair("Espacial", "103"),
Pair("Espaço", "108"),
Pair("Espionagem", "150"),
Pair("Esporte", "29"),
Pair("Esportes", "52"),
Pair("eSports", "180"),
Pair("Família", "121"),
Pair("Fantasia", "25"),
Pair("Fantasia científica", "192"),
Pair("Fatia de Vida", "146"),
Pair("Ficção", "98"),
Pair("Ficção Científica", "27"),
Pair("Ficção de aventura", "161"),
Pair("Filme de super-herói", "46"),
Pair("Fuji TV.", "202"),
Pair("Futebol", "111"),
Pair("Game", "47"),
Pair("Gourmet", "209"),
Pair("Harém?", "20"),
Pair("Historia", "122"),
Pair("História de super-herói", "162"),
Pair("Histórico", "54"),
Pair("Horror", "78"),
Pair("Horror e Mistério", "198"),
Pair("Idol", "211"),
Pair("Infantil", "112"),
Pair("Insanidade", "197"),
Pair("Isekai", "51"),
Pair("Jogo", "48"),
Pair("Jogos", "38"),
Pair("Josei", "84"),
Pair("Juujin", "153"),
Pair("Kodomo", "39"),
Pair("Light novel", "99"),
Pair("Live Action", "179"),
Pair("Lolicon", "191"),
Pair("Luta", "189"),
Pair("Magia", "33"),
Pair("Magica", "61"),
Pair("Mahou Shoujo", "157"),
Pair("Mangá", "117"),
Pair("Mecha", "37"),
Pair("Mechas", "143"),
Pair("Medieval", "144"),
Pair("Melodrama", "184"),
Pair("Militar", "55"),
Pair("Mistério", "31"),
Pair("Música", "50"),
Pair("Novel", "107"),
Pair("Nudez", "181"),
Pair("Paródia", "72"),
Pair("Pastelão", "147"),
Pair("Piratas", "217"),
Pair("Policial", "60"),
Pair("Programa de TV japoneses", "139"),
Pair("Programa infantis", "175"),
Pair("Programas e séries brasileiras", "176"),
Pair("Programas Infantis", "178"),
Pair("Psicológico", "71"),
Pair("Realidade Virtual", "164"),
Pair("Robô", "145"),
Pair("Romance", "21"),
Pair("Samurai", "57"),
Pair("sci-fi", "49"),
Pair("Seinen", "23"),
Pair("Série baseado em mangás", "138"),
Pair("Série baseado em quadrinhos", "87"),
Pair("Séries", "65"),
Pair("Shonen", "127"),
Pair("Shoujo", "32"),
Pair("Shoujo Mahou", "216"),
Pair("Shoujo-ai", "59"),
Pair("Shounen", "14"),
Pair("Shounen-ai", "95"),
Pair("Slice Of Life", "35"),
Pair("Sobrenatural", "16"),
Pair("Sports", "186"),
Pair("Steampunk", "80"),
Pair("Super Heróis", "148"),
Pair("Super Poderes", "12"),
Pair("Superaventura", "170"),
Pair("Superhero fiction", "173"),
Pair("Supernatural", "86"),
Pair("Superpoderes", "41"),
Pair("Suspense", "15"),
Pair("Terror", "90"),
Pair("Thriller", "94"),
Pair("TMS Entertainment", "104"),
Pair("Tokusatsu", "171"),
Pair("Tragédia", "85"),
Pair("Vampiro.", "106"),
Pair("Vida Colegial", "196"),
Pair("Vida Cotidiana", "142"),
Pair("Vida de trabalho", "206"),
Pair("Vida Diaria", "97"),
Pair("Vida Escolar", "22"),
Pair("Violência.", "56"),
Pair("Violentos", "167"),
Pair("Visual Novel", "129"),
Pair("White Fox", "109"),
Pair("WIT", "213"),
Pair("Yaoi", "115"),
Pair("Yuri", "34"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital
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://animesdigital.org//anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesDigitalUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 2) {
val item = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimesDigital.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,22 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
private const val HOST = "https://sabornutritivo.com"
class ProtectorExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val fixedUrl = if (!url.startsWith("https")) "https:$url" else url
val token = fixedUrl.toHttpUrl().queryParameter("token")!!
val headers = Headers.headersOf("cookie", "token=$token;")
val doc = client.newCall(GET("$HOST/social.php", headers)).execute().asJsoup()
val videoHeaders = Headers.headersOf("referer", doc.location())
val iframeUrl = doc.selectFirst("iframe")!!.attr("src").trim()
return listOf(Video(iframeUrl, "Animes Digital", iframeUrl, videoHeaders))
}
}

View file

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.animeextension.pt.animesdigital.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import okhttp3.Headers
object ScriptExtractor {
fun videosFromScript(scriptData: String, headers: Headers): List<Video> {
val script = when {
"eval(function" in scriptData -> Unpacker.unpack(scriptData)
else -> scriptData
}.ifEmpty { null }?.replace("\\", "") ?: return emptyList()
return script.substringAfter("sources:").substringAfter(".src(")
.substringBefore(")")
.substringAfter("[")
.substringBefore("]")
.split("{")
.drop(1)
.map {
val quality = it.substringAfter("label", "")
.substringAfterKey()
.trim()
.ifEmpty { "Animes Digital" }
val url = it.substringAfter("file").substringAfter("src")
.substringAfterKey()
.trim()
Video(url, quality, url, headers)
}
}
private fun String.substringAfterKey() = substringAfter(':')
.substringAfter('"')
.substringBefore('"')
.substringAfter("'")
.substringBefore("'")
}

View file

@ -0,0 +1,9 @@
ext {
extName = 'AnimesFox BR'
extClass = '.AnimesFoxBR'
themePkg = 'dooplay'
baseUrl = 'https://animesfox.net'
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,209 @@
package eu.kanade.tachiyomi.animeextension.pt.animesfoxbr
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class AnimesFoxBR : DooPlay(
"pt-BR",
"AnimesFox BR",
"https://animesfox.net",
) {
// ============================== Popular ===============================
// The site doesn't have a true popular anime tab,
// so we use the latest added anime page instead.
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/page/$page")
override fun popularAnimeSelector() = "div.clw div.b_flex > div > a"
override fun popularAnimeNextPageSelector() = "div.pagination i#nextpagination"
// ============================== Episodes ==============================
override fun episodeListSelector() = "div.se-a > div.anime_item > a"
override fun episodeFromElement(element: Element, seasonName: String) =
super.episodeFromElement(element, seasonName).apply {
name = name.substringBefore("- ")
}
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("360p ~ SD", "720p ~ HD")
override val prefQualityEntries = prefQualityValues
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
return doc.select("ul#playeroptionsul li").mapNotNull {
val url = getPlayerUrl(it)
val language = it.selectFirst("span.title")?.text() ?: "Linguagem desconhecida"
when {
baseUrl in url -> extractVideos(url, language)
else -> null
}
}.flatten()
}
private fun extractVideos(url: String, language: String): List<Video> {
return client.newCall(GET(url, headers)).execute().let { response ->
response.body.string()
.substringAfter("sources:[")
.substringBefore("]")
.split("},")
.mapNotNull {
val videoUrl = it.substringAfter("file: \"")
.substringBefore('"')
.ifBlank { return@mapNotNull null }
val quality = it.substringAfter("label:\"").substringBefore('"')
Video(videoUrl, "$language($quality)", videoUrl, headers = headers)
}
}
}
private fun getPlayerUrl(player: Element): String {
val body = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("post", player.attr("data-post"))
.add("nume", player.attr("data-nume"))
.add("type", player.attr("data-type"))
.build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
.execute()
.let { response ->
response.body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
.let { url ->
when {
url.contains("token=") -> {
url.substringAfter("token=")
.substringBefore("' ")
.let { Base64.decode(it, Base64.DEFAULT) }
.let(::String)
}
url.contains("iframe") -> {
url.substringAfter("?link=").substringBefore("'")
}
else -> ""
}
}
}
}
// =============================== Search ===============================
override fun searchAnimeNextPageSelector() = "div.pagination > *:last-child:not(.current)"
// ============================== Filters ===============================
override fun genresListRequest() = GET("$baseUrl/categorias")
override fun genresListSelector() = "div.box_category > a"
override fun genresListParse(document: Document) =
super.genresListParse(document).map {
Pair(it.first.substringAfter(" "), it.second)
}.toTypedArray()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val doc = getRealAnimeDoc(document)
return SAnime.create().apply {
setUrlWithoutDomain(doc.location())
thumbnail_url = doc.selectFirst("div.capa_poster img")!!.attr("src")
val container = doc.selectFirst("div.container_anime_r")!!
title = container.selectFirst("div > h1")!!.text().let {
when {
"email protected" in it -> {
val decoded = container.selectFirst("div > h1 > a")!!
.attr("data-cfemail")
.decodeEmail()
it.replace("[email protected]", decoded)
}
else -> it
}
}
genre = container.select("div.btn_gen").eachText().joinToString()
description = buildString {
container.selectFirst("div.sinopse")?.let {
append(it.text() + "\n\n")
}
container.selectFirst("div.container_anime_nome > h2")?.let {
append("Nome alternativo: ${it.text()}\n")
}
container.select("div.container_anime_back").forEach {
val infoType = it.selectFirst("div.info-nome")?.text() ?: return@forEach
val infoData = it.selectFirst("span")?.text() ?: return@forEach
append("$infoType: $infoData\n")
}
}
}
}
// =============================== Latest ===============================
override val latestUpdatesPath = "episodios"
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoLanguagePref = ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY
title = PREF_LANGUAGE_TITLE
entries = PREF_LANGUAGE_ENTRIES
entryValues = PREF_LANGUAGE_VALUES
setDefaultValue(PREF_LANGUAGE_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()
}
}
screen.addPreference(videoLanguagePref)
super.setupPreferenceScreen(screen)
}
// ============================= Utilities ==============================
override val animeMenuSelector = "div.epsL i.material-icons:contains(library)"
private fun String.decodeEmail(): String {
val hex = chunked(2).map { it.toInt(16) }
return hex.drop(1).joinToString("") {
Char(it xor hex.first()).toString()
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(videoSortPrefKey, videoSortPrefDefault)!!
val language = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.lowercase().contains(quality.lowercase()) },
{ it.quality.lowercase().contains(language.lowercase()) },
),
).reversed()
}
companion object {
private const val PREF_LANGUAGE_KEY = "preferred_language"
private const val PREF_LANGUAGE_DEFAULT = "Legendado"
private const val PREF_LANGUAGE_TITLE = "Língua preferida"
private val PREF_LANGUAGE_VALUES = arrayOf("Legendado", "Dublado")
private val PREF_LANGUAGE_ENTRIES = PREF_LANGUAGE_VALUES
}
}

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)
}
}

View file

@ -0,0 +1,17 @@
ext {
extName = 'AnimesOnline'
extClass = '.AnimesOnline'
themePkg = 'dooplay'
baseUrl = 'https://animesonline.nz'
overrideVersionCode = 9
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:blogger-extractor"))
implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:mixdrop-extractor"))
implementation(project(":lib:streamtape-extractor"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,146 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.AnimesOnlinePlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.RuplayExtractor
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.bloggerextractor.BloggerExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class AnimesOnline : DooPlay(
"pt-BR",
"AnimesOnline",
"https://animesonline.nz",
) {
override val id: Long = 2969482460524685571L
// ============================== Popular ===============================
override fun popularAnimeSelector() = "div.sidebar.right article > a"
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/")
// =============================== Search ===============================
override fun searchAnimeSelector() = "div.result-item article div.thumbnail > a"
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) =
getRealAnimeDoc(response.asJsoup())
.select(episodeListSelector())
.map(::episodeFromElement)
.reversed()
override fun episodeListSelector() = "ul.episodios > li > div.episodiotitle > a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.text().also {
name = it
episode_number = it.substringAfter(" ").toFloatOrNull() ?: 0F
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("ul#playeroptionsul li")
return players.parallelCatchingFlatMapBlocking(::getPlayerVideos)
}
override val prefQualityValues = arrayOf("360p", "480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues
private val ruplayExtractor by lazy { RuplayExtractor(client) }
private val animesOnlineExtractor by lazy { AnimesOnlinePlayerExtractor(client) }
private val bloggerExtractor by lazy { BloggerExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val mixDropExtractor by lazy { MixDropExtractor(client) }
private fun getPlayerVideos(player: Element): List<Video> {
val name = player.selectFirst("span.title")!!.text().lowercase()
val url = getPlayerUrl(player) ?: return emptyList()
return when {
"ruplay" in name -> ruplayExtractor.videosFromUrl(url)
"streamwish" in name -> streamWishExtractor.videosFromUrl(url)
"filemoon" in name -> filemoonExtractor.videosFromUrl(url)
"mixdrop" in name -> mixDropExtractor.videoFromUrl(url)
"streamtape" in name ->
streamTapeExtractor.videoFromUrl(url)
?.let(::listOf)
?: emptyList()
"/player1/" in url || "/player2/" in url ->
animesOnlineExtractor.videosFromUrl(url)
"/player/" in url -> bloggerExtractor.videosFromUrl(url, headers)
else -> emptyList()
}
}
private fun getPlayerUrl(player: Element): String? {
val body = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("post", player.attr("data-post"))
.add("nume", player.attr("data-nume"))
.add("type", player.attr("data-type"))
.build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
.execute()
.let { response ->
response.body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
.takeIf(String::isNotBlank)
?.let {
when {
it.contains("$baseUrl/aviso/") ->
it.toHttpUrl().queryParameter("url")
else -> it
}
}
}
}
// ============================== Filters ===============================
override fun genresListRequest() = GET("$baseUrl/animes/")
override fun genresListSelector() = "div.filter > div.select:first-child option:not([disabled])"
override fun genresListParse(document: Document): Array<Pair<String, String>> {
val items = document.select(genresListSelector()).map {
val name = it.text()
val value = it.attr("value").substringAfter("$baseUrl/")
Pair(name, value)
}.toTypedArray()
return if (items.isEmpty()) {
items
} else {
arrayOf(Pair(selectFilterText, "")) + items
}
}
// ============================= Utilities ==============================
override fun getRealAnimeDoc(document: Document): Document {
if (!document.location().contains("/episodio/")) return document
return document.selectFirst("div.pag_episodes div.item > a:has(i.fa-th)")?.let {
client.newCall(GET(it.attr("href"), headers)).execute()
.asJsoup()
} ?: document
}
}

View file

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.OkHttpClient
class AnimesOnlinePlayerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
return client.newCall(GET(url)).execute()
.body.string()
.substringAfter("sources: [")
.substringBefore("]")
.split("{")
.drop(1)
.map {
val label = it.substringAfter("label").substringAfter(":\"").substringBefore('"')
val videoUrl = it.substringAfter("file")
.substringAfter(":\"")
.substringBefore('"')
.replace("\\", "")
Video(videoUrl, "Player - $label", videoUrl)
}
}
}

View file

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class RuplayExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
return client.newCall(GET(url)).execute()
.body.string()
.substringAfter("Playerjs({")
.substringAfter("file:\"")
.substringBefore("\"")
.split(",")
.map {
val videoUrl = it.substringAfter("]")
val quality = it.substringAfter("[").substringBefore("]")
val headers = Headers.headersOf("Referer", videoUrl)
Video(videoUrl, "Ruplay - $quality", videoUrl, headers = headers)
}
}
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.animesroll.AnimesROLLUrlActivity"
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="www.anroll.net"
android:pathPattern="/a/..*"
android:scheme="https" />
<data
android:host="www.anroll.net"
android:pathPattern="/f/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,7 @@
ext {
extName = 'AnimesROLL'
extClass = '.AnimesROLL'
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,192 @@
package eu.kanade.tachiyomi.animeextension.pt.animesroll
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.AnimeDataDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.EpisodeListDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.LatestAnimeDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.MovieInfoDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.PagePropDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.SearchResultsDto
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.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
class AnimesROLL : AnimeHttpSource() {
override val name = "AnimesROLL"
override val baseUrl = "https://www.anroll.net"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
}
// ============================== Popular ===============================
// The site doesn't have a popular anime tab, so we use the home page instead (latest anime).
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeParse(response: Response) = latestUpdatesParse(response)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos")
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsed = response.asJsoup().parseAs<LatestAnimeDto>()
val animes = parsed.episodes.map { it.episode.anime!!.toSAnime() }
return AnimesPage(animes, false)
}
// =============================== Search ===============================
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
return AnimesPage(listOf(details), false)
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) {
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path"))
.awaitSuccess()
.use(::searchAnimeByPathParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
val results = response.parseAs<SearchResultsDto>()
val animes = (results.animes + results.movies).map { it.toSAnime() }
return AnimesPage(animes, false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return GET("$OLD_API_URL/search?q=$query")
}
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime {
val doc = response.asJsoup()
val anime = when {
doc.location().contains("/f/") -> doc.parseAs<MovieInfoDto>().movieData
else -> doc.parseAs<AnimeDataDto>()
}
return anime.toSAnime().apply {
setUrlWithoutDomain(doc.location())
author = anime.director.takeIf { it != "0" }
description = buildString {
append(anime.description.ifNotEmpty { it + "\n" })
append(anime.duration.ifNotEmpty { "\nDuração: $it" })
append(anime.animeCalendar?.ifNotEmpty { "\nLança toda(o) $it" }.orEmpty())
}
genre = doc.select("div#generos > a").eachText().joinToString()
status = if (anime.animeCalendar == null) SAnime.COMPLETED else SAnime.ONGOING
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
val originalUrl = doc.location()
return if ("/f/" in originalUrl) {
val od = doc.parseAs<MovieInfoDto>().movieData.od
SEpisode.create().apply {
url = "$OLD_API_URL/od/$od/filme.mp4"
name = "Filme"
episode_number = 0F
}.let(::listOf)
} else {
val anime = doc.parseAs<AnimeDataDto>()
val urlStart = "https://cdn-01.gamabunta.xyz/hls/animes/${anime.slug}"
return fetchEpisodesRecursively(anime.id).map { episode ->
SEpisode.create().apply {
val epNum = episode.episodeNumber
name = "Episódio #$epNum"
episode_number = epNum.toFloat()
url = "$urlStart/$epNum.mp4/media-1/stream.m3u8"
}
}
}
}
private fun fetchEpisodesRecursively(animeId: String, page: Int = 1): List<EpisodeDto> {
val response = client.newCall(episodeListRequest(animeId, page))
.execute()
.parseAs<EpisodeListDto>()
return response.episodes.let { episodes ->
when {
response.meta.totalOfPages > page ->
episodes + fetchEpisodesRecursively(animeId, page + 1)
else -> episodes
}
}
}
private fun episodeListRequest(animeId: String, page: Int) =
GET("$NEW_API_URL/animes/$animeId/episodes?page=$page%order=desc")
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val epUrl = episode.url
return listOf(Video(epUrl, "default", epUrl))
}
override fun videoListRequest(episode: SEpisode): Request {
TODO("Not yet implemented")
}
override fun videoListParse(response: Response): List<Video> {
TODO("Not yet implemented")
}
// ============================= Utilities ==============================
private inline fun <reified T> Document.parseAs(): T {
val nextData = this.selectFirst("script#__NEXT_DATA__")!!
.data()
.substringAfter(":")
.substringBeforeLast(",\"page\"")
return json.decodeFromString<PagePropDto<T>>(nextData).data
}
private fun String.ifNotEmpty(block: (String) -> String): String {
return if (isNotEmpty() && this != "0") block(this) else ""
}
fun AnimeDataDto.toSAnime() = SAnime.create().apply {
val ismovie = slug == ""
url = if (ismovie) "/f/$id" else "/anime/$slug"
thumbnail_url = "https://static.anroll.net/images/".let {
if (ismovie) {
it + "filmes/capas/$slug_movie.jpg"
} else {
it + "animes/capas/$slug.jpg"
}
}
title = anititle
}
companion object {
private const val OLD_API_URL = "https://apiv2-prd.anroll.net"
private const val NEW_API_URL = "https://apiv3-prd.anroll.net"
const val PREFIX_SEARCH = "path:"
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animesroll
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://www.anroll.net/<type>/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesROLLUrlActivity : 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 path = "${pathSegments[0]}/${pathSegments[1]}"
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimesROLL.PREFIX_SEARCH}$path")
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,73 @@
package eu.kanade.tachiyomi.animeextension.pt.animesroll.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
data class PagePropDto<T>(val pageProps: DataPropDto<T>) {
val data by lazy { pageProps.data }
}
@Serializable
data class DataPropDto<T>(val data: T)
@Serializable
data class LatestAnimeDto(
@SerialName("data_releases")
val episodes: List<EpisodeAnimeDto>,
) {
@Serializable
data class EpisodeAnimeDto(val episode: EpisodeDto)
}
@Serializable
data class MovieInfoDto(
@SerialName("data_movie")
val movieData: AnimeDataDto,
)
@Serializable
data class AnimeDataDto(
@SerialName("diretor")
val director: String = "",
@JsonNames("nome_filme", "titulo")
val anititle: String,
@JsonNames("sinopse", "sinopse_filme")
val description: String = "",
@SerialName("slug_serie")
val slug: String = "",
@SerialName("slug_filme")
val slug_movie: String = "",
@SerialName("duracao")
val duration: String = "",
@SerialName("generate_id")
val id: String = "",
val animeCalendar: String? = null,
val od: String = "",
)
@Serializable
data class EpisodeListDto(
@SerialName("data")
val episodes: List<EpisodeDto>,
val meta: MetadataDto,
) {
@Serializable
data class MetadataDto(val totalOfPages: Int)
}
@Serializable
data class EpisodeDto(
@SerialName("n_episodio")
val episodeNumber: String,
val anime: AnimeDataDto? = null,
)
@Serializable
data class SearchResultsDto(
@SerialName("data_anime")
val animes: List<AnimeDataDto>,
@SerialName("data_filme")
val movies: List<AnimeDataDto>,
)

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.animestc.AnimesTCUrlActivity"
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="www.animestc.net"
android:pathPattern="/animes/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,174 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object ATCFilters {
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", ATCFiltersData.TYPES)
class YearFilter : QueryPartFilter("Ano", ATCFiltersData.YEARS)
class GenreFilter : QueryPartFilter("Gênero", ATCFiltersData.GENRES)
class StatusFilter : QueryPartFilter("Status", ATCFiltersData.STATUS)
val FILTER_LIST get() = AnimeFilterList(
TypeFilter(),
YearFilter(),
GenreFilter(),
StatusFilter(),
)
data class FilterSearchParams(
val type: String = "series",
val year: String = "",
val genre: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<GenreFilter>(),
filters.asQueryPart<StatusFilter>(),
)
}
private object ATCFiltersData {
val TYPES = arrayOf(
Pair("Anime", "series"),
Pair("Filme", "movie"),
Pair("OVA", "ova"),
)
val SELECT = Pair("Selecione", "")
val STATUS = arrayOf(
SELECT,
Pair("Cancelado", "canceled"),
Pair("Completo", "complete"),
Pair("Em Lançamento", "airing"),
Pair("Pausado", "onhold"),
)
val YEARS = arrayOf(SELECT) + (1997..2024).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val GENRES = arrayOf(
SELECT,
Pair("Ação", "acao"),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Artes Marciais", "artes-marciais"),
Pair("Artes Marcial", "artes-marcial"),
Pair("Aventura", "aventura"),
Pair("Beisebol", "beisebol"),
Pair("Boys Love", "boys-love"),
Pair("Comédia", "comedia"),
Pair("Comédia Romântica", "comedia-romantica"),
Pair("Comedy", "comedy"),
Pair("Crianças", "criancas"),
Pair("Culinária", "culinaria"),
Pair("Cyberpunk", "cyberpunk"),
Pair("Demônios", "demonios"),
Pair("Distopia", "distopia"),
Pair("Documentário", "documentario"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escola", "escola"),
Pair("Escolar", "escolar"),
Pair("Espaço", "espaco"),
Pair("Esporte", "esporte"),
Pair("Esportes", "esportes"),
Pair("Fantasia", "fantasia"),
Pair("Ficção Científica", "ficcao-cientifica"),
Pair("Futebol", "futebol"),
Pair("Game", "game"),
Pair("Girl battleships", "girl-battleships"),
Pair("Gourmet", "gourmet"),
Pair("Gundam", "gundam"),
Pair("Harém", "harem"),
Pair("Hentai", "hentai"),
Pair("Historia", "historia"),
Pair("Historial", "historial"),
Pair("Historical", "historical"),
Pair("Histórico", "historico"),
Pair("Horror", "horror"),
Pair("Humor Negro", "humor-negro"),
Pair("Ídolo", "idolo"),
Pair("Infantis", "infantis"),
Pair("Investigação", "investigacao"),
Pair("Isekai", "isekai"),
Pair("Jogo", "jogo"),
Pair("Jogos", "jogos"),
Pair("Josei", "josei"),
Pair("Kids", "kids"),
Pair("Luta", "luta"),
Pair("Maduro", "maduro"),
Pair("Máfia", "mafia"),
Pair("Magia", "magia"),
Pair("Mágica", "magica"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Militares", "militares"),
Pair("Mistério", "misterio"),
Pair("Música", "musica"),
Pair("Musical", "musical"),
Pair("Não Informado!", "nao-informado"),
Pair("Paródia", "parodia"),
Pair("Piratas", "piratas"),
Pair("Polícia", "policia"),
Pair("Policial", "policial"),
Pair("Político", "politico"),
Pair("Pós-Apocalíptico", "pos-apocaliptico"),
Pair("Psico", "psico"),
Pair("Psicológico", "psicologico"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Samurais", "samurais"),
Pair("Sátiro", "satiro"),
Pair("School Life", "school-life"),
Pair("SciFi", "scifi"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shotacon", "shotacon"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounem", "shounem"),
Pair("Shounen", "shounen"),
Pair("Shounen-ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Space", "space"),
Pair("Supernatural", "supernatural"),
Pair("Super Poder", "super-poder"),
Pair("Super-Poderes", "super-poderes"),
Pair("Suspense", "suspense"),
Pair("tear-studio", "tear-studio"),
Pair("Terror", "terror"),
Pair("Thriller", "thriller"),
Pair("Tragédia", "tragedia"),
Pair("Vampiro", "vampiro"),
Pair("Vampiros", "vampiros"),
Pair("Vida Escolar", "vida-escolar"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Zombie", "zombie"),
)
}
}

View file

@ -0,0 +1,290 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.AnimeDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.ResponseDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.VideoDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.extractors.LinkBypasser
import eu.kanade.tachiyomi.animeextension.pt.animestc.extractors.SendcmExtractor
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.AnimeHttpSource
import eu.kanade.tachiyomi.lib.googledriveextractor.GoogleDriveExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AnimesTC"
override val baseUrl = "https://api2.animestc.com"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$HOST_URL/")
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/series?order=id&direction=asc&page=1&top=true", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val data = response.parseAs<List<AnimeDto>>()
val animes = data.map(::searchAnimeFromObject)
return AnimesPage(animes, false)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET(HOST_URL, headers)
override fun latestUpdatesParse(response: Response): AnimesPage {
val doc = response.asJsoup()
val animes = doc.select("div > article.episode").map {
SAnime.create().apply {
val ahref = it.selectFirst("h3 > a.episode-info-title-orange")!!
title = ahref.text()
val slug = ahref.attr("href").substringAfterLast("/")
setUrlWithoutDomain("/series?slug=$slug")
thumbnail_url = it.selectFirst("img.episode-image")?.attr("abs:data-src")
}
}
.filter { it.thumbnail_url?.contains("/_nuxt/img/") == false }
.distinctBy { it.url }
return AnimesPage(animes, false)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = ATCFilters.getSearchParameters(filters)
val url = "$baseUrl/series?order=title&direction=asc&page=$page".toHttpUrl()
.newBuilder()
.addQueryParameter("type", params.type)
.addQueryParameter("search", query)
.addQueryParameter("year", params.year)
.addQueryParameter("releaseStatus", params.status)
.addQueryParameter("tag", params.genre)
.build()
return GET(url, headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val data = response.parseAs<ResponseDto<AnimeDto>>()
val animes = data.items.map(::searchAnimeFromObject)
val hasNextPage = data.lastPage > data.page
return AnimesPage(animes, hasNextPage)
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/series?slug=$slug"))
.awaitSuccess()
.use(::searchAnimeBySlugParse)
} else {
return super.getSearchAnime(page, query, filters)
}
}
override fun getFilterList(): AnimeFilterList = ATCFilters.FILTER_LIST
private fun searchAnimeFromObject(anime: AnimeDto) = SAnime.create().apply {
thumbnail_url = anime.cover.url
title = anime.title
setUrlWithoutDomain("/series/${anime.id}")
}
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
return AnimesPage(listOf(details), false)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val anime = response.getAnimeDto()
setUrlWithoutDomain("/series/${anime.id}")
title = anime.title
status = anime.status
thumbnail_url = anime.cover.url
artist = anime.producer
genre = anime.genres
description = buildString {
append(anime.synopsis + "\n")
anime.classification?.also { append("\nClassificação: ", it, " anos") }
anime.year?.also { append("\nAno de lançamento: ", it) }
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val id = response.getAnimeDto().id
return getEpisodeList(id)
}
private fun episodeListRequest(animeId: Int, page: Int) =
GET("$baseUrl/episodes?order=id&direction=desc&page=$page&seriesId=$animeId&specialOrder=true")
private fun getEpisodeList(animeId: Int, page: Int = 1): List<SEpisode> {
val response = client.newCall(episodeListRequest(animeId, page)).execute()
val parsed = response.parseAs<ResponseDto<EpisodeDto>>()
val episodes = parsed.items.map(::episodeFromObject)
if (parsed.page < parsed.lastPage) {
return episodes + getEpisodeList(animeId, page + 1)
} else {
return episodes
}
}
private fun episodeFromObject(episode: EpisodeDto) = SEpisode.create().apply {
name = episode.title
setUrlWithoutDomain("/episodes?slug=${episode.slug}")
episode_number = episode.number.toFloat()
date_upload = episode.created_at.toDate()
}
// ============================ Video Links =============================
private val sendcmExtractor by lazy { SendcmExtractor(client) }
private val gdriveExtractor by lazy { GoogleDriveExtractor(client, headers) }
private val linkBypasser by lazy { LinkBypasser(client, json) }
private val supportedPlayers = listOf("send", "drive")
override fun videoListParse(response: Response): List<Video> {
val videoDto = response.parseAs<ResponseDto<VideoDto>>().items.first()
val links = videoDto.links
val allLinks = listOf(links.low, links.medium, links.high).flatten()
.filter { it.name in supportedPlayers }
val online = links.online?.run {
filterNot { "mega" in it }.map {
Video(it, "Player ATC", it, headers)
}
}.orEmpty()
val videoId = videoDto.id
return online + allLinks.parallelCatchingFlatMapBlocking { extractVideosFromLink(it, videoId) }
}
private fun extractVideosFromLink(video: VideoDto.VideoLink, videoId: Int): List<Video> {
val playerUrl = linkBypasser.bypass(video, videoId)
?: return emptyList()
val quality = when (video.quality) {
"low" -> "SD"
"medium" -> "HD"
"high" -> "FULLHD"
else -> "SD"
}
return when (video.name) {
"send" -> sendcmExtractor.videosFromUrl(playerUrl, quality)
"drive" -> {
val id = GDRIVE_REGEX.find(playerUrl)?.groupValues?.get(0) ?: return emptyList()
gdriveExtractor.videosFromUrl(id, "GDrive - $quality")
}
else -> emptyList()
}
}
// ============================== 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"
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_PLAYER_KEY
title = PREF_PLAYER_TITLE
entries = PREF_PLAYER_VALUES
entryValues = PREF_PLAYER_VALUES
setDefaultValue(PREF_PLAYER_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun Response.getAnimeDto(): AnimeDto {
val jsonString = body.string()
return try {
jsonString.parseAs<AnimeDto>()
} catch (e: Exception) {
// URL intent handler moment
jsonString.parseAs<ResponseDto<AnimeDto>>().items.first()
}
}
private fun String.toDate(): Long {
return try {
DATE_FORMATTER.parse(this)?.time
} catch (_: Throwable) { null } ?: 0L
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val player = preferences.getString(PREF_PLAYER_KEY, PREF_PLAYER_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(player) },
{ it.quality.contains("- $quality") },
),
).reversed()
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
const val PREFIX_SEARCH = "slug:"
private const val HOST_URL = "https://www.animestc.net"
private const val PREF_QUALITY_KEY = "pref_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")
private const val PREF_PLAYER_KEY = "pref_player"
private const val PREF_PLAYER_TITLE = "Player preferido"
private const val PREF_PLAYER_DEFAULT = "Sendcm"
private val PREF_PLAYER_VALUES = arrayOf("Sendcm", "GDrive", "Player ATC")
private val GDRIVE_REGEX = Regex("[\\w-]{28,}")
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc
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://wwww.animestc.net/animes/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesTCUrlActivity : 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", "${AnimesTC.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,78 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc.dto
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ResponseDto<T>(
@SerialName("data")
val items: List<T>,
val lastPage: Int,
val page: Int,
)
@Serializable
data class AnimeDto(
val classification: String?,
val cover: CoverDto,
val id: Int,
val producer: String?,
val releaseStatus: String,
val synopsis: String,
val tags: List<TagDto>,
val title: String,
val year: Int?,
) {
val status by lazy {
when (releaseStatus) {
"complete" -> SAnime.COMPLETED
"airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
val genres by lazy { tags.joinToString(", ") { it.name } }
@Serializable
data class TagDto(val name: String)
}
@Serializable
data class EpisodeDto(
@SerialName("seriesId")
val animeId: Int,
val cover: CoverDto?,
val created_at: String,
val number: String,
val slug: String,
val title: String,
)
@Serializable
data class VideoDto(
val id: Int,
val links: VideoLinksDto,
) {
@Serializable
data class VideoLinksDto(
val low: List<VideoLink> = emptyList(),
val medium: List<VideoLink> = emptyList(),
val high: List<VideoLink> = emptyList(),
val online: List<String>? = null,
)
@Serializable
data class VideoLink(
val index: Int,
val name: String,
val quality: String,
)
}
@Serializable
data class CoverDto(
val originalName: String,
) {
val url by lazy { "https://stc.animestc.com/$originalName" }
}

View file

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.VideoDto.VideoLink
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
class LinkBypasser(
private val client: OkHttpClient,
private val json: Json,
) {
fun bypass(video: VideoLink, episodeId: Int): String? {
val joined = "$episodeId/${video.quality}/${video.index}"
val encoded = Base64.encodeToString(joined.toByteArray(), Base64.NO_WRAP)
val url = "$PROTECTOR_URL/link/$encoded"
val res = client.newCall(GET(url)).execute()
if (res.code != 200) {
return null
}
// Sadly we MUST wait 6s or we are going to get a HTTP 500
Thread.sleep(6000L)
val id = res.asJsoup().selectFirst("meta#link-id")!!.attr("value")
val apiCall = client.newCall(GET("$PROTECTOR_URL/api/link/$id")).execute()
if (apiCall.code != 200) {
return null
}
val apiBody = apiCall.body.string()
return json.decodeFromString<LinkDto>(apiBody).link
}
@Serializable
data class LinkDto(val link: String)
companion object {
private const val PROTECTOR_URL = "https://protetor.animestc.xyz"
}
}

Some files were not shown because too many files have changed in this diff Show more