From 86ea37c3a53f36b01ad36f5f14c600f6d1e49959 Mon Sep 17 00:00:00 2001 From: Chopper <156493704+choppeh@users.noreply.github.com> Date: Sun, 18 May 2025 02:34:42 -0300 Subject: [PATCH] AnisaManga: Migrate source (#8857) Migrate from Madara to Etoshore --- src/tr/anisamanga/build.gradle | 4 +- .../extension/tr/anisamanga/AnisaManga.kt | 284 +++++++++++++++++- 2 files changed, 283 insertions(+), 5 deletions(-) diff --git a/src/tr/anisamanga/build.gradle b/src/tr/anisamanga/build.gradle index 44a2a7ec1..811746d75 100644 --- a/src/tr/anisamanga/build.gradle +++ b/src/tr/anisamanga/build.gradle @@ -1,9 +1,7 @@ ext { extName = 'Anisa Manga' extClass = '.AnisaManga' - themePkg = 'madara' - baseUrl = 'https://anisamanga.com' - overrideVersionCode = 0 + extVersionCode = 43 isNsfw = true } diff --git a/src/tr/anisamanga/src/eu/kanade/tachiyomi/extension/tr/anisamanga/AnisaManga.kt b/src/tr/anisamanga/src/eu/kanade/tachiyomi/extension/tr/anisamanga/AnisaManga.kt index bb530f3e7..b868f4a98 100644 --- a/src/tr/anisamanga/src/eu/kanade/tachiyomi/extension/tr/anisamanga/AnisaManga.kt +++ b/src/tr/anisamanga/src/eu/kanade/tachiyomi/extension/tr/anisamanga/AnisaManga.kt @@ -1,5 +1,285 @@ package eu.kanade.tachiyomi.extension.tr.anisamanga -import eu.kanade.tachiyomi.multisrc.madara.Madara +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import okio.IOException +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable -class AnisaManga : Madara("Anisa Manga", "https://anisamanga.com", "tr") +// Etoshore +class AnisaManga : ParsedHttpSource() { + + override val name = "Anisa Manga" + + override val baseUrl = "https://anisamanga.net" + + override val lang = "tr" + + override val supportsLatest = true + + // Migrate from Madara to Etoshore + override val versionId = 2 + + override val client = super.client.newBuilder() + .rateLimit(2) + .build() + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + // ============================== Popular ============================== + + open val popularFilter = FilterList( + SelectionList("", listOf(Tag(value = "views", query = "sort"))), + ) + + override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter) + + override fun popularMangaParse(response: Response) = searchMangaParse(response) + + override fun popularMangaSelector() = throw UnsupportedOperationException() + + override fun popularMangaNextPageSelector() = throw UnsupportedOperationException() + + override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException() + + // ============================== Latest =============================== + + open val latestFilter = FilterList( + SelectionList("", listOf(Tag(value = "date", query = "sort"))), + ) + + override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter) + + override fun latestUpdatesParse(response: Response) = searchMangaParse(response) + + override fun latestUpdatesSelector() = throw UnsupportedOperationException() + + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException() + + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException() + + // ============================== Search =============================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/page/$page".toHttpUrl().newBuilder() + .addQueryParameter("s", query) + filters.forEach { filter -> + when (filter) { + is SelectionList -> { + val selected = filter.selected().takeIf { it.value.isNotBlank() } + ?: return@forEach + url.addQueryParameter(selected.query, selected.value) + } + else -> {} + } + } + return GET(url.build(), headers) + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + if (query.startsWith(PREFIX_SEARCH)) { + val slug = query.substringAfter(PREFIX_SEARCH) + return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" }) + .map { manga -> MangasPage(listOf(manga), false) } + } + + return super.fetchSearchManga(page, query, filters) + } + + override fun searchMangaSelector() = ".search-posts .chapter-box .poster a" + + override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)" + + override fun searchMangaFromElement(element: Element) = SManga.create().apply { + title = element.attr("title") + thumbnail_url = element.selectFirst("img")?.let(::imageFromElement) + setUrlWithoutDomain(element.absUrl("href")) + } + + override fun searchMangaParse(response: Response): MangasPage { + if (filterList.isEmpty()) { + filterParse(response) + } + return super.searchMangaParse(response) + } + + // ============================== Details =============================== + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.selectFirst("h1")!!.text() + + description = document.selectFirst(".excerpt p")?.text() + + document.selectFirst(".details-right-con img")?.let { thumbnail_url = imageFromElement(it) } + + genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a") + .joinToString { it.text() } + + author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a") + + ?.text() + + document.selectFirst(".status")?.text()?.let { + status = it.toMangaStatus() + } + + setUrlWithoutDomain(document.location()) + } + + protected open fun imageFromElement(element: Element): String? { + return when { + element.hasAttr("data-src") -> element.attr("abs:data-src") + element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src") + element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage() + element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc") + else -> element.attr("abs:src") + } + } + + protected open fun String.getSrcSetImage(): String? { + return this.split(" ") + .filter(URL_REGEX::matches) + .maxOfOrNull(String::toString) + } + + protected val completedStatusList: Array = arrayOf( + "Finished", + "Completo", + ) + + protected open val ongoingStatusList: Array = arrayOf( + "Publishing", + "Ativo", + ) + + protected val hiatusStatusList: Array = arrayOf( + "on hiatus", + ) + + protected val canceledStatusList: Array = arrayOf( + "Canceled", + "Discontinued", + ) + + open fun String.toMangaStatus(): Int { + return when { + containsIn(completedStatusList) -> SManga.COMPLETED + containsIn(ongoingStatusList) -> SManga.ONGOING + containsIn(hiatusStatusList) -> SManga.ON_HIATUS + containsIn(canceledStatusList) -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + } + + // ============================== Chapters ============================ + + override fun chapterListSelector() = ".chapter-list li a" + + override fun chapterListParse(response: Response): List { + return super.chapterListParse(response) + } + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + name = element.selectFirst(".title")!!.text() + setUrlWithoutDomain(element.absUrl("href")) + } + + // ============================== Pages =============================== + + override fun pageListParse(document: Document): List { + verifyLoginRequired(document) + + return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element -> + Page(index, document.location(), imageFromElement(element)) + } + } + + private fun verifyLoginRequired(document: Document) { + val alert = document.selectFirst("h1")?.text() ?: return + if (alert.contains("İçerik Kısıtlaması", ignoreCase = true)) { + throw IOException("Web görünümünde oturum açın") + } + } + + override fun imageUrlParse(document: Document) = "" + + // ============================= Filters ============================== + + private var filterList = emptyList>>() + + override fun getFilterList(): FilterList { + val filters = mutableListOf>() + + filters += if (filterList.isNotEmpty()) { + filterList.map { SelectionList(it.first, it.second) } + } else { + listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros")) + } + + return FilterList(filters) + } + + protected open fun parseSelection(document: Document, selector: String): Pair>? { + val selectorFilter = "#filter-form $selector .select-item-head .text" + + return document.selectFirst(selectorFilter)?.text()?.let { displayName -> + val tags = document.select("#filter-form $selector li").map { element -> + element.selectFirst("input")!!.let { input -> + Tag( + name = element.selectFirst(".text")!!.text(), + value = input.attr("value"), + query = input.attr("name"), + ) + } + } + displayName to mutableListOf().apply { + this += Tag("Default") + this += tags + } + } + } + + open val filterListSelector: List = listOf( + ".filter-genre", + ".filter-status", + ".filter-type", + ".filter-year", + ".filter-sort", + ) + + open fun filterParse(response: Response) { + val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string()) + filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) } + } + + protected data class Tag(val name: String = "", val value: String = "", val query: String = "") + + private open class SelectionList(displayName: String, private val vals: List, state: Int = 0) : + Filter.Select(displayName, vals.map { it.name }.toTypedArray(), state) { + fun selected() = vals[state] + } + + // ============================= Utils ============================== + + private fun String.containsIn(array: Array): Boolean { + return this.lowercase() in array.map { it.lowercase() } + } + + companion object { + const val PREFIX_SEARCH = "id:" + val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex() + } +}