Rewrite MMRCMS (#931)
* Rewrite MMRCMS * linting * Mangas.in: Fix latest, search by query, manga details * use HashSet instead of Set for manga detail keys * use buildList for building filter list * mangas.in: Copy over changes to MangasInDto * Use a better metric for determining if filter fetching failed. Also merged types and tags. * Move to using named constructor parameters instead of open vals This improves the discoverability of configurable stuff. * use normal try/catch instead of runCatching * Elaborate on the reason for not using Nothing * Make most configuration options private * forbidden -> useNamedArgumentsBelow * Address lint failures * Close the thingies * <:shitting:1130237162105876490> * <:shitting:1130237162105876490>
|
@ -3,58 +3,21 @@ package eu.kanade.tachiyomi.extension.fr.bentoscan
|
|||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Bentoscan : MMRCMS("Bentoscan", "https://bentoscan.com", "fr") {
|
||||
class Bentoscan : MMRCMS(
|
||||
"Bentoscan",
|
||||
"https://bentoscan.com",
|
||||
"fr",
|
||||
supportsAdvancedSearch = false,
|
||||
chapterNamePrefix = "Scan ",
|
||||
) {
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", IMG_URL)
|
||||
.set("Referer", "https://scansmangas.me/")
|
||||
.set("Accept", "image/avif,image/webp,*/*")
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, newHeaders)
|
||||
}
|
||||
|
||||
override fun nullableChapterFromElement(element: Element): SChapter? {
|
||||
val chapter = SChapter.create()
|
||||
|
||||
val titleWrapper = element.select("[class^=chapter-title-rtl]").first()!!
|
||||
val chapterElement = titleWrapper.getElementsByTag("a")!!
|
||||
val url = chapterElement.attr("href")
|
||||
|
||||
chapter.url = getUrlWithoutBaseUrl(url)
|
||||
|
||||
// Construct chapter names
|
||||
// Before -> Scan <manga_name> <chapter_number> VF: <chapter_number>
|
||||
// Now -> Chapitre <chapter_number> : <chapter_title> OR Chapitre <chapter_number>
|
||||
val chapterText = chapterElement.text()
|
||||
val numberRegex = Regex("""[1-9]\d*(\.\d+)*""")
|
||||
val chapterNumber = numberRegex.find(chapterText)?.value.orEmpty()
|
||||
val chapterTitle = titleWrapper.getElementsByTag("em")!!.text()
|
||||
if (chapterTitle.toIntOrNull() != null) {
|
||||
chapter.name = "Chapitre $chapterNumber"
|
||||
} else {
|
||||
chapter.name = "Chapitre $chapterNumber : $chapterTitle"
|
||||
}
|
||||
|
||||
// Parse date
|
||||
val dateText = element.getElementsByClass("date-chapter-title-rtl").text().trim()
|
||||
|
||||
chapter.date_upload = runCatching {
|
||||
dateFormat.parse(dateText)?.time
|
||||
}.getOrNull() ?: 0L
|
||||
|
||||
return chapter
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IMG_URL = "https://scansmangas.me"
|
||||
val dateFormat by lazy {
|
||||
SimpleDateFormat("d MMM. yyyy", Locale.US)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.fr.jpmangas
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
|
||||
class Jpmangas : MMRCMS(
|
||||
"Jpmangas",
|
||||
"https://jpmangas.xyz",
|
||||
"fr",
|
||||
supportsAdvancedSearch = false,
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.id.komikid
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
|
||||
class Komikid : MMRCMS(
|
||||
"Komikid",
|
||||
"https://www.komikid.com",
|
||||
"id",
|
||||
supportsAdvancedSearch = false,
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.fr.lelscanvf
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
|
||||
class LelscanVF : MMRCMS(
|
||||
"Lelscan-VF",
|
||||
"https://lelscanvf.cc",
|
||||
"fr",
|
||||
supportsAdvancedSearch = false,
|
||||
)
|
|
@ -1,55 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.fr.mangafr
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class MangaFR : MMRCMS("Manga-FR", "https://manga-fr.cc", "fr") {
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return super.mangaDetailsParse(response).apply {
|
||||
title = title.replace("Chapitres ", "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun nullableChapterFromElement(element: Element): SChapter? {
|
||||
val chapter = SChapter.create()
|
||||
|
||||
val titleWrapper = element.select("[class^=chapter-title-rtl]").first()!!
|
||||
val chapterElement = titleWrapper.getElementsByTag("a")!!
|
||||
val url = chapterElement.attr("href")
|
||||
|
||||
chapter.url = getUrlWithoutBaseUrl(url)
|
||||
|
||||
// Construct chapter names
|
||||
// Before -> Scan <manga_name> <chapter_number> VF: <chapter_number>
|
||||
// Now -> Chapitre <chapter_number> : <chapter_title> OR Chapitre <chapter_number>
|
||||
val chapterText = chapterElement.text()
|
||||
val numberRegex = Regex("""[1-9]\d*(\.\d+)*""")
|
||||
val chapterNumber = numberRegex.find(chapterText)?.value.orEmpty()
|
||||
val chapterTitle = titleWrapper.getElementsByTag("em")!!.text()
|
||||
if (chapterTitle.toIntOrNull() != null) {
|
||||
chapter.name = "Chapitre $chapterNumber"
|
||||
} else {
|
||||
chapter.name = "Chapitre $chapterNumber : $chapterTitle"
|
||||
}
|
||||
|
||||
// Parse date
|
||||
val dateText = element.getElementsByClass("date-chapter-title-rtl").text().trim()
|
||||
|
||||
chapter.date_upload = runCatching {
|
||||
dateFormat.parse(dateText)?.time
|
||||
}.getOrNull() ?: 0L
|
||||
|
||||
return chapter
|
||||
}
|
||||
|
||||
companion object {
|
||||
val dateFormat by lazy {
|
||||
SimpleDateFormat("d MMM. yyyy", Locale.US)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,21 +3,18 @@ package eu.kanade.tachiyomi.extension.fr.mangascan
|
|||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class MangaScan : MMRCMS("Manga-Scan", "https://mangascan-fr.com", "fr") {
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return super.mangaDetailsParse(response).apply {
|
||||
title = title.substringBefore("Chapitres en ligne").substringAfter("Scan").trim()
|
||||
}
|
||||
}
|
||||
|
||||
class MangaScan : MMRCMS(
|
||||
"Manga-Scan",
|
||||
"https://mangascan-fr.com",
|
||||
"fr",
|
||||
supportsAdvancedSearch = false,
|
||||
detailsTitleSelector = "div.col-sm-12 h1",
|
||||
) {
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", baseUrl)
|
||||
.set("Referer", "$baseUrl/")
|
||||
.set("Accept", "image/avif,image/webp,*/*")
|
||||
.build()
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package eu.kanade.tachiyomi.extension.es.mangasin
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMSUtils
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.SuggestionDto
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
@ -13,18 +14,19 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import org.jsoup.nodes.Document
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class MangasIn : MMRCMS("Mangas.in", "https://mangas.in", "es") {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
class MangasIn : MMRCMS(
|
||||
"Mangas.in",
|
||||
"https://mangas.in",
|
||||
"es",
|
||||
supportsAdvancedSearch = false,
|
||||
) {
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 1, 1)
|
||||
.build()
|
||||
|
@ -32,6 +34,57 @@ class MangasIn : MMRCMS("Mangas.in", "https://mangas.in", "es") {
|
|||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lasted?p=$page", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
runCatching { fetchFilterOptions() }
|
||||
|
||||
val data = json.decodeFromString<LatestUpdateResponse>(response.body.string())
|
||||
val manga = data.data.map {
|
||||
SManga.create().apply {
|
||||
url = "/$itemPath/${it.slug}"
|
||||
title = it.name
|
||||
thumbnail_url = MMRCMSUtils.guessCover(baseUrl, url, null)
|
||||
}
|
||||
}
|
||||
val hasNextPage = response.request.url.queryParameter("p")!!.toInt() < data.totalPages
|
||||
|
||||
return MangasPage(manga, hasNextPage)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isEmpty()) {
|
||||
return super.searchMangaRequest(page, query, filters)
|
||||
}
|
||||
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("q", query)
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val searchType = response.request.url.pathSegments.last()
|
||||
|
||||
if (searchType != "search") {
|
||||
return super.searchMangaParse(response)
|
||||
}
|
||||
|
||||
searchDirectory = json.decodeFromString<List<SuggestionDto>>(response.body.string())
|
||||
|
||||
return parseSearchDirectory(1)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = super.mangaDetailsParse(document).apply {
|
||||
status = when (document.selectFirst("div.manga-name span.label")?.text()?.lowercase()) {
|
||||
in detailStatusComplete -> SManga.COMPLETED
|
||||
in detailStatusOngoing -> SManga.ONGOING
|
||||
in detailStatusDropped -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private var key = ""
|
||||
|
||||
private fun getKey(): String {
|
||||
|
@ -43,41 +96,6 @@ class MangasIn : MMRCMS("Mangas.in", "https://mangas.in", "es") {
|
|||
?: throw Exception("No se pudo encontrar la clave")
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url: Uri.Builder
|
||||
when {
|
||||
query.isNotBlank() -> {
|
||||
url = Uri.parse("$baseUrl/search")!!.buildUpon()
|
||||
url.appendQueryParameter("q", query)
|
||||
}
|
||||
else -> {
|
||||
url = Uri.parse("$baseUrl/filterList?page=$page")!!.buildUpon()
|
||||
filters.filterIsInstance<UriFilter>()
|
||||
.forEach { it.addToUri(url) }
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
return if (listOf("query", "q").any { it in response.request.url.queryParameterNames }) {
|
||||
val searchResult = json.decodeFromString<List<SearchResult>>(response.body.string())
|
||||
MangasPage(
|
||||
searchResult
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
url = getUrlWithoutBaseUrl(itemUrl + it.slug)
|
||||
title = it.name
|
||||
thumbnail_url = "$baseUrl/uploads/manga/${it.slug}/cover/cover_250x350.jpg"
|
||||
}
|
||||
},
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
internalMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val mangaUrl = document.location().removeSuffix("/")
|
||||
|
@ -100,7 +118,12 @@ class MangasIn : MMRCMS("Mangas.in", "https://mangas.in", "es") {
|
|||
|
||||
return chapters.map {
|
||||
SChapter.create().apply {
|
||||
name = "Capítulo ${it.number}: ${it.name}"
|
||||
name = if (it.name == "Capítulo ${it.number}") {
|
||||
it.name
|
||||
} else {
|
||||
"Capítulo ${it.number}: ${it.name}"
|
||||
}
|
||||
|
||||
date_upload = it.createdAt.parseDate()
|
||||
setUrlWithoutDomain("$mangaUrl/${it.slug}")
|
||||
}
|
||||
|
|
|
@ -15,7 +15,13 @@ data class Chapter(
|
|||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchResult(
|
||||
@SerialName("value") val name: String,
|
||||
@SerialName("data") val slug: String,
|
||||
data class LatestManga(
|
||||
@SerialName("manga_name") val name: String,
|
||||
@SerialName("manga_slug") val slug: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LatestUpdateResponse(
|
||||
val data: List<LatestManga>,
|
||||
val totalPages: Int,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.onma
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class Onma : MMRCMS(
|
||||
"مانجا اون لاين",
|
||||
"https://onma.top",
|
||||
"ar",
|
||||
detailsTitleSelector = ".panel-heading",
|
||||
) {
|
||||
override fun searchMangaSelector() = "div.chapter-container"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return super.mangaDetailsParse(document).apply {
|
||||
document.select(".panel-body h3").forEach { element ->
|
||||
when (element.ownText().lowercase().removeSuffix(" :")) {
|
||||
in detailAuthor -> author = element.selectFirst("div.text")!!.text()
|
||||
in detailArtist -> artist = element.selectFirst("div.text")!!.text()
|
||||
in detailGenre -> genre = element.select("div.text a").joinToString { it.text() }
|
||||
in detailStatus -> status = when (element.selectFirst("div.text")!!.text()) {
|
||||
in detailStatusComplete -> SManga.COMPLETED
|
||||
in detailStatusOngoing -> SManga.ONGOING
|
||||
in detailStatusDropped -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 59 KiB |
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.en.readcomicsonline
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
|
||||
class ReadComicsOnline : MMRCMS(
|
||||
"Read Comics Online",
|
||||
"https://readcomicsonline.ru",
|
||||
"en",
|
||||
itemPath = "comic",
|
||||
)
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 69 KiB |
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.extension.fr.scanvf
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
||||
|
||||
class ScanVF : MMRCMS(
|
||||
"Scan VF",
|
||||
"https://www.scan-vf.net",
|
||||
"fr",
|
||||
supportsAdvancedSearch = false,
|
||||
)
|
|
@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS
|
|||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Request
|
||||
|
||||
class Utsukushii : MMRCMS("Utsukushii", "https://manga.utsukushii-bg.com", "bg") {
|
||||
class Utsukushii : MMRCMS("Utsukushii", "https://utsukushii-bg.com", "bg") {
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga-list", headers)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mmrcms
|
||||
|
||||
/**
|
||||
* A class similar to [kotlin.Nothing].
|
||||
*
|
||||
* This class has no instances, and is used as a placeholder
|
||||
* for hacking in forced named arguments, similar to Python's
|
||||
* `kwargs`.
|
||||
*
|
||||
* This is used instead of [kotlin.Nothing] because that class
|
||||
* is specifically forbidden from being a vararg parameter.
|
||||
*/
|
||||
class Forbidden private constructor()
|
|
@ -1,8 +1,11 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mmrcms
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMSUtils.imgAttr
|
||||
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMSUtils.textWithNewlines
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
@ -10,520 +13,464 @@ 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.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import rx.Subscription
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
abstract class MMRCMS(
|
||||
/**
|
||||
* @param dateFormat The date format used for parsing chapter dates.
|
||||
* @param itemPath The path used in the URL for entries.
|
||||
* @param fetchFilterOptions Whether to fetch filtering options (categories, types, tags).
|
||||
* @param supportsAdvancedSearch Whether the source supports advanced search under /advanced-search.
|
||||
* @param detailsTitleSelector Selector for the entry's title in its details page.
|
||||
* @param chapterNamePrefix A word that always precedes the chapter title, e.g. "Scan "
|
||||
* @param chapterString The word for "Chapter" in the source's language.
|
||||
*/
|
||||
abstract class MMRCMS
|
||||
@Suppress("UNUSED")
|
||||
constructor(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
sourceInfo: String = "",
|
||||
) : HttpSource() {
|
||||
open val jsonData = if (sourceInfo == "") {
|
||||
SourceData.giveMetaData(baseUrl)
|
||||
} else {
|
||||
sourceInfo
|
||||
final override val lang: String,
|
||||
|
||||
vararg useNamedArgumentsBelow: Forbidden,
|
||||
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("d MMM. yyyy", Locale.US),
|
||||
protected val itemPath: String = "manga",
|
||||
private val fetchFilterOptions: Boolean = true,
|
||||
private val supportsAdvancedSearch: Boolean = true,
|
||||
private val detailsTitleSelector: String = ".listmanga-header, .widget-title",
|
||||
private val chapterNamePrefix: String = "",
|
||||
private val chapterString: String = when (lang) {
|
||||
"es" -> "Capítulo"
|
||||
"fr" -> "Chapitre"
|
||||
else -> "Chapter"
|
||||
},
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
protected val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/filterList?page=$page&sortBy=views&asc=false")
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
runCatching { fetchFilterOptions() }
|
||||
return super.popularMangaParse(response)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = searchMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
||||
|
||||
/**
|
||||
* Parse a List of JSON sources into a list of `MyMangaReaderCMSSource`s
|
||||
*
|
||||
* Example JSON :
|
||||
* ```
|
||||
* {
|
||||
* "language": "en",
|
||||
* "name": "Example manga reader",
|
||||
* "base_url": "https://example.com",
|
||||
* "supports_latest": true,
|
||||
* "item_url": "https://example.com/manga/",
|
||||
* "categories": [
|
||||
* {"id": "stuff", "name": "Stuff"},
|
||||
* {"id": "test", "name": "Test"}
|
||||
* ],
|
||||
* "tags": [
|
||||
* {"id": "action", "name": "Action"},
|
||||
* {"id": "adventure", "name": "Adventure"}
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
*
|
||||
* Sources that do not supports tags may use `null` instead of a list of json objects
|
||||
*
|
||||
* @param sourceString The List of JSON strings 1 entry = one source
|
||||
* @return The list of parsed sources
|
||||
*
|
||||
* isNSFW, language, name and base_url are no longer needed as that is handled by multisrc
|
||||
* supports_latest, item_url, categories and tags are still needed
|
||||
*
|
||||
*
|
||||
* A cache of all titles that have already appeared in latest updates.
|
||||
*/
|
||||
private val json: Json by injectLazy()
|
||||
val jsonObject = json.decodeFromString<JsonObject>(jsonData)
|
||||
override val supportsLatest = jsonObject["supports_latest"]!!.jsonPrimitive.boolean
|
||||
open val itemUrl = jsonObject["item_url"]!!.jsonPrimitive.content
|
||||
open val categoryMappings = mapToPairs(jsonObject["categories"]!!.jsonArray)
|
||||
open var tagMappings = jsonObject["tags"]?.jsonArray?.let { mapToPairs(it) } ?: emptyList()
|
||||
|
||||
/**
|
||||
* Map an array of JSON objects to pairs. Each JSON object must have
|
||||
* the following properties:
|
||||
*
|
||||
* id: first item in pair
|
||||
* name: second item in pair
|
||||
*
|
||||
* @param array The array to process
|
||||
* @return The new list of pairs
|
||||
*/
|
||||
open fun mapToPairs(array: JsonArray): List<Pair<String, String>> = array.map {
|
||||
it as JsonObject
|
||||
|
||||
it["id"]!!.jsonPrimitive.content to it["name"]!!.jsonPrimitive.content
|
||||
}
|
||||
|
||||
private val itemUrlPath = Uri.parse(itemUrl).pathSegments.firstOrNull()
|
||||
private val parsedBaseUrl = Uri.parse(baseUrl)
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/filterList?page=$page&sortBy=views&asc=false", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url: Uri.Builder
|
||||
when {
|
||||
query.isNotBlank() -> {
|
||||
url = Uri.parse("$baseUrl/search")!!.buildUpon()
|
||||
url.appendQueryParameter("query", query)
|
||||
}
|
||||
else -> {
|
||||
url = Uri.parse("$baseUrl/filterList?page=$page")!!.buildUpon()
|
||||
filters.filterIsInstance<UriFilter>()
|
||||
.forEach { it.addToUri(url) }
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the usual search engine isn't available, search through the list of titles with this
|
||||
*/
|
||||
private fun selfSearch(query: String): Observable<MangasPage> {
|
||||
return client.newCall(GET("$baseUrl/changeMangaList?type=text", headers))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val mangas = response.asJsoup().select("ul.manga-list a").toList()
|
||||
.filter { it.text().contains(query, ignoreCase = true) }
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
title = it.text()
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
thumbnail_url = coverGuess(null, it.attr("abs:href"))
|
||||
}
|
||||
}
|
||||
MangasPage(mangas, false)
|
||||
}
|
||||
}
|
||||
private val latestTitles = mutableSetOf<String>()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/latest-release?page=$page", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response) = internalMangaParse(response)
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
return if (listOf("query", "q").any { it in response.request.url.queryParameterNames }) {
|
||||
// If a search query was specified, use search instead!
|
||||
val jsonArray = json.decodeFromString<JsonObject>(response.body.string()).let {
|
||||
it["suggestions"]!!.jsonArray
|
||||
}
|
||||
MangasPage(
|
||||
jsonArray
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
val segment = it.jsonObject["data"]!!.jsonPrimitive.content
|
||||
url = getUrlWithoutBaseUrl(itemUrl + segment)
|
||||
title = it.jsonObject["value"]!!.jsonPrimitive.content
|
||||
|
||||
// Guess thumbnails
|
||||
// thumbnail_url = "$baseUrl/uploads/manga/$segment/cover/cover_250x350.jpg"
|
||||
}
|
||||
},
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
internalMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
private val latestTitles = mutableSetOf<String>()
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
runCatching { fetchFilterOptions() }
|
||||
|
||||
val document = response.asJsoup()
|
||||
|
||||
if (document.location().contains("page=1")) latestTitles.clear()
|
||||
if (response.request.url.queryParameter("page") == "1") {
|
||||
latestTitles.clear()
|
||||
}
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector())
|
||||
.let { elements ->
|
||||
when {
|
||||
// List layout (most sources)
|
||||
elements.select("a[href]").firstOrNull()?.hasText() == true -> elements.map { latestUpdatesFromElement(it, "a[href]") }
|
||||
// Grid layout (e.g. MangaID)
|
||||
else -> document.select(gridLatestUpdatesSelector()).map { gridLatestUpdatesFromElement(it) }
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
val manga = document.select(latestUpdatesSelector()).mapNotNull {
|
||||
val item = latestUpdatesFromElement(it)
|
||||
|
||||
return MangasPage(mangas, document.selectFirst(latestUpdatesNextPageSelector()) != null)
|
||||
}
|
||||
private fun latestUpdatesSelector() = "div.mangalist div.manga-item"
|
||||
private fun latestUpdatesNextPageSelector() = "a[rel=next]"
|
||||
protected open fun latestUpdatesFromElement(element: Element, urlSelector: String): SManga? {
|
||||
return element.select(urlSelector).first()!!.let { titleElement ->
|
||||
if (titleElement.text() in latestTitles) {
|
||||
if (latestTitles.contains(item.url)) {
|
||||
null
|
||||
} else {
|
||||
latestTitles.add(titleElement.text())
|
||||
SManga.create().apply {
|
||||
url = titleElement.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||
title = titleElement.text().trim()
|
||||
thumbnail_url = "$baseUrl/uploads/manga/${url.substringAfterLast('/')}/cover/cover_250x350.jpg"
|
||||
}
|
||||
latestTitles.add(item.url)
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun gridLatestUpdatesSelector() = "div.mangalist div.manga-item, div.grid-manga tr"
|
||||
protected open fun gridLatestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
|
||||
element.select("a.chart-title").let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let {
|
||||
document.selectFirst(it)
|
||||
} != null
|
||||
|
||||
return MangasPage(manga, hasNextPage)
|
||||
}
|
||||
|
||||
protected open fun internalMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
override fun latestUpdatesSelector() = "div.mangalist div.manga-item"
|
||||
|
||||
val internalMangaSelector = when (name) {
|
||||
"Utsukushii" -> "div.content div.col-sm-6"
|
||||
else -> "div[class^=col-sm], div.col-xs-6"
|
||||
}
|
||||
return MangasPage(
|
||||
document.select(internalMangaSelector).map {
|
||||
SManga.create().apply {
|
||||
val urlElement = it.getElementsByClass("chart-title")
|
||||
if (urlElement.size == 0) {
|
||||
url = getUrlWithoutBaseUrl(it.select("a").attr("href"))
|
||||
title = it.select("div.caption").text()
|
||||
it.select("div.caption div").text().let { if (it.isNotEmpty()) title = title.substringBefore(it) } // To clean submanga's titles without breaking hentaishark's
|
||||
} else {
|
||||
url = getUrlWithoutBaseUrl(urlElement.attr("href"))
|
||||
title = urlElement.text().trim()
|
||||
}
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
it.select("img").let { img ->
|
||||
thumbnail_url = when {
|
||||
it.hasAttr("data-background-image") -> it.attr("data-background-image") // Utsukushii
|
||||
img.hasAttr("data-src") -> coverGuess(img.attr("abs:data-src"), url)
|
||||
else -> coverGuess(img.attr("abs:src"), url)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
document.select(".pagination a[rel=next]").isNotEmpty(),
|
||||
)
|
||||
}
|
||||
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
|
||||
|
||||
// Guess thumbnails on broken websites
|
||||
fun coverGuess(url: String?, mangaUrl: String): String? {
|
||||
return if (url?.endsWith("no-image.png") == true) {
|
||||
"$baseUrl/uploads/manga/${mangaUrl.substringAfterLast('/')}/cover/cover_250x350.jpg"
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
protected var searchDirectory = emptyList<SuggestionDto>()
|
||||
|
||||
fun getUrlWithoutBaseUrl(newUrl: String): String {
|
||||
val parsedNewUrl = Uri.parse(newUrl)
|
||||
val newPathSegments = parsedNewUrl.pathSegments.toMutableList()
|
||||
private var searchQuery = ""
|
||||
|
||||
for (i in parsedBaseUrl.pathSegments) {
|
||||
if (i.trim().equals(newPathSegments.first(), true)) {
|
||||
newPathSegments.removeAt(0)
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
return if (query.isNotEmpty()) {
|
||||
if (page == 1 && query != searchQuery) {
|
||||
searchQuery = query
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { searchMangaParse(it) }
|
||||
} else {
|
||||
break
|
||||
Observable.just(parseSearchDirectory(page))
|
||||
}
|
||||
} else {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
val builtUrl = parsedNewUrl.buildUpon().path("/")
|
||||
newPathSegments.forEach { builtUrl.appendPath(it) }
|
||||
|
||||
var out = builtUrl.build().encodedPath!!
|
||||
if (parsedNewUrl.encodedQuery != null) {
|
||||
out += "?" + parsedNewUrl.encodedQuery
|
||||
}
|
||||
if (parsedNewUrl.encodedFragment != null) {
|
||||
out += "#" + parsedNewUrl.encodedFragment
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
val filterList = filters.ifEmpty { getFilterList() }
|
||||
|
||||
if (query.isNotEmpty()) {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("query", query)
|
||||
} else {
|
||||
addPathSegment(if (supportsAdvancedSearch) "advanced-search" else "filterList")
|
||||
addQueryParameter("page", page.toString())
|
||||
filterList.filterIsInstance<UriFilter>().forEach { it.addToUri(this) }
|
||||
}
|
||||
}.build()
|
||||
|
||||
return if (query.isEmpty() && supportsAdvancedSearch) {
|
||||
GET(url.toString().replaceFirst("?", "#"), headers)
|
||||
} else {
|
||||
GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
private val searchTokenRegex = Regex("""['"]_token['"]\s*:\s*['"]([0-9A-Za-z]+)['"]""")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
runCatching { fetchFilterOptions() }
|
||||
|
||||
val searchType = response.request.url.pathSegments.last()
|
||||
|
||||
if (searchType == "filterList") {
|
||||
return super.searchMangaParse(response)
|
||||
}
|
||||
|
||||
if (searchType == "advanced-search") {
|
||||
val document = response.asJsoup()
|
||||
val fragment = response.request.url.fragment!!
|
||||
val body = FormBody.Builder().apply {
|
||||
val page = fragment.substringAfter("page=").substringBefore("&")
|
||||
|
||||
add("params", fragment.substringAfter("page=$page&"))
|
||||
add("page", page)
|
||||
|
||||
document.selectFirst("script:containsData(_token)")?.data()?.let {
|
||||
add("_token", searchTokenRegex.find(it)!!.groupValues[1])
|
||||
}
|
||||
}.build()
|
||||
val request = POST("$baseUrl/advSearchFilter", headers, body)
|
||||
|
||||
return super.searchMangaParse(client.newCall(request).execute())
|
||||
}
|
||||
|
||||
searchDirectory = json.decodeFromString<SearchResultDto>(response.body.string()).suggestions
|
||||
return parseSearchDirectory(1)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.media"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
val anchor = element.selectFirst(".media-heading a, .manga-heading a")!!
|
||||
|
||||
setUrlWithoutDomain(anchor.attr("href"))
|
||||
title = anchor.text()
|
||||
thumbnail_url = MMRCMSUtils.guessCover(baseUrl, url, element.selectFirst("img")?.imgAttr())
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = ".pagination a[rel=next]"
|
||||
|
||||
protected fun parseSearchDirectory(page: Int): MangasPage {
|
||||
val manga = mutableListOf<SManga>()
|
||||
val endRange = ((page * 24) - 1).let { if (it <= searchDirectory.lastIndex) it else searchDirectory.lastIndex }
|
||||
|
||||
for (i in (((page - 1) * 24)..endRange)) {
|
||||
manga.add(
|
||||
SManga.create().apply {
|
||||
url = "/$itemPath/${searchDirectory[i].data}"
|
||||
title = searchDirectory[i].value
|
||||
thumbnail_url = MMRCMSUtils.guessCover(baseUrl, url, null)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return MangasPage(manga, endRange < searchDirectory.lastIndex)
|
||||
}
|
||||
|
||||
protected val detailAuthor = hashSetOf("author(s)", "autor(es)", "auteur(s)", "著作", "yazar(lar)", "mangaka(lar)", "pengarang/penulis", "pengarang", "penulis", "autor", "المؤلف", "перевод", "autor/autorzy")
|
||||
protected val detailArtist = hashSetOf("artist(s)", "artiste(s)", "sanatçi(lar)", "artista(s)", "artist(s)/ilustrator", "الرسام", "seniman", "rysownik/rysownicy", "artista")
|
||||
protected val detailGenre = hashSetOf("categories", "categorías", "catégories", "ジャンル", "kategoriler", "categorias", "kategorie", "التصنيفات", "жанр", "kategori", "tagi", "género")
|
||||
protected val detailStatus = hashSetOf("status", "statut", "estado", "状態", "durum", "الحالة", "статус")
|
||||
protected val detailStatusComplete = hashSetOf("complete", "مكتملة", "complet", "completo", "zakończone", "concluído", "finalizado")
|
||||
protected val detailStatusOngoing = hashSetOf("ongoing", "مستمرة", "en cours", "em lançamento", "prace w toku", "ativo", "em andamento", "activo")
|
||||
protected val detailStatusDropped = hashSetOf("dropped")
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
document.select("h2.listmanga-header, h2.widget-title").firstOrNull()?.text()?.trim()?.let { title = it }
|
||||
thumbnail_url = coverGuess(document.select(".row [class^=img-responsive]").firstOrNull()?.attr("abs:src"), document.location())
|
||||
description = document.select(".row .well p").text().trim()
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.selectFirst(detailsTitleSelector)!!.text()
|
||||
thumbnail_url = MMRCMSUtils.guessCover(
|
||||
baseUrl,
|
||||
document.location(),
|
||||
document.selectFirst(".row img.img-responsive")?.imgAttr(),
|
||||
)
|
||||
description = document.select(".row .well").let {
|
||||
it.select("h5").remove()
|
||||
it.textWithNewlines()
|
||||
}
|
||||
|
||||
val detailAuthor = setOf("author(s)", "autor(es)", "auteur(s)", "著作", "yazar(lar)", "mangaka(lar)", "pengarang/penulis", "pengarang", "penulis", "autor", "المؤلف", "перевод", "autor/autorzy")
|
||||
val detailArtist = setOf("artist(s)", "artiste(s)", "sanatçi(lar)", "artista(s)", "artist(s)/ilustrator", "الرسام", "seniman", "rysownik/rysownicy")
|
||||
val detailGenre = setOf("categories", "categorías", "catégories", "ジャンル", "kategoriler", "categorias", "kategorie", "التصنيفات", "жанр", "kategori", "tagi")
|
||||
val detailStatus = setOf("status", "statut", "estado", "状態", "durum", "الحالة", "статус")
|
||||
val detailStatusComplete = setOf("complete", "مكتملة", "complet", "completo", "zakończone", "concluído")
|
||||
val detailStatusOngoing = setOf("ongoing", "مستمرة", "en cours", "em lançamento", "prace w toku", "ativo", "em andamento")
|
||||
val detailDescription = setOf("description", "resumen")
|
||||
|
||||
for (element in document.select(".row .dl-horizontal dt")) {
|
||||
when (element.text().trim().lowercase().removeSuffix(":")) {
|
||||
document.select(".row .dl-horizontal dt").forEach { element ->
|
||||
when (element.text().lowercase().removeSuffix(":")) {
|
||||
in detailAuthor -> author = element.nextElementSibling()!!.text()
|
||||
in detailArtist -> artist = element.nextElementSibling()!!.text()
|
||||
in detailGenre -> genre = element.nextElementSibling()!!.select("a").joinToString {
|
||||
it.text().trim()
|
||||
it.text()
|
||||
}
|
||||
in detailStatus -> status = when (element.nextElementSibling()!!.text().trim().lowercase()) {
|
||||
in detailStatus -> status = when (element.nextElementSibling()!!.text().lowercase()) {
|
||||
in detailStatusComplete -> SManga.COMPLETED
|
||||
in detailStatusOngoing -> SManga.ONGOING
|
||||
in detailStatusDropped -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
// When details are in a .panel instead of .row (ES sources)
|
||||
for (element in document.select("div.panel span.list-group-item")) {
|
||||
when (element.select("b").text().lowercase().substringBefore(":")) {
|
||||
in detailAuthor -> author = element.select("b + a").text()
|
||||
in detailArtist -> artist = element.select("b + a").text()
|
||||
in detailGenre -> genre = element.getElementsByTag("a").joinToString {
|
||||
it.text().trim()
|
||||
}
|
||||
in detailStatus -> status = when (element.select("b + span.label").text().lowercase()) {
|
||||
in detailStatusComplete -> SManga.COMPLETED
|
||||
in detailStatusOngoing -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
in detailDescription -> description = element.ownText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* Overriden to allow for null chapters
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).mapNotNull { nullableChapterFromElement(it) }
|
||||
val title = document.selectFirst(detailsTitleSelector)!!.text()
|
||||
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, title) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul.chapters > li:not(.btn)"
|
||||
|
||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
protected open fun chapterFromElement(element: Element, mangaTitle: String) = SChapter.create().apply {
|
||||
val titleWrapper = element.selectFirst(".chapter-title-rtl")!!
|
||||
val anchor = titleWrapper.selectFirst("a")!!
|
||||
|
||||
setUrlWithoutDomain(anchor.attr("href"))
|
||||
name = cleanChapterName(mangaTitle, titleWrapper.text())
|
||||
date_upload = runCatching {
|
||||
val date = element.selectFirst(".date-chapter-title-rtl")!!.text()
|
||||
|
||||
dateFormat.parse(date)!!.time
|
||||
}.getOrDefault(0L)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||
* The word for "Chapter" in your language.
|
||||
*/
|
||||
protected open fun chapterListSelector() = "ul[class^=chapters] > li:not(.btn), table.table tr"
|
||||
// Some websites add characters after "chapters" thus the need of checking classes that starts with "chapters"
|
||||
|
||||
/**
|
||||
* titleWrapper can have multiple "a" elements, filter to the first that contains letters (i.e. not "" or # as is possible)
|
||||
* Function to clean up chapter names. Mostly useful for sites that
|
||||
* don't know what a chapter title is and do "One Piece 1234 : Chapter 1234".
|
||||
*/
|
||||
private val urlRegex = Regex("""[a-zA-z]""")
|
||||
protected open fun cleanChapterName(mangaTitle: String, name: String): String {
|
||||
val initialName = name.replaceFirst(chapterNamePrefix + mangaTitle, chapterString)
|
||||
|
||||
/**
|
||||
* Returns a chapter from the given element.
|
||||
*
|
||||
* @param element an element obtained from [chapterListSelector].
|
||||
*/
|
||||
protected open fun nullableChapterFromElement(element: Element): SChapter? {
|
||||
val chapter = SChapter.create()
|
||||
val splits = initialName.split(":", limit = 2).map { it.trim() }
|
||||
|
||||
try {
|
||||
val titleWrapper = element.select("[class^=chapter-title-rtl]").first()!!
|
||||
// Some websites add characters after "..-rtl" thus the need of checking classes that starts with that
|
||||
val url = titleWrapper.getElementsByTag("a")
|
||||
.first { it.attr("href").contains(urlRegex) }
|
||||
.attr("href")
|
||||
|
||||
// Ensure chapter actually links to a manga
|
||||
// Some websites use the chapters box to link to post announcements
|
||||
// The check is skipped if mangas are stored in the root of the website (ex '/one-piece' without a segment like '/manga/one-piece')
|
||||
if (itemUrlPath != null && !Uri.parse(url).pathSegments.firstOrNull().equals(itemUrlPath, true)) {
|
||||
return null
|
||||
}
|
||||
|
||||
chapter.url = getUrlWithoutBaseUrl(url)
|
||||
chapter.name = titleWrapper.text()
|
||||
|
||||
// Parse date
|
||||
val dateText = element.getElementsByClass("date-chapter-title-rtl").text().trim()
|
||||
chapter.date_upload = parseDate(dateText)
|
||||
|
||||
return chapter
|
||||
} catch (e: NullPointerException) {
|
||||
// For chapter list in a table
|
||||
if (element.select("td").hasText()) {
|
||||
element.select("td a").let {
|
||||
chapter.setUrlWithoutDomain(it.attr("href"))
|
||||
chapter.name = it.text()
|
||||
}
|
||||
val tableDateText = element.select("td + td").text()
|
||||
chapter.date_upload = parseDate(tableDateText)
|
||||
|
||||
return chapter
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseDate(dateText: String): Long {
|
||||
return try {
|
||||
DATE_FORMAT.parse(dateText)?.time ?: 0
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
return if (splits[0] == splits[1]) {
|
||||
splits[0]
|
||||
} else {
|
||||
"${splits[0]}: ${splits[1]}"
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response) = response.asJsoup().select("#all > .img-responsive")
|
||||
.mapIndexed { i, e ->
|
||||
var url = (if (e.hasAttr("data-src")) e.attr("abs:data-src") else e.attr("abs:src")).trim()
|
||||
|
||||
Page(i, response.request.url.toString(), url)
|
||||
override fun pageListParse(document: Document) =
|
||||
document.select("#all > img.img-responsive").mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.imgAttr())
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
private fun getInitialFilterList() = listOf<Filter<*>>(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
AuthorFilter(),
|
||||
UriSelectFilter(
|
||||
"Category",
|
||||
"cat",
|
||||
arrayOf(
|
||||
"" to "Any",
|
||||
*categoryMappings.toTypedArray(),
|
||||
),
|
||||
),
|
||||
UriSelectFilter(
|
||||
"Begins with",
|
||||
"alpha",
|
||||
arrayOf(
|
||||
"" to "Any",
|
||||
*"#ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray().map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.toTypedArray(),
|
||||
),
|
||||
),
|
||||
SortFilter(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList(): FilterList {
|
||||
return when {
|
||||
tagMappings != emptyList<Pair<String, String>>() -> {
|
||||
FilterList(
|
||||
getInitialFilterList() + UriSelectFilter(
|
||||
"Tag",
|
||||
"tag",
|
||||
val filters = buildList<Filter<*>> {
|
||||
add(Filter.Header("Note: Ignored if using text search!"))
|
||||
|
||||
if (supportsAdvancedSearch) {
|
||||
if (fetchFilterOptions && fetchFiltersAttempts > 0 && fetchFiltersFailed) {
|
||||
add(Filter.Header("Press 'Reset' to attempt to show filter options"))
|
||||
}
|
||||
|
||||
add(Filter.Separator())
|
||||
|
||||
if (categories.isNotEmpty()) {
|
||||
add(
|
||||
UriMultiSelectFilter(
|
||||
"Categories",
|
||||
"categories[]",
|
||||
categories.toTypedArray(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (statuses.isNotEmpty()) {
|
||||
add(
|
||||
UriMultiSelectFilter(
|
||||
"Statuses",
|
||||
"status[]",
|
||||
statuses.toTypedArray(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
add(
|
||||
UriMultiSelectFilter(
|
||||
"Types",
|
||||
"types[]",
|
||||
tags.toTypedArray(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
add(TextFilter("Year of release", "release"))
|
||||
add(TextFilter("Author", "author"))
|
||||
} else {
|
||||
if (fetchFilterOptions && fetchFiltersAttempts > 0 && fetchFiltersFailed) {
|
||||
add(Filter.Header("Press 'Reset' to attempt to show filter options"))
|
||||
}
|
||||
|
||||
add(Filter.Separator())
|
||||
|
||||
if (categories.isNotEmpty()) {
|
||||
add(
|
||||
UriPartFilter(
|
||||
"Category",
|
||||
"cat",
|
||||
arrayOf(
|
||||
"Any" to "",
|
||||
*categories.toTypedArray(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
add(
|
||||
UriPartFilter(
|
||||
"Title begins with",
|
||||
"alpha",
|
||||
arrayOf(
|
||||
"" to "Any",
|
||||
*tagMappings.toTypedArray(),
|
||||
"Any" to "",
|
||||
*"#ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray().map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.toTypedArray(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> FilterList(getInitialFilterList())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
||||
* If an entry is selected it is appended as a query parameter onto the end of the URI.
|
||||
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
|
||||
*/
|
||||
// vals: <name, display>
|
||||
open class UriSelectFilter(
|
||||
displayName: String,
|
||||
private val uriParam: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
private val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0,
|
||||
) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
if (state != 0 || !firstIsUnspecified) {
|
||||
uri.appendQueryParameter(uriParam, vals[state].first)
|
||||
if (tags.isNotEmpty()) {
|
||||
add(
|
||||
UriPartFilter(
|
||||
"Tag",
|
||||
"tag",
|
||||
arrayOf(
|
||||
"Any" to "",
|
||||
*tags.toTypedArray(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
add(SortFilter())
|
||||
}
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
class AuthorFilter : Filter.Text("Author"), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter("author", state)
|
||||
}
|
||||
}
|
||||
private var categories = emptyList<Pair<String, String>>()
|
||||
|
||||
class SortFilter :
|
||||
Filter.Sort(
|
||||
"Sort",
|
||||
sortables.map { it.second }.toTypedArray(),
|
||||
Selection(0, true),
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter("sortBy", sortables[state!!.index].first)
|
||||
uri.appendQueryParameter("asc", state!!.ascending.toString())
|
||||
private var statuses = emptyList<Pair<String, String>>()
|
||||
|
||||
private var tags = emptyList<Pair<String, String>>()
|
||||
|
||||
private var fetchFiltersFailed = false
|
||||
|
||||
private var fetchFiltersAttempts = 0
|
||||
|
||||
private val fetchFiltersLock = ReentrantLock()
|
||||
|
||||
protected open fun fetchFilterOptions(): Subscription = Single.fromCallable {
|
||||
if (!fetchFilterOptions) {
|
||||
return@fromCallable
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val sortables = arrayOf(
|
||||
"name" to "Name",
|
||||
"views" to "Popularity",
|
||||
"last_release" to "Last update",
|
||||
)
|
||||
fetchFiltersLock.lock()
|
||||
|
||||
if (fetchFiltersAttempts > 3 || (fetchFiltersAttempts > 0 && !fetchFiltersFailed)) {
|
||||
fetchFiltersLock.unlock()
|
||||
return@fromCallable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a filter that is able to modify a URI.
|
||||
*/
|
||||
interface UriFilter {
|
||||
fun addToUri(uri: Uri.Builder)
|
||||
}
|
||||
fetchFiltersFailed = try {
|
||||
if (supportsAdvancedSearch) {
|
||||
val document = client.newCall(GET("$baseUrl/advanced-search", headers)).execute().asJsoup()
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT = SimpleDateFormat("d MMM. yyyy", Locale.US)
|
||||
categories = document.select("select[name='categories[]'] option").map {
|
||||
it.text() to it.attr("value")
|
||||
}
|
||||
statuses = document.select("select[name='status[]'] option").map {
|
||||
it.text() to it.attr("value")
|
||||
}
|
||||
tags = document.select("select[name='types[]'] option").map {
|
||||
it.text() to it.attr("value")
|
||||
}
|
||||
} else {
|
||||
val document = client.newCall(GET("$baseUrl/$itemPath-list", headers)).execute().asJsoup()
|
||||
|
||||
categories = document.select("a.category").map {
|
||||
it.text() to it.attr("href").toHttpUrl().queryParameter("cat")!!
|
||||
}
|
||||
tags = document.select("div.tag-links a").map {
|
||||
it.text() to it.attr("href").toHttpUrl().pathSegments.last()
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
} catch (e: Throwable) {
|
||||
Log.e(name, "Could not fetch filtering options", e)
|
||||
true
|
||||
}
|
||||
|
||||
fetchFiltersAttempts++
|
||||
fetchFiltersLock.unlock()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mmrcms
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SearchResultDto(
|
||||
val suggestions: List<SuggestionDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SuggestionDto(
|
||||
val value: String,
|
||||
val data: String,
|
||||
)
|
|
@ -0,0 +1,73 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mmrcms
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
class TextFilter(name: String, private val param: String) : Filter.Text(name), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
builder.addQueryParameter(param, state)
|
||||
}
|
||||
}
|
||||
|
||||
class UriPartFilter(
|
||||
name: String,
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
private val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0,
|
||||
) : Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (state == 0 && firstIsUnspecified) {
|
||||
return
|
||||
}
|
||||
|
||||
builder.addQueryParameter(param, vals[state].second)
|
||||
}
|
||||
}
|
||||
|
||||
class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
class UriMultiSelectFilter(
|
||||
name: String,
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val checked = state.filter { it.state }
|
||||
|
||||
if (checked.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
checked.forEach { builder.addQueryParameter(param, it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter(selection: Selection = Selection(0, true)) :
|
||||
Filter.Sort(
|
||||
"Sort by",
|
||||
sortables.map { it.second }.toTypedArray(),
|
||||
selection,
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val state = state!!
|
||||
|
||||
builder.apply {
|
||||
addQueryParameter("sortBy", sortables[state.index].first)
|
||||
addQueryParameter("asc", state.ascending.toString())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val sortables = arrayOf(
|
||||
"name" to "Name",
|
||||
"views" to "Popularity",
|
||||
"last_release" to "Last update",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -9,26 +9,21 @@ class MMRCMSGenerator : ThemeSourceGenerator {
|
|||
|
||||
override val themeClass = "MMRCMS"
|
||||
|
||||
override val baseVersionCode = 7
|
||||
override val baseVersionCode = 8
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("مانجا اون لاين", "https://onma.top", "ar", className = "onma"),
|
||||
SingleLang("مانجا اون لاين", "https://onma.top", "ar", className = "Onma"),
|
||||
SingleLang("Read Comics Online", "https://readcomicsonline.ru", "en"),
|
||||
SingleLang("Scan FR", "https://www.scan-fr.org", "fr", overrideVersionCode = 2),
|
||||
SingleLang("Scan VF", "https://www.scan-vf.net", "fr", overrideVersionCode = 1),
|
||||
SingleLang("Komikid", "https://www.komikid.com", "id"),
|
||||
SingleLang("Mangadoor", "https://mangadoor.com", "es", overrideVersionCode = 1),
|
||||
SingleLang("Mangadoor", "https://mangadoor.com", "es", overrideVersionCode = 1, isNsfw = true),
|
||||
SingleLang("Mangas.in", "https://mangas.in", "es", isNsfw = true, className = "MangasIn", overrideVersionCode = 2),
|
||||
SingleLang("Utsukushii", "https://manga.utsukushii-bg.com", "bg", overrideVersionCode = 1),
|
||||
SingleLang("Phoenix-Scans", "https://phoenix-scans.pl", "pl", className = "PhoenixScans", overrideVersionCode = 1),
|
||||
SingleLang("Utsukushii", "https://utsukushii-bg.com", "bg", overrideVersionCode = 1),
|
||||
SingleLang("Lelscan-VF", "https://lelscanvf.cc", "fr", className = "LelscanVF", overrideVersionCode = 2),
|
||||
SingleLang("MangaID", "https://mangaid.click", "id", overrideVersionCode = 1),
|
||||
SingleLang("Jpmangas", "https://jpmangas.xyz", "fr", overrideVersionCode = 2),
|
||||
SingleLang("Manga-FR", "https://manga-fr.cc", "fr", className = "MangaFR", overrideVersionCode = 2),
|
||||
SingleLang("Manga-Scan", "https://mangascan-fr.com", "fr", className = "MangaScan", overrideVersionCode = 4),
|
||||
SingleLang("Bentoscan", "https://bentoscan.com", "fr"),
|
||||
// NOTE: THIS SOURCE CONTAINS A CUSTOM LANGUAGE SYSTEM (which will be ignored)!
|
||||
SingleLang("HentaiShark", "https://www.hentaishark.com", "all", isNsfw = true),
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,225 +0,0 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mmrcms
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.io.PrintWriter
|
||||
import java.security.cert.CertificateException
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* This class generates the sources for MMRCMS.
|
||||
* Credit to nulldev for writing the original shell script
|
||||
*
|
||||
* CMS: https://getcyberworks.com/product/manga-reader-cms/
|
||||
*/
|
||||
class MMRCMSJsonGen {
|
||||
// private var preRunTotal: String
|
||||
|
||||
init {
|
||||
System.setProperty("https.protocols", "TLSv1,TLSv1.1,TLSv1.2,TLSv1.3")
|
||||
// preRunTotal = Regex("""-> (\d+)""").findAll(File(relativePath).readText(Charsets.UTF_8)).last().groupValues[1]
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
fun generate() {
|
||||
val buffer = StringBuffer()
|
||||
val dateTime = ZonedDateTime.now()
|
||||
val formattedDate = dateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME)
|
||||
buffer.append("package eu.kanade.tachiyomi.multisrc.mmrcms")
|
||||
buffer.append("\n\n// GENERATED FILE, DO NOT MODIFY!\n// Generated $formattedDate\n\n")
|
||||
buffer.append("object SourceData {\n")
|
||||
buffer.append(" fun giveMetaData(url: String) = when (url) {\n")
|
||||
var number = 1
|
||||
sources.forEach {
|
||||
println("Generating ${it.name}")
|
||||
try {
|
||||
val advancedSearchDocument = getDocument("${it.baseUrl}/advanced-search", false)
|
||||
|
||||
var parseCategories = mutableListOf<Map<String, String>>()
|
||||
if (advancedSearchDocument != null) {
|
||||
parseCategories = parseCategories(advancedSearchDocument)
|
||||
}
|
||||
|
||||
val homePageDocument = getDocument(it.baseUrl)
|
||||
|
||||
val itemUrl = getItemUrl(homePageDocument, it.baseUrl)
|
||||
|
||||
var prefix = itemUrl.substringAfterLast("/").substringBeforeLast("/")
|
||||
|
||||
// Sometimes itemUrl is the root of the website, and thus the prefix found is the website address.
|
||||
// In this case, we set the default prefix as "manga".
|
||||
if (prefix.startsWith("www") || prefix.startsWith("wwv")) {
|
||||
prefix = "manga"
|
||||
}
|
||||
|
||||
val mangaListDocument = getDocument("${it.baseUrl}/$prefix-list")!!
|
||||
|
||||
if (parseCategories.isEmpty()) {
|
||||
parseCategories = parseCategories(mangaListDocument)
|
||||
}
|
||||
|
||||
val tags = parseTags(mangaListDocument)
|
||||
|
||||
val source = SourceDataModel(
|
||||
name = it.name,
|
||||
base_url = it.baseUrl,
|
||||
supports_latest = supportsLatest(it.baseUrl),
|
||||
item_url = "$itemUrl/",
|
||||
categories = parseCategories,
|
||||
tags = if (tags.size in 1..49) tags else null,
|
||||
)
|
||||
|
||||
if (!itemUrl.startsWith(it.baseUrl)) println("**Note: ${it.name} URL does not match! Check for changes: \n ${it.baseUrl} vs $itemUrl")
|
||||
|
||||
buffer.append(" \"${it.baseUrl}\" -> \"\"\"${Json.encodeToString(source)}\"\"\"\n")
|
||||
number++
|
||||
} catch (e: Exception) {
|
||||
println("error generating source ${it.name} ${e.printStackTrace()}")
|
||||
}
|
||||
}
|
||||
|
||||
buffer.append(" else -> \"\"\n")
|
||||
buffer.append(" }\n")
|
||||
buffer.append("}\n")
|
||||
// println("Pre-run sources: $preRunTotal")
|
||||
println("Post-run sources: ${number - 1}")
|
||||
PrintWriter(relativePath).use {
|
||||
it.write(buffer.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDocument(url: String, printStackTrace: Boolean = true): Document? {
|
||||
val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
|
||||
try {
|
||||
val request = Request.Builder().url(url)
|
||||
getOkHttpClient().newCall(request.build()).execute().let { response ->
|
||||
// Bypass Cloudflare ("Please wait 5 seconds" page)
|
||||
if (response.code == 503 && response.header("Server") in serverCheck) {
|
||||
var cookie = "${response.header("Set-Cookie")!!.substringBefore(";")}; "
|
||||
Jsoup.parse(response.body.string()).let { document ->
|
||||
val path = document.select("[id=\"challenge-form\"]").attr("action")
|
||||
val chk = document.select("[name=\"s\"]").attr("value")
|
||||
getOkHttpClient().newCall(Request.Builder().url("$url/$path?s=$chk").build()).execute().let { solved ->
|
||||
cookie += solved.header("Set-Cookie")!!.substringBefore(";")
|
||||
request.addHeader("Cookie", cookie).build().let {
|
||||
return Jsoup.parse(getOkHttpClient().newCall(it).execute().body.string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.code == 200) {
|
||||
return Jsoup.parse(response.body.string())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (printStackTrace) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseTags(mangaListDocument: Document): List<Map<String, String>> {
|
||||
val elements = mangaListDocument.select("div.tag-links a")
|
||||
return elements.map {
|
||||
mapOf(
|
||||
"id" to it.attr("href").substringAfterLast("/"),
|
||||
"name" to it.text(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getItemUrl(document: Document?, url: String): String {
|
||||
document ?: throw Exception("Couldn't get document for: $url")
|
||||
return document.toString().substringAfter("showURL = \"").substringAfter("showURL=\"").substringBefore("/SELECTION\";")
|
||||
|
||||
// Some websites like mangasyuri use javascript minifiers, and thus "showURL = " becomes "showURL="https://mangasyuri.net/manga/SELECTION""
|
||||
// (without spaces). Hence the double substringAfter.
|
||||
}
|
||||
|
||||
private fun supportsLatest(third: String): Boolean {
|
||||
val document = getDocument("$third/latest-release?page=1", false) ?: return false
|
||||
return document.select("div.mangalist div.manga-item a, div.grid-manga tr").isNotEmpty()
|
||||
}
|
||||
|
||||
private fun parseCategories(document: Document): MutableList<Map<String, String>> {
|
||||
val elements = document.select("select[name^=categories] option, a.category")
|
||||
return elements.mapIndexed { index, element ->
|
||||
mapOf(
|
||||
"id" to (index + 1).toString(),
|
||||
"name" to element.text(),
|
||||
)
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun getOkHttpClient(): OkHttpClient {
|
||||
// Create all-trusting host name verifier
|
||||
val trustAllCerts = arrayOf<TrustManager>(
|
||||
object : X509TrustManager {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
@Throws(CertificateException::class)
|
||||
override fun checkClientTrusted(chain: Array<java.security.cert.X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
@Throws(CertificateException::class)
|
||||
override fun checkServerTrusted(chain: Array<java.security.cert.X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> {
|
||||
return arrayOf()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Install the all-trusting trust manager
|
||||
val sc = SSLContext.getInstance("SSL").apply {
|
||||
init(null, trustAllCerts, java.security.SecureRandom())
|
||||
}
|
||||
val sslSocketFactory = sc.socketFactory
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
||||
.hostnameVerifier { _, _ -> true }
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class SourceDataModel(
|
||||
val name: String,
|
||||
val base_url: String,
|
||||
val supports_latest: Boolean,
|
||||
val item_url: String,
|
||||
val categories: List<Map<String, String>>,
|
||||
val tags: List<Map<String, String>>? = null,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val sources = MMRCMSGenerator().sources
|
||||
|
||||
val relativePath = System.getProperty("user.dir")!! + "/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mmrcms/SourceData.kt"
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MMRCMSJsonGen().generate()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mmrcms
|
||||
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
|
||||
object MMRCMSUtils {
|
||||
fun guessCover(baseUrl: String, mangaUrl: String, url: String?): String {
|
||||
return if (url == null || url.endsWith("no-image.png")) {
|
||||
"$baseUrl/uploads/manga/${mangaUrl.substringAfterLast('/')}/cover/cover_250x350.jpg"
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-background-image") -> absUrl("data-background-image")
|
||||
hasAttr("data-cfsrc") -> absUrl("data-cfsrc")
|
||||
hasAttr("data-lazy-src") -> absUrl("data-lazy-src")
|
||||
hasAttr("data-src") -> absUrl("data-src")
|
||||
else -> absUrl("src")
|
||||
}
|
||||
|
||||
fun Elements.textWithNewlines() = run {
|
||||
select("p, br").prepend("\\n")
|
||||
text().replace("\\n", "\n").replace("\n ", "\n")
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mmrcms
|
||||
|
||||
// GENERATED FILE, DO NOT MODIFY!
|
||||
// Generated Sun, 16 Apr 2022 14:18:00 GMT
|
||||
|
||||
object SourceData {
|
||||
fun giveMetaData(url: String) = when (url) {
|
||||
"https://onma.top" -> """{"name":"مانجا اون لاين","base_url":"https://onma.top","supports_latest":true,"item_url":"https://onma.top/manga/","categories":[{"id":"1","name":"أكشن"},{"id":"2","name":"مغامرة"},{"id":"3","name":"كوميدي"},{"id":"4","name":"شياطين"},{"id":"5","name":"دراما"},{"id":"6","name":"إيتشي"},{"id":"7","name":"خيال"},{"id":"8","name":"انحراف جنسي"},{"id":"9","name":"حريم"},{"id":"10","name":"تاريخي"},{"id":"11","name":"رعب"},{"id":"12","name":"جوسي"},{"id":"13","name":"فنون قتالية"},{"id":"14","name":"ناضج"},{"id":"15","name":"ميكا"},{"id":"16","name":"غموض"},{"id":"17","name":"وان شوت"},{"id":"18","name":"نفسي"},{"id":"19","name":"رومنسي"},{"id":"20","name":"حياة مدرسية"},{"id":"21","name":"خيال علمي"},{"id":"22","name":"سينين"},{"id":"23","name":"شوجو"},{"id":"24","name":"شوجو أي"},{"id":"25","name":"شونين"},{"id":"26","name":"شونين أي"},{"id":"27","name":"شريحة من الحياة"},{"id":"28","name":"رياضة"},{"id":"29","name":"خارق للطبيعة"},{"id":"30","name":"مأساة"},{"id":"31","name":"مصاصي الدماء"},{"id":"32","name":"سحر"},{"id":"33","name":"ويب تون"},{"id":"34","name":"دوجينشي"}]}"""
|
||||
"https://readcomicsonline.ru" -> """{"name":"Read Comics Online","base_url":"https://readcomicsonline.ru","supports_latest":true,"item_url":"https://readcomicsonline.ru/comic/","categories":[{"id":"1","name":"One Shots \u0026 TPBs"},{"id":"2","name":"DC Comics"},{"id":"3","name":"Marvel Comics"},{"id":"4","name":"Boom Studios"},{"id":"5","name":"Dynamite"},{"id":"6","name":"Rebellion"},{"id":"7","name":"Dark Horse"},{"id":"8","name":"IDW"},{"id":"9","name":"Archie"},{"id":"10","name":"Graphic India"},{"id":"11","name":"Darby Pop"},{"id":"12","name":"Oni Press"},{"id":"13","name":"Icon Comics"},{"id":"14","name":"United Plankton"},{"id":"15","name":"Udon"},{"id":"16","name":"Image Comics"},{"id":"17","name":"Valiant"},{"id":"18","name":"Vertigo"},{"id":"19","name":"Devils Due"},{"id":"20","name":"Aftershock Comics"},{"id":"21","name":"Antartic Press"},{"id":"22","name":"Action Lab"},{"id":"23","name":"American Mythology"},{"id":"24","name":"Zenescope"},{"id":"25","name":"Top Cow"},{"id":"26","name":"Hermes Press"},{"id":"27","name":"451"},{"id":"28","name":"Black Mask"},{"id":"29","name":"Chapterhouse Comics"},{"id":"30","name":"Red 5"},{"id":"31","name":"Heavy Metal"},{"id":"32","name":"Bongo"},{"id":"33","name":"Top Shelf"},{"id":"34","name":"Bubble"},{"id":"35","name":"Boundless"},{"id":"36","name":"Avatar Press"},{"id":"37","name":"Space Goat Productions"},{"id":"38","name":"BroadSword Comics"},{"id":"39","name":"AAM-Markosia"},{"id":"40","name":"Fantagraphics"},{"id":"41","name":"Aspen"},{"id":"42","name":"American Gothic Press"},{"id":"43","name":"Vault"},{"id":"44","name":"215 Ink"},{"id":"45","name":"Abstract Studio"},{"id":"46","name":"Albatross"},{"id":"47","name":"ARH Comix"},{"id":"48","name":"Legendary Comics"},{"id":"49","name":"Monkeybrain"},{"id":"50","name":"Joe Books"},{"id":"51","name":"MAD"},{"id":"52","name":"Comics Experience"},{"id":"53","name":"Alterna Comics"},{"id":"54","name":"Lion Forge"},{"id":"55","name":"Benitez"},{"id":"56","name":"Storm King"},{"id":"57","name":"Sucker"},{"id":"58","name":"Amryl Entertainment"},{"id":"59","name":"Ahoy Comics"},{"id":"60","name":"Mad Cave"},{"id":"61","name":"Coffin Comics"},{"id":"62","name":"Magnetic Press"},{"id":"63","name":"Ablaze"},{"id":"64","name":"Europe Comics"},{"id":"65","name":"Humanoids"},{"id":"66","name":"TKO"},{"id":"67","name":"Soleil"},{"id":"68","name":"SAF Comics"},{"id":"69","name":"Scholastic"},{"id":"70","name":"Upshot"},{"id":"71","name":"Stranger Comics"},{"id":"72","name":"Inverse"},{"id":"73","name":"Virus"}]}"""
|
||||
"https://zahard.xyz" -> """{"name":"Zahard","base_url":"https://zahard.xyz","supports_latest":true,"item_url":"https://zahard.xyz/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Adventure"},{"id":"3","name":"Comedy"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drama"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historical"},{"id":"11","name":"Horror"},{"id":"12","name":"Josei"},{"id":"13","name":"Martial Arts"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystery"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychological"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Sci-fi"},{"id":"22","name":"Seinen"},{"id":"23","name":"Shoujo"},{"id":"24","name":"Shoujo Ai"},{"id":"25","name":"Shounen"},{"id":"26","name":"Shounen Ai"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sports"},{"id":"29","name":"Supernatural"},{"id":"30","name":"Tragedy"},{"id":"31","name":"Yaoi"},{"id":"32","name":"Yuri"}]}"""
|
||||
"https://www.scan-fr.org" -> """{"name":"Scan FR","base_url":"https://www.scan-fr.org","supports_latest":true,"item_url":"https://www.scan-fr.org/manga/","categories":[{"id":"1","name":"Comedy"},{"id":"2","name":"Doujinshi"},{"id":"3","name":"Drama"},{"id":"4","name":"Ecchi"},{"id":"5","name":"Fantasy"},{"id":"6","name":"Gender Bender"},{"id":"7","name":"Josei"},{"id":"8","name":"Mature"},{"id":"9","name":"Mecha"},{"id":"10","name":"Mystery"},{"id":"11","name":"One Shot"},{"id":"12","name":"Psychological"},{"id":"13","name":"Romance"},{"id":"14","name":"School Life"},{"id":"15","name":"Sci-fi"},{"id":"16","name":"Seinen"},{"id":"17","name":"Shoujo"},{"id":"18","name":"Shoujo Ai"},{"id":"19","name":"Shounen"},{"id":"20","name":"Shounen Ai"},{"id":"21","name":"Slice of Life"},{"id":"22","name":"Sports"},{"id":"23","name":"Supernatural"},{"id":"24","name":"Tragedy"},{"id":"25","name":"Yaoi"},{"id":"26","name":"Yuri"},{"id":"27","name":"Comics"},{"id":"28","name":"Autre"},{"id":"29","name":"BD Occidentale"},{"id":"30","name":"Manhwa"},{"id":"31","name":"Action"},{"id":"32","name":"Aventure"}]}"""
|
||||
"https://www.scan-vf.net" -> """{"name":"Scan VF","base_url":"https://www.scan-vf.net","supports_latest":true,"item_url":"https://www.scan-vf.net/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Adventure"},{"id":"3","name":"Comedy"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drama"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historical"},{"id":"11","name":"Horror"},{"id":"12","name":"Josei"},{"id":"13","name":"Martial Arts"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystery"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychological"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Sci-fi"},{"id":"22","name":"Seinen"},{"id":"23","name":"Shoujo"},{"id":"24","name":"Shoujo Ai"},{"id":"25","name":"Shounen"},{"id":"26","name":"Shounen Ai"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sports"},{"id":"29","name":"Supernatural"},{"id":"30","name":"Tragedy"},{"id":"31","name":"Yaoi"},{"id":"32","name":"Yuri"}]}"""
|
||||
"https://www.komikid.com" -> """{"name":"Komikid","base_url":"https://www.komikid.com","supports_latest":true,"item_url":"https://www.komikid.com/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Adventure"},{"id":"3","name":"Comedy"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drama"},{"id":"6","name":"Fantasy"},{"id":"7","name":"Gender Bender"},{"id":"8","name":"Historical"},{"id":"9","name":"Horror"},{"id":"10","name":"Josei"},{"id":"11","name":"Martial Arts"},{"id":"12","name":"Mature"},{"id":"13","name":"Mecha"},{"id":"14","name":"Mystery"},{"id":"15","name":"One Shot"},{"id":"16","name":"Psychological"},{"id":"17","name":"Romance"},{"id":"18","name":"School Life"},{"id":"19","name":"Sci-fi"},{"id":"20","name":"Seinen"},{"id":"21","name":"Shoujo"},{"id":"22","name":"Shoujo Ai"},{"id":"23","name":"Shounen"},{"id":"24","name":"Shounen Ai"},{"id":"25","name":"Slice of Life"},{"id":"26","name":"Sports"},{"id":"27","name":"Supernatural"},{"id":"28","name":"Tragedy"},{"id":"29","name":"Yaoi"},{"id":"30","name":"Yuri"}]}"""
|
||||
"http://azbivo.webd.pro" -> """{"name":"Nikushima","base_url":"http://azbivo.webd.pro","supports_latest":false,"item_url":"\u003chtml\u003e \n \u003chead\u003e \n \u003cmeta http-equiv\u003d\"Content-Language\" content\u003d\"pl\"\u003e \n \u003cmeta http-equiv name\u003d\"pragma\" content\u003d\"no-cache\"\u003e \n \u003clink href\u003d\"style/style.css\" rel\u003d\"stylesheet\" type\u003d\"text/css\"\u003e \n \u003cmeta http-equiv\u003d\"Refresh\" content\u003d\"0; url\u003dhttps://www.webd.pl/_errnda.php?utm_source\u003dwn07\u0026amp;utm_medium\u003dwww\u0026amp;utm_campaign\u003dblock\"\u003e \n \u003cmeta name\u003d\"Robots\" content\u003d\"index, follow\"\u003e \n \u003cmeta name\u003d\"revisit-after\" content\u003d\"2 days\"\u003e \n \u003cmeta name\u003d\"rating\" content\u003d\"general\"\u003e \n \u003cmeta name\u003d\"keywords\" content\u003d\"STRONA ZAWIESZONA, WEBD, DOMENY, DOMENA, HOSTING, SERWER, INTERNET, PHP, MySQL, FTP, WEBMASTER, SERWERY WIRTUALNE, WWW, KONTO, MAIL, POCZTA, E-MAIL, NET, .COM, .ORG, TANIE, PHP+MySQL, DOMENY, DOMENA, HOSTING, SERWER, INTERNET, PHP, MySQL, FTP, WEBMASTER, SERWERY WIRTUALNE, WWW, KONTO, MAIL, POCZTA, E-MAIL, DOMENY, DOMENA, NET, .COM, .ORG, TANIE, PHP+MySQL, HOSTING, SERWER, INTERNET, PHP, MySQL, FTP, WEBMASTER, SERWERY WIRTUALNE, WWW, KONTO, MAIL, POCZTA, E-MAIL, NET, .COM, .ORG, TANIE, PHP+MySQL\"\u003e \n \u003cmeta name\u003d\"description\" content\u003d\"STRONA ZAWIESZONA - Oferujemy profesjonalny hosting z PHP + MySQL, rejestrujemy domeny. Sprawdz nasz hosting i przetestuj nasze serwery. Kupuj tanio domeny i serwery!\"\u003e \n \u003ctitle\u003eSTRONA ZAWIESZONA - WEBD.PL - Tw<54>j profesjonalny hosting za jedyne 4.99PLN! Serwery z PHP+MySQL, tanie domeny, serwer + domena .pl - taniej sie nie da!\u003c/title\u003e \n \u003cscript type\u003d\"text/javascript\"\u003e\nfunction init() {\n if (!document.getElementById) return\n var imgOriginSrc;\n var imgTemp \u003d new Array();\n var imgarr \u003d document.getElementsByTagName(\u0027img\u0027);\n for (var i \u003d 0; i \u003c imgarr.length; i++) {\n if (imgarr[i].getAttribute(\u0027hsrc\u0027)) {\n imgTemp[i] \u003d new Image();\n imgTemp[i].src \u003d imgarr[i].getAttribute(\u0027hsrc\u0027);\n imgarr[i].onmouseover \u003d function() {\n imgOriginSrc \u003d this.getAttribute(\u0027src\u0027);\n this.setAttribute(\u0027src\u0027,this.getAttribute(\u0027hsrc\u0027))\n }\n imgarr[i].onmouseout \u003d function() {\n this.setAttribute(\u0027src\u0027,imgOriginSrc)\n }\n }\n }\n}\nonload\u003dinit;\n\u003c/script\u003e \n \u003c/head\u003e \n \u003cbody\u003e\n Trwa przekierowanie .... \u0026gt;\u0026gt;\u0026gt;\u0026gt; \u003c!--\n--\u003e \n \u003c/body\u003e\n\u003c/html\u003e/","categories":[]}"""
|
||||
"https://mangadoor.com" -> """{"name":"Mangadoor","base_url":"https://mangadoor.com","supports_latest":true,"item_url":"https://mangadoor.com/manga/","categories":[{"id":"1","name":"Acción"},{"id":"2","name":"Aventura"},{"id":"3","name":"Comedia"},{"id":"4","name":"Drama"},{"id":"5","name":"Ecchi"},{"id":"6","name":"Fantasía"},{"id":"7","name":"Gender Bender"},{"id":"8","name":"Harem"},{"id":"9","name":"Histórico"},{"id":"10","name":"Horror"},{"id":"11","name":"Josei"},{"id":"12","name":"Artes Marciales"},{"id":"13","name":"Maduro"},{"id":"14","name":"Mecha"},{"id":"15","name":"Misterio"},{"id":"16","name":"One Shot"},{"id":"17","name":"Psicológico"},{"id":"18","name":"Romance"},{"id":"19","name":"Escolar"},{"id":"20","name":"Ciencia Ficción"},{"id":"21","name":"Seinen"},{"id":"22","name":"Shoujo"},{"id":"23","name":"Shoujo Ai"},{"id":"24","name":"Shounen"},{"id":"25","name":"Shounen Ai"},{"id":"26","name":"Recuentos de la vida"},{"id":"27","name":"Deportes"},{"id":"28","name":"Supernatural"},{"id":"29","name":"Tragedia"},{"id":"30","name":"Yaoi"},{"id":"31","name":"Yuri"},{"id":"32","name":"Demonios"},{"id":"33","name":"Juegos"},{"id":"34","name":"Policial"},{"id":"35","name":"Militar"},{"id":"36","name":"Thriller"},{"id":"37","name":"Autos"},{"id":"38","name":"Música"},{"id":"39","name":"Vampiros"},{"id":"40","name":"Magia"},{"id":"41","name":"Samurai"},{"id":"42","name":"Boys love"},{"id":"43","name":"Hentai"}]}"""
|
||||
"https://mangas.in" -> """{"name":"Mangas.in","base_url":"https://mangas.in","supports_latest":true,"item_url":"https://mangas.in/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Adventure"},{"id":"3","name":"Comedy"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drama"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historical"},{"id":"11","name":"Horror"},{"id":"12","name":"Josei"},{"id":"13","name":"Martial Arts"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystery"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychological"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Sci-fi"},{"id":"22","name":"Seinen"},{"id":"23","name":"Shoujo"},{"id":"24","name":"Shoujo Ai"},{"id":"25","name":"Shounen"},{"id":"26","name":"Shounen Ai"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sports"},{"id":"29","name":"Supernatural"},{"id":"30","name":"Tragedy"},{"id":"31","name":"Yaoi"},{"id":"32","name":"Yuri"},{"id":"33","name":"Hentai"},{"id":"34","name":"Smut"}]}"""
|
||||
"https://manga.utsukushii-bg.com" -> """{"name":"Utsukushii","base_url":"https://manga.utsukushii-bg.com","supports_latest":true,"item_url":"https://manga.utsukushii-bg.com/manga/","categories":[{"id":"1","name":"Екшън"},{"id":"2","name":"Приключенски"},{"id":"3","name":"Комедия"},{"id":"4","name":"Драма"},{"id":"5","name":"Фентъзи"},{"id":"6","name":"Исторически"},{"id":"7","name":"Ужаси"},{"id":"8","name":"Джосей"},{"id":"9","name":"Бойни изкуства"},{"id":"10","name":"Меха"},{"id":"11","name":"Мистерия"},{"id":"12","name":"Самостоятелна/Пилотна глава"},{"id":"13","name":"Психологически"},{"id":"14","name":"Романтика"},{"id":"15","name":"Училищни"},{"id":"16","name":"Научна фантастика"},{"id":"17","name":"Сейнен"},{"id":"18","name":"Шоджо"},{"id":"19","name":"Реализъм"},{"id":"20","name":"Спорт"},{"id":"21","name":"Свръхестествено"},{"id":"22","name":"Трагедия"},{"id":"23","name":"Йокаи"},{"id":"24","name":"Паралелна вселена"},{"id":"25","name":"Супер сили"},{"id":"26","name":"Пародия"},{"id":"27","name":"Шонен"}]}"""
|
||||
"https://phoenix-scans.pl" -> """{"name":"Phoenix-Scans","base_url":"https://phoenix-scans.pl","supports_latest":true,"item_url":"https://phoenix-scans.pl/manga/","categories":[{"id":"1","name":"Shounen"},{"id":"2","name":"Tragedia"},{"id":"3","name":"Szkolne życie"},{"id":"4","name":"Romans"},{"id":"5","name":"Zagadka"},{"id":"6","name":"Horror"},{"id":"7","name":"Dojrzałe"},{"id":"8","name":"Psychologiczne"},{"id":"9","name":"Przygodowe"},{"id":"10","name":"Akcja"},{"id":"11","name":"Komedia"},{"id":"12","name":"Zboczone"},{"id":"13","name":"Fantasy"},{"id":"14","name":"Harem"},{"id":"15","name":"Historyczne"},{"id":"16","name":"Manhua"},{"id":"17","name":"Manhwa"},{"id":"18","name":"Sztuki walki"},{"id":"19","name":"One shot"},{"id":"20","name":"Sci fi"},{"id":"21","name":"Seinen"},{"id":"22","name":"Shounen ai"},{"id":"23","name":"Spokojne życie"},{"id":"24","name":"Sport"},{"id":"25","name":"Nadprzyrodzone"},{"id":"26","name":"Webtoons"},{"id":"27","name":"Dramat"},{"id":"28","name":"Hentai"},{"id":"29","name":"Mecha"},{"id":"30","name":"Gender Bender"},{"id":"31","name":"Gry"},{"id":"32","name":"Yaoi"}],"tags":[{"id":"aktywne","name":"aktywne"},{"id":"zakonczone","name":"zakończone"},{"id":"porzucone","name":"porzucone"},{"id":"zawieszone","name":"zawieszone"},{"id":"zlicencjonowane","name":"zlicencjonowane"},{"id":"hentai","name":"Hentai"}]}"""
|
||||
"https://lelscanvf.cc" -> """{"name":"Lelscan-VF","base_url":"https://lelscanvf.cc","supports_latest":true,"item_url":"https://lelscanvf.cc/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Adventure"},{"id":"3","name":"Comedy"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drama"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historical"},{"id":"11","name":"Horror"},{"id":"12","name":"Josei"},{"id":"13","name":"Martial Arts"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystery"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychological"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Sci-fi"},{"id":"22","name":"Seinen"},{"id":"23","name":"Shoujo"},{"id":"24","name":"Shoujo Ai"},{"id":"25","name":"Shounen"},{"id":"26","name":"Shounen Ai"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sports"},{"id":"29","name":"Supernatural"},{"id":"30","name":"Tragedy"},{"id":"31","name":"Yaoi"},{"id":"32","name":"Yuri"}]}"""
|
||||
"https://mangaid.click" -> """{"name":"MangaID","base_url":"https://mangaid.click","supports_latest":true,"item_url":"https://mangaid.click/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Adventure"},{"id":"3","name":"Comedy"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drama"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historical"},{"id":"11","name":"Horror"},{"id":"12","name":"Josei"},{"id":"13","name":"Martial Arts"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystery"},{"id":"17","name":"Psychological"},{"id":"18","name":"Romance"},{"id":"19","name":"School Life"},{"id":"20","name":"Sci-fi"},{"id":"21","name":"Seinen"},{"id":"22","name":"Shoujo"},{"id":"23","name":"Shoujo Ai"},{"id":"24","name":"Shounen"},{"id":"25","name":"Shounen Ai"},{"id":"26","name":"Slice of Life"},{"id":"27","name":"Sports"},{"id":"28","name":"Supernatural"},{"id":"29","name":"Tragedy"},{"id":"30","name":"Yaoi"},{"id":"31","name":"Yuri"},{"id":"32","name":"School"},{"id":"33","name":"Isekai"},{"id":"34","name":"Military"}]}"""
|
||||
"https://jpmangas.xyz" -> """{"name":"Jpmangas","base_url":"https://jpmangas.xyz","supports_latest":true,"item_url":"https://jpmangas.xyz/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Adventure"},{"id":"3","name":"Comedy"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drama"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historical"},{"id":"11","name":"Horror"},{"id":"12","name":"Josei"},{"id":"13","name":"Martial Arts"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystery"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychological"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Sci-fi"},{"id":"22","name":"Seinen"},{"id":"23","name":"Shoujo"},{"id":"24","name":"Shoujo Ai"},{"id":"25","name":"Shounen"},{"id":"26","name":"Shounen Ai"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sports"},{"id":"29","name":"Supernatural"},{"id":"30","name":"Tragedy"},{"id":"31","name":"Yaoi"},{"id":"32","name":"Yuri"}]}"""
|
||||
"https://www.hentaishark.com" -> """{"name":"HentaiShark","base_url":"https://www.hentaishark.com","supports_latest":true,"item_url":"https://www.hentaishark.com/manga/","categories":[{"id":"1","name":"Doujinshi"},{"id":"2","name":"Manga"},{"id":"3","name":"Western"},{"id":"4","name":"non-h"},{"id":"5","name":"imageset"},{"id":"6","name":"artistcg"},{"id":"7","name":"misc"}]}"""
|
||||
"https://manga-fr.cc" -> """{"name":"Manga-FR","base_url":"https://manga-fr.cc","supports_latest":true,"item_url":"https://manga-fr.cc/lecture-en-ligne/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Aventure"},{"id":"3","name":"Comédie"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drame"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasie"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historique"},{"id":"11","name":"Horreur"},{"id":"12","name":"Josei"},{"id":"13","name":"Martial Arts"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystery"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychological"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Sci-fi"},{"id":"22","name":"Seinen"},{"id":"23","name":"Shoujo"},{"id":"24","name":"Shoujo Ai"},{"id":"25","name":"Shounen"},{"id":"26","name":"Shounen Ai"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sports"},{"id":"29","name":"Supernatural"},{"id":"30","name":"Tragédie"},{"id":"31","name":"Yaoi"},{"id":"32","name":"Yuri"},{"id":"33","name":"Fantastique"},{"id":"34","name":"Webtoon"},{"id":"35","name":"Manhwa"},{"id":"36","name":"Amour"},{"id":"37","name":"Combats"},{"id":"38","name":"Amitié"},{"id":"39","name":"Psychologique"},{"id":"40","name":"Magie"}]}"""
|
||||
"https://mangascan-fr.com" -> """{"name":"Manga-Scan","base_url":"https://mangascan-fr.com","supports_latest":true,"item_url":"https://mangascan-fr.com/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Aventure"},{"id":"3","name":"Comédie"},{"id":"4","name":"Doujinshi"},{"id":"5","name":"Drame"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Webtoon"},{"id":"9","name":"Harem"},{"id":"10","name":"Historique"},{"id":"11","name":"Horreur"},{"id":"12","name":"Thriller"},{"id":"13","name":"Arts Martiaux"},{"id":"14","name":"Mature"},{"id":"15","name":"Tragique"},{"id":"16","name":"Mystère"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychologique"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Science-fiction"},{"id":"22","name":"Seinen"},{"id":"23","name":"Erotique"},{"id":"24","name":"Shoujo Ai"},{"id":"25","name":"Shounen"},{"id":"26","name":"Shounen Ai"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sport"},{"id":"29","name":"Surnaturel"},{"id":"30","name":"Tragedy"},{"id":"31","name":"Gangster"},{"id":"32","name":"Crime"},{"id":"33","name":"Biographique"},{"id":"34","name":"Fantastique"}]}"""
|
||||
"https://bentoscan.com" -> """{"name":"Bentoscan","base_url":"https://bentoscan.com","supports_latest":true,"item_url":"https://bentoscan.com/manga/","categories":[{"id":"1","name":"Action"},{"id":"2","name":"Aventure"},{"id":"3","name":"Comédie"},{"id":"4","name":"Crime"},{"id":"5","name":"Drame"},{"id":"6","name":"Ecchi"},{"id":"7","name":"Fantasy"},{"id":"8","name":"Fantastique"},{"id":"9","name":"Harem"},{"id":"10","name":"Gangster"},{"id":"11","name":"Erotique"},{"id":"12","name":"Historique"},{"id":"13","name":"Arts Martiaux"},{"id":"14","name":"Mature"},{"id":"15","name":"Horreur"},{"id":"16","name":"Mystère"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychologique"},{"id":"19","name":"Romance"},{"id":"20","name":"School Life"},{"id":"21","name":"Science-fiction"},{"id":"22","name":"Seinen"},{"id":"23","name":"Suspense"},{"id":"24","name":"Biographique"},{"id":"25","name":"Social"},{"id":"26","name":"Tranche-de-vie"},{"id":"27","name":"Slice of Life"},{"id":"28","name":"Sport"},{"id":"29","name":"Surnaturel"},{"id":"30","name":"Thriller"},{"id":"31","name":"Tragique"},{"id":"32","name":"Webtoon"}]}"""
|
||||
else -> ""
|
||||
}
|
||||
}
|