Rewrite Mangabox (Mangakakalot, Manganato, Mangabat) to Allow Mirrors and CDN Fallbacks (#7915)
* Added CDN Fallback For Mangabox-based extensions
* Improved CDN testing
Now prioritizes last-worked CDNs
Seems like they "fixed" the issue by changing the alternative/backup CDNs to a single, working CDN.
* re-added the removed null check at line 68
* refactored, made fallbacks configurable
* Removed mangairo
* Added mirrors
* lint
* lint again
* final lint
* review changes, lint
* refactor, lint
* lint again 😩
This commit is contained in:
parent
ff9732e42b
commit
57e51e8ef1
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 5
|
baseVersionCode = 6
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangabox
|
package eu.kanade.tachiyomi.multisrc.mangabox
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@ -9,42 +13,144 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.TimeZone
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
// Based off of Mangakakalot 1.2.8
|
|
||||||
abstract class MangaBox(
|
abstract class MangaBox(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
private val mirrorEntries: Array<String>,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val dateformat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yy", Locale.ENGLISH),
|
private val dateFormat: SimpleDateFormat = SimpleDateFormat(
|
||||||
) : ParsedHttpSource() {
|
"MMM-dd-yyyy HH:mm",
|
||||||
|
Locale.ENGLISH,
|
||||||
|
).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
},
|
||||||
|
) : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val baseUrl: String get() = mirror
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.connectTimeout(15, TimeUnit.SECONDS)
|
.addInterceptor(::useAltCdnInterceptor)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
private fun SharedPreferences.getMirrorPref(): String =
|
||||||
|
getString(PREF_USE_MIRROR, mirrorEntries[0])!!
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by getPreferencesLazy {
|
||||||
|
// if current mirror is not in mirrorEntries, set default
|
||||||
|
if (getMirrorPref() !in mirrorEntries.map { "${URL_PREFIX}$it" }) {
|
||||||
|
edit().putString(PREF_USE_MIRROR, "${URL_PREFIX}${mirrorEntries[0]}").apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mirror = ""
|
||||||
|
get() {
|
||||||
|
if (field.isNotEmpty()) {
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
field = preferences.getMirrorPref()
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cdnSet =
|
||||||
|
MangaBoxLinkedCdnSet() // Stores all unique CDNs that the extension can use to retrieve chapter images
|
||||||
|
|
||||||
|
private class MangaBoxFallBackTag // Custom empty class tag to use as an identifier that the specific request is fallback-able
|
||||||
|
|
||||||
|
private fun HttpUrl.getBaseUrl(): String =
|
||||||
|
"${URL_PREFIX}${this.host}${
|
||||||
|
when (this.port) {
|
||||||
|
80, 443 -> ""
|
||||||
|
else -> ":${this.port}"
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
|
||||||
|
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
|
||||||
|
val originalResponse: Response? = try {
|
||||||
|
chain.proceed(request)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (requestTag == null) {
|
||||||
|
throw e
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestTag == null || originalResponse?.isSuccessful == true) {
|
||||||
|
requestTag?.let {
|
||||||
|
// Move working cdn to first so it gets priority during iteration
|
||||||
|
cdnSet.moveItemToFirst(request.url.getBaseUrl())
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalResponse!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the original response if it's not successful
|
||||||
|
originalResponse?.close()
|
||||||
|
|
||||||
|
for (cdnUrl in cdnSet) {
|
||||||
|
var tryResponse: Response? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val newUrl = cdnUrl.toHttpUrl().newBuilder()
|
||||||
|
.encodedPath(request.url.encodedPath)
|
||||||
|
.fragment(request.url.fragment)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Create a new request with the updated URL
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.url(newUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Proceed with the new request
|
||||||
|
tryResponse = chain.proceed(newRequest)
|
||||||
|
|
||||||
|
// Check if the response is successful
|
||||||
|
if (tryResponse.isSuccessful) {
|
||||||
|
// Move working cdn to first so it gets priority during iteration
|
||||||
|
cdnSet.moveItemToFirst(newRequest.url.getBaseUrl())
|
||||||
|
|
||||||
|
return tryResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
tryResponse.close()
|
||||||
|
} catch (_: IOException) {
|
||||||
|
tryResponse?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all CDNs fail, throw an error
|
||||||
|
return throw IOException("All CDN attempts failed.")
|
||||||
|
}
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||||
.add("Referer", baseUrl) // for covers
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
open val popularUrlPath = "manga_list?type=topview&category=all&state=all&page="
|
open val popularUrlPath = "manga-list/hot-manga?page="
|
||||||
|
|
||||||
open val latestUrlPath = "manga_list?type=latest&category=all&state=all&page="
|
open val latestUrlPath = "manga-list/latest-manga?page="
|
||||||
|
|
||||||
open val simpleQueryPath = "search/"
|
open val simpleQueryPath = "search/story/"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
|
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
|
||||||
|
|
||||||
@ -58,10 +164,11 @@ abstract class MangaBox(
|
|||||||
return GET("$baseUrl/$latestUrlPath$page", headers)
|
return GET("$baseUrl/$latestUrlPath$page", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
|
private fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
element.select(urlSelector).first()!!.let {
|
element.select(urlSelector).first()!!.let {
|
||||||
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
url = it.attr("abs:href")
|
||||||
|
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||||
title = it.text()
|
title = it.text()
|
||||||
}
|
}
|
||||||
thumbnail_url = element.select("img").first()!!.attr("abs:src")
|
thumbnail_url = element.select("img").first()!!.attr("abs:src")
|
||||||
@ -72,62 +179,47 @@ abstract class MangaBox(
|
|||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = mangaFromElement(element)
|
override fun latestUpdatesFromElement(element: Element): SManga = mangaFromElement(element)
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
|
override fun popularMangaNextPageSelector() =
|
||||||
|
"div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
|
return if (query.isNotBlank()) {
|
||||||
GET("$baseUrl/$simpleQueryPath${normalizeSearchQuery(query)}?page=$page", headers)
|
val url = "$baseUrl/$simpleQueryPath".toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment(normalizeSearchQuery(query))
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
} else {
|
} else {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder()
|
val url = "$baseUrl/genre".toHttpUrl().newBuilder()
|
||||||
if (getAdvancedGenreFilters().isNotEmpty()) {
|
|
||||||
url.addPathSegment("advanced_search")
|
|
||||||
url.addQueryParameter("page", page.toString())
|
|
||||||
url.addQueryParameter("keyw", normalizeSearchQuery(query))
|
|
||||||
var genreInclude = ""
|
|
||||||
var genreExclude = ""
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is KeywordFilter -> filter.toUriPart()?.let { url.addQueryParameter("keyt", it) }
|
|
||||||
is SortFilter -> url.addQueryParameter("orby", filter.toUriPart())
|
|
||||||
is StatusFilter -> url.addQueryParameter("sts", filter.toUriPart())
|
|
||||||
is AdvGenreFilter -> {
|
|
||||||
filter.state.forEach { if (it.isIncluded()) genreInclude += "_${it.id}" }
|
|
||||||
filter.state.forEach { if (it.isExcluded()) genreExclude += "_${it.id}" }
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
url.addQueryParameter("g_i", genreInclude)
|
|
||||||
url.addQueryParameter("g_e", genreExclude)
|
|
||||||
} else {
|
|
||||||
url.addPathSegment("manga_list")
|
|
||||||
url.addQueryParameter("page", page.toString())
|
url.addQueryParameter("page", page.toString())
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
||||||
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
||||||
is GenreFilter -> url.addQueryParameter("category", filter.toUriPart())
|
is GenreFilter -> url.addPathSegment(filter.toUriPart()!!)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
GET(url.build(), headers)
|
GET(url.build(), headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".panel_story_list .story_item"
|
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
|
override fun searchMangaNextPageSelector() =
|
||||||
|
"a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
|
||||||
|
|
||||||
open val mangaDetailsMainSelector = "div.manga-info-top, div.panel-story-info"
|
open val mangaDetailsMainSelector = "div.manga-info-top, div.panel-story-info"
|
||||||
|
|
||||||
open val thumbnailSelector = "div.manga-info-pic img, span.info-image img"
|
open val thumbnailSelector = "div.manga-info-pic img, span.info-image img"
|
||||||
|
|
||||||
open val descriptionSelector = "div#noidungm, div#panel-story-info-description"
|
open val descriptionSelector = "div#noidungm, div#panel-story-info-description, div#contentBox"
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
if (manga.url.startsWith("http")) {
|
if (manga.url.startsWith("http")) {
|
||||||
@ -146,11 +238,15 @@ abstract class MangaBox(
|
|||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
|
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
|
||||||
title = infoElement.select("h1, h2").first()!!.text()
|
title = infoElement.select("h1, h2").first()!!.text()
|
||||||
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a").eachText().joinToString()
|
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a")
|
||||||
status = parseStatus(infoElement.select("li:contains(status), td:containsOwn(status) + td").text())
|
.eachText().joinToString()
|
||||||
|
status = parseStatus(
|
||||||
|
infoElement.select("li:contains(status), td:containsOwn(status) + td").text(),
|
||||||
|
)
|
||||||
genre = infoElement.select("div.manga-info-top li:contains(genres)").firstOrNull()
|
genre = infoElement.select("div.manga-info-top li:contains(genres)").firstOrNull()
|
||||||
?.select("a")?.joinToString { it.text() } // kakalot
|
?.select("a")?.joinToString { it.text() } // kakalot
|
||||||
?: infoElement.select("td:containsOwn(genres) + td a").joinToString { it.text() } // nelo
|
?: infoElement.select("td:containsOwn(genres) + td a")
|
||||||
|
.joinToString { it.text() } // nelo
|
||||||
} ?: checkForRedirectMessage(document)
|
} ?: checkForRedirectMessage(document)
|
||||||
description = document.select(descriptionSelector).firstOrNull()?.ownText()
|
description = document.select(descriptionSelector).firstOrNull()?.ownText()
|
||||||
?.replace("""^$title summary:\s""".toRegex(), "")
|
?.replace("""^$title summary:\s""".toRegex(), "")
|
||||||
@ -199,44 +295,23 @@ abstract class MangaBox(
|
|||||||
|
|
||||||
protected open val alternateChapterDateSelector = String()
|
protected open val alternateChapterDateSelector = String()
|
||||||
|
|
||||||
protected fun Element.selectDateFromElement(): Element {
|
private fun Element.selectDateFromElement(): Element {
|
||||||
val defaultChapterDateSelector = "span"
|
val defaultChapterDateSelector = "span"
|
||||||
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(alternateChapterDateSelector).last()!!
|
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(
|
||||||
|
alternateChapterDateSelector,
|
||||||
|
).last()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
return SChapter.create().apply {
|
return SChapter.create().apply {
|
||||||
element.select("a").let {
|
element.select("a").let {
|
||||||
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
url = it.attr("abs:href")
|
||||||
|
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||||
name = it.text()
|
name = it.text()
|
||||||
scanlator =
|
scanlator =
|
||||||
it.attr("abs:href").toHttpUrl().host // show where chapters are actually from
|
it.attr("abs:href").toHttpUrl().host // show where chapters are actually from
|
||||||
}
|
}
|
||||||
date_upload = parseChapterDate(element.selectDateFromElement().text(), scanlator!!) ?: 0
|
date_upload = dateFormat.tryParse(element.selectDateFromElement().attr("title"))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String, host: String): Long? {
|
|
||||||
return if ("ago" in date) {
|
|
||||||
val value = date.split(' ')[0].toIntOrNull()
|
|
||||||
val cal = Calendar.getInstance()
|
|
||||||
when {
|
|
||||||
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, -value) }
|
|
||||||
value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, -value) }
|
|
||||||
value != null && "day" in date -> cal.apply { add(Calendar.DATE, -value) }
|
|
||||||
else -> null
|
|
||||||
}?.timeInMillis
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
if (host.contains("manganato", ignoreCase = true)) {
|
|
||||||
// Nelo's date format
|
|
||||||
SimpleDateFormat("MMM dd,yy", Locale.ENGLISH).parse(date)
|
|
||||||
} else {
|
|
||||||
dateformat.parse(date)
|
|
||||||
}
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
null
|
|
||||||
}?.time
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,26 +322,59 @@ abstract class MangaBox(
|
|||||||
return super.pageListRequest(chapter)
|
return super.pageListRequest(chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
open val pageListSelector = "div#vungdoc img, div.container-chapter-reader img"
|
private fun extractArray(scriptContent: String, arrayName: String): List<String> {
|
||||||
|
val pattern = Pattern.compile("$arrayName\\s*=\\s*\\[([^]]+)]")
|
||||||
|
val matcher = pattern.matcher(scriptContent)
|
||||||
|
val arrayValues = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (matcher.find()) {
|
||||||
|
val arrayContent = matcher.group(1)
|
||||||
|
val values = arrayContent?.split(",")
|
||||||
|
if (values != null) {
|
||||||
|
for (value in values) {
|
||||||
|
arrayValues.add(
|
||||||
|
value.trim()
|
||||||
|
.removeSurrounding("\"")
|
||||||
|
.replace("\\/", "/")
|
||||||
|
.removeSuffix("/"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrayValues
|
||||||
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
return document.select(pageListSelector)
|
val element = document.select("head > script").lastOrNull()
|
||||||
// filter out bad elements for mangakakalots
|
?: return emptyList()
|
||||||
.filterNot { it.attr("src").endsWith("log") }
|
val cdns =
|
||||||
.mapIndexed { i, element ->
|
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
|
||||||
val url = element.attr("abs:src").let { src ->
|
val chapterImages = extractArray(element.html(), "chapterImages")
|
||||||
if (src.startsWith("https://convert_image_digi.mgicdn.com")) {
|
|
||||||
"https://images.weserv.nl/?url=" + src.substringAfter("//")
|
// Add all parsed cdns to set
|
||||||
} else {
|
cdnSet.addAll(cdns)
|
||||||
src
|
|
||||||
|
return chapterImages.mapIndexed { i, imagePath ->
|
||||||
|
val parsedUrl = cdns[0].toHttpUrl().run {
|
||||||
|
newBuilder()
|
||||||
|
.encodedPath(
|
||||||
|
"/$imagePath".replace(
|
||||||
|
"//",
|
||||||
|
"/",
|
||||||
|
),
|
||||||
|
) // replace ensures that there's at least one trailing slash prefix
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Page(i, document.location(), url)
|
Page(i, document.location(), parsedUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
override fun imageRequest(page: Page): Request {
|
||||||
return GET(page.imageUrl!!, headersBuilder().set("Referer", page.url).build())
|
return GET(page.imageUrl!!, headers).newBuilder()
|
||||||
|
.tag(MangaBoxFallBackTag::class.java, MangaBoxFallBackTag()).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
@ -282,46 +390,26 @@ abstract class MangaBox(
|
|||||||
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
|
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
|
||||||
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
|
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
|
||||||
str = str.replace("đ".toRegex(), "d")
|
str = str.replace("đ".toRegex(), "d")
|
||||||
str = str.replace("""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(), "_")
|
str = str.replace(
|
||||||
|
"""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(),
|
||||||
|
"_",
|
||||||
|
)
|
||||||
str = str.replace("_+_".toRegex(), "_")
|
str = str.replace("_+_".toRegex(), "_")
|
||||||
str = str.replace("""^_+|_+$""".toRegex(), "")
|
str = str.replace("""^_+|_+$""".toRegex(), "")
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList() = if (getAdvancedGenreFilters().isNotEmpty()) {
|
override fun getFilterList() = FilterList(
|
||||||
FilterList(
|
|
||||||
KeywordFilter(getKeywordFilters()),
|
|
||||||
SortFilter(getSortFilters()),
|
|
||||||
StatusFilter(getStatusFilters()),
|
|
||||||
AdvGenreFilter(getAdvancedGenreFilters()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
FilterList(
|
|
||||||
Filter.Header("NOTE: Ignored if using text search!"),
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
SortFilter(getSortFilters()),
|
SortFilter(getSortFilters()),
|
||||||
StatusFilter(getStatusFilters()),
|
StatusFilter(getStatusFilters()),
|
||||||
GenreFilter(getGenreFilters()),
|
GenreFilter(getGenreFilters()),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Technically, only Sort, Status, and Genre need to be non-private for Mangakakalot and Manganato, but I'll include Keyword to make it uniform.
|
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Order by", vals)
|
||||||
protected class KeywordFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Keyword search ", vals)
|
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
||||||
protected class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Order by", vals)
|
private class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Category", vals)
|
||||||
protected class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
|
||||||
protected class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Category", vals)
|
|
||||||
|
|
||||||
// For advanced search, specifically tri-state genres
|
|
||||||
private class AdvGenreFilter(vals: List<AdvGenre>) : Filter.Group<AdvGenre>("Category", vals)
|
|
||||||
class AdvGenre(val id: String?, name: String) : Filter.TriState(name)
|
|
||||||
|
|
||||||
// keyt query parameter
|
|
||||||
private fun getKeywordFilters(): Array<Pair<String?, String>> = arrayOf(
|
|
||||||
Pair(null, "Everything"),
|
|
||||||
Pair("title", "Title"),
|
|
||||||
Pair("alternative", "Alt title"),
|
|
||||||
Pair("author", "Author"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
|
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||||
Pair("latest", "Latest"),
|
Pair("latest", "Latest"),
|
||||||
@ -338,53 +426,72 @@ abstract class MangaBox(
|
|||||||
|
|
||||||
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||||
Pair("all", "ALL"),
|
Pair("all", "ALL"),
|
||||||
Pair("2", "Action"),
|
Pair("action", "Action"),
|
||||||
Pair("3", "Adult"),
|
Pair("adult", "Adult"),
|
||||||
Pair("4", "Adventure"),
|
Pair("adventure", "Adventure"),
|
||||||
Pair("6", "Comedy"),
|
Pair("comedy", "Comedy"),
|
||||||
Pair("7", "Cooking"),
|
Pair("cooking", "Cooking"),
|
||||||
Pair("9", "Doujinshi"),
|
Pair("doujinshi", "Doujinshi"),
|
||||||
Pair("10", "Drama"),
|
Pair("drama", "Drama"),
|
||||||
Pair("11", "Ecchi"),
|
Pair("ecchi", "Ecchi"),
|
||||||
Pair("12", "Fantasy"),
|
Pair("fantasy", "Fantasy"),
|
||||||
Pair("13", "Gender bender"),
|
Pair("gender-bender", "Gender bender"),
|
||||||
Pair("14", "Harem"),
|
Pair("harem", "Harem"),
|
||||||
Pair("15", "Historical"),
|
Pair("historical", "Historical"),
|
||||||
Pair("16", "Horror"),
|
Pair("horror", "Horror"),
|
||||||
Pair("45", "Isekai"),
|
Pair("isekai", "Isekai"),
|
||||||
Pair("17", "Josei"),
|
Pair("josei", "Josei"),
|
||||||
Pair("44", "Manhua"),
|
Pair("manhua", "Manhua"),
|
||||||
Pair("43", "Manhwa"),
|
Pair("manhwa", "Manhwa"),
|
||||||
Pair("19", "Martial arts"),
|
Pair("martial-arts", "Martial arts"),
|
||||||
Pair("20", "Mature"),
|
Pair("mature", "Mature"),
|
||||||
Pair("21", "Mecha"),
|
Pair("mecha", "Mecha"),
|
||||||
Pair("22", "Medical"),
|
Pair("medical", "Medical"),
|
||||||
Pair("24", "Mystery"),
|
Pair("mystery", "Mystery"),
|
||||||
Pair("25", "One shot"),
|
Pair("one-shot", "One shot"),
|
||||||
Pair("26", "Psychological"),
|
Pair("psychological", "Psychological"),
|
||||||
Pair("27", "Romance"),
|
Pair("romance", "Romance"),
|
||||||
Pair("28", "School life"),
|
Pair("school-life", "School life"),
|
||||||
Pair("29", "Sci fi"),
|
Pair("sci-fi", "Sci fi"),
|
||||||
Pair("30", "Seinen"),
|
Pair("seinen", "Seinen"),
|
||||||
Pair("31", "Shoujo"),
|
Pair("shoujo", "Shoujo"),
|
||||||
Pair("32", "Shoujo ai"),
|
Pair("shoujo-ai", "Shoujo ai"),
|
||||||
Pair("33", "Shounen"),
|
Pair("shounen", "Shounen"),
|
||||||
Pair("34", "Shounen ai"),
|
Pair("shounen-ai", "Shounen ai"),
|
||||||
Pair("35", "Slice of life"),
|
Pair("slice-of-life", "Slice of life"),
|
||||||
Pair("36", "Smut"),
|
Pair("smut", "Smut"),
|
||||||
Pair("37", "Sports"),
|
Pair("sports", "Sports"),
|
||||||
Pair("38", "Supernatural"),
|
Pair("supernatural", "Supernatural"),
|
||||||
Pair("39", "Tragedy"),
|
Pair("tragedy", "Tragedy"),
|
||||||
Pair("40", "Webtoons"),
|
Pair("webtoons", "Webtoons"),
|
||||||
Pair("41", "Yaoi"),
|
Pair("yaoi", "Yaoi"),
|
||||||
Pair("42", "Yuri"),
|
Pair("yuri", "Yuri"),
|
||||||
)
|
)
|
||||||
|
|
||||||
// To be overridden if using tri-state genres
|
|
||||||
protected open fun getAdvancedGenreFilters(): List<AdvGenre> = emptyList()
|
|
||||||
|
|
||||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
||||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||||
fun toUriPart() = vals[state].first
|
fun toUriPart() = vals[state].first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = PREF_USE_MIRROR
|
||||||
|
title = "Mirror"
|
||||||
|
entries = mirrorEntries
|
||||||
|
entryValues = mirrorEntries.map { "${URL_PREFIX}$it" }.toTypedArray()
|
||||||
|
setDefaultValue(entryValues[0])
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
// Update values
|
||||||
|
mirror = newValue as String
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}.let(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREF_USE_MIRROR = "pref_use_mirror"
|
||||||
|
private const val URL_PREFIX = "https://"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.mangabox
|
||||||
|
|
||||||
|
class MangaBoxLinkedCdnSet : LinkedHashSet<String>() {
|
||||||
|
fun moveItemToFirst(item: String) {
|
||||||
|
// Lock the object to avoid multi threading issues
|
||||||
|
synchronized(this) {
|
||||||
|
if (this.contains(item) && this.first() != item) {
|
||||||
|
// Remove the item from the current set
|
||||||
|
this.remove(item)
|
||||||
|
// Create a new list with the item at the first position
|
||||||
|
val newItems = mutableListOf(item)
|
||||||
|
// Add the remaining items
|
||||||
|
newItems.addAll(this)
|
||||||
|
// Clear the current set and add all items from the new list
|
||||||
|
this.clear()
|
||||||
|
this.addAll(newItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,8 @@ ext {
|
|||||||
extName = 'Mangabat'
|
extName = 'Mangabat'
|
||||||
extClass = '.Mangabat'
|
extClass = '.Mangabat'
|
||||||
themePkg = 'mangabox'
|
themePkg = 'mangabox'
|
||||||
baseUrl = 'https://h.mangabat.com'
|
baseUrl = 'https://www.mangabats.com'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 6
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.mangabat
|
package eu.kanade.tachiyomi.extension.en.mangabat
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangabox.MangaBox
|
import eu.kanade.tachiyomi.multisrc.mangabox.MangaBox
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import okhttp3.Request
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Mangabat : MangaBox("Mangabat", "https://h.mangabat.com", "en", SimpleDateFormat("MMM dd,yy", Locale.ENGLISH)) {
|
class Mangabat : MangaBox(
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/manga-list-all/$page?type=topview", headers)
|
"Mangabat",
|
||||||
override fun popularMangaSelector() = "div.list-story-item"
|
arrayOf(
|
||||||
override val latestUrlPath = "manga-list-all/"
|
"www.mangabats.com",
|
||||||
override fun searchMangaSelector() = "div.list-story-item"
|
),
|
||||||
override fun getAdvancedGenreFilters(): List<AdvGenre> = getGenreFilters()
|
"en",
|
||||||
.drop(1)
|
)
|
||||||
.map { AdvGenre(it.first, it.second) }
|
|
||||||
}
|
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Mangairo'
|
|
||||||
extClass = '.Mangairo'
|
|
||||||
themePkg = 'mangabox'
|
|
||||||
baseUrl = 'https://h.mangairo.com'
|
|
||||||
overrideVersionCode = 4
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.mangairo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangabox.MangaBox
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Mangairo : MangaBox("Mangairo", "https://h.mangairo.com", "en", SimpleDateFormat("MMM-dd-yy", Locale.ENGLISH)) {
|
|
||||||
override val popularUrlPath = "manga-list/type-topview/ctg-all/state-all/page-"
|
|
||||||
override fun popularMangaSelector() = "div.story-item"
|
|
||||||
override val latestUrlPath = "manga-list/type-latest/ctg-all/state-all/page-"
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
return GET("$baseUrl/list/$simpleQueryPath${normalizeSearchQuery(query)}?page=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.story-item"
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = mangaFromElement(element, "h2 a")
|
|
||||||
override fun searchMangaNextPageSelector() = "div.group-page a.select + a:not(.go-p-end)"
|
|
||||||
override val mangaDetailsMainSelector = "${super.mangaDetailsMainSelector}, div.story_content"
|
|
||||||
override val thumbnailSelector = "${super.thumbnailSelector}, div.story_info_left img"
|
|
||||||
override val descriptionSelector = "${super.descriptionSelector}, div#story_discription p"
|
|
||||||
override fun chapterListSelector() = "${super.chapterListSelector()}, div#chapter_list li"
|
|
||||||
override val alternateChapterDateSelector = "p"
|
|
||||||
override val pageListSelector = "${super.pageListSelector}, div.panel-read-story img"
|
|
||||||
|
|
||||||
// will have to write a separate searchMangaRequest to get filters working for this source
|
|
||||||
override fun getFilterList() = FilterList()
|
|
||||||
}
|
|
@ -3,7 +3,7 @@ ext {
|
|||||||
extClass = '.Mangakakalot'
|
extClass = '.Mangakakalot'
|
||||||
themePkg = 'mangabox'
|
themePkg = 'mangabox'
|
||||||
baseUrl = 'https://www.mangakakalot.gg'
|
baseUrl = 'https://www.mangakakalot.gg'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,115 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.mangakakalot
|
package eu.kanade.tachiyomi.extension.en.mangakakalot
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangabox.MangaBox
|
import eu.kanade.tachiyomi.multisrc.mangabox.MangaBox
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Mangakakalot : MangaBox("Mangakakalot", "https://www.mangakakalot.gg", "en") {
|
class Mangakakalot : MangaBox(
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yyyy HH:mm", Locale.ENGLISH)
|
"Mangakakalot",
|
||||||
|
arrayOf(
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder().set("Referer", "$baseUrl/") // for covers
|
"www.mangakakalot.gg",
|
||||||
override val popularUrlPath = "manga-list/hot-manga?page="
|
"www.mangakakalove.com",
|
||||||
override val latestUrlPath = "manga-list/latest-manga?page="
|
),
|
||||||
override val simpleQueryPath = "search/story/"
|
"en",
|
||||||
override fun searchMangaSelector() = "${super.searchMangaSelector()}, div.list-truyen-item-wrap"
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
|
|
||||||
val url = "$baseUrl/$simpleQueryPath".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment(normalizeSearchQuery(query))
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
} else {
|
|
||||||
val url = "$baseUrl/genre".toHttpUrl().newBuilder()
|
|
||||||
url.addQueryParameter("page", page.toString())
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
|
||||||
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
|
||||||
is GenreFilter -> url.addPathSegment(filter.toUriPart()!!)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
// parse on title attribute rather than the value
|
|
||||||
val dateUploadAttr: Long? = try {
|
|
||||||
dateFormat.parse(element.selectDateFromElement().attr("title"))?.time
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.chapterFromElement(element).apply {
|
|
||||||
date_upload = dateUploadAttr ?: date_upload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val descriptionSelector = "div#contentBox"
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
return if (page.url.contains(baseUrl)) {
|
|
||||||
GET(page.imageUrl!!, headersBuilder().build())
|
|
||||||
} else { // Avoid 403 errors on non-migrated mangas
|
|
||||||
super.imageRequest(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
|
||||||
Pair("all", "ALL"),
|
|
||||||
Pair("action", "Action"),
|
|
||||||
Pair("adult", "Adult"),
|
|
||||||
Pair("adventure", "Adventure"),
|
|
||||||
Pair("comedy", "Comedy"),
|
|
||||||
Pair("cooking", "Cooking"),
|
|
||||||
Pair("doujinshi", "Doujinshi"),
|
|
||||||
Pair("drama", "Drama"),
|
|
||||||
Pair("ecchi", "Ecchi"),
|
|
||||||
Pair("fantasy", "Fantasy"),
|
|
||||||
Pair("gender-bender", "Gender bender"),
|
|
||||||
Pair("harem", "Harem"),
|
|
||||||
Pair("historical", "Historical"),
|
|
||||||
Pair("horror", "Horror"),
|
|
||||||
Pair("isekai", "Isekai"),
|
|
||||||
Pair("josei", "Josei"),
|
|
||||||
Pair("manhua", "Manhua"),
|
|
||||||
Pair("manhwa", "Manhwa"),
|
|
||||||
Pair("martial-arts", "Martial arts"),
|
|
||||||
Pair("mature", "Mature"),
|
|
||||||
Pair("mecha", "Mecha"),
|
|
||||||
Pair("medical", "Medical"),
|
|
||||||
Pair("mystery", "Mystery"),
|
|
||||||
Pair("one-shot", "One shot"),
|
|
||||||
Pair("psychological", "Psychological"),
|
|
||||||
Pair("romance", "Romance"),
|
|
||||||
Pair("school-life", "School life"),
|
|
||||||
Pair("sci-fi", "Sci fi"),
|
|
||||||
Pair("seinen", "Seinen"),
|
|
||||||
Pair("shoujo", "Shoujo"),
|
|
||||||
Pair("shoujo-ai", "Shoujo ai"),
|
|
||||||
Pair("shounen", "Shounen"),
|
|
||||||
Pair("shounen-ai", "Shounen ai"),
|
|
||||||
Pair("slice-of-life", "Slice of life"),
|
|
||||||
Pair("smut", "Smut"),
|
|
||||||
Pair("sports", "Sports"),
|
|
||||||
Pair("supernatural", "Supernatural"),
|
|
||||||
Pair("tragedy", "Tragedy"),
|
|
||||||
Pair("webtoons", "Webtoons"),
|
|
||||||
Pair("yaoi", "Yaoi"),
|
|
||||||
Pair("yuri", "Yuri"),
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
@ -3,7 +3,7 @@ ext {
|
|||||||
extClass = '.Manganato'
|
extClass = '.Manganato'
|
||||||
themePkg = 'mangabox'
|
themePkg = 'mangabox'
|
||||||
baseUrl = 'https://www.natomanga.com'
|
baseUrl = 'https://www.natomanga.com'
|
||||||
overrideVersionCode = 3
|
overrideVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,117 +1,16 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.manganelo
|
package eu.kanade.tachiyomi.extension.en.manganelo
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangabox.MangaBox
|
import eu.kanade.tachiyomi.multisrc.mangabox.MangaBox
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Manganato : MangaBox("Manganato", "https://www.natomanga.com", "en") {
|
class Manganato : MangaBox(
|
||||||
|
"Manganato",
|
||||||
|
arrayOf(
|
||||||
|
"www.natomanga.com",
|
||||||
|
"www.nelomanga.com",
|
||||||
|
"www.manganato.gg",
|
||||||
|
),
|
||||||
|
"en",
|
||||||
|
) {
|
||||||
|
|
||||||
override val id: Long = 1024627298672457456
|
override val id: Long = 1024627298672457456
|
||||||
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yyyy HH:mm", Locale.ENGLISH)
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder().set("Referer", "$baseUrl/") // for covers
|
|
||||||
override val popularUrlPath = "manga-list/hot-manga?page="
|
|
||||||
override val latestUrlPath = "manga-list/latest-manga?page="
|
|
||||||
override val simpleQueryPath = "search/story/"
|
|
||||||
override fun searchMangaSelector() = "${super.searchMangaSelector()}, div.list-truyen-item-wrap"
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
|
|
||||||
val url = "$baseUrl/$simpleQueryPath".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment(normalizeSearchQuery(query))
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
} else {
|
|
||||||
val url = "$baseUrl/genre".toHttpUrl().newBuilder()
|
|
||||||
url.addQueryParameter("page", page.toString())
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
|
||||||
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
|
||||||
is GenreFilter -> url.addPathSegment(filter.toUriPart()!!)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
// parse on title attribute rather than the value
|
|
||||||
val dateUploadAttr: Long? = try {
|
|
||||||
dateFormat.parse(element.selectDateFromElement().attr("title"))?.time
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.chapterFromElement(element).apply {
|
|
||||||
date_upload = dateUploadAttr ?: date_upload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val descriptionSelector = "div#contentBox"
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
return if (page.url.contains(baseUrl)) {
|
|
||||||
GET(page.imageUrl!!, headersBuilder().build())
|
|
||||||
} else { // Avoid 403 errors on non-migrated mangas
|
|
||||||
super.imageRequest(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
|
||||||
Pair("all", "ALL"),
|
|
||||||
Pair("action", "Action"),
|
|
||||||
Pair("adult", "Adult"),
|
|
||||||
Pair("adventure", "Adventure"),
|
|
||||||
Pair("comedy", "Comedy"),
|
|
||||||
Pair("cooking", "Cooking"),
|
|
||||||
Pair("doujinshi", "Doujinshi"),
|
|
||||||
Pair("drama", "Drama"),
|
|
||||||
Pair("ecchi", "Ecchi"),
|
|
||||||
Pair("fantasy", "Fantasy"),
|
|
||||||
Pair("gender-bender", "Gender bender"),
|
|
||||||
Pair("harem", "Harem"),
|
|
||||||
Pair("historical", "Historical"),
|
|
||||||
Pair("horror", "Horror"),
|
|
||||||
Pair("isekai", "Isekai"),
|
|
||||||
Pair("josei", "Josei"),
|
|
||||||
Pair("manhua", "Manhua"),
|
|
||||||
Pair("manhwa", "Manhwa"),
|
|
||||||
Pair("martial-arts", "Martial arts"),
|
|
||||||
Pair("mature", "Mature"),
|
|
||||||
Pair("mecha", "Mecha"),
|
|
||||||
Pair("medical", "Medical"),
|
|
||||||
Pair("mystery", "Mystery"),
|
|
||||||
Pair("one-shot", "One shot"),
|
|
||||||
Pair("psychological", "Psychological"),
|
|
||||||
Pair("romance", "Romance"),
|
|
||||||
Pair("school-life", "School life"),
|
|
||||||
Pair("sci-fi", "Sci fi"),
|
|
||||||
Pair("seinen", "Seinen"),
|
|
||||||
Pair("shoujo", "Shoujo"),
|
|
||||||
Pair("shoujo-ai", "Shoujo ai"),
|
|
||||||
Pair("shounen", "Shounen"),
|
|
||||||
Pair("shounen-ai", "Shounen ai"),
|
|
||||||
Pair("slice-of-life", "Slice of life"),
|
|
||||||
Pair("smut", "Smut"),
|
|
||||||
Pair("sports", "Sports"),
|
|
||||||
Pair("supernatural", "Supernatural"),
|
|
||||||
Pair("tragedy", "Tragedy"),
|
|
||||||
Pair("webtoons", "Webtoons"),
|
|
||||||
Pair("yaoi", "Yaoi"),
|
|
||||||
Pair("yuri", "Yuri"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user