diff --git a/src/pt/mangalivre/AndroidManifest.xml b/lib-multisrc/etoshore/AndroidManifest.xml similarity index 72% rename from src/pt/mangalivre/AndroidManifest.xml rename to lib-multisrc/etoshore/AndroidManifest.xml index ea54504a385..24530a9e10d 100644 --- a/src/pt/mangalivre/AndroidManifest.xml +++ b/lib-multisrc/etoshore/AndroidManifest.xml @@ -2,7 +2,7 @@ @@ -13,9 +13,9 @@ + android:host="${SOURCEHOST}" + android:pathPattern="/.*/..*" + android:scheme="${SOURCESCHEME}" /> diff --git a/lib-multisrc/etoshore/build.gradle.kts b/lib-multisrc/etoshore/build.gradle.kts new file mode 100644 index 00000000000..dc076cc3785 --- /dev/null +++ b/lib-multisrc/etoshore/build.gradle.kts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000000..441b12ce5f2 --- /dev/null +++ b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/Etoshore.kt @@ -0,0 +1,242 @@ +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/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivreUrlActivity.kt b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/EtoshoreUrlActivity.kt similarity index 85% rename from src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivreUrlActivity.kt rename to lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/EtoshoreUrlActivity.kt index 136e120de16..89c5dd58e37 100644 --- a/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivreUrlActivity.kt +++ b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/EtoshoreUrlActivity.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.extension.pt.mangalivre +package eu.kanade.tachiyomi.multisrc.etoshore 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 MangaLivreUrlActivity : Activity() { +class EtoshoreUrlActivity : Activity() { private val tag = javaClass.simpleName @@ -18,7 +18,7 @@ class MangaLivreUrlActivity : Activity() { val item = pathSegments[1] val mainIntent = Intent().apply { action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${MangaLivre.PREFIX_SEARCH}$item") + putExtra("query", "${Etoshore.PREFIX_SEARCH}$item") putExtra("filter", packageName) } diff --git a/src/pt/crystalcomics/build.gradle b/src/pt/crystalcomics/build.gradle index fa740059201..45535291902 100644 --- a/src/pt/crystalcomics/build.gradle +++ b/src/pt/crystalcomics/build.gradle @@ -1,9 +1,9 @@ ext { extName = 'CrystalComics' extClass = '.CrystalComics' - themePkg = 'madara' + themePkg = 'etoshore' baseUrl = 'https://crystalcomics.com' - overrideVersionCode = 0 + overrideVersionCode = 37 } apply from: "$rootDir/common.gradle" diff --git a/src/pt/crystalcomics/res/mipmap-hdpi/ic_launcher.png b/src/pt/crystalcomics/res/mipmap-hdpi/ic_launcher.png index 3a490e31477..7cbdc492900 100644 Binary files a/src/pt/crystalcomics/res/mipmap-hdpi/ic_launcher.png and b/src/pt/crystalcomics/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/crystalcomics/res/mipmap-mdpi/ic_launcher.png b/src/pt/crystalcomics/res/mipmap-mdpi/ic_launcher.png index 19738442b8b..8721eb7f0e6 100644 Binary files a/src/pt/crystalcomics/res/mipmap-mdpi/ic_launcher.png and b/src/pt/crystalcomics/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/crystalcomics/res/mipmap-xhdpi/ic_launcher.png b/src/pt/crystalcomics/res/mipmap-xhdpi/ic_launcher.png index 90220134b22..1a7c8b499e1 100644 Binary files a/src/pt/crystalcomics/res/mipmap-xhdpi/ic_launcher.png and b/src/pt/crystalcomics/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/crystalcomics/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/crystalcomics/res/mipmap-xxhdpi/ic_launcher.png index 59a5e49bc91..29eee456a8e 100644 Binary files a/src/pt/crystalcomics/res/mipmap-xxhdpi/ic_launcher.png and b/src/pt/crystalcomics/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/crystalcomics/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/crystalcomics/res/mipmap-xxxhdpi/ic_launcher.png index b84e90fdc13..40f24039106 100644 Binary files a/src/pt/crystalcomics/res/mipmap-xxxhdpi/ic_launcher.png and b/src/pt/crystalcomics/res/mipmap-xxxhdpi/ic_launcher.png differ 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 7752cd46b09..0d8972e56a4 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,14 @@ package eu.kanade.tachiyomi.extension.pt.crystalcomics -import eu.kanade.tachiyomi.multisrc.madara.Madara -import java.text.SimpleDateFormat -import java.util.Locale +import eu.kanade.tachiyomi.multisrc.etoshore.Etoshore +import eu.kanade.tachiyomi.network.interceptor.rateLimit -class CrystalComics : Madara( +class CrystalComics : Etoshore( "Crystal Comics", "https://crystalcomics.com", "pt-BR", - SimpleDateFormat("MMMM dd, yyyy", Locale("pt", "BR")), ) { - override val useNewChapterEndpoint = true + override val client = super.client.newBuilder() + .rateLimit(2) + .build() } diff --git a/src/pt/mangalivre/build.gradle b/src/pt/mangalivre/build.gradle index 653384aa662..943b0b3425a 100644 --- a/src/pt/mangalivre/build.gradle +++ b/src/pt/mangalivre/build.gradle @@ -1,7 +1,9 @@ ext { extName = 'Manga Livre' extClass = '.MangaLivre' - extVersionCode = 2 + themePkg = 'etoshore' + baseUrl = 'https://mangalivre.one' + overrideVersionCode = 1 isNsfw = true } diff --git a/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivre.kt b/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivre.kt index 82e3f466b5c..181c2be8495 100644 --- a/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivre.kt +++ b/src/pt/mangalivre/src/eu/kanade/tachiyomi/extension/pt/mangalivre/MangaLivre.kt @@ -1,206 +1,23 @@ package eu.kanade.tachiyomi.extension.pt.mangalivre -import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.multisrc.etoshore.Etoshore 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 - -/** - * Etoshore - Manga Theme - */ -class MangaLivre : ParsedHttpSource() { - - override val name = "Manga Livre" - - override val baseUrl = "https://mangalivre.one" - - override val lang = "pt-BR" - - override val supportsLatest = true +class MangaLivre : Etoshore( + name = "Manga Livre", + baseUrl = "https://mangalivre.one", + lang = "pt-BR", +) { override val versionId = 2 override val client = network.cloudflareClient.newBuilder() .rateLimit(2) .build() - // ============================== Popular ============================== - - private 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 =============================== - - private 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")?.absUrl("src") - 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() - thumbnail_url = document.selectFirst(".details-right-con img")?.absUrl("src") - 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()) - } - - private fun String.toMangaStatus(): Int { - return when (this.lowercase().trim()) { - "publishing" -> SManga.ONGOING - "finished" -> SManga.COMPLETED - "discontinued" -> SManga.CANCELLED - "on hiatus" -> SManga.ON_HIATUS - 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")) - } - - override fun chapterListParse(response: Response) = super.chapterListParse(response).reversed() - - // ============================== Pages =============================== - - override fun pageListParse(document: Document): List { - return document.select(".chapter-images .chapter-item img").mapIndexed { index, element -> - Page(index, imageUrl = element.absUrl("src")) - } - } - - override fun imageUrlParse(document: Document) = "" - - // ============================= Filters ============================== - - private var filterList = emptyList() - - override fun getFilterList(): FilterList { - val filters = filterList.takeIf(List<*>::isNotEmpty) - ?: listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros")) - return FilterList(filters) - } - - private fun parseSelection(document: Document, selector: String): SelectionList? { - val selectorFilter = "#filter-form $selector .select-item-head .text" - return document.selectFirst(selectorFilter)?.text()?.let { displayName -> - val values = 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"), - ) - } - } - SelectionList(displayName, values) - } - } - - private val filterListSelector: List = listOf( - ".filter-genre", - ".filter-status", - ".filter-type", - ".filter-sort", - ) - - private fun filterParse(response: Response) { - val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string()) - filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) } - } - - private 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] - } + override fun chapterListParse(response: Response) = + super.chapterListParse(response).reversed() - companion object { - const val PREFIX_SEARCH = "id:" - } + override fun imageFromElement(element: Element): String? = element.attr("abs:src") }