refactor(src/pt): Refactorate some extensions (#1713)

This commit is contained in:
Claudemirovsky
2023-06-12 06:38:55 +00:00
committed by GitHub
parent e2682e85b1
commit 6a1fb2c074
25 changed files with 419 additions and 575 deletions

View File

@ -1,6 +1,8 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
apply plugin: 'kotlinx-serialization' alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext { ext {
extName = 'Anime Fire' extName = 'Anime Fire'
@ -10,5 +12,4 @@ ext {
libVersion = '13' libVersion = '13'
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,9 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire
object AFConstants {
const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
const val USER_AGENT = "Mozilla/5.0 (Linux; Android 10; SM-A307GT Build/QP1A.190711.020;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/102.0.5005.125 Mobile Safari/537.36"
const val PREFERRED_QUALITY = "preferred_quality"
const val PREFIX_SEARCH = "id:"
val QUALITY_LIST = arrayOf("360p", "720p")
}

View File

@ -16,7 +16,7 @@ object AFFilters {
} }
private inline fun <reified R> AnimeFilterList.asQueryPart(): String { private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") { return first { it is R }.let {
(it as QueryPartFilter).toQueryPart() (it as QueryPartFilter).toQueryPart()
} }
} }

View File

@ -20,7 +20,7 @@ class AFUrlActivity : Activity() {
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) { if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1] val id = pathSegments[1]
val searchQuery = AFConstants.PREFIX_SEARCH + id val searchQuery = AnimeFire.PREFIX_SEARCH + id
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH" action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", searchQuery) putExtra("query", searchQuery)

View File

@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -26,6 +25,7 @@ import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception import java.lang.Exception
class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -40,17 +40,15 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val json = Json { private val json: Json by injectLazy()
ignoreUnknownKeys = true
}
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl) .add("Referer", baseUrl)
.add("Accept-Language", AFConstants.ACCEPT_LANGUAGE) .add("Accept-Language", ACCEPT_LANGUAGE)
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeSelector() = latestUpdatesSelector() override fun popularAnimeSelector() = latestUpdatesSelector()
@ -62,25 +60,21 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListSelector(): String = "div.div_video_list > a" override fun episodeListSelector(): String = "div.div_video_list > a"
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed() override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val episode = SEpisode.create()
val url = element.attr("href") val url = element.attr("href")
episode.setUrlWithoutDomain(url) setUrlWithoutDomain(url)
episode.name = element.text() name = element.text()
episode.episode_number = try { episode_number = url.substringAfterLast("/").toFloatOrNull() ?: 0F
url.substringAfterLast("/").toFloat()
} catch (e: NumberFormatException) { 0F }
return episode
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document: Document = response.asJsoup() val document = response.asJsoup()
val videoElement = document.selectFirst("video#my-video") val videoElement = document.selectFirst("video#my-video")
return if (videoElement != null) { return if (videoElement != null) {
AnimeFireExtractor(client, json).videoListFromElement(videoElement) AnimeFireExtractor(client, json).videoListFromElement(videoElement)
} else { } else {
IframeExtractor(client).videoListFromDocument(document) IframeExtractor(client).videoListFromDocument(document, headers)
} }
} }
@ -94,36 +88,27 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector() override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> { override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(AFConstants.PREFIX_SEARCH)) { return if (query.startsWith(PREFIX_SEARCH)) {
val id = query.removePrefix(AFConstants.PREFIX_SEARCH) val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/animes/$id")) client.newCall(GET("$baseUrl/animes/$id"))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map(::searchAnimeByIdParse)
searchAnimeByIdParse(response, id)
}
} else { } else {
val params = AFFilters.getSearchParameters(filters) super.fetchSearchAnime(page, query, filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
} }
} }
private fun searchAnimeByIdParse(response: Response, id: String): AnimesPage { private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response) val details = animeDetailsParse(response)
details.url = "/animes/$id"
return AnimesPage(listOf(details), false) return AnimesPage(listOf(details), false)
} }
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used") override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AFFilters.getSearchParameters(filters)
private fun searchAnimeRequest(page: Int, query: String, filters: AFFilters.FilterSearchParams): Request {
if (query.isBlank()) { if (query.isBlank()) {
return when { return when {
filters.season.isNotBlank() -> GET("$baseUrl/temporada/${filters.season}/$page") params.season.isNotBlank() -> GET("$baseUrl/temporada/${params.season}/$page")
else -> GET("$baseUrl/genero/${filters.genre}/$page") else -> GET("$baseUrl/genero/${params.genre}/$page")
} }
} }
val fixedQuery = query.trim().replace(" ", "-").lowercase() val fixedQuery = query.trim().replace(" ", "-").lowercase()
@ -131,58 +116,58 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val anime = SAnime.create()
val content = document.selectFirst("div.divDivAnimeInfo")!! val content = document.selectFirst("div.divDivAnimeInfo")!!
val names = content.selectFirst("div.div_anime_names")!! val names = content.selectFirst("div.div_anime_names")!!
val infos = content.selectFirst("div.divAnimePageInfo")!! val infos = content.selectFirst("div.divAnimePageInfo")!!
anime.thumbnail_url = content.selectFirst("div.sub_animepage_img > img")!! setUrlWithoutDomain(document.location())
thumbnail_url = content.selectFirst("div.sub_animepage_img > img")!!
.attr("data-src") .attr("data-src")
anime.title = names.selectFirst("h1")!!.text() title = names.selectFirst("h1")!!.text()
anime.genre = infos.select("a.spanGeneros").joinToString(", ") { it.text() } genre = infos.select("a.spanGeneros").eachText().joinToString()
anime.author = infos.getInfo("Estúdios") author = infos.getInfo("Estúdios")
anime.status = parseStatus(infos.getInfo("Status")) status = parseStatus(infos.getInfo("Status"))
var desc = content.selectFirst("div.divSinopse > span")!!.text() + "\n" description = buildString {
names.selectFirst("h6")?.let { desc += "\nNome alternativo: ${it.text()}" } append(content.selectFirst("div.divSinopse > span")!!.text() + "\n")
infos.getInfo("Dia de")?.let { desc += "\nDia de lançamento: $it" } names.selectFirst("h6")?.let { append("\nNome alternativo: ${it.text()}") }
infos.getInfo("Áudio")?.let { desc += "\nTipo: $it" } infos.getInfo("Dia de")?.let { append("\nDia de lançamento: $it") }
infos.getInfo("Ano")?.let { desc += "\nAno: $it" } infos.getInfo("Áudio")?.let { append("\nTipo: $it") }
infos.getInfo("Episódios")?.let { desc += "\nEpisódios: $it" } infos.getInfo("Ano")?.let { append("\nAno: $it") }
infos.getInfo("Temporada")?.let { desc += "\nTemporada: $it" } infos.getInfo("Episódios")?.let { append("\nEpisódios: $it") }
anime.description = desc infos.getInfo("Temporada")?.let { append("\nTemporada: $it") }
}
return anime
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "ul.pagination img.seta-right" override fun latestUpdatesNextPageSelector(): String = "ul.pagination img.seta-right"
override fun latestUpdatesSelector(): String = "article.cardUltimosEps > a" override fun latestUpdatesSelector(): String = "article.cardUltimosEps > a"
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
val anime = SAnime.create()
val url = element.attr("href") 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")
}
}
if (url.substringAfterLast("/").toIntOrNull() != null) { title = element.selectFirst("h3.animeTitle")!!.text()
val newUrl = url.substringBeforeLast("/") + "-todos-os-episodios" thumbnail_url = element.selectFirst("img")!!.attr("data-src")
anime.setUrlWithoutDomain(newUrl)
} else { anime.setUrlWithoutDomain(url) }
anime.title = element.selectFirst("h3.animeTitle")!!.text()
anime.thumbnail_url = element.selectFirst("img")!!.attr("data-src")
return anime
} }
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/$page") override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/home/$page")
// ============================== Settings ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = AFConstants.PREFERRED_QUALITY key = PREF_QUALITY_KEY
title = "Qualidade preferida" title = PREF_QUALITY_TITLE
entries = AFConstants.QUALITY_LIST entries = PREF_QUALITY_VALUES
entryValues = AFConstants.QUALITY_LIST entryValues = PREF_QUALITY_VALUES
setDefaultValue(AFConstants.QUALITY_LIST.last()) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val selected = newValue as String
@ -190,9 +175,7 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(videoQualityPref)
} }
override fun getFilterList(): AnimeFilterList = AFFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = AFFilters.FILTER_LIST
@ -208,27 +191,23 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
private fun Element.getInfo(key: String): String? { private fun Element.getInfo(key: String): String? {
val div = this.selectFirst("div.animeInfo:contains($key)") return selectFirst("div.animeInfo:contains($key) span")?.text()
if (div == null) return div
val span = div.selectFirst("span")
return span?.text()
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(AFConstants.PREFERRED_QUALITY, null) val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
if (quality != null) { return sortedWith(
val newList = mutableListOf<Video>() compareBy { it.quality.contains(quality) },
var preferred = 0 ).reversed()
for (video in this) { }
if (video.quality == quality) {
newList.add(preferred, video) companion object {
preferred++ const val PREFIX_SEARCH = "id:"
} else { private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
newList.add(video)
} private const val PREF_QUALITY_KEY = "preferred_quality"
} private const val PREF_QUALITY_TITLE = "Qualidade preferida"
return newList private const val PREF_QUALITY_DEFAULT = "720p"
} private val PREF_QUALITY_VALUES = arrayOf("360p", "720p")
return this
} }
} }

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.animeextension.pt.animefire.extractors package eu.kanade.tachiyomi.animeextension.pt.animefire.extractors
import eu.kanade.tachiyomi.animeextension.pt.animefire.AFConstants
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers import okhttp3.Headers
@ -8,10 +7,7 @@ import okhttp3.OkHttpClient
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
class IframeExtractor(private val client: OkHttpClient) { class IframeExtractor(private val client: OkHttpClient) {
fun videoListFromDocument(doc: Document, headers: Headers): List<Video> {
private val headers = Headers.headersOf("User-Agent", AFConstants.USER_AGENT)
fun videoListFromDocument(doc: Document): List<Video> {
val iframeElement = doc.selectFirst("div#div_video iframe")!! val iframeElement = doc.selectFirst("div#div_video iframe")!!
val iframeUrl = iframeElement.attr("src") val iframeUrl = iframeElement.attr("src")
val response = client.newCall(GET(iframeUrl, headers)).execute() val response = client.newCall(GET(iframeUrl, headers)).execute()

View File

@ -29,12 +29,10 @@ class AnimesAria : ParsedAnimeHttpSource() {
override val supportsLatest = true override val supportsLatest = true
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
return SAnime.create().apply { setUrlWithoutDomain(element.attr("href"))
setUrlWithoutDomain(element.attr("href")) title = element.attr("title")
title = element.attr("title") thumbnail_url = element.selectFirst("img")!!.attr("src")
thumbnail_url = element.selectFirst("img")!!.attr("src")
}
} }
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector() override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
@ -46,46 +44,43 @@ class AnimesAria : ParsedAnimeHttpSource() {
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed() override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
return SEpisode.create().apply { element.parent()!!.selectFirst("a > b")!!.ownText().let {
element.parent()!!.selectFirst("a > b")!!.ownText().let { name = it
name = it episode_number = it.substringAfter(" ").toFloatOrNull() ?: 0F
episode_number = it.substringAfter(" ").toFloat()
}
setUrlWithoutDomain(element.attr("href"))
scanlator = element.text().substringAfter(" ") // sub/dub
} }
setUrlWithoutDomain(element.attr("href"))
scanlator = element.text().substringAfter(" ") // sub/dub
} }
override fun episodeListSelector() = "td div.clear > a.btn-xs" override fun episodeListSelector() = "td div.clear > a.btn-xs"
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
return SAnime.create().apply { setUrlWithoutDomain(document.location())
val row = document.selectFirst("div.anime_background_w div.row")!! val row = document.selectFirst("div.anime_background_w div.row")!!
title = row.selectFirst("h1 > span")!!.text() title = row.selectFirst("h1 > span")!!.text()
status = row.selectFirst("div.clear span.btn")?.text().toStatus() 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() genre = row.select("div.clear a.btn").eachText().joinToString()
description = buildString { description = buildString {
document.selectFirst("li.active > small")!! document.selectFirst("li.active > small")!!
.ownText() .ownText()
.substringAfter(": ") .substringAfter(": ")
.let(::append) .let(::append)
append("\n\n") append("\n\n")
row.selectFirst("h1 > small")?.text()?.let { row.selectFirst("h1 > small")?.text()?.let {
append("Títulos Alternativos: $it\n") append("Títulos Alternativos: $it\n")
} }
// Additional info // Additional info
row.select("div.pull-right > a").forEach { row.select("div.pull-right > a").forEach {
val title = it.selectFirst("small")!!.text() val title = it.selectFirst("small")!!.text()
val value = it.selectFirst("span")!!.text() val value = it.selectFirst("span")!!.text()
append("$title: $value\n") append("$title: $value\n")
}
} }
} }
} }
@ -154,28 +149,23 @@ class AnimesAria : ParsedAnimeHttpSource() {
val id = query.removePrefix(PREFIX_SEARCH) val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/anime/$id")) client.newCall(GET("$baseUrl/anime/$id"))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map(::searchAnimeByIdParse)
searchAnimeByIdParse(response, id)
}
} else { } else {
super.fetchSearchAnime(page, query, filters) super.fetchSearchAnime(page, query, filters)
} }
} }
private fun searchAnimeByIdParse(response: Response, id: String): AnimesPage { private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup()) val details = animeDetailsParse(response.asJsoup())
details.url = "/anime/$id"
return AnimesPage(listOf(details), false) return AnimesPage(listOf(details), false)
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
return SAnime.create().apply { thumbnail_url = element.selectFirst("img")!!.attr("src")
thumbnail_url = element.selectFirst("img")!!.attr("src") val ahref = element.selectFirst("a")!!
val ahref = element.selectFirst("a")!! title = ahref.attr("title")
title = ahref.attr("title") setUrlWithoutDomain(ahref.attr("href").substringBefore("/episodio/"))
setUrlWithoutDomain(ahref.attr("href").substringBefore("/episodio/"))
}
} }
override fun latestUpdatesNextPageSelector() = "a:containsOwn(Próximo):not(.disabled)" override fun latestUpdatesNextPageSelector() = "a:containsOwn(Próximo):not(.disabled)"

View File

@ -1,6 +1,8 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
apply plugin: 'kotlinx-serialization' alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext { ext {
extName = 'AnimesTC' extName = 'AnimesTC'

View File

@ -21,11 +21,11 @@ object ATCFilters {
private class TriStateVal(name: String) : AnimeFilter.TriState(name) private class TriStateVal(name: String) : AnimeFilter.TriState(name)
private inline fun <reified R> AnimeFilterList.getFirst(): R { private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first() return first { it is R } as R
} }
private inline fun <reified R> AnimeFilterList.asQueryPart(): String { private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.getFirst<R>().let { return getFirst<R>().let {
(it as QueryPartFilter).toQueryPart() (it as QueryPartFilter).toQueryPart()
} }
} }
@ -41,7 +41,7 @@ object ATCFilters {
class GenresFilter : TriStateFilterList( class GenresFilter : TriStateFilterList(
"Gêneros", "Gêneros",
ATCFiltersData.GENRES.map { TriStateVal(it) }, ATCFiltersData.GENRES.map(::TriStateVal),
) )
val FILTER_LIST get() = AnimeFilterList( val FILTER_LIST get() = AnimeFilterList(
@ -88,38 +88,33 @@ object ATCFilters {
return searchParams return searchParams
} }
private fun compareLower(first: String, second: String): Boolean {
return first.lowercase() in second.lowercase()
}
private fun mustRemove(anime: AnimeDto, params: FilterSearchParams): Boolean { private fun mustRemove(anime: AnimeDto, params: FilterSearchParams): Boolean {
return when { return when {
params.animeName != "" && !compareLower(params.animeName, anime.title) -> true params.animeName != "" && !anime.title.contains(params.animeName, true) -> true
params.initialLetter != "" && !anime.title.lowercase().startsWith(params.initialLetter) -> true params.initialLetter != "" && !anime.title.lowercase().startsWith(params.initialLetter) -> true
params.blackListedGenres.size > 0 && params.blackListedGenres.any { params.blackListedGenres.size > 0 && params.blackListedGenres.any {
compareLower(it, anime.genres) anime.genres.contains(it, true)
} -> true } -> true
params.includedGenres.size > 0 && params.includedGenres.any { params.includedGenres.size > 0 && params.includedGenres.any {
!compareLower(it, anime.genres) !anime.genres.contains(it, true)
} -> true } -> true
params.status != "" && anime.status != SAnime.UNKNOWN && anime.status != params.status.toInt() -> true params.status != "" && anime.status != SAnime.UNKNOWN && anime.status != params.status.toInt() -> true
else -> false else -> false
} }
} }
private inline fun <T, R : Comparable<R>> List<out T>.sortedByIf( private inline fun <T, R : Comparable<R>> List<T>.sortedByIf(
condition: Boolean, isAscending: Boolean,
crossinline selector: (T) -> R?, crossinline selector: (T) -> R,
): List<T> { ): List<T> {
return if (condition) { return when {
sortedBy(selector) isAscending -> sortedBy(selector)
} else { else -> sortedByDescending(selector)
sortedByDescending(selector)
} }
} }
fun List<AnimeDto>.applyFilterParams(params: FilterSearchParams): List<AnimeDto> { fun List<AnimeDto>.applyFilterParams(params: FilterSearchParams): List<AnimeDto> {
return this.filterNot { mustRemove(it, params) }.let { results -> return filterNot { mustRemove(it, params) }.let { results ->
when (params.sortBy) { when (params.sortBy) {
"A-Z" -> results.sortedByIf(params.orderAscending) { it.title.lowercase() } "A-Z" -> results.sortedByIf(params.orderAscending) { it.title.lowercase() }
"year" -> results.sortedByIf(params.orderAscending) { it.year ?: 0 } "year" -> results.sortedByIf(params.orderAscending) { it.year ?: 0 }

View File

@ -27,12 +27,12 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit.DAYS import java.util.concurrent.TimeUnit.DAYS
@ -47,16 +47,14 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun headersBuilder() = Headers.Builder() override fun headersBuilder() = super.headersBuilder()
.add("Referer", "https://www.animestc.net/") .add("Referer", "https://www.animestc.net/")
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val json = Json { private val json: Json by injectLazy()
ignoreUnknownKeys = true
}
// ============================== Popular =============================== // ============================== Popular ===============================
// This source doesnt have a popular animes page, // This source doesnt have a popular animes page,
@ -77,14 +75,7 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
private fun getEpisodeList(animeId: Int, page: Int = 1): List<SEpisode> { private fun getEpisodeList(animeId: Int, page: Int = 1): List<SEpisode> {
val response = client.newCall(episodeListRequest(animeId, page)).execute() val response = client.newCall(episodeListRequest(animeId, page)).execute()
val parsed = response.parseAs<ResponseDto<EpisodeDto>>() val parsed = response.parseAs<ResponseDto<EpisodeDto>>()
val episodes = parsed.items.map { val episodes = parsed.items.map(::episodeFromObject)
SEpisode.create().apply {
name = it.title
setUrlWithoutDomain("/episodes?slug=${it.slug}")
episode_number = it.number.toFloat()
date_upload = it.created_at.toDate()
}
}
if (parsed.page < parsed.lastPage) { if (parsed.page < parsed.lastPage) {
return episodes + getEpisodeList(animeId, page + 1) return episodes + getEpisodeList(animeId, page + 1)
@ -93,6 +84,13 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
} }
} }
private fun episodeFromObject(episode: EpisodeDto) = SEpisode.create().apply {
name = episode.title
setUrlWithoutDomain("/episodes?slug=${episode.slug}")
episode_number = episode.number.toFloat()
date_upload = episode.created_at.toDate()
}
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
@ -125,15 +123,13 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime { override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val anime = response.getAnimeDto() val anime = response.getAnimeDto()
return SAnime.create().apply { setUrlWithoutDomain("/series/${anime.id}")
setUrlWithoutDomain("/series/${anime.id}") title = anime.title
title = anime.title status = anime.status
status = anime.status genre = anime.genres
genre = anime.genres description = anime.synopsis
description = anime.synopsis
}
} }
// =============================== Search =============================== // =============================== Search ===============================
@ -150,12 +146,9 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
val slug = query.removePrefix(PREFIX_SEARCH) val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/series?slug=$slug")) client.newCall(GET("$baseUrl/series?slug=$slug"))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map(::searchAnimeBySlugParse)
searchAnimeBySlugParse(response)
}
} else { } else {
val params = ATCFilters.getSearchParameters(filters) return Observable.just(searchAnime(page, query, filters))
return Observable.just(searchAnime(page, query, params))
} }
} }
@ -170,9 +163,11 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
override fun getFilterList(): AnimeFilterList = ATCFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = ATCFilters.FILTER_LIST
private fun searchAnime(page: Int, query: String, filterParams: ATCFilters.FilterSearchParams): AnimesPage { private fun searchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
filterParams.animeName = query val params = ATCFilters.getSearchParameters(filters).apply {
val filtered = allAnimesList.applyFilterParams(filterParams) animeName = query
}
val filtered = allAnimesList.applyFilterParams(params)
val results = filtered.chunked(30) val results = filtered.chunked(30)
val hasNextPage = results.size > page val hasNextPage = results.size > page
val currentPage = if (results.size == 0) { val currentPage = if (results.size == 0) {
@ -267,12 +262,12 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
private fun String.toDate(): Long { private fun String.toDate(): Long {
return runCatching { return runCatching {
DATE_FORMATTER.parse(this)?.time ?: 0L DATE_FORMATTER.parse(this)?.time
}.getOrNull() ?: 0L }.getOrNull() ?: 0L
} }
private inline fun <reified T> Response.parseAs(preloaded: String? = null): T { private inline fun <reified T> Response.parseAs(preloaded: String? = null): T {
val responseBody = preloaded ?: body.string() val responseBody = preloaded ?: use { it.body.string() }
return json.decodeFromString(responseBody) return json.decodeFromString(responseBody)
} }

View File

@ -46,16 +46,14 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.add("Accept-Language", ACCEPT_LANGUAGE) .add("Accept-Language", ACCEPT_LANGUAGE)
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.lista_de_animes div.ani_loop_item_img > a" override fun popularAnimeSelector() = "div.lista_de_animes div.ani_loop_item_img > a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime/page/$page") override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime/page/$page")
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
return SAnime.create().apply { setUrlWithoutDomain(element.attr("href"))
setUrlWithoutDomain(element.attr("href")) val img = element.selectFirst("img")!!
val img = element.selectFirst("img")!! title = img.attr("title")
title = img.attr("title") thumbnail_url = img.attr("src")
thumbnail_url = img.attr("src")
}
} }
override fun popularAnimeNextPageSelector(): String = "a.page-numbers:contains(Próximo)" override fun popularAnimeNextPageSelector(): String = "a.page-numbers:contains(Próximo)"
@ -70,45 +68,36 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListSelector(): String = "div.animepag_episodios_container > div.animepag_episodios_item > a" override fun episodeListSelector() = "div.animepag_episodios_container > div.animepag_episodios_item > a"
private fun getAllEps(response: Response): List<SEpisode> {
val doc = response.asJsoup().let {
if (response.request.url.toString().contains("/video/")) {
getRealDoc(it)
} else { it }
}
val epElementList = doc.select(episodeListSelector())
val epList = mutableListOf<SEpisode>()
epList.addAll(epElementList.map(::episodeFromElement))
if (hasNextPage(doc)) {
val next = doc.selectFirst(popularAnimeNextPageSelector())!!.attr("href")
val request = GET(baseUrl + next)
val newResponse = client.newCall(request).execute()
epList.addAll(getAllEps(newResponse))
}
return epList
}
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
return getAllEps(response).reversed() var doc = getRealDoc(response.asJsoup())
var first = true
val epList = buildList {
do {
if (!first) {
val path = doc.selectFirst(popularAnimeNextPageSelector())!!.attr("href")
doc = client.newCall(GET(baseUrl + path)).execute().asJsoup()
}
first = false
doc.select(episodeListSelector())
.map(::episodeFromElement)
.let(::addAll)
} while (hasNextPage(doc))
}
return epList.reversed()
} }
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
return SEpisode.create().apply { setUrlWithoutDomain(element.attr("href"))
setUrlWithoutDomain(element.attr("href")) episode_number = element.selectFirst("div.animepag_episodios_item_views")!!
episode_number = runCatching { .text()
element.selectFirst("div.animepag_episodios_item_views")!! .substringAfter(" ")
.text() .toFloatOrNull() ?: 0F
.substringAfter(" ") name = element.selectFirst("div.animepag_episodios_item_nome")!!.text()
.toFloat() date_upload = element.selectFirst("div.animepag_episodios_item_date")!!
}.getOrDefault(0F) .text()
name = element.selectFirst("div.animepag_episodios_item_nome")!!.text() .toDate()
date_upload = element.selectFirst("div.animepag_episodios_item_date")!!
.text()
.toDate()
}
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
@ -120,10 +109,8 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = throw Exception("not used") override fun searchAnimeFromElement(element: Element) = throw Exception("not used")
override fun searchAnimeNextPageSelector() = throw Exception("not used") override fun searchAnimeNextPageSelector() = throw Exception("not used")
override fun searchAnimeSelector() = throw Exception("not used") override fun searchAnimeSelector() = throw Exception("not used")
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response)
override fun getFilterList(): AnimeFilterList = AnitubeFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = AnitubeFilters.FILTER_LIST
@ -145,46 +132,42 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document) val doc = getRealDoc(document)
return SAnime.create().apply { setUrlWithoutDomain(doc.location())
setUrlWithoutDomain(doc.location()) val content = doc.selectFirst("div.anime_container_content")!!
val content = doc.selectFirst("div.anime_container_content")!! val infos = content.selectFirst("div.anime_infos")!!
val infos = content.selectFirst("div.anime_infos")!!
title = doc.selectFirst("div.anime_container_titulo")!!.text() 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") genre = infos.getInfo("Gêneros")
author = infos.getInfo("Autor") author = infos.getInfo("Autor")
artist = infos.getInfo("Estúdio") artist = infos.getInfo("Estúdio")
status = parseStatus(infos.getInfo("Status")) status = parseStatus(infos.getInfo("Status"))
val infoItems = listOf("Ano", "Direção", "Episódios", "Temporada", "Título Alternativo") val infoItems = listOf("Ano", "Direção", "Episódios", "Temporada", "Título Alternativo")
description = buildString { description = buildString {
append(doc.selectFirst("div.sinopse_container_content")!!.text() + "\n") append(doc.selectFirst("div.sinopse_container_content")!!.text() + "\n")
infoItems.forEach { item -> infoItems.forEach { item ->
infos.getInfo(item)?.let { append("\n$item: $it") } infos.getInfo(item)?.let { append("\n$item: $it") }
}
} }
} }
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector() override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesSelector(): String = "div.mContainer_content.threeItensPerContent > div.epi_loop_item" override fun latestUpdatesSelector() = "div.mContainer_content.threeItensPerContent > div.epi_loop_item"
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
return SAnime.create().apply { val img = element.selectFirst("img")!!
val img = element.selectFirst("img")!! setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href")) title = img.attr("title")
title = img.attr("title") thumbnail_url = img.attr("src")
thumbnail_url = img.attr("src")
}
} }
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?page=$page") override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page")
override fun latestUpdatesParse(response: Response): AnimesPage { override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
@ -216,15 +199,11 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun getRealDoc(document: Document): Document { private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst("div.controles_ep > a[href] > i.spr.listaEP") return document.selectFirst("div.controles_ep > a[href]:has(i.spr.listaEP)")
if (menu != null) { ?.let {
val parent = menu.parent()!! val path = it.attr("href")
val parentPath = parent.attr("href") client.newCall(GET(baseUrl + path)).execute().asJsoup()
val req = client.newCall(GET(baseUrl + parentPath)).execute() } ?: document
return req.asJsoup()
} else {
return document
}
} }
private fun parseStatus(statusString: String?): Int { private fun parseStatus(statusString: String?): Int {
@ -248,12 +227,12 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return currentPage != lastPage return currentPage != lastPage
} }
private fun String.toPageNum(): Int = try { private fun String.toPageNum(): Int =
this.substringAfter("page/") substringAfter("page/")
.substringAfter("page=") .substringAfter("page=")
.substringBefore("/") .substringBefore("/")
.substringBefore("&").toInt() .substringBefore("&")
} catch (e: NumberFormatException) { 1 } .toIntOrNull() ?: 1
private fun Element.getInfo(key: String): String? { private fun Element.getInfo(key: String): String? {
val parent = selectFirst("b:contains($key)")?.parent() val parent = selectFirst("b:contains($key)")?.parent()
@ -261,10 +240,9 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val text = if (genres?.size == 0) { val text = if (genres?.size == 0) {
parent.ownText() parent.ownText()
} else { } else {
genres?.joinToString(", ") { it.text() } genres?.eachText()?.joinToString()
} }
if (text == "") return null return text?.ifEmpty { null }
return text
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
@ -276,7 +254,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun String.toDate(): Long { private fun String.toDate(): Long {
return runCatching { return runCatching {
DATE_FORMATTER.parse(this)?.time ?: 0L DATE_FORMATTER.parse(this)?.time
}.getOrNull() ?: 0L }.getOrNull() ?: 0L
} }

View File

@ -1,6 +1,8 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
apply plugin: 'kotlinx-serialization' alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext { ext {
extName = 'Better Anime' extName = 'Better Anime'

View File

@ -18,7 +18,7 @@ class BAUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) { if (pathSegments != null && pathSegments.size > 2) {
val type = pathSegments[0] val type = pathSegments[0]
val lang = pathSegments[1] val lang = pathSegments[1]
val item = pathSegments[2] val item = pathSegments[2]

View File

@ -34,6 +34,7 @@ import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.lang.Exception import java.lang.Exception
@ -51,9 +52,7 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.addInterceptor(::loginInterceptor) .addInterceptor(::loginInterceptor)
.build() .build()
private val json = Json { private val json: Json by injectLazy()
ignoreUnknownKeys = true
}
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
@ -66,14 +65,14 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================== Popular =============================== // ============================== Popular ===============================
// The site doesn't have a true popular anime tab, // The site doesn't have a true popular anime tab,
// so we use the latest added anime page instead. // so we use the latest added anime page instead.
override fun popularAnimeSelector() = latestUpdatesSelector() override fun popularAnimeParse(response: Response) = latestUpdatesParse(response)
override fun popularAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularAnimeRequest(page: Int): Request = override fun popularAnimeRequest(page: Int): Request =
GET("$baseUrl/ultimosAdicionados?page=$page") GET("$baseUrl/ultimosAdicionados?page=$page")
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector() override fun popularAnimeSelector() = TODO()
override fun popularAnimeFromElement(element: Element) = TODO()
override fun popularAnimeNextPageSelector() = TODO()
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListSelector(): String = "ul#episodesList > li.list-group-item-action > a" override fun episodeListSelector(): String = "ul#episodesList > li.list-group-item-action > a"
@ -81,13 +80,11 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListParse(response: Response) = override fun episodeListParse(response: Response) =
super.episodeListParse(response).reversed() super.episodeListParse(response).reversed()
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
return SEpisode.create().apply { val episodeName = element.text()
val episodeName = element.text() setUrlWithoutDomain(element.attr("href"))
setUrlWithoutDomain(element.attr("href")) name = episodeName
name = episodeName episode_number = episodeName.substringAfterLast(" ").toFloatOrNull() ?: 0F
episode_number = episodeName.substringAfterLast(" ").toFloatOrNull() ?: 0F
}
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
@ -123,17 +120,14 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val path = query.removePrefix(PREFIX_SEARCH_PATH) val path = query.removePrefix(PREFIX_SEARCH_PATH)
client.newCall(GET("$baseUrl/$path")) client.newCall(GET("$baseUrl/$path"))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map(::searchAnimeByPathParse)
searchAnimeByPathParse(response, path)
}
} else { } else {
super.fetchSearchAnime(page, query, filters) super.fetchSearchAnime(page, query, filters)
} }
} }
private fun searchAnimeByPathParse(response: Response, path: String): AnimesPage { private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response) val details = animeDetailsParse(response)
details.url = "/$path"
return AnimesPage(listOf(details), false) return AnimesPage(listOf(details), false)
} }
@ -171,23 +165,23 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
return SAnime.create().apply { val doc = getRealDoc(document)
val doc = getRealDoc(document) setUrlWithoutDomain(doc.location())
val infos = doc.selectFirst("div.infos_left > div.anime-info")!! val infos = doc.selectFirst("div.infos_left > div.anime-info")!!
val img = doc.selectFirst("div.infos-img > img")!! val img = doc.selectFirst("div.infos-img > img")!!
thumbnail_url = "https:" + img.attr("src") thumbnail_url = "https:" + img.attr("src")
title = img.attr("alt") title = img.attr("alt")
genre = infos.select("div.anime-genres > a") genre = infos.select("div.anime-genres > a")
.eachText() .eachText()
.joinToString() .joinToString()
author = infos.getInfo("Produtor") author = infos.getInfo("Produtor")
artist = infos.getInfo("Estúdio") artist = infos.getInfo("Estúdio")
status = parseStatus(infos.getInfo("Estado")) status = parseStatus(infos.getInfo("Estado"))
var desc = infos.selectFirst("div.anime-description")!!.text() + "\n\n" description = buildString {
desc += infos.select(">p").eachText().joinToString("\n") append(infos.selectFirst("div.anime-description")!!.text() + "\n\n")
description = desc infos.select(">p").eachText().forEach { append("$it\n") }
} }
} }
@ -198,14 +192,12 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun latestUpdatesRequest(page: Int): Request = override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/ultimosLancamentos?page=$page") GET("$baseUrl/ultimosLancamentos?page=$page")
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
return SAnime.create().apply { val img = element.selectFirst("img")!!
val img = element.selectFirst("img")!! val url = element.selectFirst("a")?.attr("href")!!
val url = element.selectFirst("a")?.attr("href")!! setUrlWithoutDomain(url)
setUrlWithoutDomain(url) title = element.selectFirst("h3")?.text()!!
title = element.selectFirst("h3")?.text()!! thumbnail_url = "https:" + img.attr("src")
thumbnail_url = "https:" + img.attr("src")
}
} }
// ============================== Settings ============================== // ============================== Settings ==============================

View File

@ -1,5 +1,7 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
ext { ext {
extName = 'Hinata Soul' extName = 'Hinata Soul'

View File

@ -15,8 +15,6 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -37,14 +35,13 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client = network.cloudflareClient
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
.add("Referer", baseUrl)
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeSelector() = "div.FsssItem:contains(Mais Vistos) > a" override fun popularAnimeSelector() = "div.FsssItem:contains(Mais Vistos) > a"
@ -58,21 +55,22 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListSelector() = "div.aniContainer a" override fun episodeListSelector() = "div.aniContainer a"
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val totalEpisodes = mutableListOf<SEpisode>()
var doc = getRealDoc(response.asJsoup()) var doc = getRealDoc(response.asJsoup())
val originalUrl = doc.location() val originalUrl = doc.location()
var pageNum = 1 var pageNum = 1
do { val totalEpisodes = buildList {
if (pageNum > 1) { do {
doc = client.newCall(GET(originalUrl + "/page/$pageNum")) if (pageNum > 1) {
.execute() doc = client.newCall(GET(originalUrl + "/page/$pageNum"))
.asJsoup() .execute()
} .asJsoup()
doc.select(episodeListSelector()).forEach { }
totalEpisodes.add(episodeFromElement(it)) doc.select(episodeListSelector()).forEach {
} add(episodeFromElement(it))
pageNum++ }
} while (hasNextPage(doc)) pageNum++
} while (hasNextPage(doc))
}
return totalEpisodes.reversed() return totalEpisodes.reversed()
} }
@ -80,8 +78,7 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val title = element.attr("title") val title = element.attr("title")
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
name = title name = title
episode_number = runCatching { title.substringAfterLast(" ").toFloat() } episode_number = title.substringAfterLast(" ").toFloatOrNull() ?: 0F
.getOrNull() ?: 0F
date_upload = element.selectFirst("div.lancaster_episodio_info_data")!! date_upload = element.selectFirst("div.lancaster_episodio_info_data")!!
.text() .text()
.toDate() .toDate()
@ -120,21 +117,14 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val slug = query.removePrefix(PREFIX_SEARCH) val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/animes/$slug")) client.newCall(GET("$baseUrl/animes/$slug"))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map(::searchAnimeBySlugParse)
searchAnimeBySlugParse(response, slug)
}
} else { } else {
client.newCall(searchAnimeRequest(page, query, filters)) super.fetchSearchAnime(page, query, filters)
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
} }
} }
private fun searchAnimeBySlugParse(response: Response, slug: String): AnimesPage { private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response) val details = animeDetailsParse(response)
details.url = "/animes/$slug"
return AnimesPage(listOf(details), false) return AnimesPage(listOf(details), false)
} }
@ -142,51 +132,53 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
GET("$baseUrl/busca?busca=$query&page=$page") GET("$baseUrl/busca?busca=$query&page=$page")
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val anime = SAnime.create()
val doc = getRealDoc(document) val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
val infos = doc.selectFirst("div.aniInfosSingle")!! val infos = doc.selectFirst("div.aniInfosSingle")!!
val img = infos.selectFirst("img")!! val img = infos.selectFirst("img")!!
anime.thumbnail_url = img.attr("src") thumbnail_url = img.attr("src")
anime.title = img.attr("alt") title = img.attr("alt")
anime.genre = infos.select("div.aniInfosSingleGeneros > span") genre = infos.select("div.aniInfosSingleGeneros > span")
.joinToString(", ") { it.text() } .eachText()
.joinToString()
anime.author = infos.getInfo("AUTOR") author = infos.getInfo("AUTOR")
anime.artist = infos.getInfo("ESTÚDIO") artist = infos.getInfo("ESTÚDIO")
anime.status = parseStatus(infos.selectFirst("div.anime_status")!!) status = parseStatus(infos.selectFirst("div.anime_status")!!)
var desc = infos.selectFirst("div.aniInfosSingleSinopse > p")!!.text() + "\n" description = buildString {
infos.getInfo("Título")?.let { desc += "\nTítulos Alternativos: $it" } append(infos.selectFirst("div.aniInfosSingleSinopse > p")!!.text() + "\n")
infos.selectFirst("div.aniInfosSingleNumsItem:contains(Ano)")?.let { infos.getInfo("Título")?.also { append("\nTítulos Alternativos: $it") }
desc += "\nAno: ${it.ownText()}" infos.selectFirst("div.aniInfosSingleNumsItem:contains(Ano)")?.also {
append("\nAno: ${it.ownText()}")
}
infos.getInfo("Temporada")?.also { append("\nTemporada: $it") }
} }
infos.getInfo("Temporada")?.let { desc += "\nTemporada: $it" }
anime.description = desc
return anime
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String? = null override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesSelector(): String = override fun latestUpdatesSelector(): String =
"div.tituloContainer:contains(lançamento) + div.epiContainer a" "div.tituloContainer:contains(lançamento) + div.epiContainer a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!! val img = element.selectFirst("img")!!
thumbnail_url = img.attr("src") thumbnail_url = img.attr("src")
title = img.attr("alt") title = img.attr("alt")
} }
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl) override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
// ============================== Settings ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREFERRED_QUALITY key = PREF_QUALITY_KEY
title = "Qualidade preferida" title = PREF_QUALITY_TITLE
entries = QUALITY_LIST entries = PREF_QUALITY_VALUES
entryValues = QUALITY_LIST entryValues = PREF_QUALITY_VALUES
setDefaultValue(QUALITY_LIST.last()) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val selected = newValue as String
@ -194,13 +186,10 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(videoQualityPref)
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun parseStatus(element: Element): Int { private fun parseStatus(element: Element): Int {
return when { return when {
element.hasClass("completed") -> SAnime.COMPLETED element.hasClass("completed") -> SAnime.COMPLETED
@ -213,26 +202,20 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val currentUrl = doc.location() val currentUrl = doc.location()
val nextUrl = doc.selectFirst("a:contains(»)")!!.attr("href") val nextUrl = doc.selectFirst("a:contains(»)")!!.attr("href")
val endings = listOf("/1", "page=1") val endings = listOf("/1", "page=1")
return !endings.any { nextUrl.endsWith(it) } && currentUrl != nextUrl return !endings.any(nextUrl::endsWith) && currentUrl != nextUrl
} }
private val animeMenuSelector = "div.controlesBoxItem > a > i.iconLista" private val animeMenuSelector = "div.controlesBoxItem > a:has(i.iconLista)"
private fun getRealDoc(document: Document): Document { private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst(animeMenuSelector) return document.selectFirst(animeMenuSelector)?.let {
if (menu != null) { client.newCall(GET(it.attr("href"), headers)).execute().asJsoup()
val originalUrl = menu.parent()!!.attr("href") } ?: document
val req = client.newCall(GET(originalUrl, headers)).execute()
return req.asJsoup()
} else {
return document
}
} }
private fun Element.getInfo(key: String): String? { private fun Element.getInfo(key: String): String? {
val div = this.selectFirst("div.aniInfosSingleInfoItem:contains($key)") return selectFirst("div.aniInfosSingleInfoItem:contains($key) span")
if (div == null) return div ?.text()
val span = div.selectFirst("span") ?.trim()
return span?.text()
} }
private fun String.toDate(): Long { private fun String.toDate(): Long {
@ -241,18 +224,10 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREFERRED_QUALITY, "FULLHD")!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val newList = mutableListOf<Video>() return sortedWith(
var preferred = 0 compareBy { it.quality.contains(quality) },
for (video in this) { ).reversed()
if (video.quality.trim() == quality) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
} }
companion object { companion object {
@ -260,9 +235,11 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
SimpleDateFormat("dd/MM/yyyy à's' HH:mm", Locale.ENGLISH) SimpleDateFormat("dd/MM/yyyy à's' HH:mm", Locale.ENGLISH)
} }
private const val PREFERRED_QUALITY = "preferred_quality"
private val QUALITY_LIST = arrayOf("SD", "HD", "FULLHD")
const val PREFIX_SEARCH = "slug:" const val PREFIX_SEARCH = "slug:"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "FULLHD"
private val PREF_QUALITY_VALUES = arrayOf("SD", "HD", "FULLHD")
} }
} }

View File

@ -1,5 +1,7 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
ext { ext {
extName = 'Meus Animes' extName = 'Meus Animes'

View File

@ -16,7 +16,7 @@ object MAFilters {
} }
private inline fun <reified R> AnimeFilterList.asQueryPart(): String { private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") { return first { it is R }.let {
(it as QueryPartFilter).toQueryPart() (it as QueryPartFilter).toQueryPart()
} }
} }

View File

@ -45,16 +45,12 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "div.ultAnisContainerItem > a" override fun popularAnimeSelector(): String = "div.ultAnisContainerItem > a"
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
return SEpisode.create().apply { element.attr("href")!!.also {
element.attr("href")!!.also { setUrlWithoutDomain(it)
setUrlWithoutDomain(it) episode_number = it.substringAfterLast("/").toFloatOrNull() ?: 0F
episode_number = try {
it.substringAfterLast("/").toFloat()
} catch (e: NumberFormatException) { 0F }
}
name = element.text()
} }
name = element.text()
} }
override fun episodeListSelector(): String = "div#aba_epi > a" override fun episodeListSelector(): String = "div#aba_epi > a"
@ -62,23 +58,21 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed() override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
return SAnime.create().apply { val infos = document.selectFirst("div.animeInfos")!!
val infos = document.selectFirst("div.animeInfos")!! val right = document.selectFirst("div.right")!!
val right = document.selectFirst("div.right")!!
setUrlWithoutDomain(document.location()) setUrlWithoutDomain(document.location())
title = right.selectFirst("h1")!!.text() title = right.selectFirst("h1")!!.text()
genre = right.select("ul.animeGen a").joinToString(", ") { it.text() } genre = right.select("ul.animeGen a").eachText().joinToString()
thumbnail_url = infos.selectFirst("img")!!.attr("data-lazy-src") thumbnail_url = infos.selectFirst("img")!!.attr("data-lazy-src")
description = right.selectFirst("div.animeSecondContainer > p:gt(0)")!!.text() description = right.selectFirst("div.animeSecondContainer > p:gt(0)")!!.text()
}
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document: Document = response.asJsoup() val document = response.asJsoup()
val videoElement = document.selectFirst("div.playerBox > *")!! val videoElement = document.selectFirst("div.playerBox > *")!!
return if (videoElement.tagName() == "video") { return if (videoElement.tagName() == "video") {
MeusAnimesExtractor(client).videoListFromElement(videoElement) MeusAnimesExtractor(client).videoListFromElement(videoElement)
@ -92,19 +86,19 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document) = throw Exception("not used") override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element) override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchAnimeSelector() = popularAnimeSelector() override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeNextPageSelector() = "div.paginacao > a.next" override fun searchAnimeNextPageSelector() = "div.paginacao > a.next"
override fun getFilterList(): AnimeFilterList = MAFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = MAFilters.FILTER_LIST
private fun searchAnimeRequest(page: Int, query: String, filters: MAFilters.FilterSearchParams): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val defaultUrl = "$baseUrl/lista-de-animes/$page" val defaultUrl = "$baseUrl/lista-de-animes/$page"
val params = MAFilters.getSearchParameters(filters)
return when { return when {
filters.letter.isNotBlank() -> GET("$defaultUrl?letra=${filters.letter}") params.letter.isNotBlank() -> GET("$defaultUrl?letra=${params.letter}")
filters.year.isNotBlank() -> GET("$defaultUrl?ano=${filters.year}") params.year.isNotBlank() -> GET("$defaultUrl?ano=${params.year}")
filters.audio.isNotBlank() -> GET("$defaultUrl?audio=${filters.audio}") params.audio.isNotBlank() -> GET("$defaultUrl?audio=${params.audio}")
filters.genre.isNotBlank() -> GET("$defaultUrl?genero=${filters.genre}") params.genre.isNotBlank() -> GET("$defaultUrl?genero=${params.genre}")
query.isNotBlank() -> GET("$defaultUrl?s=$query") query.isNotBlank() -> GET("$defaultUrl?s=$query")
else -> GET(defaultUrl) else -> GET(defaultUrl)
} }
@ -115,36 +109,26 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val id = query.removePrefix(PREFIX_SEARCH) val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/animes/$id")) client.newCall(GET("$baseUrl/animes/$id"))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map(::searchAnimeByIdParse)
searchAnimeByIdParse(response, id)
}
} else { } else {
val params = MAFilters.getSearchParameters(filters) super.fetchSearchAnime(page, query, filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
} }
} }
private fun searchAnimeByIdParse(response: Response, id: String): AnimesPage { private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup()) val details = animeDetailsParse(response.asJsoup())
details.url = "/animes/$id"
return AnimesPage(listOf(details), false) return AnimesPage(listOf(details), false)
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
return SAnime.create().apply { title = element.attr("title")
title = element.attr("title") thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src")
thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src") val epUrl = element.attr("href")
val epUrl = element.attr("href")
if (epUrl.substringAfterLast("/").toIntOrNull() != null) { if (epUrl.substringAfterLast("/").toIntOrNull() != null) {
setUrlWithoutDomain(epUrl.substringBeforeLast("/") + "-todos-os-episodios") setUrlWithoutDomain(epUrl.substringBeforeLast("/") + "-todos-os-episodios")
} else { setUrlWithoutDomain(epUrl) } } else { setUrlWithoutDomain(epUrl) }
}
} }
override fun latestUpdatesNextPageSelector(): String? = null override fun latestUpdatesNextPageSelector(): String? = null
@ -158,7 +142,7 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_QUALITY_TITLE title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_ENTRIES.last()) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val selected = newValue as String
@ -172,7 +156,7 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, "HD")!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith( return sortedWith(
compareBy { it.quality.contains(quality) }, compareBy { it.quality.contains(quality) },
).reversed() ).reversed()
@ -183,6 +167,7 @@ class MeusAnimes : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val PREF_QUALITY_KEY = "pref_quality" private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida" private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "HD"
private val PREF_QUALITY_ENTRIES = arrayOf("SD", "HD") private val PREF_QUALITY_ENTRIES = arrayOf("SD", "HD")
} }
} }

View File

@ -19,10 +19,6 @@ class MeusAnimesUrlActivity : Activity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) { if (pathSegments != null && pathSegments.size > 1) {
// https://<host>/<segment 0>/<segment 1>...
// ex: pattern "/animes/..*" -> pathSegments[1]
// ex: pattern "/animes/info/..*" -> pathSegments[2]
// etc..
val item = pathSegments[1] val item = pathSegments[1]
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH" action = "eu.kanade.tachiyomi.ANIMESEARCH"

View File

@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.net.ProtocolException
class MeusAnimesExtractor(private val client: OkHttpClient) { class MeusAnimesExtractor(private val client: OkHttpClient) {
@ -13,14 +12,15 @@ class MeusAnimesExtractor(private val client: OkHttpClient) {
fun videoListFromElement(element: Element): List<Video> { fun videoListFromElement(element: Element): List<Video> {
val headers = Headers.headersOf("Range", "bytes=0-1") val headers = Headers.headersOf("Range", "bytes=0-1")
val urls = mutableMapOf<String, String>().apply { val urls = buildMap {
val sdUrl = element.attr("src") val sdUrl = element.attr("src")
put("SD", sdUrl) put("SD", sdUrl)
val hdUrl = sdUrl.replace("/sd/", "/hd/") val hdUrl = sdUrl.replace("/sd/", "/hd/")
try { runCatching {
val testIt = client.newCall(head(hdUrl, headers)).execute() // Check if the url is playing
client.newCall(head(hdUrl, headers)).execute()
put("HD", hdUrl) put("HD", hdUrl)
} catch (e: ProtocolException) {} }
} }
return urls.map { (quality, url) -> Video(url, quality, url) } return urls.map { (quality, url) -> Video(url, quality, url) }
} }

View File

@ -1,6 +1,8 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' alias(libs.plugins.android.application)
apply plugin: 'kotlinx-serialization' alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
ext { ext {
extName = 'Vizer.tv' extName = 'Vizer.tv'
@ -12,8 +14,8 @@ ext {
} }
dependencies { dependencies {
implementation(project(':lib-mixdrop-extractor'))
implementation(project(':lib-streamtape-extractor')) implementation(project(':lib-streamtape-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchItemDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchResultDto import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchResultDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoDto import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoLanguagesDto import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoLanguagesDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.extractors.MixDropExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
@ -35,6 +35,7 @@ import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class Vizer : ConfigurableAnimeSource, AnimeHttpSource() { class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
@ -49,16 +50,13 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val json = Json { private val json: Json by injectLazy()
ignoreUnknownKeys = true
}
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request {
val pageType = preferences.getString(PREF_POPULAR_PAGE_KEY, PREF_POPULAR_PAGE_DEFAULT)!! val pageType = preferences.getString(PREF_POPULAR_PAGE_KEY, PREF_POPULAR_PAGE_DEFAULT)!!
val params = FilterSearchParams( val params = FilterSearchParams(
@ -71,41 +69,39 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val result = response.parseAs<SearchResultDto>() val result = response.parseAs<SearchResultDto>()
val animes = result.list.map(::animeFromObject).toList() val animes = result.list.map(::animeFromObject)
val hasNext = result.quantity == 35 val hasNext = result.quantity == 35
return AnimesPage(animes, hasNext) return AnimesPage(animes, hasNext)
} }
private fun animeFromObject(item: SearchItemDto): SAnime = private fun animeFromObject(item: SearchItemDto) = SAnime.create().apply {
SAnime.create().apply { val (urlslug, imgslug) = when {
var slug = if (item.status.isBlank()) "filme" else "serie" item.status.isBlank() -> Pair("filme", "movies")
url = "/$slug/online/${item.url}" else -> Pair("serie", "series")
slug = if (slug == "filme") "movies" else "series"
title = item.title
status = when (item.status) {
"Retornando" -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
thumbnail_url = "$baseUrl/content/$slug/posterPt/342/${item.id}.webp"
} }
url = "/$urlslug/online/${item.url}"
title = item.title
status = when (item.status) {
"Retornando" -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
thumbnail_url = "$baseUrl/content/$imgslug/posterPt/342/${item.id}.webp"
}
// ============================== Episodes ============================== // ============================== Episodes ==============================
private fun getSeasonEps(seasonElement: Element): List<SEpisode> { private fun getSeasonEps(seasonElement: Element): List<SEpisode> {
val id = seasonElement.attr("data-season-id") val id = seasonElement.attr("data-season-id")
val sname = seasonElement.text() val sname = seasonElement.text()
val response = client.newCall(apiRequest("getEpisodes=$id")).execute() val response = client.newCall(apiRequest("getEpisodes=$id")).execute()
val episodes = response.parseAs<EpisodeListDto>().episodes.mapNotNull { val episodes = response.parseAs<EpisodeListDto>().episodes
if (it.released) { .filter { it.released }
.map {
SEpisode.create().apply { SEpisode.create().apply {
name = "Temp $sname: Ep ${it.name} - ${it.title}" name = "Temp $sname: Ep ${it.name} - ${it.title}"
episode_number = it.name.toFloatOrNull() ?: 0F episode_number = it.name.toFloatOrNull() ?: 0F
url = it.id url = it.id
} }
} else {
null
} }
}
return episodes return episodes
} }
@ -126,7 +122,6 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request { override fun videoListRequest(episode: SEpisode): Request {
val url = episode.url val url = episode.url
return if (url.startsWith("https")) { return if (url.startsWith("https")) {
@ -161,9 +156,7 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
val url = getPlayerUrl(videoObj.id, name) val url = getPlayerUrl(videoObj.id, name)
when (name) { when (name) {
"mixdrop" -> "mixdrop" ->
MixDropExtractor(client) MixDropExtractor(client).videoFromUrl(url, langPrefix)
.videoFromUrl(url, langPrefix)
?.let(::listOf)
"streamtape" -> "streamtape" ->
StreamTapeExtractor(client) StreamTapeExtractor(client)
.videoFromUrl(url, "StreamTape($langPrefix)") .videoFromUrl(url, "StreamTape($langPrefix)")
@ -175,7 +168,6 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// =============================== Search =============================== // =============================== Search ===============================
override fun getFilterList(): AnimeFilterList = VizerFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = VizerFilters.FILTER_LIST
override fun searchAnimeParse(response: Response) = popularAnimeParse(response) override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
@ -185,17 +177,14 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
val path = query.removePrefix(PREFIX_SEARCH).replace("/", "/online/") val path = query.removePrefix(PREFIX_SEARCH).replace("/", "/online/")
client.newCall(GET("$baseUrl/$path")) client.newCall(GET("$baseUrl/$path"))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map(::searchAnimeByPathParse)
searchAnimeByPathParse(response, path)
}
} else { } else {
super.fetchSearchAnime(page, query, filters) super.fetchSearchAnime(page, query, filters)
} }
} }
private fun searchAnimeByPathParse(response: Response, path: String): AnimesPage { private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response) val details = animeDetailsParse(response)
details.url = "/" + path
return AnimesPage(listOf(details), false) return AnimesPage(listOf(details), false)
} }
@ -230,23 +219,22 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
override fun animeDetailsParse(response: Response): SAnime {
val doc = response.asJsoup() val doc = response.asJsoup()
return SAnime.create().apply { setUrlWithoutDomain(doc.location())
title = doc.selectFirst("section.ai > h2")!!.text() title = doc.selectFirst("section.ai > h2")!!.text()
thumbnail_url = doc.selectFirst("meta[property=og:image]")!!.attr("content") thumbnail_url = doc.selectFirst("meta[property=og:image]")!!.attr("content")
var desc = doc.selectFirst("span.desc")!!.text() + "\n"
doc.selectFirst("div.year")?.let { desc += "\nAno: ${it.text()}" } description = buildString {
doc.selectFirst("div.tm")?.let { desc += "\nDuração: ${it.text()}" } append(doc.selectFirst("span.desc")!!.text() + "\n")
doc.selectFirst("a.rating")?.let { desc += "\nNota: ${it.text()}" } doc.selectFirst("div.year")?.let { append("\nAno: ${it.text()}") }
description = desc doc.selectFirst("div.tm")?.let { append("\nDuração: ${it.text()}") }
doc.selectFirst("a.rating")?.let { append("\nNota: ${it.text()}") }
} }
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = apiRequest("getHomeSliderSeries=1")
override fun latestUpdatesRequest(page: Int): Request = apiRequest("getHomeSliderSeries=1")
override fun latestUpdatesParse(response: Response): AnimesPage { override fun latestUpdatesParse(response: Response): AnimesPage {
val parsedData = response.parseAs<SearchResultDto>() val parsedData = response.parseAs<SearchResultDto>()
@ -255,7 +243,6 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// ============================== Settings ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val popularPage = ListPreference(screen.context).apply { val popularPage = ListPreference(screen.context).apply {
key = PREF_POPULAR_PAGE_KEY key = PREF_POPULAR_PAGE_KEY
@ -308,7 +295,6 @@ class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun getPlayerUrl(id: String, name: String): String { private fun getPlayerUrl(id: String, name: String): String {
val req = GET("$baseUrl/embed/getPlay.php?id=$id&sv=$name") 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().body.string()

View File

@ -17,11 +17,11 @@ object VizerFilters {
} }
private inline fun <reified R> AnimeFilterList.getFirst(): R { private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first() return first { it is R } as R
} }
private inline fun <reified R> AnimeFilterList.asQueryPart(): String { private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.getFirst<R>().let { return getFirst<R>().let {
(it as QueryPartFilter).toQueryPart() (it as QueryPartFilter).toQueryPart()
} }
} }

View File

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.vizer.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
class MixDropExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String, lang: String = ""): Video? {
val doc = client.newCall(GET(url)).execute().asJsoup()
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
?.data()
?.let { JsUnpacker.unpackAndCombine(it) }
?: return null
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
.substringBefore("\"")
val quality = ("MixDrop").let {
if (lang.isNotBlank()) {
"$it($lang)"
} else {
it
}
}
return Video(url, quality, videoUrl)
}
}