From f9996f592126be8ce95e83c9a245a935e9da3dc9 Mon Sep 17 00:00:00 2001 From: Chopper <156493704+choppeh@users.noreply.github.com> Date: Sat, 18 Jan 2025 15:32:33 -0300 Subject: [PATCH] Crystal: Fix image loading (#7200) * Crystal: Fix image loading * Merge Etoshore into CrystalComics --- lib-multisrc/etoshore/build.gradle.kts | 5 - .../eu/kanade/tachiyomi/multisrc/Etoshore.kt | 242 ----------------- .../pt/crystalcomics}/AndroidManifest.xml | 6 +- src/pt/crystalcomics/build.gradle | 6 +- .../pt/crystalcomics/CrystalComics.kt | 253 +++++++++++++++++- .../crystalcomics/CrystalComicsUrlActivity.kt | 6 +- 6 files changed, 253 insertions(+), 265 deletions(-) delete mode 100644 lib-multisrc/etoshore/build.gradle.kts delete mode 100644 lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/Etoshore.kt rename {lib-multisrc/etoshore => src/pt/crystalcomics}/AndroidManifest.xml (78%) rename lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/EtoshoreUrlActivity.kt => src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComicsUrlActivity.kt (84%) diff --git a/lib-multisrc/etoshore/build.gradle.kts b/lib-multisrc/etoshore/build.gradle.kts deleted file mode 100644 index dc076cc37..000000000 --- a/lib-multisrc/etoshore/build.gradle.kts +++ /dev/null @@ -1,5 +0,0 @@ -plugins { - id("lib-multisrc") -} - -baseVersionCode = 1 diff --git a/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/Etoshore.kt b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/Etoshore.kt deleted file mode 100644 index 441b12ce5..000000000 --- a/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/Etoshore.kt +++ /dev/null @@ -1,242 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.etoshore - -import eu.kanade.tachiyomi.network.GET -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 org.jsoup.Jsoup -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import rx.Observable - -abstract class Etoshore( - override val name: String, - override val baseUrl: String, - final override val lang: String, -) : ParsedHttpSource() { - - override val supportsLatest = true - - override val client = network.cloudflareClient - - // ============================== 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() - 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 chapterFromElement(element: Element) = SChapter.create().apply { - name = element.selectFirst(".title")!!.text() - setUrlWithoutDomain(element.absUrl("href")) - } - - // ============================== Pages =============================== - - override fun pageListParse(document: Document): List { - return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element -> - Page(index, imageUrl = imageFromElement(element)) - } - } - - 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 -> - displayName to 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"), - ) - } - } - } - } - - 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() - } -} diff --git a/lib-multisrc/etoshore/AndroidManifest.xml b/src/pt/crystalcomics/AndroidManifest.xml similarity index 78% rename from lib-multisrc/etoshore/AndroidManifest.xml rename to src/pt/crystalcomics/AndroidManifest.xml index 24530a9e1..bcbed1536 100644 --- a/lib-multisrc/etoshore/AndroidManifest.xml +++ b/src/pt/crystalcomics/AndroidManifest.xml @@ -2,7 +2,7 @@ @@ -13,9 +13,9 @@ + android:scheme="https" /> diff --git a/src/pt/crystalcomics/build.gradle b/src/pt/crystalcomics/build.gradle index 455352919..37c4c76e4 100644 --- a/src/pt/crystalcomics/build.gradle +++ b/src/pt/crystalcomics/build.gradle @@ -1,9 +1,7 @@ ext { - extName = 'CrystalComics' + extName = 'Crystal Comics' extClass = '.CrystalComics' - themePkg = 'etoshore' - baseUrl = 'https://crystalcomics.com' - overrideVersionCode = 37 + extVersionCode = 39 } apply from: "$rootDir/common.gradle" diff --git a/src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComics.kt b/src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComics.kt index 0d8972e56..42fb11bee 100644 --- a/src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComics.kt +++ b/src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComics.kt @@ -1,14 +1,251 @@ package eu.kanade.tachiyomi.extension.pt.crystalcomics -import eu.kanade.tachiyomi.multisrc.etoshore.Etoshore +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 org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable -class CrystalComics : Etoshore( - "Crystal Comics", - "https://crystalcomics.com", - "pt-BR", -) { - override val client = super.client.newBuilder() - .rateLimit(2) +// Etoshore +class CrystalComics : ParsedHttpSource() { + + override val name = "Crystal Comics" + + override val baseUrl = "https://crystalcomics.com" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(1, 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() + 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 chapterFromElement(element: Element) = SChapter.create().apply { + name = element.selectFirst(".title")!!.text() + setUrlWithoutDomain(element.absUrl("href")) + } + + // ============================== Pages =============================== + + override fun pageListParse(document: Document): List { + return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element -> + Page(index, document.location(), imageFromElement(element)) + } + } + + 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 -> + displayName to 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"), + ) + } + } + } + } + + 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() + } } diff --git a/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/EtoshoreUrlActivity.kt b/src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComicsUrlActivity.kt similarity index 84% rename from lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/EtoshoreUrlActivity.kt rename to src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComicsUrlActivity.kt index 89c5dd58e..17f8d87ca 100644 --- a/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/EtoshoreUrlActivity.kt +++ b/src/pt/crystalcomics/src/eu/kanade/tachiyomi/extension/pt/crystalcomics/CrystalComicsUrlActivity.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.multisrc.etoshore +package eu.kanade.tachiyomi.extension.pt.crystalcomics import android.app.Activity import android.content.ActivityNotFoundException @@ -7,7 +7,7 @@ import android.os.Bundle import android.util.Log import kotlin.system.exitProcess -class EtoshoreUrlActivity : Activity() { +class CrystalComicsUrlActivity : Activity() { private val tag = javaClass.simpleName @@ -18,7 +18,7 @@ class EtoshoreUrlActivity : Activity() { val item = pathSegments[1] val mainIntent = Intent().apply { action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${Etoshore.PREFIX_SEARCH}$item") + putExtra("query", "${CrystalComics.PREFIX_SEARCH}$item") putExtra("filter", packageName) }