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:
Jake 2025-03-12 19:32:53 +08:00 committed by Draff
parent ff9732e42b
commit 57e51e8ef1
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
16 changed files with 331 additions and 456 deletions

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 5 baseVersionCode = 6

View File

@ -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://"
}
} }

View File

@ -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)
}
}
}
}

View File

@ -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
} }

View File

@ -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) }
}

View File

@ -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

View File

@ -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()
}

View File

@ -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
} }

View File

@ -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"),
) )
}

View File

@ -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
} }

View File

@ -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"),
)
} }