fix(pt/animestc): Fix search & extractor (#2731)

This commit is contained in:
Claudemirovsky
2024-01-13 06:42:24 -03:00
committed by GitHub
parent e7483f0671
commit e649535595
5 changed files with 182 additions and 194 deletions

View File

@ -8,7 +8,7 @@ ext {
extName = 'AnimesTC' extName = 'AnimesTC'
pkgNameSuffix = 'pt.animestc' pkgNameSuffix = 'pt.animestc'
extClass = '.AnimesTC' extClass = '.AnimesTC'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
} }

View File

@ -1,10 +1,7 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc 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
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
object ATCFilters { object ATCFilters {
open class QueryPartFilter( open class QueryPartFilter(
@ -17,172 +14,161 @@ object ATCFilters {
fun toQueryPart() = vals[state].second fun toQueryPart() = vals[state].second
} }
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 { private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (first { it is R } as QueryPartFilter).toQueryPart() return (first { it is R } as QueryPartFilter).toQueryPart()
} }
private inline fun <reified R> AnimeFilterList.parseTriFilter(): List<List<String>> { class TypeFilter : QueryPartFilter("Tipo", ATCFiltersData.TYPES)
return (first { it is R } as TriStateFilterList).state class YearFilter : QueryPartFilter("Ano", ATCFiltersData.YEARS)
.filterNot { it.isIgnored() } class GenreFilter : QueryPartFilter("Gênero", ATCFiltersData.GENRES)
.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)
class StatusFilter : QueryPartFilter("Status", ATCFiltersData.STATUS) class StatusFilter : QueryPartFilter("Status", ATCFiltersData.STATUS)
class SortFilter : AnimeFilter.Sort(
"Ordenar",
ATCFiltersData.ORDERS.map { it.first }.toTypedArray(),
Selection(0, true),
)
class GenresFilter : TriStateFilterList(
"Gêneros",
ATCFiltersData.GENRES.map(::TriStateVal),
)
val FILTER_LIST get() = AnimeFilterList( val FILTER_LIST get() = AnimeFilterList(
InitialLetterFilter(), TypeFilter(),
YearFilter(),
GenreFilter(),
StatusFilter(), StatusFilter(),
SortFilter(),
AnimeFilter.Separator(),
GenresFilter(),
) )
data class FilterSearchParams( data class FilterSearchParams(
val initialLetter: String = "", val type: String = "series",
val year: String = "",
val genre: String = "",
val status: String = "", val status: String = "",
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 { internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams() if (filters.isEmpty()) return 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( return FilterSearchParams(
filters.asQueryPart<InitialLetterFilter>(), filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<GenreFilter>(),
filters.asQueryPart<StatusFilter>(), filters.asQueryPart<StatusFilter>(),
ascending,
orderBy,
includedGenres,
excludedGenres,
) )
} }
private fun mustRemove(anime: AnimeDto, params: FilterSearchParams): Boolean {
return when {
params.animeName != "" && !anime.title.contains(params.animeName, true) -> true
params.initialLetter != "" && !anime.title.lowercase().startsWith(params.initialLetter) -> true
params.blackListedGenres.size > 0 && params.blackListedGenres.any {
anime.genres.contains(it, true)
} -> true
params.includedGenres.size > 0 && params.includedGenres.any {
!anime.genres.contains(it, true)
} -> true
params.status != "" && anime.status != SAnime.UNKNOWN && anime.status != params.status.toInt() -> true
else -> false
}
}
private inline fun <T, R : Comparable<R>> List<T>.sortedByIf(
isAscending: Boolean,
crossinline selector: (T) -> R,
): List<T> {
return when {
isAscending -> sortedBy(selector)
else -> sortedByDescending(selector)
}
}
fun List<AnimeDto>.applyFilterParams(params: FilterSearchParams): List<AnimeDto> {
return filterNot { mustRemove(it, params) }.let { results ->
when (params.sortBy) {
"A-Z" -> results.sortedByIf(params.orderAscending) { it.title.lowercase() }
"year" -> results.sortedByIf(params.orderAscending) { it.year ?: 0 }
else -> results
}
}
}
private object ATCFiltersData { private object ATCFiltersData {
val TYPES = arrayOf(
val ORDERS = arrayOf( Pair("Anime", "series"),
Pair("Alfabeticamente", "A-Z"), Pair("Filme", "movie"),
Pair("Por ano", "year"), Pair("OVA", "ova"),
) )
val SELECT = Pair("Selecione", "")
val STATUS = arrayOf( val STATUS = arrayOf(
Pair("Selecione", ""), SELECT,
Pair("Completo", SAnime.COMPLETED.toString()), Pair("Cancelado", "canceled"),
Pair("Em Lançamento", SAnime.ONGOING.toString()), Pair("Completo", "complete"),
Pair("Em Lançamento", "airing"),
Pair("Pausado", "onhold"),
) )
val INITIAL_LETTER = arrayOf(Pair("Selecione", "")) + ('A'..'Z').map { val YEARS = arrayOf(SELECT) + (1997..2024).map {
Pair(it.toString(), it.toString().lowercase()) Pair(it.toString(), it.toString())
}.toTypedArray() }.toTypedArray()
val GENRES = arrayOf( val GENRES = arrayOf(
"Ação", SELECT,
"Action", Pair("Ação", "acao"),
"Adventure", Pair("Action", "action"),
"Artes Marciais", Pair("Adventure", "adventure"),
"Aventura", Pair("Artes Marciais", "artes-marciais"),
"Carros", Pair("Artes Marcial", "artes-marcial"),
"Comédia", Pair("Aventura", "aventura"),
"Comédia Romântica", Pair("Beisebol", "beisebol"),
"Demônios", Pair("Boys Love", "boys-love"),
"Drama", Pair("Comédia", "comedia"),
"Ecchi", Pair("Comédia Romântica", "comedia-romantica"),
"Escolar", Pair("Comedy", "comedy"),
"Esporte", Pair("Crianças", "criancas"),
"Fantasia", Pair("Culinária", "culinaria"),
"Historical", Pair("Cyberpunk", "cyberpunk"),
"Histórico", Pair("Demônios", "demonios"),
"Horror", Pair("Distopia", "distopia"),
"Jogos", Pair("Documentário", "documentario"),
"Kids", Pair("Drama", "drama"),
"Live Action", Pair("Ecchi", "ecchi"),
"Magia", Pair("Escola", "escola"),
"Mecha", Pair("Escolar", "escolar"),
"Militar", Pair("Espaço", "espaco"),
"Mistério", Pair("Esporte", "esporte"),
"Psicológico", Pair("Esportes", "esportes"),
"Romance", Pair("Fantasia", "fantasia"),
"Samurai", Pair("Ficção Científica", "ficcao-cientifica"),
"School Life", Pair("Futebol", "futebol"),
"Sci-Fi", // Yeah Pair("Game", "game"),
"SciFi", Pair("Girl battleships", "girl-battleships"),
"Seinen", Pair("Gourmet", "gourmet"),
"Shoujo", Pair("Gundam", "gundam"),
"Shounen", Pair("Harém", "harem"),
"Sobrenatural", Pair("Hentai", "hentai"),
"Super Poder", Pair("Historia", "historia"),
"Supernatural", Pair("Historial", "historial"),
"Terror", Pair("Historical", "historical"),
"Tragédia", Pair("Histórico", "historico"),
"Vampiro", Pair("Horror", "horror"),
"Vida Escolar", Pair("Humor Negro", "humor-negro"),
Pair("Ídolo", "idolo"),
Pair("Infantis", "infantis"),
Pair("Investigação", "investigacao"),
Pair("Isekai", "isekai"),
Pair("Jogo", "jogo"),
Pair("Jogos", "jogos"),
Pair("Josei", "josei"),
Pair("Kids", "kids"),
Pair("Luta", "luta"),
Pair("Maduro", "maduro"),
Pair("Máfia", "mafia"),
Pair("Magia", "magia"),
Pair("Mágica", "magica"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Militares", "militares"),
Pair("Mistério", "misterio"),
Pair("Música", "musica"),
Pair("Musical", "musical"),
Pair("Não Informado!", "nao-informado"),
Pair("Paródia", "parodia"),
Pair("Piratas", "piratas"),
Pair("Polícia", "policia"),
Pair("Policial", "policial"),
Pair("Político", "politico"),
Pair("Pós-Apocalíptico", "pos-apocaliptico"),
Pair("Psico", "psico"),
Pair("Psicológico", "psicologico"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Samurais", "samurais"),
Pair("Sátiro", "satiro"),
Pair("School Life", "school-life"),
Pair("SciFi", "scifi"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shotacon", "shotacon"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounem", "shounem"),
Pair("Shounen", "shounen"),
Pair("Shounen-ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Space", "space"),
Pair("Supernatural", "supernatural"),
Pair("Super Poder", "super-poder"),
Pair("Super-Poderes", "super-poderes"),
Pair("Suspense", "suspense"),
Pair("tear-studio", "tear-studio"),
Pair("Terror", "terror"),
Pair("Thriller", "thriller"),
Pair("Tragédia", "tragedia"),
Pair("Vampiro", "vampiro"),
Pair("Vampiros", "vampiros"),
Pair("Vida Escolar", "vida-escolar"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Zombie", "zombie"),
) )
} }
} }

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.animeextension.pt.animestc
import android.app.Application import android.app.Application
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animestc.ATCFilters.applyFilterParams
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.AnimeDto import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.AnimeDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.EpisodeDto import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.ResponseDto import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.ResponseDto
@ -26,7 +25,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll 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.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
@ -35,7 +34,6 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy 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
class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() { class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
@ -87,11 +85,24 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
TODO("Not yet implemented") val params = ATCFilters.getSearchParameters(filters)
val url = "$baseUrl/series?order=title&direction=asc&page=$page".toHttpUrl()
.newBuilder()
.addQueryParameter("type", params.type)
.addQueryParameter("search", query)
.addQueryParameter("year", params.year)
.addQueryParameter("releaseStatus", params.status)
.addQueryParameter("tag", params.genre)
.build()
return GET(url.toString(), headers)
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
TODO("Not yet implemented") val data = response.parseAs<ResponseDto<AnimeDto>>()
val animes = data.items.map(::searchAnimeFromObject)
val hasNextPage = data.lastPage > data.page
return AnimesPage(animes, hasNextPage)
} }
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> { override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
@ -101,36 +112,12 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
.asObservableSuccess() .asObservableSuccess()
.map(::searchAnimeBySlugParse) .map(::searchAnimeBySlugParse)
} else { } else {
return Observable.just(searchAnime(page, query, filters)) return super.fetchSearchAnime(page, query, filters)
} }
} }
private val allAnimesList by lazy {
val cache = CacheControl.Builder().maxAge(1, DAYS).build()
listOf("movie", "ova", "series").map { type ->
val url = "$baseUrl/series?order=title&direction=asc&page=1&full=true&type=$type"
val response = client.newCall(GET(url, cache = cache)).execute()
response.parseAs<ResponseDto<AnimeDto>>().items
}.flatten()
}
override fun getFilterList(): AnimeFilterList = ATCFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = ATCFilters.FILTER_LIST
private fun searchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
val params = ATCFilters.getSearchParameters(filters).apply {
animeName = query
}
val filtered = allAnimesList.applyFilterParams(params)
val results = filtered.chunked(30)
val hasNextPage = results.size > page
val currentPage = if (results.size == 0) {
emptyList<SAnime>()
} else {
results.get(page - 1).map(::searchAnimeFromObject)
}
return AnimesPage(currentPage, hasNextPage)
}
private fun searchAnimeFromObject(anime: AnimeDto) = SAnime.create().apply { private fun searchAnimeFromObject(anime: AnimeDto) = SAnime.create().apply {
thumbnail_url = anime.cover.url thumbnail_url = anime.cover.url
title = anime.title title = anime.title
@ -190,32 +177,43 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================ Video Links ============================= // ============================ Video Links =============================
private val anonFilesExtractor by lazy { AnonFilesExtractor(client) } private val anonFilesExtractor by lazy { AnonFilesExtractor(client) }
private val sendcmExtractor by lazy { SendcmExtractor(client) } private val sendcmExtractor by lazy { SendcmExtractor(client) }
private val linkBypasser by lazy { LinkBypasser(client, json) }
private val supportedPlayers = listOf("anonfiles", "send")
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val videoDto = response.parseAs<ResponseDto<VideoDto>>().items.first() val videoDto = response.parseAs<ResponseDto<VideoDto>>().items.first()
val links = videoDto.links val links = videoDto.links
val allLinks = listOf(links.low, links.medium, links.high).flatten() val allLinks = listOf(links.low, links.medium, links.high).flatten()
val supportedPlayers = listOf("anonfiles", "send") .filter { it.name in supportedPlayers }
val online = links.online?.run { val online = links.online?.run {
filterNot { "mega" in it }.map { filterNot { "mega" in it }.map {
Video(it, "Player ATC", it, headers) Video(it, "Player ATC", it, headers)
} }
}.orEmpty() }.orEmpty()
return online + allLinks.filter { it.name in supportedPlayers }.parallelMap {
val playerUrl = LinkBypasser(client, json).bypass(it, videoDto.id) val videoId = videoDto.id
?: return@parallelMap null
val quality = when (it.quality) { return online + allLinks.parallelCatchingFlatMap { extractVideosFromLink(it, videoId) }
"low" -> "SD" }
"medium" -> "HD"
"high" -> "FULLHD" private fun extractVideosFromLink(video: VideoDto.VideoLink, videoId: Int): List<Video> {
else -> "SD" val playerUrl = linkBypasser.bypass(video, videoId)
} ?: return emptyList()
when (it.name) {
"anonfiles" -> anonFilesExtractor.videoFromUrl(playerUrl, quality) val quality = when (video.quality) {
"send" -> sendcmExtractor.videoFromUrl(playerUrl, quality) "low" -> "SD"
else -> null "medium" -> "HD"
} "high" -> "FULLHD"
}.filterNotNull() else -> "SD"
}
return when (video.name) {
"anonfiles" -> anonFilesExtractor.videosFromUrl(playerUrl, quality)
"send" -> sendcmExtractor.videosFromUrl(playerUrl, quality)
else -> emptyList()
}
} }
// ============================== Settings ============================== // ============================== Settings ==============================
@ -254,9 +252,13 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> = private inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
runBlocking { runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map {
async(Dispatchers.Default) {
runCatching { f(it) }.getOrElse { emptyList() }
}
}.awaitAll().flatten()
} }
private fun Response.getAnimeDto(): AnimeDto { private fun Response.getAnimeDto(): AnimeDto {

View File

@ -8,11 +8,11 @@ import okhttp3.OkHttpClient
class AnonFilesExtractor(private val client: OkHttpClient) { class AnonFilesExtractor(private val client: OkHttpClient) {
private val playerName = "AnonFiles" private val playerName = "AnonFiles"
fun videoFromUrl(url: String, quality: String): Video? { fun videosFromUrl(url: String, quality: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup() val doc = client.newCall(GET(url)).execute().asJsoup()
val downloadUrl = doc.selectFirst("a#download-url")?.attr("href") val downloadUrl = doc.selectFirst("a#download-url")?.attr("href")
return downloadUrl?.let { return downloadUrl?.let {
Video(it, "$playerName - $quality", it) listOf(Video(it, "$playerName - $quality", it))
} }.orEmpty()
} }
} }

View File

@ -9,12 +9,12 @@ import okhttp3.OkHttpClient
class SendcmExtractor(private val client: OkHttpClient) { class SendcmExtractor(private val client: OkHttpClient) {
private val playerName = "Sendcm" private val playerName = "Sendcm"
fun videoFromUrl(url: String, quality: String): Video? { fun videosFromUrl(url: String, quality: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup() val doc = client.newCall(GET(url)).execute().use { it.asJsoup() }
val videoUrl = doc.selectFirst("video#vjsplayer > source")?.attr("src") val videoUrl = doc.selectFirst("video#vjsplayer > source")?.attr("src")
return videoUrl?.let { return videoUrl?.let {
val headers = Headers.headersOf("Referer", url) val headers = Headers.headersOf("Referer", url)
Video(it, "$playerName - $quality", it, headers = headers) listOf(Video(it, "$playerName - $quality", it, headers = headers))
} }.orEmpty()
} }
} }