diff --git a/src/pt/animesgames/AndroidManifest.xml b/src/pt/animesgames/AndroidManifest.xml new file mode 100644 index 000000000..1c467da2a --- /dev/null +++ b/src/pt/animesgames/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/animesgames/build.gradle b/src/pt/animesgames/build.gradle new file mode 100644 index 000000000..c228981e5 --- /dev/null +++ b/src/pt/animesgames/build.gradle @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) +} + +ext { + extName = 'Animes Games' + pkgNameSuffix = 'pt.animesgames' + extClass = '.AnimesGames' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/animesgames/res/mipmap-hdpi/ic_launcher.png b/src/pt/animesgames/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..f20822d1a Binary files /dev/null and b/src/pt/animesgames/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/animesgames/res/mipmap-mdpi/ic_launcher.png b/src/pt/animesgames/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ae8a62c90 Binary files /dev/null and b/src/pt/animesgames/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/animesgames/res/mipmap-xhdpi/ic_launcher.png b/src/pt/animesgames/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d31319893 Binary files /dev/null and b/src/pt/animesgames/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/animesgames/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/animesgames/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..1af79ea0b Binary files /dev/null and b/src/pt/animesgames/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/animesgames/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/animesgames/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5bca0c0e0 Binary files /dev/null and b/src/pt/animesgames/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/animesgames/src/eu/kanade/tachiyomi/animeextension/pt/animesgames/AnimesGames.kt b/src/pt/animesgames/src/eu/kanade/tachiyomi/animeextension/pt/animesgames/AnimesGames.kt new file mode 100644 index 000000000..29a899c91 --- /dev/null +++ b/src/pt/animesgames/src/eu/kanade/tachiyomi/animeextension/pt/animesgames/AnimesGames.kt @@ -0,0 +1,282 @@ +package eu.kanade.tachiyomi.animeextension.pt.animesgames + +import eu.kanade.tachiyomi.animeextension.pt.animesgames.extractors.BloggerExtractor +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.AnimesPage +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class AnimesGames : ParsedAnimeHttpSource() { + + override val name = "Animes Games" + + override val baseUrl = "https://animesgames.net" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", baseUrl) + .add("Origin", baseUrl) + + private val json: Json by injectLazy() + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET(baseUrl) + + override fun popularAnimeSelector() = "ul.top10 > li > a" + + override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.text() + } + + override fun popularAnimeNextPageSelector() = null + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page") + + override fun latestUpdatesSelector() = "div.conteudo section.episodioItem > a" + + override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.selectFirst("div.tituloEP")!!.text() + thumbnail_url = element.selectFirst("img")!!.attr("data-lazy-src") + } + + override fun latestUpdatesNextPageSelector() = "ol.pagination > a:contains(>)" + + // =============================== Search =============================== + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/animes/$id")) + .asObservableSuccess() + .map(::searchAnimeByIdParse) + } else { + super.fetchSearchAnime(page, query, filters) + } + } + + private fun searchAnimeByIdParse(response: Response): AnimesPage { + val details = animeDetailsParse(response.asJsoup()) + return AnimesPage(listOf(details), false) + } + + @Serializable + data class SearchResponseDto( + val results: List, + val page: Int, + val total_page: Int = 1, + ) + + private val searchToken by lazy { + client.newCall(GET("$baseUrl/lista-de-animes", headers)).execute() + .use { it.asJsoup() } + .selectFirst("div.menu_filter_box")!! + .attr("data-secury") + } + + override fun getFilterList() = AnimesGamesFilters.FILTER_LIST + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val params = AnimesGamesFilters.getSearchParameters(filters) + val body = FormBody.Builder().apply { + add("pagina", "$page") + add("type", "lista") + add("type_url", "anime") + add("limit", "30") + add("token", searchToken) + add("search", query.ifBlank { "0" }) + val filterData = baseUrl.toHttpUrl().newBuilder().apply { + addQueryParameter("filter_audio", params.audio) + addQueryParameter("filter_letter", params.letter) + addQueryParameter("filter_order", params.orderBy) + addQueryParameter("filter_sort", "abc") + }.build().encodedQuery + + val genres = params.genres.joinToString { "\"$it\"" } + val delgenres = params.deleted_genres.joinToString { "\"$it\"" } + + add("filters", """{"filter_data": "$filterData", "filter_genre_add": [$genres], "filter_genre_del": [$delgenres]}""") + }.build() + + return POST("$baseUrl/func/listanime", body = body, headers = headers) + } + + override fun searchAnimeParse(response: Response): AnimesPage { + return runCatching { + val data = response.parseAs() + val animes = data.results.map(Jsoup::parse) + .mapNotNull { it.selectFirst(searchAnimeSelector()) } + .map(::searchAnimeFromElement) + val hasNext = data.total_page > data.page + AnimesPage(animes, hasNext) + }.getOrElse { AnimesPage(emptyList(), false) } + } + + override fun searchAnimeSelector() = "section.animeItem > a" + + override fun searchAnimeFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.selectFirst("div.tituloAnime")!!.text() + thumbnail_url = element.selectFirst("img")!!.attr("src") + } + + override fun searchAnimeNextPageSelector(): String? { + throw UnsupportedOperationException("Not used.") + } + + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document) = SAnime.create().apply { + val doc = getRealDoc(document) + setUrlWithoutDomain(doc.location()) + val content = doc.selectFirst("section.conteudoPost")!! + title = content.selectFirst("section > h1")!!.text() + .removePrefix("Assistir ") + .removeSuffix("Temporada Online") + thumbnail_url = content.selectFirst("img")!!.attr("data-lazy-src") + description = content.select("section.sinopseEp p").eachText().joinToString("\n") + + val infos = content.selectFirst("div.info > ol")!! + + author = infos.getInfo("Autor") ?: infos.getInfo("Diretor") + artist = infos.getInfo("Estúdio") + status = when (infos.getInfo("Status")) { + "Completo" -> SAnime.COMPLETED + "Lançamento" -> SAnime.ONGOING + else -> SAnime.UNKNOWN + } + } + + private fun Element.getInfo(info: String) = + selectFirst("li:has(span:contains($info))")?.let { + it.selectFirst("span[data]")?.text() ?: it.ownText() + } + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response): List { + return getRealDoc(response.use { it.asJsoup() }) + .select(episodeListSelector()) + .map(::episodeFromElement) + .reversed() + } + + override fun episodeListSelector() = "div.listaEp > section.episodioItem > a" + + override fun episodeFromElement(element: Element) = SEpisode.create().apply { + setUrlWithoutDomain(element.attr("href")) + element.selectFirst("div.tituloEP")!!.text().also { + name = it + episode_number = it.substringAfterLast(" ").toFloatOrNull() ?: 1F + } + date_upload = element.selectFirst("span.data")?.text()?.toDate() ?: 0L + } + + // ============================ Video Links ============================= + private val bloggerExtractor by lazy { BloggerExtractor(client) } + override fun videoListParse(response: Response): List