diff --git a/lib-multisrc/etoshore/AndroidManifest.xml b/lib-multisrc/etoshore/AndroidManifest.xml new file mode 100644 index 000000000..1635f61cd --- /dev/null +++ b/lib-multisrc/etoshore/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/lib-multisrc/etoshore/build.gradle.kts b/lib-multisrc/etoshore/build.gradle.kts new file mode 100644 index 000000000..dc076cc37 --- /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/Etoshore.kt b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/etoshore/Etoshore.kt new file mode 100644 index 000000000..c57f2aa77 --- /dev/null +++ b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/etoshore/Etoshore.kt @@ -0,0 +1,247 @@ +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, + override val lang: String, +) : ParsedHttpSource() { + + override val supportsLatest = true + + override val client = network.cloudflareClient + + 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() + + with(document) { + status = when { + containsClass(".finished") -> SManga.COMPLETED + containsClass(".publishing") -> SManga.ONGOING + containsClass(".on-hiatus") -> SManga.ON_HIATUS + containsClass(".discontinued") -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + } + + setUrlWithoutDomain(document.location()) + } + + private fun Element.containsClass(cssSelector: String) = select(cssSelector).isNotEmpty() + + protected open fun imageFromElement(element: Element): String? { + val attributes = listOf( + "data-src", + "data-lazy-src", + "data-cfsrc", + "src", + ) + return attributes + .mapNotNull { attr -> element.takeIf { it.hasAttr(attr) }?.attr("abs:$attr") } + .maxOrNull() + ?: element.takeIf { it.hasAttr("srcset") }?.attr("abs:srcset")?.getSrcSetImage() + } + + protected open fun String.getSrcSetImage(): String? { + return this.split(" ") + .filter(URL_REGEX::matches) + .maxOfOrNull(String::toString) + } + + // ============================== 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 { + 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 -> + 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() + } +} diff --git a/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/etoshore/EtoshoreUrlActivity.kt b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/etoshore/EtoshoreUrlActivity.kt new file mode 100644 index 000000000..f31329ba5 --- /dev/null +++ b/lib-multisrc/etoshore/src/eu/kanade/tachiyomi/multisrc/etoshore/EtoshoreUrlActivity.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.multisrc.etoshore + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class EtoshoreUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + + if (pathSegments != null && pathSegments.size >= 2) { + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${getSLUG(pathSegments)}") + putExtra("filter", packageName) + } + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("EtoshoreUrl", e.toString()) + } + } else { + Log.e("EtoshoreUrl", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } + + private fun getSLUG(pathSegments: MutableList): String? { + return if (pathSegments.size >= 2) { + val slug = pathSegments[1] + "${Etoshore.PREFIX_SEARCH}$slug" + } else { + null + } + } +} diff --git a/src/pt/nextscan/build.gradle b/src/pt/nextscan/build.gradle new file mode 100644 index 000000000..2b5551959 --- /dev/null +++ b/src/pt/nextscan/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'Next Scan' + extClass = '.NextScan' + themePkg = 'etoshore' + baseUrl = 'https://nextscan.cloud' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/nextscan/res/mipmap-hdpi/ic_launcher.png b/src/pt/nextscan/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..742686639 Binary files /dev/null and b/src/pt/nextscan/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/nextscan/res/mipmap-mdpi/ic_launcher.png b/src/pt/nextscan/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..03bca957f Binary files /dev/null and b/src/pt/nextscan/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/nextscan/res/mipmap-xhdpi/ic_launcher.png b/src/pt/nextscan/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..834f1a96d Binary files /dev/null and b/src/pt/nextscan/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/nextscan/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/nextscan/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b64b2a815 Binary files /dev/null and b/src/pt/nextscan/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/nextscan/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/nextscan/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..121ef4aa6 Binary files /dev/null and b/src/pt/nextscan/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/nextscan/src/eu/kanade/tachiyomi/extension/pt/nextscan/NextScan.kt b/src/pt/nextscan/src/eu/kanade/tachiyomi/extension/pt/nextscan/NextScan.kt new file mode 100644 index 000000000..33d10c038 --- /dev/null +++ b/src/pt/nextscan/src/eu/kanade/tachiyomi/extension/pt/nextscan/NextScan.kt @@ -0,0 +1,19 @@ +package eu.kanade.tachiyomi.extension.pt.nextscan + +import eu.kanade.tachiyomi.multisrc.etoshore.Etoshore +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.SChapter +import okhttp3.Response + +class NextScan : Etoshore( + "Next Scan", + "https://nextscan.cloud", + "pt-BR", +) { + override val client = super.client.newBuilder() + .rateLimit(3) + .build() + + override fun chapterListParse(response: Response): List = + super.chapterListParse(response).reversed() +} diff --git a/src/tr/anisamanga/build.gradle b/src/tr/anisamanga/build.gradle index 811746d75..683e944e1 100644 --- a/src/tr/anisamanga/build.gradle +++ b/src/tr/anisamanga/build.gradle @@ -1,7 +1,9 @@ ext { extName = 'Anisa Manga' extClass = '.AnisaManga' - extVersionCode = 43 + themePkg = 'etoshore' + baseUrl = 'https://anisamanga.net' + overrideVersionCode = 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 b868f4a98..576e0408f 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,34 +1,16 @@ package eu.kanade.tachiyomi.extension.tr.anisamanga -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 okio.IOException -import org.jsoup.Jsoup import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import rx.Observable - -// Etoshore -class AnisaManga : ParsedHttpSource() { - - override val name = "Anisa Manga" - - override val baseUrl = "https://anisamanga.net" - - override val lang = "tr" - - override val supportsLatest = true +import java.io.IOException +class AnisaManga : Etoshore( + "Anisa Manga", + "https://anisamanga.net", + "tr", +) { // Migrate from Madara to Etoshore override val versionId = 2 @@ -36,175 +18,9 @@ class AnisaManga : ParsedHttpSource() { .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)) - } + return super.pageListParse(document) } private fun verifyLoginRequired(document: Document) { @@ -213,73 +29,4 @@ class AnisaManga : ParsedHttpSource() { 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() - } }