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