New source: Anime Fire (#507)

This commit is contained in:
Claudemirovsky
2022-04-18 05:53:00 -03:00
committed by GitHub
parent 5df63fb5ca
commit 4565e08ff3
11 changed files with 373 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension"/>

View File

@ -0,0 +1,14 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Anime Fire'
pkgNameSuffix = 'pt.animefire'
extClass = '.AnimeFire'
extVersionCode = 1
libVersion = '12'
}
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,95 @@
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 this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class GenreFilter : QueryPartFilter("Gênero", AFFiltersData.genres)
class SeasonFilter : QueryPartFilter("Temporada", AFFiltersData.seasons)
val filterList = 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,220 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animefire.extractors.AnimeFireExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Anime Fire"
override val baseUrl = "https://animefire.net"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val json = Json {
ignoreUnknownKeys = true
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl)
.add("Accept-Language", ACCEPT_LANGUAGE)
// ============================== Popular ===============================
override fun popularAnimeSelector() = latestUpdatesSelector()
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/top-animes/$page")
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "div.div_video_list > a"
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val url = element.attr("href")
episode.setUrlWithoutDomain(url)
episode.name = element.text()
episode.episode_number = try {
url.substringAfterLast("/").toFloat()
} catch (e: NumberFormatException) { 0F }
return episode
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document: Document = response.asJsoup()
val extractor = AnimeFireExtractor(client, json)
return extractor.videoListFromDocument(document)
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeSelector() = latestUpdatesSelector()
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val params = AFFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: AFFilters.FilterSearchParams): Request {
if (query.isBlank()) {
return when {
!filters.season.isBlank() -> GET("$baseUrl/temporada/${filters.season}/$page")
else -> GET("$baseUrl/genero/${filters.genre}/$page")
}
}
val fixedQuery = query.trim().replace(" ", "-").toLowerCase()
return GET("$baseUrl/pesquisar/$fixedQuery/$page")
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val content = document.selectFirst("div.divDivAnimeInfo")
val names = content.selectFirst("div.div_anime_names")
val infos = content.selectFirst("div.divAnimePageInfo")
anime.thumbnail_url = content.selectFirst("div.sub_animepage_img > img")
.attr("data-src")
anime.title = names.selectFirst("h1").text()
anime.genre = infos.select("a.spanGeneros").joinToString(", ") { it.text() }
anime.author = infos.getInfo("Estúdios")
anime.status = parseStatus(infos.getInfo("Status"))
var desc = content.selectFirst("div.divSinopse > span").text() + "\n"
names.selectFirst("h6")?.let { desc += "\nNome alternativo: ${it.text()}" }
infos.getInfo("Dia de")?.let { desc += "\nDia de lançamento: $it" }
infos.getInfo("Áudio")?.let { desc += "\nTipo: $it" }
infos.getInfo("Ano")?.let { desc += "\nAno: $it" }
infos.getInfo("Episódios")?.let { desc += "\nEpisódios: $it" }
infos.getInfo("Temporada")?.let { desc += "\nTemporada: $it" }
anime.description = desc
return anime
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "ul.pagination img.seta-right"
override fun latestUpdatesSelector(): String = "article.cardUltimosEps > a"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
val url = element.attr("href")
if (url.substringAfterLast("/").toIntOrNull() != null) {
val newUrl = url.substringBeforeLast("/") + "-todos-os-episodios"
anime.setUrlWithoutDomain(newUrl)
} else { anime.setUrlWithoutDomain(url) }
anime.title = element.selectFirst("h3.animeTitle").text()
anime.thumbnail_url = element.selectFirst("img").attr("data-src")
return anime
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/$page")
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PREFERRED_QUALITY
title = "Qualidade preferida"
entries = QUALITY_LIST
entryValues = QUALITY_LIST
setDefaultValue(QUALITY_LIST.last())
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
override fun getFilterList(): AnimeFilterList = AFFilters.filterList
// ============================= 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? {
val div = this.selectFirst("div.animeInfo:contains($key)")
if (div == null) return div
val span = div.selectFirst("span")
return span.text()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREFERRED_QUALITY, null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.equals(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
companion object {
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
private const val PREFERRED_QUALITY = "preferred_quality"
private val QUALITY_LIST = 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,24 @@
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.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
class AnimeFireExtractor(private val client: OkHttpClient, private val json: Json) {
fun videoListFromDocument(doc: Document): List<Video> {
val jsonUrl = doc.selectFirst("video#my-video").attr("data-video-src")
val response = client.newCall(GET(jsonUrl)).execute()
val responseDto = json.decodeFromString<AFResponseDto>(
response.body?.string().orEmpty()
)
val videoList = responseDto.videos.map {
val url = it.url.replace("\\", "")
Video(url, it.quality, url, null)
}
return videoList
}
}