refactor(src/pt): General refactorations + Purge AnimeTV (#2458)
This commit is contained in:
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
@ -43,6 +42,10 @@ class AniDong : ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
// ============================== 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")
|
||||
@ -50,20 +53,134 @@ class AniDong : ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
|
||||
override fun popularAnimeSelector() = "article.top10_animes_item > a"
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
// =============================== 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 fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/anime/$id"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByIdParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.use { it.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()
|
||||
.use { it.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.use { it.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("Not used.")
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String? {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
// =========================== 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("Not used.")
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val doc = getRealDoc(response.asJsoup())
|
||||
val doc = getRealDoc(response.use { it.asJsoup() })
|
||||
|
||||
val id = doc.selectFirst("link[rel=shortlink]")!!.attr("href").substringAfter("=")
|
||||
val body = FormBody.Builder()
|
||||
@ -71,8 +188,9 @@ class AniDong : ParsedAnimeHttpSource() {
|
||||
.add("anime_id", id)
|
||||
.build()
|
||||
|
||||
val res = client.newCall(POST("$baseUrl/api", headers = apiHeaders, body = body)).execute()
|
||||
val data = json.decodeFromString<EpisodeListDto>(res.body.string())
|
||||
val res = client.newCall(POST("$baseUrl/api", apiHeaders, body)).execute()
|
||||
.use { it.body.string() }
|
||||
val data = json.decodeFromString<EpisodeListDto>(res)
|
||||
|
||||
return buildList {
|
||||
data.episodes.forEach { add(episodeFromObject(it, "Episódio")) }
|
||||
@ -88,40 +206,9 @@ class AniDong : ParsedAnimeHttpSource() {
|
||||
name = "$prefix ${episode.epi_num}"
|
||||
}
|
||||
|
||||
// =========================== 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()?.let {
|
||||
append("Nomes alternativos: $it\n")
|
||||
}
|
||||
|
||||
doc.selectFirst("div[itemprop=description]")?.text()?.let {
|
||||
append("\n$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val doc = response.asJsoup()
|
||||
val doc = response.use { it.asJsoup() }
|
||||
return doc.select("div.player_option").flatMap {
|
||||
val url = it.attr("data-playerlink")
|
||||
val playerName = it.text().trim()
|
||||
@ -158,97 +245,13 @@ class AniDong : ParsedAnimeHttpSource() {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String? {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val searchData: SearchResultDto = response.use { it.body.string() }
|
||||
.takeIf { it.trim() != "402" }
|
||||
?.let(json::decodeFromString)
|
||||
?: return AnimesPage(emptyList<SAnime>(), 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 getFilterList() = AniDongFilters.FILTER_LIST
|
||||
|
||||
private val nonce by lazy {
|
||||
client.newCall(GET("$baseUrl/?js_global=1&ver=6.2.2")).execute()
|
||||
.use { it.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 searchAnimeSelector(): String {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/anime/$id"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByIdParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.asJsoup())
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.paginacao > a.next"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page/")
|
||||
|
||||
override fun latestUpdatesSelector() = "article.main_content_article > a"
|
||||
|
||||
// ============================= 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()
|
||||
client.newCall(GET(it.attr("href"), headers)).execute()
|
||||
.use { req -> req.asJsoup() }
|
||||
} ?: document
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ object AniDongFilters {
|
||||
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)
|
||||
|
@ -4,7 +4,6 @@ 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>>,
|
||||
@ -16,9 +15,7 @@ object AFFilters {
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return first { it is R }.let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
class GenreFilter : QueryPartFilter("Gênero", AFFiltersData.GENRES)
|
||||
|
@ -1,7 +1,6 @@
|
||||
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
|
||||
@ -17,7 +16,6 @@ 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.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@ -38,11 +36,11 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -51,42 +49,34 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/top-animes/$page")
|
||||
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()
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/home/$page")
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
override fun latestUpdatesSelector() = "article.cardUltimosEps > a"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.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)
|
||||
} else {
|
||||
IframeExtractor(client).videoListFromDocument(document, headers)
|
||||
// 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 videoListSelector() = throw Exception("not used")
|
||||
override fun videoFromElement(element: Element) = throw Exception("not used")
|
||||
override fun videoUrlParse(document: Document) = throw Exception("not used")
|
||||
override fun latestUpdatesNextPageSelector() = "ul.pagination img.seta-right"
|
||||
|
||||
// =============================== 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> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) {
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
@ -115,50 +105,60 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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")
|
||||
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 {
|
||||
append(content.selectFirst("div.divSinopse > span")!!.text() + "\n")
|
||||
names.selectFirst("h6")?.let { append("\nNome alternativo: ${it.text()}") }
|
||||
infos.getInfo("Dia de")?.let { append("\nDia de lançamento: $it") }
|
||||
infos.getInfo("Áudio")?.let { append("\nTipo: $it") }
|
||||
infos.getInfo("Ano")?.let { append("\nAno: $it") }
|
||||
infos.getInfo("Episódios")?.let { append("\nEpisódios: $it") }
|
||||
infos.getInfo("Temporada")?.let { append("\nTemporada: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector(): String = "ul.pagination img.seta-right"
|
||||
override fun latestUpdatesSelector(): String = "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")
|
||||
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") }
|
||||
}
|
||||
|
||||
title = element.selectFirst("h3.animeTitle")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/home/$page")
|
||||
// ============================== 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.use { it.asJsoup() }
|
||||
val videoElement = document.selectFirst("video#my-video")
|
||||
return if (videoElement != null) {
|
||||
AnimeFireExtractor(client, json).videoListFromElement(videoElement)
|
||||
} else {
|
||||
IframeExtractor(client).videoListFromDocument(document, headers)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
@ -181,7 +181,6 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun getFilterList(): AnimeFilterList = AFFilters.FILTER_LIST
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun parseStatus(statusString: String?): Int {
|
||||
return when (statusString?.trim()) {
|
||||
"Completo" -> SAnime.COMPLETED
|
||||
|
@ -3,22 +3,20 @@ 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.Element
|
||||
|
||||
class AnimeFireExtractor(private val client: OkHttpClient, private val json: Json) {
|
||||
|
||||
fun videoListFromElement(videoElement: Element): List<Video> {
|
||||
val jsonUrl = videoElement.attr("data-video-src")
|
||||
val response = client.newCall(GET(jsonUrl)).execute()
|
||||
val responseDto = json.decodeFromString<AFResponseDto>(
|
||||
response.body.string(),
|
||||
)
|
||||
val videoList = responseDto.videos.map {
|
||||
.use { it.body.string() }
|
||||
val responseDto = json.decodeFromString<AFResponseDto>(response)
|
||||
return responseDto.videos.map {
|
||||
val url = it.url.replace("\\", "")
|
||||
Video(url, it.quality, url)
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,8 @@ class IframeExtractor(private val client: OkHttpClient) {
|
||||
val iframeElement = doc.selectFirst("div#div_video iframe")!!
|
||||
val iframeUrl = iframeElement.attr("src")
|
||||
val response = client.newCall(GET(iframeUrl, headers)).execute()
|
||||
val html = response.body.string()
|
||||
val url = html.substringAfter("play_url")
|
||||
.use { it.body.string() }
|
||||
val url = response.substringAfter("play_url")
|
||||
.substringAfter(":\"")
|
||||
.substringBefore("\"")
|
||||
val video = Video(url, "Default", url, headers = headers)
|
||||
|
@ -29,6 +29,10 @@ class AnimesAria : ParsedAnimeHttpSource() {
|
||||
override val supportsLatest = true
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/novos/animes?page=$page")
|
||||
|
||||
override fun popularAnimeSelector() = "div.item > a"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
title = element.attr("title")
|
||||
@ -37,23 +41,60 @@ class AnimesAria : ParsedAnimeHttpSource() {
|
||||
|
||||
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/novos/animes?page=$page")
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/novos/episodios?page=$page")
|
||||
|
||||
override fun popularAnimeSelector() = "div.item > a"
|
||||
override fun latestUpdatesSelector() = "div.item > div.pos-rlt"
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
element.parent()!!.selectFirst("a > b")!!.ownText().let {
|
||||
name = it
|
||||
episode_number = it.substringAfter(" ").toFloatOrNull() ?: 0F
|
||||
}
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
scanlator = element.text().substringAfter(" ") // sub/dub
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
val ahref = element.selectFirst("a")!!
|
||||
title = ahref.attr("title")
|
||||
setUrlWithoutDomain(ahref.attr("href").substringBefore("/episodio/"))
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "td div.clear > a.btn-xs"
|
||||
override fun latestUpdatesNextPageSelector() = "a:containsOwn(Próximo):not(.disabled)"
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun getFilterList() = AnimesAriaFilters.FILTER_LIST
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/anime/$id"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByIdParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.use { it.asJsoup() })
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val params = AnimesAriaFilters.getSearchParameters(filters)
|
||||
val url = "$baseUrl/anime/buscar".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("tipo", params.type)
|
||||
.addQueryParameter("genero", params.genre)
|
||||
.addQueryParameter("status", params.status)
|
||||
.addQueryParameter("letra", params.letter)
|
||||
.addQueryParameter("audio", params.audio)
|
||||
.addQueryParameter("ano", params.year)
|
||||
.addQueryParameter("temporada", params.season)
|
||||
.build()
|
||||
return GET(url.toString())
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
@ -61,18 +102,18 @@ class AnimesAria : ParsedAnimeHttpSource() {
|
||||
val row = document.selectFirst("div.anime_background_w div.row")!!
|
||||
title = row.selectFirst("h1 > span")!!.text()
|
||||
status = row.selectFirst("div.clear span.btn")?.text().toStatus()
|
||||
thumbnail_url = document.selectFirst("link[as=image]")!!.attr("href")
|
||||
thumbnail_url = document.selectFirst("link[as=image]")?.attr("href")
|
||||
genre = row.select("div.clear a.btn").eachText().joinToString()
|
||||
|
||||
description = buildString {
|
||||
document.selectFirst("li.active > small")!!
|
||||
.ownText()
|
||||
.substringAfter(": ")
|
||||
.let(::append)
|
||||
.also(::append)
|
||||
|
||||
append("\n\n")
|
||||
|
||||
row.selectFirst("h1 > small")?.text()?.let {
|
||||
row.selectFirst("h1 > small")?.text()?.also {
|
||||
append("Títulos Alternativos: $it\n")
|
||||
}
|
||||
|
||||
@ -85,6 +126,20 @@ class AnimesAria : ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
element.parent()!!.selectFirst("a > b")!!.ownText().also {
|
||||
name = it
|
||||
episode_number = it.substringAfter(" ").toFloatOrNull() ?: 0F
|
||||
}
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
scanlator = element.text().substringAfter(" ") // sub/dub
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "td div.clear > a.btn-xs"
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
@ -119,61 +174,6 @@ class AnimesAria : ParsedAnimeHttpSource() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimesAriaFilters.FILTER_LIST
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val params = AnimesAriaFilters.getSearchParameters(filters)
|
||||
val url = "$baseUrl/anime/buscar".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("tipo", params.type)
|
||||
.addQueryParameter("genero", params.genre)
|
||||
.addQueryParameter("status", params.status)
|
||||
.addQueryParameter("letra", params.letter)
|
||||
.addQueryParameter("audio", params.audio)
|
||||
.addQueryParameter("ano", params.year)
|
||||
.addQueryParameter("temporada", params.season)
|
||||
.build()
|
||||
return GET(url.toString())
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/anime/$id"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByIdParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.asJsoup())
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
val ahref = element.selectFirst("a")!!
|
||||
title = ahref.attr("title")
|
||||
setUrlWithoutDomain(ahref.attr("href").substringBefore("/episodio/"))
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a:containsOwn(Próximo):not(.disabled)"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/novos/episodios?page=$page")
|
||||
|
||||
override fun latestUpdatesSelector() = "div.item > div.pos-rlt"
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private fun String?.toStatus() = when (this) {
|
||||
"Finalizado" -> SAnime.COMPLETED
|
||||
|
@ -16,9 +16,7 @@ object AnimesAriaFilters {
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return this.first { it is R }.let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
class TypeFilter : QueryPartFilter("Tipo", AnimesAriaFiltersData.TYPES)
|
||||
@ -40,16 +38,18 @@ object AnimesAriaFilters {
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val type: String,
|
||||
val genre: String,
|
||||
val status: String,
|
||||
val letter: String,
|
||||
val audio: String,
|
||||
val year: String,
|
||||
val season: String,
|
||||
val type: String = "",
|
||||
val genre: String = "",
|
||||
val status: String = "",
|
||||
val letter: String = "",
|
||||
val audio: String = "",
|
||||
val year: String = "",
|
||||
val season: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<TypeFilter>(),
|
||||
filters.asQueryPart<GenreFilter>(),
|
||||
|
@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
@ -49,50 +48,109 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
|
||||
override fun popularAnimeSelector() = latestUpdatesSelector()
|
||||
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
|
||||
// ============================== 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.use { it.asJsoup() }
|
||||
episodes += pageDoc.select(episodeListSelector()).map(::episodeFromElement)
|
||||
}
|
||||
episodes
|
||||
} else doc.select(episodeListSelector()).map(::episodeFromElement)
|
||||
// =============================== 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 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()?.let {
|
||||
if (!it.contains("Ainda não tem um titulo oficial")) {
|
||||
append(" - $it")
|
||||
}
|
||||
}
|
||||
override fun latestUpdatesNextPageSelector() = "ul > li.next"
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun getFilterList() = AnimesDigitalFilters.FILTER_LIST
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/anime/a/$id"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByIdParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "div.item_ep > a"
|
||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.use { it.asJsoup() })
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
private val searchToken by lazy {
|
||||
client.newCall(GET("$baseUrl/animes-legendado")).execute()
|
||||
.use {
|
||||
it.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::parse)
|
||||
.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("Not used.")
|
||||
}
|
||||
|
||||
// =========================== 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")
|
||||
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
|
||||
@ -110,11 +168,47 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
description = infos.selectFirst("div.sinopse")?.text()
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val doc = getRealDoc(response.use { it.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.use { it.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()?.let {
|
||||
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 {
|
||||
it.select(videoListSelector()).flatMap { element ->
|
||||
val player = response.use { it.asJsoup() }.selectFirst("div#player")!!
|
||||
return player.select("div.tab-video").flatMap { div ->
|
||||
div.select(videoListSelector()).flatMap { element ->
|
||||
runCatching {
|
||||
videosFromElement(element)
|
||||
}.onFailure { it.printStackTrace() }.getOrElse { emptyList() }
|
||||
@ -133,18 +227,18 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
client.newCall(GET(url, headers)).execute()
|
||||
.asJsoup()
|
||||
.use { it.asJsoup() }
|
||||
.select(videoListSelector())
|
||||
.flatMap(::videosFromElement)
|
||||
}
|
||||
"script" -> {
|
||||
val scriptData = element.data().let {
|
||||
when {
|
||||
"eval(function" in it -> Unpacker.unpack(it).ifEmpty { null }
|
||||
"eval(function" in it -> Unpacker.unpack(it)
|
||||
else -> it
|
||||
}
|
||||
}?.replace("\\", "")
|
||||
scriptData?.let(::videosFromScript) ?: emptyList()
|
||||
}.ifEmpty { null }?.replace("\\", "")
|
||||
scriptData?.let(::videosFromScript).orEmpty()
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
@ -177,99 +271,6 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
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::parse)
|
||||
.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("Not used.")
|
||||
}
|
||||
|
||||
private val searchToken by lazy {
|
||||
client.newCall(GET("$baseUrl/animes-legendado")).execute()
|
||||
.use {
|
||||
it.asJsoup().selectFirst("div.menu_filter_box")!!.attr("data-secury")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = AnimesDigitalFilters.FILTER_LIST
|
||||
|
||||
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
|
||||
|
||||
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 fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/anime/a/$id"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByIdParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.asJsoup())
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
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"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page")
|
||||
|
||||
override fun latestUpdatesSelector() = "div.b_flex:nth-child(2) > div.itemE > a"
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
@ -300,7 +301,7 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return document.selectFirst("div.subitem > a:contains(menu)")?.let { link ->
|
||||
client.newCall(GET(link.attr("href")))
|
||||
.execute()
|
||||
.asJsoup()
|
||||
.use { it.asJsoup() }
|
||||
} ?: document
|
||||
}
|
||||
|
||||
@ -309,15 +310,11 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
private fun Element.getInfo(key: String): String? {
|
||||
return selectFirst("div.info:has(span:containsOwn($key))")
|
||||
?.ownText()
|
||||
?.trim()
|
||||
?.let {
|
||||
when (it) {
|
||||
"", "?" -> null
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
return selectFirst("div.info:has(span:containsOwn($key))")?.run {
|
||||
ownText()
|
||||
.trim()
|
||||
.takeUnless { it.isBlank() || it == "?" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.substringAfterKey() = substringAfter(":")
|
||||
|
@ -19,26 +19,20 @@ object AnimesDigitalFilters {
|
||||
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.getFirst(): R {
|
||||
return first { it is R } as R
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return getFirst<R>().let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseTriFilter(
|
||||
options: Array<Pair<String, String>>,
|
||||
): List<List<String>> {
|
||||
return (getFirst<R>() as TriStateFilterList).state
|
||||
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 {
|
||||
val included = it.get(TriState.STATE_INCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
val excluded = it.get(TriState.STATE_EXCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class AnimesGames : ParsedAnimeHttpSource() {
|
||||
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")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "ol.pagination > a:contains(>)"
|
||||
@ -113,7 +113,7 @@ class AnimesGames : ParsedAnimeHttpSource() {
|
||||
addQueryParameter("filter_letter", params.letter)
|
||||
addQueryParameter("filter_order", params.orderBy)
|
||||
addQueryParameter("filter_sort", "abc")
|
||||
}.build().encodedQuery
|
||||
}.build().encodedQuery.orEmpty()
|
||||
|
||||
val genres = params.genres.joinToString { "\"$it\"" }
|
||||
val delgenres = params.deleted_genres.joinToString { "\"$it\"" }
|
||||
@ -155,7 +155,7 @@ class AnimesGames : ParsedAnimeHttpSource() {
|
||||
title = content.selectFirst("section > h1")!!.text()
|
||||
.removePrefix("Assistir ")
|
||||
.removeSuffix("Temporada Online")
|
||||
thumbnail_url = content.selectFirst("img")!!.attr("data-lazy-src")
|
||||
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")!!
|
||||
@ -170,8 +170,8 @@ class AnimesGames : ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
private fun Element.getInfo(info: String) =
|
||||
selectFirst("li:has(span:contains($info))")?.let {
|
||||
it.selectFirst("span[data]")?.text() ?: it.ownText()
|
||||
selectFirst("li:has(span:contains($info))")?.run {
|
||||
selectFirst("span[data]")?.text() ?: ownText()
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
@ -19,24 +19,20 @@ object AnimesGamesFilters {
|
||||
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.getFirst(): R {
|
||||
return first { it is R } as R
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return (getFirst<R>() as QueryPartFilter).toQueryPart()
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseTriFilter(
|
||||
options: Array<Pair<String, String>>,
|
||||
): List<List<String>> {
|
||||
return (getFirst<R>() as TriStateFilterList).state
|
||||
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 {
|
||||
val included = it.get(TriState.STATE_INCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
val excluded = it.get(TriState.STATE_EXCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class AnimesOrion : ParsedAnimeHttpSource() {
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
title = element.selectFirst("h2.ttl")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
@ -49,7 +49,7 @@ class AnimesOrion : ParsedAnimeHttpSource() {
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
|
||||
title = element.selectFirst("h2")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "nav.pagination > a.next"
|
||||
@ -100,7 +100,7 @@ class AnimesOrion : ParsedAnimeHttpSource() {
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
val doc = getRealDoc(document)
|
||||
setUrlWithoutDomain(doc.location())
|
||||
thumbnail_url = doc.selectFirst("img.lnk-blk")!!.attr("src")
|
||||
thumbnail_url = doc.selectFirst("img.lnk-blk")?.attr("src")
|
||||
|
||||
val infos = doc.selectFirst("header.hd > div.rght")!!
|
||||
title = infos.selectFirst("h2.title")!!.text()
|
||||
@ -110,7 +110,7 @@ class AnimesOrion : ParsedAnimeHttpSource() {
|
||||
description = buildString {
|
||||
infos.selectFirst("h2.ttl")?.text()
|
||||
?.takeIf(String::isNotBlank)
|
||||
?.let { append("Títulos alternativos: $it\n\n") }
|
||||
?.also { append("Títulos alternativos: $it\n\n") }
|
||||
|
||||
doc.select("div.entry > p").eachText().forEach {
|
||||
append("$it\n")
|
||||
@ -159,7 +159,7 @@ class AnimesOrion : ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
private fun extractVideoFromResponse(response: Response): Video {
|
||||
val decodedBody = LinkfunBypasser.decodeAtob(response.body.string())
|
||||
val decodedBody = LinkfunBypasser.decodeAtob(response.use { it.body.string() })
|
||||
val url = decodedBody
|
||||
.substringAfter("sources")
|
||||
.substringAfter("file: \"")
|
||||
@ -185,7 +185,8 @@ class AnimesOrion : ParsedAnimeHttpSource() {
|
||||
if (!document.location().contains("/episodio/")) return document
|
||||
|
||||
return document.selectFirst("div.epsdsnv > a:has(i.fa-indent)")?.let {
|
||||
client.newCall(GET(baseUrl + it.attr("href"), headers)).execute().asJsoup()
|
||||
client.newCall(GET(it.attr("href"), headers)).execute()
|
||||
.use { req -> req.asJsoup() }
|
||||
} ?: document
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@ -45,18 +44,75 @@ class AnimesROLL : AnimeHttpSource() {
|
||||
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.use { it.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 fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) {
|
||||
val path = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/$path"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByPathParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(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.use { it.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 originalUrl = response.request.url.toString()
|
||||
val doc = response.use { it.asJsoup() }
|
||||
val originalUrl = doc.location()
|
||||
return if ("/f/" in originalUrl) {
|
||||
val od = response.asJsoup().parseAs<MovieInfoDto>().movieData.od
|
||||
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 = response.asJsoup().parseAs<AnimeDataDto>()
|
||||
val anime = doc.parseAs<AnimeDataDto>()
|
||||
val urlStart = "https://cdn-01.gamabunta.xyz/hls/animes/${anime.slug}"
|
||||
|
||||
return fetchEpisodesRecursively(anime.id).map { episode ->
|
||||
@ -101,68 +157,7 @@ class AnimesROLL : AnimeHttpSource() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val doc = response.asJsoup()
|
||||
val anime = when {
|
||||
response.request.url.toString().contains("/f/") ->
|
||||
doc.parseAs<MovieInfoDto>().movieData
|
||||
else -> doc.parseAs<AnimeDataDto>()
|
||||
}
|
||||
return anime.toSAnime().apply {
|
||||
author = if (anime.director != "0") anime.director else null
|
||||
var desc = anime.description.ifNotEmpty { it + "\n" }
|
||||
desc += anime.duration.ifNotEmpty { "\nDuração: $it" }
|
||||
desc += anime.animeCalendar?.let {
|
||||
it.ifNotEmpty { "\nLança toda(o) $it" }
|
||||
} ?: ""
|
||||
description = desc
|
||||
genre = doc.select("div#generos > a").joinToString(", ") { it.text() }
|
||||
status = if (anime.animeCalendar == null) SAnime.COMPLETED else SAnime.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
private fun searchAnimeByPathParse(response: Response, path: String): AnimesPage {
|
||||
val details = animeDetailsParse(response)
|
||||
details.url = "/$path"
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) {
|
||||
val path = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/$path"))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchAnimeByPathParse(response, path)
|
||||
}
|
||||
} else {
|
||||
super.fetchSearchAnime(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")
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
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)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private inline fun <reified T> Document.parseAs(): T {
|
||||
val nextData = this.selectFirst("script#__NEXT_DATA__")!!
|
||||
.data()
|
||||
@ -172,27 +167,24 @@ class AnimesROLL : AnimeHttpSource() {
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
val responseBody = body.string()
|
||||
return json.decodeFromString(responseBody)
|
||||
return use { it.body.string() }.let(json::decodeFromString)
|
||||
}
|
||||
|
||||
private fun String.ifNotEmpty(block: (String) -> String): String {
|
||||
return if (isNotEmpty() && this != "0") block(this) else ""
|
||||
}
|
||||
|
||||
fun AnimeDataDto.toSAnime(): SAnime {
|
||||
return 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"
|
||||
}
|
||||
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
|
||||
}
|
||||
title = anititle
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.animeextension.pt.animestc
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.AnimeDto
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
||||
object ATCFilters {
|
||||
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
@ -17,17 +17,23 @@ object ATCFilters {
|
||||
fun toQueryPart() = vals[state].second
|
||||
}
|
||||
|
||||
open class TriStateFilterList(name: String, values: List<TriState>) : AnimeFilter.Group<AnimeFilter.TriState>(name, values)
|
||||
private class TriStateVal(name: String) : AnimeFilter.TriState(name)
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return first { it is R } as R
|
||||
}
|
||||
open class TriStateFilterList(name: String, values: List<TriState>) : AnimeFilter.Group<TriState>(name, values)
|
||||
private class TriStateVal(name: String) : TriState(name)
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return getFirst<R>().let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseTriFilter(): List<List<String>> {
|
||||
return (first { it is R } as TriStateFilterList).state
|
||||
.filterNot { it.isIgnored() }
|
||||
.map { filter -> filter.state to filter.name }
|
||||
.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", ATCFiltersData.INITIAL_LETTER)
|
||||
@ -56,36 +62,32 @@ object ATCFilters {
|
||||
data class FilterSearchParams(
|
||||
val initialLetter: String = "",
|
||||
val status: String = "",
|
||||
var orderAscending: Boolean = true,
|
||||
var sortBy: String = "",
|
||||
val blackListedGenres: ArrayList<String> = ArrayList(),
|
||||
val includedGenres: ArrayList<String> = ArrayList(),
|
||||
val orderAscending: Boolean = true,
|
||||
val sortBy: String = "",
|
||||
val blackListedGenres: List<String> = emptyList(),
|
||||
val includedGenres: List<String> = emptyList(),
|
||||
var animeName: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
val searchParams = FilterSearchParams(
|
||||
val (includedGenres, excludedGenres) = filters.parseTriFilter<GenresFilter>()
|
||||
|
||||
val sortFilter = filters.firstOrNull { it is SortFilter } as? SortFilter
|
||||
val (orderBy, ascending) = sortFilter?.state?.run {
|
||||
val order = ATCFiltersData.ORDERS[index].second
|
||||
val orderAscending = ascending
|
||||
Pair(order, orderAscending)
|
||||
} ?: Pair("", true)
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<InitialLetterFilter>(),
|
||||
filters.asQueryPart<StatusFilter>(),
|
||||
ascending,
|
||||
orderBy,
|
||||
includedGenres,
|
||||
excludedGenres,
|
||||
)
|
||||
|
||||
filters.getFirst<SortFilter>().state?.let {
|
||||
val order = ATCFiltersData.ORDERS[it.index].second
|
||||
searchParams.orderAscending = it.ascending
|
||||
searchParams.sortBy = order
|
||||
}
|
||||
|
||||
filters.getFirst<GenresFilter>()
|
||||
.state.forEach { genre ->
|
||||
if (genre.isIncluded()) {
|
||||
searchParams.includedGenres.add(genre.name)
|
||||
} else if (genre.isExcluded()) {
|
||||
searchParams.blackListedGenres.add(genre.name)
|
||||
}
|
||||
}
|
||||
|
||||
return searchParams
|
||||
}
|
||||
|
||||
private fun mustRemove(anime: AnimeDto, params: FilterSearchParams): Boolean {
|
||||
|
@ -196,12 +196,14 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
val links = videoDto.links
|
||||
val allLinks = listOf(links.low, links.medium, links.high).flatten()
|
||||
val supportedPlayers = listOf("anonfiles", "send")
|
||||
val online = links.online?.filterNot { "mega" in it }?.map {
|
||||
Video(it, "Player ATC", it, headers)
|
||||
} ?: emptyList<Video>()
|
||||
val online = links.online?.run {
|
||||
filterNot { "mega" in it }.map {
|
||||
Video(it, "Player ATC", it, headers)
|
||||
}
|
||||
}.orEmpty()
|
||||
return online + allLinks.filter { it.name in supportedPlayers }.parallelMap {
|
||||
val playerUrl = LinkBypasser(client, json).bypass(it, videoDto.id)
|
||||
if (playerUrl == null) return@parallelMap null
|
||||
?: return@parallelMap null
|
||||
val quality = when (it.quality) {
|
||||
"low" -> "SD"
|
||||
"medium" -> "HD"
|
||||
@ -258,7 +260,7 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
}
|
||||
|
||||
private fun Response.getAnimeDto(): AnimeDto {
|
||||
val responseBody = body.string()
|
||||
val responseBody = use { it.body.string() }
|
||||
return try {
|
||||
parseAs<AnimeDto>(responseBody)
|
||||
} catch (e: Exception) {
|
||||
|
@ -5,7 +5,6 @@ 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.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
|
@ -26,6 +26,7 @@ object AVFilters {
|
||||
options: Array<Pair<String, String>>,
|
||||
): String {
|
||||
return (getFirst<R>() as CheckBoxFilterList).state
|
||||
.asSequence()
|
||||
.filter { it.state }
|
||||
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
|
||||
.filter(String::isNotBlank)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.animesvision
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.pt.animesvision.dto.AVResponseDto
|
||||
@ -26,10 +25,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
@ -59,7 +57,7 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -69,8 +67,8 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// ============================== Popular ===============================
|
||||
private fun nextPageSelector() = "ul.pagination li.page-item:contains(›):not(.disabled)"
|
||||
override fun popularAnimeSelector() = "div#anime-trending div.item > a.film-poster"
|
||||
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
|
||||
override fun popularAnimeSelector() = "div#anime-trending div.item > a.film-poster"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
val img = element.selectFirst("img")!!
|
||||
@ -81,6 +79,93 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos?page=$page")
|
||||
override fun latestUpdatesSelector() = episodeListSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
title = element.selectFirst("h3")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = nextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) {
|
||||
val path = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/$path"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByPathParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response)
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val params = AVFilters.getSearchParameters(filters)
|
||||
val url = "$baseUrl/search?".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("nome", query)
|
||||
.addQueryParameter("tipo", params.type)
|
||||
.addQueryParameter("idioma", params.language)
|
||||
.addQueryParameter("ordenar", params.sort)
|
||||
.addQueryParameter("ano_inicial", params.initial_year)
|
||||
.addQueryParameter("ano_final", params.last_year)
|
||||
.addQueryParameter("fansub", params.fansub)
|
||||
.addQueryParameter("status", params.status)
|
||||
.addQueryParameter("temporada", params.season)
|
||||
.addQueryParameter("estudios", params.studio)
|
||||
.addQueryParameter("produtores", params.producer)
|
||||
.addQueryParameter("generos", params.genres)
|
||||
.build()
|
||||
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector() = "div.film_list-wrap div.film-poster"
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
val elementA = element.selectFirst("a")!!
|
||||
title = elementA.attr("title")
|
||||
setUrlWithoutDomain(elementA.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img")?.attr("data-src")
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector() = nextPageSelector()
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
val doc = getRealDoc(document)
|
||||
|
||||
val content = doc.selectFirst("div#ani_detail div.anis-content")!!
|
||||
val detail = content.selectFirst("div.anisc-detail")!!
|
||||
val infos = content.selectFirst("div.anisc-info")!!
|
||||
|
||||
thumbnail_url = content.selectFirst("img")?.attr("src")
|
||||
title = detail.selectFirst("h2.film-name")!!.text()
|
||||
genre = infos.getInfo("Gêneros")
|
||||
author = infos.getInfo("Produtores")
|
||||
artist = infos.getInfo("Estúdios")
|
||||
status = parseStatus(infos.getInfo("Status"))
|
||||
|
||||
description = buildString {
|
||||
append(infos.getInfo("Sinopse") + "\n")
|
||||
infos.getInfo("Inglês")?.also { append("\nTítulo em inglês: $it") }
|
||||
infos.getInfo("Japonês")?.also { append("\nTítulo em japonês: $it") }
|
||||
infos.getInfo("Foi ao ar em")?.also { append("\nFoi ao ar em: $it") }
|
||||
infos.getInfo("Temporada")?.also { append("\nTemporada: $it") }
|
||||
infos.getInfo("Duração")?.also { append("\nDuração: $it") }
|
||||
infos.getInfo("Fansub")?.also { append("\nFansub: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListSelector() = "div.container div.screen-items > div.item"
|
||||
|
||||
@ -112,7 +197,7 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val body = response.body.string()
|
||||
val body = response.use { it.body.string() }
|
||||
val internalVideos = GlobalVisionExtractor()
|
||||
.videoListFromHtml(body)
|
||||
.toMutableList()
|
||||
@ -148,116 +233,30 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val reqBody = body.toRequestBody()
|
||||
val url = "$baseUrl/livewire/message/components.episodio.player-episodio-component"
|
||||
val response = client.newCall(POST(url, headers, reqBody)).execute()
|
||||
val responseBody = response.body.string()
|
||||
val responseBody = response.use { it.body.string() }
|
||||
val resJson = json.decodeFromString<AVResponseDto>(responseBody)
|
||||
(resJson.serverMemo?.data?.framePlay ?: resJson.effects?.html)
|
||||
?.let(::parsePlayerData)
|
||||
?: emptyList<Video>()
|
||||
?.let(::parsePlayerData).orEmpty()
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val voeExtractor by lazy { VoeExtractor(client) }
|
||||
|
||||
private fun parsePlayerData(data: String) = runCatching {
|
||||
when {
|
||||
"streamtape" in data ->
|
||||
StreamTapeExtractor(client).videoFromUrl(data)?.let(::listOf)
|
||||
"dood" in data ->
|
||||
DoodExtractor(client).videoFromUrl(data)?.let(::listOf)
|
||||
"voe.sx" in data ->
|
||||
VoeExtractor(client).videoFromUrl(data)?.let(::listOf)
|
||||
else -> null
|
||||
"streamtape" in data -> voeExtractor.videosFromUrl(data)
|
||||
"dood" in data -> doodExtractor.videosFromUrl(data)
|
||||
"voe.sx" in data -> voeExtractor.videosFromUrl(data)
|
||||
else -> emptyList()
|
||||
}
|
||||
}.getOrNull() ?: emptyList<Video>()
|
||||
}.getOrElse { emptyList() }
|
||||
|
||||
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) = SAnime.create().apply {
|
||||
val elementA = element.selectFirst("a")!!
|
||||
title = elementA.attr("title")
|
||||
setUrlWithoutDomain(elementA.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector() = nextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector() = "div.film_list-wrap div.film-poster"
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) {
|
||||
val path = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/$path"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByPathParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response)
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val params = AVFilters.getSearchParameters(filters)
|
||||
val url = "$baseUrl/search?".toHttpUrlOrNull()!!.newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("nome", query)
|
||||
.addQueryParameter("tipo", params.type)
|
||||
.addQueryParameter("idioma", params.language)
|
||||
.addQueryParameter("ordenar", params.sort)
|
||||
.addQueryParameter("ano_inicial", params.initial_year)
|
||||
.addQueryParameter("ano_final", params.last_year)
|
||||
.addQueryParameter("fansub", params.fansub)
|
||||
.addQueryParameter("status", params.status)
|
||||
.addQueryParameter("temporada", params.season)
|
||||
.addQueryParameter("estudios", params.studio)
|
||||
.addQueryParameter("produtores", params.producer)
|
||||
.addQueryParameter("generos", params.genres)
|
||||
|
||||
return GET(url.build().toString(), headers)
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
val doc = getRealDoc(document)
|
||||
|
||||
val content = doc.selectFirst("div#ani_detail div.anis-content")!!
|
||||
val detail = content.selectFirst("div.anisc-detail")!!
|
||||
val infos = content.selectFirst("div.anisc-info")!!
|
||||
|
||||
thumbnail_url = content.selectFirst("img")!!.attr("src")
|
||||
title = detail.selectFirst("h2.film-name")!!.text()
|
||||
genre = infos.getInfo("Gêneros")
|
||||
author = infos.getInfo("Produtores")
|
||||
artist = infos.getInfo("Estúdios")
|
||||
status = parseStatus(infos.getInfo("Status"))
|
||||
|
||||
description = buildString {
|
||||
append(infos.getInfo("Sinopse") + "\n")
|
||||
infos.getInfo("Inglês")?.let { append("\nTítulo em inglês: $it") }
|
||||
infos.getInfo("Japonês")?.let { append("\nTítulo em japonês: $it") }
|
||||
infos.getInfo("Foi ao ar em")?.let { append("\nFoi ao ar em: $it") }
|
||||
infos.getInfo("Temporada")?.let { append("\nTemporada: $it") }
|
||||
infos.getInfo("Duração")?.let { append("\nDuração: $it") }
|
||||
infos.getInfo("Fansub")?.let { append("\nFansub: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector() = nextPageSelector()
|
||||
override fun latestUpdatesSelector() = episodeListSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
title = element.selectFirst("h3")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos?page=$page")
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.animesvision.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
|
||||
class GlobalVisionExtractor {
|
||||
companion object {
|
||||
private val REGEX_URL = Regex(""""file":"(\S+?)",.*?"label":"(.*?)"""")
|
||||
|
@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
@ -41,7 +40,7 @@ class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
@ -62,9 +61,9 @@ class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("abs:href"))
|
||||
thumbnail_url = element.selectFirst("div.cover-image")?.let {
|
||||
it.attr("style").substringAfter("url('").substringBefore("'")
|
||||
} ?: ""
|
||||
thumbnail_url = element.selectFirst("div.cover-image")?.run {
|
||||
attr("style").substringAfter("url('").substringBefore("'")
|
||||
}
|
||||
title = element.selectFirst("span.series-title")!!.text()
|
||||
}
|
||||
|
||||
@ -176,8 +175,8 @@ class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun searchAnimeNextPageSelector(): String? = null
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("abs:href"))
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("abs:src") ?: ""
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("abs:src")
|
||||
title = element.selectFirst("div.aniInfos")?.text() ?: "Anime"
|
||||
}
|
||||
|
||||
@ -249,8 +248,8 @@ class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return SEpisode.create().apply {
|
||||
name = "Ep. ${epNumber?.text()?.trim() ?: counter} - ${epTitle.replace(EPISODE_REGEX, "")}"
|
||||
.replace(" - - ", " - ")
|
||||
episode_number = epNumber?.let {
|
||||
it.text().trim().toFloatOrNull()
|
||||
episode_number = epNumber?.run {
|
||||
text().trim().toFloatOrNull()
|
||||
} ?: counter.toFloat()
|
||||
scanlator = info
|
||||
setUrlWithoutDomain(element.selectFirst("article > a")!!.attr("href"))
|
||||
@ -287,25 +286,9 @@ class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
.selectFirst("script:containsData(sources:)")
|
||||
?.data()
|
||||
?.let(BloggerJWPlayerExtractor::videosFromScript)
|
||||
?: emptyList()
|
||||
}
|
||||
url.startsWith(baseUrl) -> {
|
||||
val videoDocument = client.newCall(GET(url, headers)).execute()
|
||||
.use { it.asJsoup() }
|
||||
|
||||
val script = videoDocument.selectFirst("script:containsData(decodeURIComponent)")?.data()
|
||||
?.let(::getDecrypted)
|
||||
?: videoDocument.selectFirst("script:containsData(sources:)")?.data()
|
||||
?: return@flatMap emptyList()
|
||||
|
||||
when {
|
||||
"/bloggerjwplayer" in url || "jwplayer-2" in url || "/ctaplayer" in url -> {
|
||||
BloggerJWPlayerExtractor.videosFromScript(script)
|
||||
}
|
||||
"/m3u8" in url -> PlaylistExtractor.videosFromScript(script)
|
||||
else -> emptyList()
|
||||
}
|
||||
.orEmpty()
|
||||
}
|
||||
url.startsWith(baseUrl) -> videosFromInternalUrl(url)
|
||||
"blogger.com" in url -> extractBloggerVideos(url, vid.text().trim())
|
||||
else -> emptyList()
|
||||
}
|
||||
@ -314,6 +297,24 @@ class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun videosFromInternalUrl(url: String): List<Video> {
|
||||
val videoDocument = client.newCall(GET(url, headers)).execute()
|
||||
.use { it.asJsoup() }
|
||||
|
||||
val script = videoDocument.selectFirst("script:containsData(decodeURIComponent)")?.data()
|
||||
?.let(::getDecrypted)
|
||||
?: videoDocument.selectFirst("script:containsData(sources:)")?.data()
|
||||
?: return emptyList()
|
||||
|
||||
return when {
|
||||
"/bloggerjwplayer" in url || "jwplayer-2" in url || "/ctaplayer" in url -> {
|
||||
BloggerJWPlayerExtractor.videosFromScript(script)
|
||||
}
|
||||
"/m3u8" in url -> PlaylistExtractor.videosFromScript(script)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractBloggerVideos(url: String, name: String): List<Video> {
|
||||
return client.newCall(GET(url, headers)).execute()
|
||||
.use { it.body.string() }
|
||||
@ -360,14 +361,14 @@ class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private fun getDecrypted(script: String): String {
|
||||
private fun getDecrypted(script: String): String? {
|
||||
val patchedScript = script.trim().split("\n").first()
|
||||
.replace("eval(function", "function a")
|
||||
.replace("decodeURIComponent(escape(r))}(", "r};a(")
|
||||
.substringBeforeLast(")")
|
||||
|
||||
return QuickJs.create().use {
|
||||
it.evaluate(patchedScript).toString()
|
||||
it.evaluate(patchedScript)?.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,38 +18,22 @@ object AnimesZoneFilters {
|
||||
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
|
||||
val value = this.filterIsInstance<R>().joinToString("") {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return if (value.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
"&$name=$value"
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return this.filterIsInstance<R>().first()
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
.takeUnless(String::isEmpty)
|
||||
?.let { "&$name=$it" }
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
||||
options: Array<Pair<String, String>>,
|
||||
name: String,
|
||||
): String {
|
||||
return (this.getFirst<R>() as CheckBoxFilterList).state
|
||||
.mapNotNull { checkbox ->
|
||||
if (checkbox.state) {
|
||||
options.find { it.first == checkbox.name }!!.second
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.joinToString("%2C").let {
|
||||
if (it.isBlank()) {
|
||||
""
|
||||
} else {
|
||||
"&$name=$it"
|
||||
}
|
||||
}
|
||||
return (first { it is R } as CheckBoxFilterList).state
|
||||
.asSequence()
|
||||
.filter { it.state }
|
||||
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString("%2C") { "&$name=$it" }
|
||||
}
|
||||
|
||||
class GenreFilter : CheckBoxFilterList(
|
||||
|
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
@ -1,13 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'AnimeTV'
|
||||
pkgNameSuffix = 'pt.animetv'
|
||||
extClass = '.AnimeTV'
|
||||
extVersionCode = 1
|
||||
libVersion = '12'
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
Before Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
@ -1,139 +0,0 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.animetv
|
||||
|
||||
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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class AnimeTV : AnimeHttpSource() {
|
||||
override val name = "AnimeTV"
|
||||
override val baseUrl = "https://appanimeplus.tk/play-api.php"
|
||||
override val lang = "pt-BR"
|
||||
override val supportsLatest = true
|
||||
|
||||
private val cdnUrl = "https://cdn.appanimeplus.tk/img/"
|
||||
|
||||
// == POPULAR ANIME ==
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl?populares")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val animes = Json.decodeFromString<JsonArray>(response.body.string())
|
||||
if (animes.isEmpty()) return AnimesPage(emptyList(), false)
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
for (anime in animes) {
|
||||
val newAnime = SAnime.create()
|
||||
newAnime.title = anime.jsonObject["category_name"]!!.jsonPrimitive.content
|
||||
newAnime.thumbnail_url = "$cdnUrl" + anime.jsonObject["category_image"]!!.jsonPrimitive.content
|
||||
newAnime.url = "$baseUrl?info=" + anime.jsonObject["id"]!!.jsonPrimitive.content
|
||||
animeList.add(newAnime)
|
||||
}
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
|
||||
// == LATEST ANIME == / DEPRECATED
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl?latest")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val animes = Json.decodeFromString<JsonArray>(response.body.string())
|
||||
if (animes.isEmpty()) return AnimesPage(emptyList(), false)
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
for (anime in animes) {
|
||||
val newAnime = SAnime.create()
|
||||
newAnime.title = anime.jsonObject["title"]!!.jsonPrimitive.content
|
||||
newAnime.thumbnail_url = "$cdnUrl" + anime.jsonObject["category_image"]!!.jsonPrimitive.content
|
||||
newAnime.url = "$baseUrl?info=" + anime.jsonObject["category_id"]!!.jsonPrimitive.content
|
||||
animeList.add(newAnime)
|
||||
}
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
|
||||
// == SEARCH ANIME ==
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl?search=$query")
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val animes = Json.decodeFromString<JsonArray>(response.body.string())
|
||||
if (animes.isEmpty()) return AnimesPage(emptyList(), false)
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
for (anime in animes) {
|
||||
val newAnime = SAnime.create()
|
||||
newAnime.title = anime.jsonObject["category_name"]!!.jsonPrimitive.content
|
||||
newAnime.thumbnail_url = "$cdnUrl" + anime.jsonObject["category_image"]!!.jsonPrimitive.content
|
||||
newAnime.url = "$baseUrl?info=" + anime.jsonObject["id"]!!.jsonPrimitive.content
|
||||
animeList.add(newAnime)
|
||||
}
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
|
||||
// == PARSE ANIME ==
|
||||
override fun animeDetailsRequest(anime: SAnime): Request = GET(anime.url)
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val animes = Json.decodeFromString<JsonArray>(response.body.string())
|
||||
if (animes.isEmpty()) return SAnime.create()
|
||||
val animeData = animes.first()
|
||||
val anime = SAnime.create()
|
||||
anime.url = "$baseUrl?info=" + animeData.jsonObject["id"]!!.jsonPrimitive.content
|
||||
anime.title = animeData.jsonObject["category_name"]!!.jsonPrimitive.content
|
||||
anime.description = animeData.jsonObject["category_description"]!!.jsonPrimitive.content
|
||||
anime.genre = animeData.jsonObject["category_genres"]!!.jsonPrimitive.content
|
||||
anime.status = 0
|
||||
anime.thumbnail_url = "$cdnUrl" + animeData.jsonObject["category_image"]!!.jsonPrimitive.content
|
||||
anime.initialized = true
|
||||
return anime
|
||||
}
|
||||
|
||||
// == PARSE EPISODES ==
|
||||
override fun episodeListRequest(anime: SAnime): Request = GET("$baseUrl?cat_id=" + anime.url.substring(42))
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodesData = Json.decodeFromString<JsonArray>(response.body.string())
|
||||
if (episodesData.isEmpty()) return emptyList()
|
||||
val episodes = mutableListOf<SEpisode>()
|
||||
val ovas = mutableListOf<SEpisode>()
|
||||
for (episode in episodesData) {
|
||||
val newEpisode = SEpisode.create()
|
||||
newEpisode.url = "$baseUrl?episodios=" + episode.jsonObject["video_id"]!!.jsonPrimitive.content
|
||||
newEpisode.name = episode.jsonObject["title"]!!.jsonPrimitive.content
|
||||
newEpisode.date_upload = System.currentTimeMillis()
|
||||
if (newEpisode.name.contains("ova", ignoreCase = true)) {
|
||||
ovas.add(newEpisode)
|
||||
} else {
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
}
|
||||
if (episodes.first().name.last() === '1') episodes.reverse()
|
||||
for (ova in ovas) { episodes.add(0, ova) }
|
||||
return episodes
|
||||
}
|
||||
|
||||
// == PARSE VIDEOS ==
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
return GET("$baseUrl?episodios=" + episode.url.substring(47))
|
||||
}
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val videosData = Json.decodeFromString<JsonArray>(response.body.string())
|
||||
val videoData = videosData.first()
|
||||
|
||||
val videos = mutableListOf<Video>()
|
||||
val hasSD = videoData.jsonObject["locationsd"]!!.jsonPrimitive.content.length > 0
|
||||
if (hasSD) {
|
||||
val url = videoData.jsonObject["locationsd"]!!.jsonPrimitive.content
|
||||
videos.add(Video(url, "HD", url))
|
||||
}
|
||||
val url = videoData.jsonObject["location"]!!.jsonPrimitive.content
|
||||
videos.add(Video(url, "SD", url))
|
||||
return videos
|
||||
}
|
||||
|
||||
// == FILTERS ==
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(emptyList())
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.anitube
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeExtractor
|
||||
@ -13,7 +12,6 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@ -34,9 +32,9 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -114,7 +112,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val infos = content.selectFirst("div.anime_infos")!!
|
||||
|
||||
title = doc.selectFirst("div.anime_container_titulo")!!.text()
|
||||
thumbnail_url = content.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = content.selectFirst("img")?.attr("src")
|
||||
genre = infos.getInfo("Gêneros")
|
||||
author = infos.getInfo("Autor")
|
||||
artist = infos.getInfo("Estúdio")
|
||||
@ -125,7 +123,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
description = buildString {
|
||||
append(doc.selectFirst("div.sinopse_container_content")!!.text() + "\n")
|
||||
infoItems.forEach { item ->
|
||||
infos.getInfo(item)?.let { append("\n$item: $it") }
|
||||
infos.getInfo(item)?.also { append("\n$item: $it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,15 +132,15 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun episodeListSelector() = "div.animepag_episodios_item > a"
|
||||
|
||||
override fun episodeListParse(response: Response) = buildList {
|
||||
var doc = getRealDoc(response.asJsoup())
|
||||
var doc = getRealDoc(response.use { it.asJsoup() })
|
||||
do {
|
||||
if (isNotEmpty()) {
|
||||
val path = doc.selectFirst(popularAnimeNextPageSelector())!!.attr("href")
|
||||
doc = client.newCall(GET(baseUrl + path, headers)).execute().asJsoup()
|
||||
doc = client.newCall(GET(baseUrl + path, headers)).execute().use { it.asJsoup() }
|
||||
}
|
||||
doc.select(episodeListSelector())
|
||||
.map(::episodeFromElement)
|
||||
.let(::addAll)
|
||||
.also(::addAll)
|
||||
} while (doc.selectFirst(popularAnimeNextPageSelector()) != null)
|
||||
reverse()
|
||||
}
|
||||
@ -192,7 +190,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return document.selectFirst("div.controles_ep > a[href]:has(i.spr.listaEP)")
|
||||
?.let {
|
||||
val path = it.attr("href")
|
||||
client.newCall(GET(baseUrl + path, headers)).execute().asJsoup()
|
||||
client.newCall(GET(baseUrl + path, headers)).execute().use { it.asJsoup() }
|
||||
} ?: document
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object BAFilters {
|
||||
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
@ -19,14 +18,18 @@ object BAFilters {
|
||||
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
|
||||
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||
|
||||
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 (first { it is R } as CheckBoxFilterList).state
|
||||
.asSequence()
|
||||
.filter { it.state }
|
||||
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return getFirst<R>().let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
class LanguageFilter : QueryPartFilter("Idioma", BAFiltersData.LANGUAGES)
|
||||
@ -52,12 +55,7 @@ object BAFilters {
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
val genres = listOf(" ") + filters.getFirst<GenresFilter>().state
|
||||
.mapNotNull { genre ->
|
||||
if (genre.state) {
|
||||
BAFiltersData.GENRES.find { it.first == genre.name }!!.second
|
||||
} else { null }
|
||||
}.toList()
|
||||
val genres = listOf(" ") + filters.parseCheckbox<GenresFilter>(BAFiltersData.GENRES)
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<LanguageFilter>(),
|
||||
|
@ -106,7 +106,7 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val body = response.body.string()
|
||||
val data = json.decodeFromString<LivewireResponseDto>(body)
|
||||
val html = data.effects.html?.unescape() ?: ""
|
||||
val html = data.effects.html?.unescape().orEmpty()
|
||||
val document = Jsoup.parse(html)
|
||||
val animes = document.select(searchAnimeSelector()).map { element ->
|
||||
searchAnimeFromElement(element)
|
||||
|
@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@ -36,24 +35,24 @@ class BetterAnimeExtractor(
|
||||
}.filterNotNull()
|
||||
}
|
||||
|
||||
private fun videoUrlFromToken(qtoken: String, _token: String): String? {
|
||||
private fun videoUrlFromToken(qtoken: String, token: String): String? {
|
||||
val body = """
|
||||
{
|
||||
"_token": "$_token",
|
||||
"_token": "$token",
|
||||
"info": "$qtoken"
|
||||
}
|
||||
""".trimIndent()
|
||||
val reqBody = body.toRequestBody("application/json".toMediaType())
|
||||
val request = POST("$baseUrl/changePlayer", headers, reqBody)
|
||||
return runCatching {
|
||||
val response = client.newCall(request).execute()
|
||||
val resJson = json.decodeFromString<ChangePlayerDto>(response.body.string())
|
||||
val response = client.newCall(request).execute().use { it.body.string() }
|
||||
val resJson = json.decodeFromString<ChangePlayerDto>(response)
|
||||
resJson.frameLink?.let(::videoUrlFromPlayer)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun videoUrlFromPlayer(url: String): String {
|
||||
val html = client.newCall(GET(url, headers)).execute().body.string()
|
||||
val html = client.newCall(GET(url, headers)).execute().use { it.body.string() }
|
||||
|
||||
val videoUrl = html.substringAfter("file\":")
|
||||
.substringAfter("\"")
|
||||
@ -63,9 +62,9 @@ class BetterAnimeExtractor(
|
||||
return videoUrl
|
||||
}
|
||||
|
||||
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||
runBlocking(Dispatchers.Default) {
|
||||
map { async { f(it) } }.awaitAll()
|
||||
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
|
||||
runBlocking {
|
||||
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.donghuanosekai
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.pt.donghuanosekai.extractors.DonghuaNoSekaiExtractor
|
||||
@ -21,7 +20,6 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
@ -53,7 +51,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -65,7 +63,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
title = element.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
@ -78,7 +76,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
title = element.selectFirst("div.title h3")!!.text()
|
||||
thumbnail_url = element.selectFirst("div.thumb img")!!.attr("src")
|
||||
thumbnail_url = element.selectFirst("div.thumb img")?.attr("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "ul.content-pagination > li.next"
|
||||
@ -132,7 +130,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
addQueryParameter("filter_order", params.orderBy)
|
||||
addQueryParameter("filter_status", params.status)
|
||||
addQueryParameter("type_url", "ONA")
|
||||
}.build().encodedQuery
|
||||
}.build().encodedQuery.orEmpty()
|
||||
|
||||
val genres = params.genres.joinToString { "\"$it\"" }
|
||||
val delgenres = params.deleted_genres.joinToString { "\"$it\"" }
|
||||
@ -166,7 +164,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
val doc = getRealDoc(document)
|
||||
setUrlWithoutDomain(doc.location())
|
||||
thumbnail_url = doc.selectFirst("div.poster > img")!!.attr("src")
|
||||
thumbnail_url = doc.selectFirst("div.poster > img")?.attr("src")
|
||||
val infos = doc.selectFirst("div.dados")!!
|
||||
|
||||
title = infos.selectFirst("h1")!!.text()
|
||||
@ -179,7 +177,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
doc.select("div.articleContent:has(div:contains(Sinopse)) > div.context > p")
|
||||
.eachText()
|
||||
.joinToString("\n\n")
|
||||
.let(::append)
|
||||
.also(::append)
|
||||
|
||||
append("\n")
|
||||
|
||||
@ -199,7 +197,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
element.selectFirst("span.episode")!!.text().let {
|
||||
element.selectFirst("span.episode")!!.text().also {
|
||||
name = it
|
||||
episode_number = it.substringAfterLast(" ").toFloatOrNull() ?: 0F
|
||||
}
|
||||
@ -207,9 +205,9 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
private val extractor by lazy { DonghuaNoSekaiExtractor(client, headers) }
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val doc = response.use { it.asJsoup() }
|
||||
val extractor = DonghuaNoSekaiExtractor(client, headers)
|
||||
|
||||
return doc.select("div.slideItem[data-video-url]").parallelMap {
|
||||
runCatching {
|
||||
@ -263,7 +261,7 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
} ?: document
|
||||
}
|
||||
|
||||
private fun String?.parseStatus() = when (this?.trim()?.lowercase()) {
|
||||
private fun String?.parseStatus() = when (this?.run { trim().lowercase() }) {
|
||||
"completo" -> SAnime.COMPLETED
|
||||
"em lançamento" -> SAnime.ONGOING
|
||||
"em pausa" -> SAnime.ON_HIATUS
|
||||
|
@ -19,26 +19,20 @@ object DonghuaNoSekaiFilters {
|
||||
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.getFirst(): R {
|
||||
return first { it is R } as R
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return getFirst<R>().let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseTriFilter(
|
||||
options: Array<Pair<String, String>>,
|
||||
): List<List<String>> {
|
||||
return (getFirst<R>() as TriStateFilterList).state
|
||||
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 {
|
||||
val included = it.get(TriState.STATE_INCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
val excluded = it.get(TriState.STATE_EXCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ class DonghuaNoSekaiExtractor(
|
||||
val iframeUrl = iframe.attr("src")
|
||||
when {
|
||||
iframeUrl.contains("nativov2.php") || iframeUrl.contains("/embed2/") -> {
|
||||
val url = iframeUrl.toHttpUrl().let {
|
||||
it.queryParameter("id") ?: it.queryParameter("v")
|
||||
val url = iframeUrl.toHttpUrl().run {
|
||||
queryParameter("id") ?: queryParameter("v")
|
||||
} ?: return emptyList()
|
||||
|
||||
val quality = url.substringAfter("_").substringBefore("_")
|
||||
@ -51,16 +51,16 @@ class DonghuaNoSekaiExtractor(
|
||||
.substringBefore("]")
|
||||
.split("{")
|
||||
.drop(1)
|
||||
.map {
|
||||
val url = it.substringAfter("file: \"").substringBefore('"')
|
||||
val quality = it.substringAfter("label: \"")
|
||||
.map { line ->
|
||||
val url = line.substringAfter("file: \"").substringBefore('"')
|
||||
val quality = line.substringAfter("label: \"")
|
||||
.substringBefore('"')
|
||||
.let { label ->
|
||||
when (label) {
|
||||
.run {
|
||||
when (this) {
|
||||
"SD" -> "480p"
|
||||
"HD" -> "720p"
|
||||
"FHD", "FULLHD" -> "1080p"
|
||||
else -> label
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
Video(url, "$playerName - $quality", url, headers)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.flixei
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.pt.flixei.dto.AnimeDto
|
||||
@ -25,7 +24,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
@ -52,7 +50,7 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -68,12 +66,10 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return AnimesPage(animes, false)
|
||||
}
|
||||
|
||||
private fun parseAnimeFromObject(anime: AnimeDto): SAnime {
|
||||
return SAnime.create().apply {
|
||||
title = anime.title
|
||||
setUrlWithoutDomain("/assistir/filme/${anime.url}/online/gratis")
|
||||
thumbnail_url = "$baseUrl/content/movies/posterPt/185/${anime.id}.webp"
|
||||
}
|
||||
private fun parseAnimeFromObject(anime: AnimeDto) = SAnime.create().apply {
|
||||
title = anime.title
|
||||
setUrlWithoutDomain("/assistir/filme/${anime.url}/online/gratis")
|
||||
thumbnail_url = "$baseUrl/content/movies/posterPt/185/${anime.id}.webp"
|
||||
}
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
@ -88,6 +84,66 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/filmes/estreia/$page")
|
||||
|
||||
override fun latestUpdatesSelector() = "div.generalMoviesList > a.gPoster"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
title = element.selectFirst("div.i span")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.paginationSystem a.next"
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val path = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/assistir/$path/online/gratis"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByPathParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.use { it.asJsoup() })
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return GET("$baseUrl/pesquisar/$query")
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector() = null
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
thumbnail_url = document.selectFirst("meta[property=og:image]")?.attr("content")
|
||||
val container = document.selectFirst("div.moviePresent")!!
|
||||
with(container) {
|
||||
title = selectFirst("h2.tit")!!.text()
|
||||
genre = select("div.genres > span").eachText().joinToString()
|
||||
author = getInfo("Diretor")
|
||||
artist = getInfo("Produtoras")
|
||||
description = buildString {
|
||||
selectFirst("p")?.text()?.also { append(it + "\n\n") }
|
||||
getInfo("Título")?.also { append("Título original: $it\n") }
|
||||
getInfo("Serie de")?.also { append("ano: $it\n") }
|
||||
getInfo("Elenco")?.also { append("Elenco: $it\n") }
|
||||
getInfo("Qualidade")?.also { append("Qualidade: $it\n") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
private fun getSeasonEps(seasonElement: Element): List<SEpisode> {
|
||||
val id = seasonElement.attr("data-load-episodes")
|
||||
@ -105,13 +161,13 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val docUrl = response.asJsoup().selectFirst("div#playButton")!!
|
||||
val docUrl = response.use { it.asJsoup() }.selectFirst("div#playButton")!!
|
||||
.attr("onclick")
|
||||
.substringAfter("'")
|
||||
.substringBefore("'")
|
||||
return if (response.request.url.toString().contains("/serie/")) {
|
||||
client.newCall(GET(docUrl)).execute()
|
||||
.asJsoup()
|
||||
.use { it.asJsoup() }
|
||||
.select("div#seasons div.item[data-load-episodes]")
|
||||
.flatMap(::getSeasonEps)
|
||||
.reversed()
|
||||
@ -133,28 +189,6 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
thumbnail_url = document.selectFirst("meta[property=og:image]")!!.attr("content")
|
||||
val container = document.selectFirst("div.moviePresent")!!
|
||||
with(container) {
|
||||
title = selectFirst("h2.tit")!!.text()
|
||||
genre = select("div.genres > span").eachText().joinToString()
|
||||
author = getInfo("Diretor")
|
||||
artist = getInfo("Produtoras")
|
||||
description = buildString {
|
||||
selectFirst("p")?.text()?.let { append(it + "\n\n") }
|
||||
getInfo("Título")?.let { append("Título original: $it\n") }
|
||||
getInfo("Serie de")?.let { append("ano: $it\n") }
|
||||
getInfo("Elenco")?.let { append("Elenco: $it\n") }
|
||||
getInfo("Qualidade")?.let { append("Qualidade: $it\n") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val url = episode.url
|
||||
@ -167,7 +201,7 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val body = response.body.string()
|
||||
val body = response.use { it.body.string() }
|
||||
// Pair<Language, Query>
|
||||
val items = if (body.startsWith("{")) {
|
||||
val data = json.decodeFromString<ApiResultsDto<PlayersDto>>(body)
|
||||
@ -203,6 +237,9 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return items.parallelMap(::getVideosFromItem).flatten()
|
||||
}
|
||||
|
||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
private val mixdropExtractor by lazy { MixDropExtractor(client) }
|
||||
|
||||
private fun getVideosFromItem(item: Pair<String, String>): List<Video> {
|
||||
val (lang, query) = item
|
||||
val headers = headersBuilder().set("referer", WAREZ_URL).build()
|
||||
@ -211,19 +248,16 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
} else {
|
||||
client.newCall(GET("$WAREZ_URL/embed/getPlay.php$query", headers))
|
||||
.execute()
|
||||
.body.string()
|
||||
.use { it.body.string() }
|
||||
.substringAfter("location.href=\"")
|
||||
.substringBefore("\";")
|
||||
}
|
||||
|
||||
return when (query.substringAfter("sv=")) {
|
||||
"streamtape" ->
|
||||
StreamTapeExtractor(client).videoFromUrl(hostUrl, "Streamtape($lang)")
|
||||
?.let(::listOf)
|
||||
"mixdrop" ->
|
||||
MixDropExtractor(client).videoFromUrl(hostUrl, lang)
|
||||
"streamtape" -> streamtapeExtractor.videosFromUrl(hostUrl, "Streamtape($lang)")
|
||||
"mixdrop" -> mixdropExtractor.videoFromUrl(hostUrl, lang)
|
||||
else -> null // TODO: Add warezcdn extractor
|
||||
} ?: emptyList()
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video {
|
||||
@ -238,86 +272,44 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
|
||||
override fun searchAnimeSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun searchAnimeNextPageSelector() = null
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return GET("$baseUrl/pesquisar/$query")
|
||||
}
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val path = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/assistir/$path/online/gratis"))
|
||||
.asObservableSuccess()
|
||||
.map(::searchAnimeByPathParse)
|
||||
} else {
|
||||
super.fetchSearchAnime(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response.asJsoup())
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
title = element.selectFirst("div.i span")!!.text()
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.paginationSystem a.next"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/filmes/estreia/$page")
|
||||
|
||||
override fun latestUpdatesSelector() = "div.generalMoviesList > a.gPoster"
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val preferredPlayer = ListPreference(screen.context).apply {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_PLAYER_KEY
|
||||
title = PREF_PLAYER_TITLE
|
||||
entries = PREF_PLAYER_ARRAY
|
||||
entryValues = PREF_PLAYER_ARRAY
|
||||
setDefaultValue(PREF_PLAYER_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)
|
||||
|
||||
val preferredLanguage = ListPreference(screen.context).apply {
|
||||
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(preferredPlayer)
|
||||
screen.addPreference(preferredLanguage)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return body.string().let(json::decodeFromString)
|
||||
return use { it.body.string() }.let(json::decodeFromString)
|
||||
}
|
||||
|
||||
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
|
||||
|
@ -21,7 +21,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@ -58,10 +57,10 @@ class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun popularAnimeSelector() = "ul.ul_sidebar > li"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
element.selectFirst("div.rt a.series")!!.also {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text().substringBefore(" - Episódios")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
element.selectFirst("div.rt a.series")!!.run {
|
||||
setUrlWithoutDomain(attr("href"))
|
||||
title = text().substringBefore(" - Episódios")
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +74,7 @@ class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href").substringBeforeLast("-") + "s")
|
||||
title = element.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||
@ -145,7 +144,7 @@ class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
val infos = document.selectFirst("div#anime")!!
|
||||
thumbnail_url = infos.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = infos.selectFirst("img")?.attr("src")
|
||||
title = infos.getInfo("Hentai:")
|
||||
genre = infos.getInfo("Tags")
|
||||
artist = infos.getInfo("Estúdio")
|
||||
@ -165,15 +164,17 @@ class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
return response.asJsoup().select(videoListSelector()).parallelMap {
|
||||
return response.use { it.asJsoup() }.select(videoListSelector()).parallelMap {
|
||||
runCatching {
|
||||
client.newCall(GET(it.attr("src"), headers)).execute().use { res ->
|
||||
extractVideosFromIframe(res.asJsoup())
|
||||
extractVideosFromIframe(res.use { it.asJsoup() })
|
||||
}
|
||||
}.getOrElse { emptyList() }
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private val bloggerExtractor by lazy { BloggerExtractor(client) }
|
||||
|
||||
private fun extractVideosFromIframe(iframe: Document): List<Video> {
|
||||
val url = iframe.location()
|
||||
return when {
|
||||
@ -185,11 +186,11 @@ class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
url.contains("/index.php") -> {
|
||||
val bloggerUrl = iframe.selectFirst("iframe")!!.attr("src")
|
||||
BloggerExtractor(client).videosFromUrl(bloggerUrl, headers)
|
||||
bloggerExtractor.videosFromUrl(bloggerUrl, headers)
|
||||
}
|
||||
url.contains("/player.php") -> {
|
||||
val ahref = iframe.selectFirst("a")!!.attr("href")
|
||||
val internal = client.newCall(GET(ahref, headers)).execute().asJsoup()
|
||||
val internal = client.newCall(GET(ahref, headers)).execute().use { it.asJsoup() }
|
||||
val videoUrl = internal.selectFirst("video > source")!!.attr("src")
|
||||
listOf(Video(videoUrl, "Alternativo", videoUrl, headers))
|
||||
}
|
||||
|
@ -35,9 +35,9 @@ object HentaisTubeFilters {
|
||||
.filterNot { it.isIgnored() }
|
||||
.map { filter -> filter.state to filter.name }
|
||||
.groupBy { it.first } // group by state
|
||||
.let {
|
||||
val included = it.get(TriState.STATE_INCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
val excluded = it.get(TriState.STATE_EXCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.hinatasoul
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors.HinataSoulExtractor
|
||||
@ -36,7 +35,7 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -95,7 +94,7 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
title = element.selectFirst("div.ultimosAnimesHomeItemInfosNome")!!.text()
|
||||
}
|
||||
|
||||
@ -144,7 +143,7 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
doc.select(episodeListSelector())
|
||||
.map(::episodeFromElement)
|
||||
.let(::addAll)
|
||||
.also(::addAll)
|
||||
} while (hasNextPage(doc))
|
||||
reverse()
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.listadeanimes
|
||||
|
||||
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
|
||||
@ -16,35 +15,65 @@ import rx.Observable
|
||||
|
||||
class ListaDeAnimes : ParsedAnimeHttpSource() {
|
||||
override val name = "Lista de Animes"
|
||||
|
||||
override val baseUrl = "https://www.listadeanimes.com"
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeSelector(): String = "article.post.excerpt > div.capa:not(:has(a[href=https://www.listadeanimes.com/anime-lista-online]))"
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/page/$page")
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
val img = element.selectFirst("img")!!
|
||||
title = titleCase(img.attr("title").substringBefore(" todos os episódios"))
|
||||
thumbnail_url = img.attr("data-src")
|
||||
initialized = false
|
||||
}
|
||||
override fun popularAnimeSelector() = "article.post.excerpt > div.capa:not(:has(a[href=$baseUrl/anime-lista-online]))"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
val img = element.selectFirst("img")!!
|
||||
title = titleCase(img.attr("title").substringBefore(" todos os episódios"))
|
||||
thumbnail_url = img.attr("data-src")
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "a.next.page-numbers"
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animes = document.select(popularAnimeSelector()).map(::popularAnimeFromElement)
|
||||
return AnimesPage(animes, document.selectFirst(searchAnimeNextPageSelector()) != null)
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/page/$page?s=$query")
|
||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
val titleText = document.selectFirst("h1.title.single-title")!!.text()
|
||||
title = titleCase(titleText.substringBefore(" todos os episódios"))
|
||||
thumbnail_url = document.selectFirst("img.aligncenter.size-full")?.attr("src")
|
||||
val infos = document.selectFirst("div#content.post-single-content > center")
|
||||
val infosText = infos?.run {
|
||||
html()
|
||||
.replace("<br>", "\n")
|
||||
.replace("<b>", "")
|
||||
.replace("</b>", "")
|
||||
}?.let { "\n\n$it" }.orEmpty()
|
||||
|
||||
val sinopse = document.selectFirst("div#content > *:contains(Sinopse)")?.nextElementSibling()
|
||||
description = (sinopse?.text() ?: "Sem sinopse.") + infosText
|
||||
genre = document.select("a[rel=tag]").joinToString { it.text() }
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListSelector(): String = "div.videos > ul"
|
||||
override fun episodeListSelector() = "div.videos > ul"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val doc = response.asJsoup()
|
||||
return doc.select("div.videos > ul > li:gt(0)")
|
||||
@ -75,53 +104,10 @@ class ListaDeAnimes : ParsedAnimeHttpSource() {
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException("Not used.")
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/page/$page?s=$query")
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response)
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(emptyList())
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
val titleText = document.selectFirst("h1.title.single-title")!!.text()
|
||||
title = titleCase(titleText.substringBefore(" todos os episódios"))
|
||||
thumbnail_url = document.selectFirst("img.aligncenter.size-full")!!.attr("src")
|
||||
val infos = document.selectFirst("div#content.post-single-content > center")
|
||||
var infosText: String? = null
|
||||
if (infos != null) {
|
||||
infosText = infos.html()
|
||||
.replace("<br>", "\n")
|
||||
.replace("<b>", "")
|
||||
.replace("</b>", "")
|
||||
}
|
||||
val sinopse = document.selectFirst("div#content > *:contains(Sinopse)")?.nextElementSibling()
|
||||
description = (if (sinopse != null) sinopse?.text() else "Sem sinopse.") + if (infosText != null) "\n\n$infosText" else ""
|
||||
genre = document.select("a[rel=tag]").joinToString(", ") { it.text() }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private fun titleCase(str: String): String {
|
||||
val builder = StringBuilder(str)
|
||||
var whitespace = true
|
||||
str.forEachIndexed { index, c ->
|
||||
if (c.isWhitespace()) {
|
||||
whitespace = true
|
||||
} else if (whitespace) {
|
||||
builder.setCharAt(index, c.uppercaseChar())
|
||||
whitespace = false
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
return str.split(' ')
|
||||
.map { it.replaceFirstChar(Char::uppercase) }
|
||||
.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.megaflix
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -42,7 +41,7 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -54,7 +53,7 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
title = element.selectFirst("h2.entry-title")!!.text()
|
||||
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
@ -110,7 +109,7 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
setUrlWithoutDomain(document.location())
|
||||
val infos = document.selectFirst("div.bd > article.post.single")!!
|
||||
title = infos.selectFirst("h1.entry-title")!!.text()
|
||||
thumbnail_url = "https:" + infos.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = infos.selectFirst("img")?.absUrl("src")
|
||||
genre = infos.select("span.genres > a").eachText().joinToString()
|
||||
description = infos.selectFirst("div.description")?.text()
|
||||
}
|
||||
@ -146,33 +145,31 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
name = element.selectFirst("h2.entry-title")!!.text()
|
||||
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
|
||||
episode_number = element.selectFirst("span.num-epi")
|
||||
?.text()
|
||||
?.split("x")
|
||||
?.let {
|
||||
episode_number = element.selectFirst("span.num-epi")?.run {
|
||||
text().split("x").let {
|
||||
val season = it.first().toFloatOrNull() ?: 0F
|
||||
val episode = it.last().toFloatOrNull() ?: 0F
|
||||
season * 100F + episode
|
||||
}
|
||||
?: 0F
|
||||
} ?: 0F
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val items = response.asJsoup().select(videoListSelector())
|
||||
val items = response.use { it.asJsoup() }.select(videoListSelector())
|
||||
return items
|
||||
.parallelMap { element ->
|
||||
val language = element.text().substringAfter("-")
|
||||
val id = element.attr("href")
|
||||
val url = element.parents().get(5)
|
||||
?.selectFirst("div$id a")
|
||||
?.attr("href")
|
||||
?.substringAfter("token=")
|
||||
?.let { Base64.decode(it, Base64.DEFAULT).let(::String) }
|
||||
?.substringAfter("||")
|
||||
?: return@parallelMap emptyList()
|
||||
val url = element.parents().get(5)?.selectFirst("div$id a")
|
||||
?.run {
|
||||
attr("href")
|
||||
.substringAfter("token=")
|
||||
.let { String(Base64.decode(it, Base64.DEFAULT)) }
|
||||
.substringAfter("||")
|
||||
} ?: return@parallelMap emptyList()
|
||||
|
||||
runCatching { getVideoList(url, language) }.getOrNull() ?: emptyList()
|
||||
runCatching { getVideoList(url, language) }.getOrNull().orEmpty()
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
@ -183,7 +180,7 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
private fun getVideoList(url: String, language: String): List<Video>? {
|
||||
return when {
|
||||
"mixdrop.co" in url -> mixdropExtractor.videoFromUrl(url, language)
|
||||
"streamtape.com" in url -> streamtapeExtractor.videoFromUrl(url, "StreamTape - $language")?.let(::listOf)
|
||||
"streamtape.com" in url -> streamtapeExtractor.videosFromUrl(url, "StreamTape - $language")
|
||||
"mflix.vip" in url -> megaflixExtractor.videosFromUrl(url, language)
|
||||
else -> null
|
||||
}
|
||||
|
@ -16,9 +16,7 @@ object MegaflixFilters {
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return this.first { it is R }.let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
class GenreFilter : QueryPartFilter("Gênero", MegaflixFiltersData.GENRES)
|
||||
|
@ -12,7 +12,7 @@ class MegaflixExtractor(private val client: OkHttpClient, private val headers: H
|
||||
|
||||
fun videosFromUrl(url: String, lang: String = ""): List<Video> {
|
||||
val unpacked = client.newCall(GET(url, headers)).execute()
|
||||
.body.string()
|
||||
.use { it.body.string() }
|
||||
.let(JsUnpacker::unpackAndCombine)
|
||||
?.replace("\\", "")
|
||||
?: return emptyList()
|
||||
|
@ -16,9 +16,7 @@ object MAFilters {
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return first { it is R }.let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
class LetterFilter : QueryPartFilter("Letra inicial", MAFiltersData.LETTERS)
|
||||
@ -45,6 +43,8 @@ object MAFilters {
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<LetterFilter>(),
|
||||
filters.asQueryPart<YearFilter>(),
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.meusanimes
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.pt.meusanimes.extractors.IframeExtractor
|
||||
@ -34,76 +33,39 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
|
||||
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.ultAnisContainerItem > a"
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
element.attr("href")!!.also {
|
||||
setUrlWithoutDomain(it)
|
||||
episode_number = it.substringAfterLast("/").toFloatOrNull() ?: 0F
|
||||
}
|
||||
name = element.text()
|
||||
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET(baseUrl)
|
||||
|
||||
override fun latestUpdatesSelector() = "div.ultEpsContainerItem > a"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
title = element.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src")
|
||||
val epUrl = element.attr("href")
|
||||
|
||||
if (epUrl.substringAfterLast("/").toIntOrNull() != null) {
|
||||
setUrlWithoutDomain(epUrl.substringBeforeLast("/") + "-todos-os-episodios")
|
||||
} else { setUrlWithoutDomain(epUrl) }
|
||||
}
|
||||
|
||||
override fun episodeListSelector(): String = "div#aba_epi > a"
|
||||
|
||||
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
val infos = document.selectFirst("div.animeInfos")!!
|
||||
val right = document.selectFirst("div.right")!!
|
||||
|
||||
setUrlWithoutDomain(document.location())
|
||||
title = right.selectFirst("h1")!!.text()
|
||||
genre = right.select("ul.animeGen a").eachText().joinToString()
|
||||
|
||||
thumbnail_url = infos.selectFirst("img")!!.attr("data-lazy-src")
|
||||
description = right.selectFirst("div.animeSecondContainer > p:gt(0)")!!.text()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoElement = document.selectFirst("div.playerBox > *")!!
|
||||
return if (videoElement.tagName() == "video") {
|
||||
MeusAnimesExtractor(client).videoListFromElement(videoElement)
|
||||
} else {
|
||||
IframeExtractor(client, headers).videoListFromIframe(videoElement)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
override fun searchAnimeNextPageSelector() = "div.paginacao > a.next"
|
||||
override fun getFilterList(): AnimeFilterList = MAFilters.FILTER_LIST
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val defaultUrl = "$baseUrl/lista-de-animes/$page"
|
||||
val params = MAFilters.getSearchParameters(filters)
|
||||
return when {
|
||||
params.letter.isNotBlank() -> GET("$defaultUrl?letra=${params.letter}")
|
||||
params.year.isNotBlank() -> GET("$defaultUrl?ano=${params.year}")
|
||||
params.audio.isNotBlank() -> GET("$defaultUrl?audio=${params.audio}")
|
||||
params.genre.isNotBlank() -> GET("$defaultUrl?genero=${params.genre}")
|
||||
query.isNotBlank() -> GET("$defaultUrl?s=$query")
|
||||
else -> GET(defaultUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
@ -120,38 +82,85 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
title = element.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src")
|
||||
val epUrl = element.attr("href")
|
||||
|
||||
if (epUrl.substringAfterLast("/").toIntOrNull() != null) {
|
||||
setUrlWithoutDomain(epUrl.substringBeforeLast("/") + "-todos-os-episodios")
|
||||
} else { setUrlWithoutDomain(epUrl) }
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val defaultUrl = "$baseUrl/lista-de-animes/$page"
|
||||
val params = MAFilters.getSearchParameters(filters)
|
||||
return when {
|
||||
params.letter.isNotBlank() -> GET("$defaultUrl?letra=${params.letter}")
|
||||
params.year.isNotBlank() -> GET("$defaultUrl?ano=${params.year}")
|
||||
params.audio.isNotBlank() -> GET("$defaultUrl?audio=${params.audio}")
|
||||
params.genre.isNotBlank() -> GET("$defaultUrl?genero=${params.genre}")
|
||||
query.isNotBlank() -> GET("$defaultUrl?s=$query")
|
||||
else -> GET(defaultUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
|
||||
override fun latestUpdatesSelector(): String = "div.ultEpsContainerItem > a"
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector() = "div.paginacao > a.next"
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
val infos = document.selectFirst("div.animeInfos")!!
|
||||
val right = document.selectFirst("div.right")!!
|
||||
|
||||
setUrlWithoutDomain(document.location())
|
||||
title = right.selectFirst("h1")!!.text()
|
||||
genre = right.select("ul.animeGen a").eachText().joinToString()
|
||||
|
||||
thumbnail_url = infos.selectFirst("img")?.attr("data-lazy-src")
|
||||
description = right.selectFirst("div.animeSecondContainer > p:gt(0)")!!.text()
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
|
||||
|
||||
override fun episodeListSelector() = "div#aba_epi > a"
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
element.attr("href").also {
|
||||
setUrlWithoutDomain(it)
|
||||
episode_number = it.substringAfterLast("/").toFloatOrNull() ?: 0F
|
||||
}
|
||||
name = element.text()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
private val meusanimesExtractor by lazy { MeusAnimesExtractor(client) }
|
||||
private val iframeExtractor by lazy { IframeExtractor(client, headers) }
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.use { it.asJsoup() }
|
||||
val videoElement = document.selectFirst("div.playerBox > *")!!
|
||||
return when (videoElement.tagName()) {
|
||||
"video" -> meusanimesExtractor.videoListFromElement(videoElement)
|
||||
else -> iframeExtractor.videoListFromIframe(videoElement)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
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()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
@ -1,13 +1,16 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
ext {
|
||||
extName = 'Muito Hentai'
|
||||
pkgNameSuffix = 'pt.muitohentai'
|
||||
extClass = '.MuitoHentai'
|
||||
extVersionCode = 1
|
||||
libVersion = '12'
|
||||
extVersionCode = 2
|
||||
libVersion = '13'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,14 +1,12 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.muitohentai
|
||||
|
||||
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.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@ -16,123 +14,106 @@ import java.text.SimpleDateFormat
|
||||
|
||||
class MuitoHentai : ParsedAnimeHttpSource() {
|
||||
override val name = "Muito Hentai"
|
||||
|
||||
override val baseUrl = "https://www.muitohentai.com"
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeSelector(): String = "ul.ul_sidebar > li"
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/ranking-hentais/?paginacao=$page")
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/ranking-hentais/?paginacao=$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
thumbnail_url = element.selectFirst("div.zeroleft > a > img")!!.attr("src")
|
||||
val a = element.selectFirst("div.lefthentais > div > b:gt(0) > a.series")!!
|
||||
url = a.attr("href")
|
||||
title = a.text()
|
||||
override fun popularAnimeSelector() = "ul.ul_sidebar > li"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
thumbnail_url = element.selectFirst("div.zeroleft > a > img")?.attr("src")
|
||||
element.selectFirst("div.lefthentais > div > b:gt(0) > a.series")!!.run {
|
||||
setUrlWithoutDomain(attr("href"))
|
||||
title = text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "div.paginacao > a:contains(»)"
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animes = document.select(popularAnimeSelector()).map(::popularAnimeFromElement)
|
||||
return AnimesPage(animes, document.selectFirst(popularAnimeNextPageSelector()) != null)
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/")
|
||||
|
||||
override fun latestUpdatesSelector() = "div.animation-2 > article:contains(Episódio)"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
||||
val slug = element.selectFirst("a")!!.attr("href")
|
||||
.substringAfter("/episodios/")
|
||||
.substringBefore("-episodio")
|
||||
url = "/info/$slug"
|
||||
val img = element.selectFirst("img")!!
|
||||
title = img.attr("alt")
|
||||
thumbnail_url = img.attr("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/buscar/$query")
|
||||
|
||||
override fun searchAnimeSelector() = "div#archive-content > article > div.poster"
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
val img = element.selectFirst("img")!!
|
||||
title = img.attr("alt")
|
||||
thumbnail_url = img.attr("src")
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector() = null
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
val data = document.selectFirst("div.sheader > div.data")!!
|
||||
title = data.selectFirst("h1")!!.text()
|
||||
genre = data.selectFirst("div.sgeneros")!!.children()
|
||||
.filterNot { it.text().contains(title) }
|
||||
.joinToString { it.text() }
|
||||
description = data.selectFirst("div#info1 > div.wp-content > p")!!.text()
|
||||
thumbnail_url = document.selectFirst("div.sheader > div.poster > img")?.attr("src")
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListSelector(): String = "article.item"
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val doc = response.asJsoup()
|
||||
return doc
|
||||
.select(episodeListSelector())
|
||||
.map(::episodeFromElement)
|
||||
.reversed()
|
||||
}
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
return SEpisode.create().apply {
|
||||
val data = element.selectFirst("div.data")!!
|
||||
url = element.selectFirst("div.poster > div.season_m > a")!!.attr("href")
|
||||
name = data.selectFirst("h3")!!.text().trim()
|
||||
date_upload = parseDate(data.selectFirst("span")!!.text())
|
||||
}
|
||||
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
|
||||
|
||||
override fun episodeListSelector() = "article.item"
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
val data = element.selectFirst("div.data")!!
|
||||
setUrlWithoutDomain(element.selectFirst("div.poster > div.season_m > a")!!.attr("href"))
|
||||
name = data.selectFirst("h3")!!.text().trim()
|
||||
date_upload = data.selectFirst("span")?.text()?.parseDate() ?: 0L
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListSelector() = "div.playex > div#option-0 > iframe"
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val doc = response.asJsoup()
|
||||
val doc = response.use { it.asJsoup() }
|
||||
val idplay = doc.selectFirst(videoListSelector())!!.attr("src").substringAfter("?idplay=")
|
||||
val res = client.newCall(GET("https://www.hentaitube.online/players_sites/mt/index.php?idplay=$idplay")).execute()
|
||||
val pdoc = res.asJsoup()
|
||||
val pdoc = res.use { it.asJsoup() }
|
||||
return pdoc
|
||||
.select("source")
|
||||
.map(::videoFromElement)
|
||||
.reversed()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video {
|
||||
val url = element.attr("src")
|
||||
return Video(url, element.attr("label"), url)
|
||||
}
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeSelector() = "div#archive-content > article > div.poster"
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/buscar/$query")
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
url = element.selectFirst("a")!!.attr("href")
|
||||
val img = element.selectFirst("img")!!
|
||||
title = img.attr("alt")
|
||||
thumbnail_url = img.attr("src")
|
||||
}
|
||||
}
|
||||
override fun searchAnimeNextPageSelector() = throw UnsupportedOperationException("Not used.")
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animes = document
|
||||
.select(searchAnimeSelector())
|
||||
.map(::searchAnimeFromElement)
|
||||
return AnimesPage(animes, false)
|
||||
}
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(emptyList())
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
val data = document.selectFirst("div.sheader > div.data")!!
|
||||
title = data.selectFirst("h1")!!.text()
|
||||
genre = data.selectFirst("div.sgeneros")!!.children().filter { it -> !it.text().contains(title) }.joinToString(", ") { it.text() }
|
||||
description = data.selectFirst("div#info1 > div.wp-content > p")!!.text()
|
||||
thumbnail_url = document.selectFirst("div.sheader > div.poster > img")!!.attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException("Not used.")
|
||||
override fun latestUpdatesSelector(): String = "div.animation-2 > article:contains(Episódio)"
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
val slug = element.selectFirst("a")!!.attr("href")
|
||||
.substringAfter("/episodios/")
|
||||
.substringBefore("-episodio")
|
||||
url = "/info/$slug"
|
||||
val img = element.selectFirst("img")!!
|
||||
title = img.attr("alt")
|
||||
thumbnail_url = img.attr("src")
|
||||
}
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/")
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animes = document
|
||||
.select(latestUpdatesSelector())
|
||||
.map(::latestUpdatesFromElement)
|
||||
return AnimesPage(animes, false)
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
|
||||
// ============================= Utilities ==============================
|
||||
private fun String.parseDate(): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(this)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
companion object {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.pt.vizer
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.VizerFilters.FilterSearchParams
|
||||
@ -27,7 +26,6 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
@ -48,11 +46,11 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
@ -88,83 +86,13 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
thumbnail_url = "$baseUrl/content/$imgslug/posterPt/342/${item.id}.webp"
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
private fun getSeasonEps(seasonElement: Element): List<SEpisode> {
|
||||
val id = seasonElement.attr("data-season-id")
|
||||
val sname = seasonElement.text()
|
||||
val response = client.newCall(apiRequest("getEpisodes=$id")).execute()
|
||||
val episodes = response.parseAs<EpisodeListDto>().episodes
|
||||
.filter { it.released }
|
||||
.map {
|
||||
SEpisode.create().apply {
|
||||
name = "Temp $sname: Ep ${it.name} - ${it.title}"
|
||||
episode_number = it.name.toFloatOrNull() ?: 0F
|
||||
url = it.id
|
||||
}
|
||||
}
|
||||
return episodes
|
||||
}
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = apiRequest("getHomeSliderSeries=1")
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val doc = response.asJsoup()
|
||||
val seasons = doc.select("div#seasonsList div.item[data-season-id]")
|
||||
return if (seasons.size > 0) {
|
||||
seasons.flatMap(::getSeasonEps).reversed()
|
||||
} else {
|
||||
listOf(
|
||||
SEpisode.create().apply {
|
||||
name = "Filme"
|
||||
episode_number = 1F
|
||||
url = response.request.url.toString()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val url = episode.url
|
||||
return if (url.startsWith("https")) {
|
||||
// Its an real url, maybe from a movie
|
||||
GET(url, headers)
|
||||
} else {
|
||||
// Fake url, its an ID that will be used to get episode languages
|
||||
// (sub/dub) and then return the video link
|
||||
apiRequest("getEpisodeLanguages=$url")
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val body = response.body.string()
|
||||
val videoObjectList = if (body.startsWith("{")) {
|
||||
json.decodeFromString<VideoLanguagesDto>(body).videos
|
||||
} else {
|
||||
val videoJson = body.substringAfterLast("videoPlayerBox(").substringBefore(");")
|
||||
json.decodeFromString<VideoLanguagesDto>(videoJson).videos
|
||||
}
|
||||
|
||||
return videoObjectList.flatMap(::getVideosFromObject)
|
||||
}
|
||||
|
||||
private fun getVideosFromObject(videoObj: VideoDto): List<Video> {
|
||||
val players = client.newCall(apiRequest("getVideoPlayers=${videoObj.id}"))
|
||||
.execute()
|
||||
.parseAs<PlayersDto>()
|
||||
val langPrefix = if (videoObj.lang == "1") "LEG" else "DUB"
|
||||
val videoList = players.iterator().mapNotNull { (name, status) ->
|
||||
if (status == "0") return@mapNotNull null
|
||||
val url = getPlayerUrl(videoObj.id, name)
|
||||
when (name) {
|
||||
"mixdrop" ->
|
||||
MixDropExtractor(client).videoFromUrl(url, langPrefix)
|
||||
"streamtape" ->
|
||||
StreamTapeExtractor(client)
|
||||
.videoFromUrl(url, "StreamTape($langPrefix)")
|
||||
?.let(::listOf)
|
||||
else -> null
|
||||
}
|
||||
}.flatten()
|
||||
return videoList
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val parsedData = response.parseAs<SearchResultDto>()
|
||||
val animes = parsedData.list.map(::animeFromObject)
|
||||
return AnimesPage(animes, false)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
@ -227,77 +155,145 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
description = buildString {
|
||||
append(doc.selectFirst("span.desc")!!.text() + "\n")
|
||||
doc.selectFirst("div.year")?.let { append("\nAno: ${it.text()}") }
|
||||
doc.selectFirst("div.tm")?.let { append("\nDuração: ${it.text()}") }
|
||||
doc.selectFirst("a.rating")?.let { append("\nNota: ${it.text()}") }
|
||||
doc.selectFirst("div.year")?.also { append("\nAno: ${it.text()}") }
|
||||
doc.selectFirst("div.tm")?.also { append("\nDuração: ${it.text()}") }
|
||||
doc.selectFirst("a.rating")?.also { append("\nNota: ${it.text()}") }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = apiRequest("getHomeSliderSeries=1")
|
||||
// ============================== Episodes ==============================
|
||||
private fun getSeasonEps(seasonElement: Element): List<SEpisode> {
|
||||
val id = seasonElement.attr("data-season-id")
|
||||
val sname = seasonElement.text()
|
||||
val response = client.newCall(apiRequest("getEpisodes=$id")).execute()
|
||||
val episodes = response.parseAs<EpisodeListDto>().episodes
|
||||
.filter { it.released }
|
||||
.map {
|
||||
SEpisode.create().apply {
|
||||
name = "Temp $sname: Ep ${it.name} - ${it.title}"
|
||||
episode_number = it.name.toFloatOrNull() ?: 0F
|
||||
url = it.id
|
||||
}
|
||||
}
|
||||
return episodes
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val parsedData = response.parseAs<SearchResultDto>()
|
||||
val animes = parsedData.list.map(::animeFromObject).toList()
|
||||
return AnimesPage(animes, false)
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val doc = response.use { it.asJsoup() }
|
||||
val seasons = doc.select("div#seasonsList div.item[data-season-id]")
|
||||
return if (seasons.size > 0) {
|
||||
seasons.flatMap(::getSeasonEps).reversed()
|
||||
} else {
|
||||
listOf(
|
||||
SEpisode.create().apply {
|
||||
name = "Filme"
|
||||
episode_number = 1F
|
||||
url = response.request.url.toString()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val url = episode.url
|
||||
return if (url.startsWith("https")) {
|
||||
// Its an real url, maybe from a movie
|
||||
GET(url, headers)
|
||||
} else {
|
||||
// Fake url, its an ID that will be used to get episode languages
|
||||
// (sub/dub) and then return the video link
|
||||
apiRequest("getEpisodeLanguages=$url")
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val body = response.use { it.body.string() }
|
||||
val videoObjectList = if (body.startsWith("{")) {
|
||||
json.decodeFromString<VideoLanguagesDto>(body).videos
|
||||
} else {
|
||||
val videoJson = body.substringAfterLast("videoPlayerBox(").substringBefore(");")
|
||||
json.decodeFromString<VideoLanguagesDto>(videoJson).videos
|
||||
}
|
||||
|
||||
return videoObjectList.flatMap(::getVideosFromObject)
|
||||
}
|
||||
|
||||
private val mixdropExtractor by lazy { MixDropExtractor(client) }
|
||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
|
||||
private fun getVideosFromObject(videoObj: VideoDto): List<Video> {
|
||||
val players = client.newCall(apiRequest("getVideoPlayers=${videoObj.id}"))
|
||||
.execute()
|
||||
.parseAs<PlayersDto>()
|
||||
val langPrefix = if (videoObj.lang == "1") "LEG" else "DUB"
|
||||
val videoList = players.iterator().mapNotNull { (name, status) ->
|
||||
if (status == "0") return@mapNotNull null
|
||||
val url = getPlayerUrl(videoObj.id, name)
|
||||
when (name) {
|
||||
"mixdrop" -> mixdropExtractor.videoFromUrl(url, langPrefix)
|
||||
"streamtape" -> streamtapeExtractor.videosFromUrl(url, "StreamTape($langPrefix)")
|
||||
else -> null
|
||||
}
|
||||
}.flatten()
|
||||
return videoList
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val popularPage = ListPreference(screen.context).apply {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_POPULAR_PAGE_KEY
|
||||
title = PREF_POPULAR_PAGE_TITLE
|
||||
entries = PREF_POPULAR_PAGE_ENTRIES
|
||||
entryValues = PREF_POPULAR_PAGE_VALUES
|
||||
setDefaultValue(PREF_POPULAR_PAGE_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)
|
||||
|
||||
val preferredPlayer = ListPreference(screen.context).apply {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_PLAYER_KEY
|
||||
title = PREF_PLAYER_TITLE
|
||||
entries = PREF_PLAYER_ARRAY
|
||||
entryValues = PREF_PLAYER_ARRAY
|
||||
setDefaultValue(PREF_PLAYER_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)
|
||||
|
||||
val preferredLanguage = ListPreference(screen.context).apply {
|
||||
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(popularPage)
|
||||
screen.addPreference(preferredPlayer)
|
||||
screen.addPreference(preferredLanguage)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private fun getPlayerUrl(id: String, name: String): String {
|
||||
val req = GET("$baseUrl/embed/getPlay.php?id=$id&sv=$name")
|
||||
val body = client.newCall(req).execute().body.string()
|
||||
val body = client.newCall(req).execute().use { it.body.string() }
|
||||
return body.substringAfter("location.href=\"").substringBefore("\";")
|
||||
}
|
||||
|
||||
@ -319,8 +315,7 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
val responseBody = body.string()
|
||||
return json.decodeFromString(responseBody)
|
||||
return use { it.body.string() }.let(json::decodeFromString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object VizerFilters {
|
||||
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
@ -16,14 +15,8 @@ object VizerFilters {
|
||||
fun toQueryPart() = vals[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return first { it is R } as R
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return getFirst<R>().let {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
class TypeFilter : QueryPartFilter("Tipo", VizerFiltersData.TYPES)
|
||||
@ -50,27 +43,32 @@ object VizerFilters {
|
||||
val minYear: String = "1890",
|
||||
val maxYear: String = "2022",
|
||||
val genre: String = "all",
|
||||
var orderBy: String = "rating",
|
||||
var orderWay: String = "desc",
|
||||
val orderBy: String = "rating",
|
||||
val orderWay: String = "desc",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
val searchParams = FilterSearchParams(
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
val sortFilter = filters.firstOrNull { it is SortFilter } as? SortFilter
|
||||
val (orderBy, ascending) = sortFilter?.state?.run {
|
||||
val order = VizerFiltersData.ORDERS[index].second
|
||||
val orderWay = if (ascending) "asc" else "desc"
|
||||
|
||||
Pair(order, orderWay)
|
||||
} ?: Pair("rating", "desc")
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<TypeFilter>(),
|
||||
filters.asQueryPart<MinYearFilter>(),
|
||||
filters.asQueryPart<MaxYearFilter>(),
|
||||
filters.asQueryPart<GenreFilter>(),
|
||||
orderBy,
|
||||
ascending,
|
||||
)
|
||||
filters.getFirst<SortFilter>().state?.let {
|
||||
val order = VizerFiltersData.ORDERS[it.index].second
|
||||
searchParams.orderBy = order
|
||||
searchParams.orderWay = if (it.ascending) "asc" else "desc"
|
||||
}
|
||||
return searchParams
|
||||
}
|
||||
|
||||
private object VizerFiltersData {
|
||||
|
||||
val TYPES = arrayOf(
|
||||
Pair("Animes", "anime"),
|
||||
Pair("Filmes", "Movies"),
|
||||
|
Reference in New Issue
Block a user