Remove MangaHub (#13174)
* Remove MangaHub * escape regex * [skip ci] dedupe
|
@ -37,7 +37,7 @@ jobs:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "both",
|
"type": "both",
|
||||||
"regex": ".*(mangago|mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|heroscan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|cocomanga|hitomi\\.la|copymanga|neox).*",
|
"regex": ".*(mangago|mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|heroscan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|cocomanga|hitomi\\.la|copymanga|neox|1manga\\.co|mangafox\\.fun|mangahere\\.onl|manga\\s*hub|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangapanda\\.onl|mangareader\\.site|mangatoday|manga\\.town|onemanga\\.info).*",
|
||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information"
|
"message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information"
|
||||||
},
|
},
|
||||||
|
|
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 55 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangafoxfun
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaFoxFun : MangaHub(
|
|
||||||
"MangaFox.fun",
|
|
||||||
"https://mangafox.fun",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mf01"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 48 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangahereonl
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaHereOnl : MangaHub(
|
|
||||||
"MangaHere.onl",
|
|
||||||
"https://mangahere.onl",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mh01"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 56 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangahubio
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaHubIo : MangaHub(
|
|
||||||
"MangaHub",
|
|
||||||
"https://mangahub.io",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "m01"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 45 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangakakalotfun
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangakakalotFun : MangaHub(
|
|
||||||
"Mangakakalot.fun",
|
|
||||||
"https://mangakakalot.fun",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mn01"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 55 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.manganel
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaNel : MangaHub(
|
|
||||||
"MangaNel",
|
|
||||||
"https://manganel.me",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mn05"
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangaonlinefun
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaOnlineFun : MangaHub(
|
|
||||||
"MangaOnline.fun",
|
|
||||||
"https://mangaonline.fun",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "m02"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 50 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangapandaonl
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaPandaOnl : MangaHub(
|
|
||||||
"MangaPanda.onl",
|
|
||||||
"https://mangapanda.onl",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mr02"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 47 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangareadersite
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaReaderSite : MangaHub(
|
|
||||||
"MangaReader.site",
|
|
||||||
"https://mangareader.site",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mr01"
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangatoday
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaToday : MangaHub(
|
|
||||||
"MangaToday",
|
|
||||||
"https://mangatoday.fun",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "m03"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 59 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangatownhub
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class MangaTownHub : MangaHub(
|
|
||||||
"MangaTown (unoriginal)",
|
|
||||||
"https://manga.town",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mt01"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 52 KiB |
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.onemangaco
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class OneMangaCo : MangaHub(
|
|
||||||
"1Manga.co",
|
|
||||||
"https://1manga.co",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mn03"
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.onemangainfo
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangahub.MangaHub
|
|
||||||
|
|
||||||
class OneMangaInfo : MangaHub(
|
|
||||||
"OneManga.info",
|
|
||||||
"https://onemanga.info",
|
|
||||||
"en"
|
|
||||||
) {
|
|
||||||
override val serverId = "mn02"
|
|
||||||
}
|
|
|
@ -1,539 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.mangahub
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import kotlinx.serialization.json.putJsonObject
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
abstract class MangaHub(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MM-dd-yyyy", Locale.US)
|
|
||||||
) : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
protected abstract val serverId: String
|
|
||||||
|
|
||||||
protected open val cdnImgUrl = "https://img.mghubcdn.com"
|
|
||||||
protected open val cdnApiUrl = "https://api.mghubcdn.com"
|
|
||||||
|
|
||||||
override val client: OkHttpClient by lazy {
|
|
||||||
super.client.newBuilder()
|
|
||||||
.addInterceptor(::apiAuthInterceptor)
|
|
||||||
.rateLimitHost(cdnImgUrl.toHttpUrl(), 1, 2)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
|
|
||||||
val cookie = client.cookieJar
|
|
||||||
.loadForRequest(baseUrl.toHttpUrl())
|
|
||||||
.firstOrNull { it.name == "mhub_access" }
|
|
||||||
|
|
||||||
val request =
|
|
||||||
if (originalRequest.url.toString() == "$cdnApiUrl/graphql" && cookie != null) {
|
|
||||||
originalRequest.newBuilder()
|
|
||||||
.header("x-mhub-access", cookie.value)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
originalRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshApiKey(chapter: SChapter) {
|
|
||||||
val now = Calendar.getInstance().time.time
|
|
||||||
|
|
||||||
val slug = "$baseUrl${chapter.url}"
|
|
||||||
.toHttpUrlOrNull()
|
|
||||||
?.pathSegments
|
|
||||||
?.get(1)
|
|
||||||
|
|
||||||
val url = if (slug != null) {
|
|
||||||
"$baseUrl/manga/$slug".toHttpUrl()
|
|
||||||
} else {
|
|
||||||
baseUrl.toHttpUrl()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear key cookie
|
|
||||||
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=8640000; Path=/; Expires=Mon, 1 Jan 2080 00:00:00 GMT")!!
|
|
||||||
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
|
||||||
|
|
||||||
// Set required cookie (for cache busting?)
|
|
||||||
val recently = buildJsonObject {
|
|
||||||
putJsonObject((now - (0..3600).random()).toString()) {
|
|
||||||
put("mangaID", (1..42_000).random())
|
|
||||||
put("number", (1..20).random())
|
|
||||||
}
|
|
||||||
}.toString()
|
|
||||||
|
|
||||||
client.cookieJar.saveFromResponse(
|
|
||||||
url,
|
|
||||||
listOf(
|
|
||||||
Cookie.Builder()
|
|
||||||
.domain(url.host)
|
|
||||||
.name("recently")
|
|
||||||
.value(URLEncoder.encode(recently, "utf-8"))
|
|
||||||
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val request = GET("$url?reloadKey=1", headers)
|
|
||||||
client.newCall(request).execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder {
|
|
||||||
val defaultUserAgent = super.headersBuilder()["User-Agent"]
|
|
||||||
|
|
||||||
val chromeVersion = defaultUserAgent
|
|
||||||
?.substringAfter("Chrome/")
|
|
||||||
?.substringBefore(".")
|
|
||||||
?.toIntOrNull()
|
|
||||||
?: "102"
|
|
||||||
|
|
||||||
val edgeVersion = defaultUserAgent
|
|
||||||
?.substringAfter("Edg/")
|
|
||||||
?.substringBefore(".")
|
|
||||||
?.toIntOrNull()
|
|
||||||
?: chromeVersion
|
|
||||||
|
|
||||||
return super.headersBuilder()
|
|
||||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
|
|
||||||
.add("Accept-Language", "en-US,en;q=0.5")
|
|
||||||
.add("DNT", "1")
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
.add("Sec-CH-UA", "\"Chromium\";v=\"$chromeVersion\", \" Not A;Brand\";v=\"99\", \"Microsoft Edge\";v=\"$edgeVersion\"")
|
|
||||||
.add("Sec-CH-UA-Mobile", "?0")
|
|
||||||
.add("Sec-CH-UA-Platform", "\"Windows\"")
|
|
||||||
.add("Sec-Fetch-Dest", "document")
|
|
||||||
.add("Sec-Fetch-Mode", "navigate")
|
|
||||||
.add("Sec-Fetch-Site", "same-origin")
|
|
||||||
.add("Upgrade-Insecure-Requests", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/popular/page/$page", headers)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage =
|
|
||||||
searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String =
|
|
||||||
"#mangalist div.media-manga.media"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga =
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String? =
|
|
||||||
searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/updates/page/$page", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
|
||||||
searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String =
|
|
||||||
popularMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String? =
|
|
||||||
searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
// Search
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
// https://mangahub.io/search/page/1?q=a&order=POPULAR&genre=all
|
|
||||||
val url = "$baseUrl/search/page/$page".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("q", query)
|
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is OrderBy -> {
|
|
||||||
val order = filter.values[filter.state]
|
|
||||||
url.addQueryParameter("order", order.key)
|
|
||||||
}
|
|
||||||
is GenreList -> {
|
|
||||||
val genre = filter.values[filter.state]
|
|
||||||
url.addQueryParameter("genre", genre.key)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GET(url.toString(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
/*
|
|
||||||
* To remove duplicates we group by the thumbnail_url, which is
|
|
||||||
* common between duplicates. The duplicates have a suffix in the
|
|
||||||
* url "-by-{name}". Here we select the shortest url, to avoid
|
|
||||||
* removing manga that has "by" in the title already.
|
|
||||||
* Example:
|
|
||||||
* /manga/tales-of-demons-and-gods (kept)
|
|
||||||
* /manga/tales-of-demons-and-gods-by-mad-snail (removed)
|
|
||||||
* /manga/leveling-up-by-only-eating (kept)
|
|
||||||
*/
|
|
||||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
}.groupBy { it.thumbnail_url }.mapValues { (_, values) ->
|
|
||||||
values.minByOrNull { it.url.length }!!
|
|
||||||
}.values.toList()
|
|
||||||
|
|
||||||
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
|
||||||
document.select(selector).first()
|
|
||||||
} != null
|
|
||||||
|
|
||||||
return MangasPage(mangas, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div#mangalist div.media-manga.media"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
|
||||||
val titleElement = element.select(".media-heading > a").first()
|
|
||||||
setUrlWithoutDomain(titleElement.attr("abs:href"))
|
|
||||||
|
|
||||||
title = titleElement.text()
|
|
||||||
thumbnail_url = element
|
|
||||||
.select("img.manga-thumb.list-item-thumb")
|
|
||||||
?.first()?.attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "ul.pager li.next > a"
|
|
||||||
|
|
||||||
// Details
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
|
||||||
title = document.select("h1._3xnDj").first().ownText()
|
|
||||||
author = document
|
|
||||||
.select("._3QCtP > div:nth-child(2) > div:nth-child(1) > span:nth-child(2)")
|
|
||||||
?.first()?.text()
|
|
||||||
artist = document
|
|
||||||
.select("._3QCtP > div:nth-child(2) > div:nth-child(2) > span:nth-child(2)")
|
|
||||||
?.first()?.text()
|
|
||||||
genre = document.select("._3Czbn a")?.joinToString { it.text() }
|
|
||||||
description = document.select("div#noanim-content-tab-pane-99 p.ZyMp7")?.first()?.text()
|
|
||||||
thumbnail_url = document.select("img.img-responsive")?.first()?.attr("abs:src")
|
|
||||||
|
|
||||||
document.select("._3QCtP > div:nth-child(2) > div:nth-child(3) > span:nth-child(2)")
|
|
||||||
?.first()?.text()?.also { statusText ->
|
|
||||||
status = when {
|
|
||||||
statusText.contains("ongoing", true) -> SManga.ONGOING
|
|
||||||
statusText.contains("completed", true) -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add alternative name to manga description
|
|
||||||
val altName = "Alternative Name: "
|
|
||||||
document.select("h1 small").firstOrNull()?.ownText()?.let {
|
|
||||||
if (it.isBlank().not()) {
|
|
||||||
description = when {
|
|
||||||
description.isNullOrBlank() -> altName + it
|
|
||||||
else -> description + "\n\n$altName" + it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
override fun chapterListSelector() = ".tab-content .tab-pane li.list-group-item > a"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("abs:href"))
|
|
||||||
name = element.select("span._8Qtbo").text()
|
|
||||||
date_upload = element.select("small.UovLc").first()?.text()
|
|
||||||
?.let { parseChapterDate(it) } ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String): Long {
|
|
||||||
val now = Calendar.getInstance().apply {
|
|
||||||
set(Calendar.HOUR_OF_DAY, 0)
|
|
||||||
set(Calendar.MINUTE, 0)
|
|
||||||
set(Calendar.SECOND, 0)
|
|
||||||
set(Calendar.MILLISECOND, 0)
|
|
||||||
}
|
|
||||||
var parsedDate = 0L
|
|
||||||
when {
|
|
||||||
"just now" in date || "less than an hour" in date -> {
|
|
||||||
parsedDate = now.timeInMillis
|
|
||||||
}
|
|
||||||
// parses: "1 hour ago" and "2 hours ago"
|
|
||||||
"hour" in date -> {
|
|
||||||
val hours = date.replaceAfter(" ", "").trim().toInt()
|
|
||||||
parsedDate = now.apply { add(Calendar.HOUR, -hours) }.timeInMillis
|
|
||||||
}
|
|
||||||
// parses: "Yesterday" and "2 days ago"
|
|
||||||
"day" in date -> {
|
|
||||||
val days = date.replace("days ago", "").trim().toIntOrNull() ?: 1
|
|
||||||
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -days) }.timeInMillis
|
|
||||||
}
|
|
||||||
// parses: "2 weeks ago"
|
|
||||||
"weeks" in date -> {
|
|
||||||
val weeks = date.replace("weeks ago", "").trim().toInt()
|
|
||||||
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -weeks) }.timeInMillis
|
|
||||||
}
|
|
||||||
// parses: "12-20-2019" and defaults everything that wasn't taken into account to 0
|
|
||||||
else -> {
|
|
||||||
try {
|
|
||||||
parsedDate = dateFormat.parse(date)?.time ?: 0L
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
/* nothing to do, parsedDate is initialized with 0L */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parsedDate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val jsonHeaders = headersBuilder()
|
|
||||||
.set("Accept", "application/json")
|
|
||||||
.add("Content-Type", "application/json")
|
|
||||||
.add("Origin", baseUrl)
|
|
||||||
.set("Sec-Fetch-Dest", "empty")
|
|
||||||
.set("Sec-Fetch-Mode", "cors")
|
|
||||||
.set("Sec-Fetch-Site", "cross-site")
|
|
||||||
.removeAll("Upgrade-Insecure-Requests")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val slug = chapter.url
|
|
||||||
.substringAfter("chapter/")
|
|
||||||
.substringBefore("/")
|
|
||||||
val number = chapter.url
|
|
||||||
.substringAfter("chapter-")
|
|
||||||
.removeSuffix("/")
|
|
||||||
val body =
|
|
||||||
"{\"query\":\"{chapter(x:$serverId,slug:\\\"$slug\\\",number:$number){id,title,mangaID,number,slug,date,pages,noAd,manga{id,title,slug,mainSlug,author,isWebtoon,isYaoi,isPorn,isSoftPorn,unauthFile,isLicensed}}}\"}".toRequestBody(
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
return POST("$cdnApiUrl/graphql", jsonHeaders, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
|
||||||
super.fetchPageList(chapter)
|
|
||||||
.doOnError { refreshApiKey(chapter) }
|
|
||||||
.retry(1)
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val cdn = "$cdnImgUrl/file/imghub"
|
|
||||||
val chapterObject = json
|
|
||||||
.decodeFromString<GraphQLDataDto<ChapterDto>>(response.body!!.string())
|
|
||||||
|
|
||||||
if (chapterObject.data?.chapter == null) {
|
|
||||||
if (chapterObject.errors != null) {
|
|
||||||
val errors = chapterObject.errors.joinToString("\n") { it.message }
|
|
||||||
throw Exception(errors)
|
|
||||||
}
|
|
||||||
throw Exception("Unknown error while processing pages")
|
|
||||||
}
|
|
||||||
|
|
||||||
val pagesObject = json
|
|
||||||
.decodeFromString<JsonObject>(chapterObject.data.chapter.pages)
|
|
||||||
val pages = pagesObject.values.map { it.jsonPrimitive.content }
|
|
||||||
|
|
||||||
return pages.mapIndexed { i, path -> Page(i, "", "$cdn/$path") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> =
|
|
||||||
throw UnsupportedOperationException("Not used.")
|
|
||||||
|
|
||||||
// Image
|
|
||||||
override fun imageUrlRequest(page: Page): Request {
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Accept", "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
|
||||||
.set("Sec-Fetch-Dest", "image")
|
|
||||||
.set("Sec-Fetch-Mode", "no-cors")
|
|
||||||
.set("Sec-Fetch-Site", "cross-site")
|
|
||||||
.removeAll("Upgrade-Insecure-Requests")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(page.url, newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String =
|
|
||||||
throw UnsupportedOperationException("Not used.")
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
private class Genre(title: String, val key: String) : Filter.TriState(title) {
|
|
||||||
override fun toString(): String = name
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Order(title: String, val key: String) : Filter.TriState(title) {
|
|
||||||
override fun toString(): String = name
|
|
||||||
}
|
|
||||||
|
|
||||||
private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Order", orders, 0)
|
|
||||||
private class GenreList(genres: Array<Genre>) : Filter.Select<Genre>("Genres", genres, 0)
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
OrderBy(orderBy),
|
|
||||||
GenreList(genres)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val orderBy = arrayOf(
|
|
||||||
Order("Popular", "POPULAR"),
|
|
||||||
Order("Updates", "LATEST"),
|
|
||||||
Order("A-Z", "ALPHABET"),
|
|
||||||
Order("New", "NEW"),
|
|
||||||
Order("Completed", "COMPLETED")
|
|
||||||
)
|
|
||||||
|
|
||||||
private val genres = arrayOf(
|
|
||||||
Genre("All Genres", "all"),
|
|
||||||
Genre("[no chapters]", "no-chapters"),
|
|
||||||
Genre("4-Koma", "4-koma"),
|
|
||||||
Genre("Action", "action"),
|
|
||||||
Genre("Adult", "adult"),
|
|
||||||
Genre("Adventure", "adventure"),
|
|
||||||
Genre("Award Winning", "award-winning"),
|
|
||||||
Genre("Comedy", "comedy"),
|
|
||||||
Genre("Cooking", "cooking"),
|
|
||||||
Genre("Crime", "crime"),
|
|
||||||
Genre("Demons", "demons"),
|
|
||||||
Genre("Doujinshi", "doujinshi"),
|
|
||||||
Genre("Drama", "drama"),
|
|
||||||
Genre("Ecchi", "ecchi"),
|
|
||||||
Genre("Fantasy", "fantasy"),
|
|
||||||
Genre("Food", "food"),
|
|
||||||
Genre("Game", "game"),
|
|
||||||
Genre("Gender bender", "gender-bender"),
|
|
||||||
Genre("Harem", "harem"),
|
|
||||||
Genre("Historical", "historical"),
|
|
||||||
Genre("Horror", "horror"),
|
|
||||||
Genre("Isekai", "isekai"),
|
|
||||||
Genre("Josei", "josei"),
|
|
||||||
Genre("Kids", "kids"),
|
|
||||||
Genre("Magic", "magic"),
|
|
||||||
Genre("Magical Girls", "magical-girls"),
|
|
||||||
Genre("Manhua", "manhua"),
|
|
||||||
Genre("Manhwa", "manhwa"),
|
|
||||||
Genre("Martial arts", "martial-arts"),
|
|
||||||
Genre("Mature", "mature"),
|
|
||||||
Genre("Mecha", "mecha"),
|
|
||||||
Genre("Medical", "medical"),
|
|
||||||
Genre("Military", "military"),
|
|
||||||
Genre("Music", "music"),
|
|
||||||
Genre("Mystery", "mystery"),
|
|
||||||
Genre("One shot", "one-shot"),
|
|
||||||
Genre("Oneshot", "oneshot"),
|
|
||||||
Genre("Parody", "parody"),
|
|
||||||
Genre("Police", "police"),
|
|
||||||
Genre("Psychological", "psychological"),
|
|
||||||
Genre("Romance", "romance"),
|
|
||||||
Genre("School life", "school-life"),
|
|
||||||
Genre("Sci fi", "sci-fi"),
|
|
||||||
Genre("Seinen", "seinen"),
|
|
||||||
Genre("Shotacon", "shotacon"),
|
|
||||||
Genre("Shoujo", "shoujo"),
|
|
||||||
Genre("Shoujo ai", "shoujo-ai"),
|
|
||||||
Genre("Shoujoai", "shoujoai"),
|
|
||||||
Genre("Shounen", "shounen"),
|
|
||||||
Genre("Shounen ai", "shounen-ai"),
|
|
||||||
Genre("Shounenai", "shounenai"),
|
|
||||||
Genre("Slice of life", "slice-of-life"),
|
|
||||||
Genre("Smut", "smut"),
|
|
||||||
Genre("Space", "space"),
|
|
||||||
Genre("Sports", "sports"),
|
|
||||||
Genre("Super Power", "super-power"),
|
|
||||||
Genre("Superhero", "superhero"),
|
|
||||||
Genre("Supernatural", "supernatural"),
|
|
||||||
Genre("Thriller", "thriller"),
|
|
||||||
Genre("Tragedy", "tragedy"),
|
|
||||||
Genre("Vampire", "vampire"),
|
|
||||||
Genre("Webtoon", "webtoon"),
|
|
||||||
Genre("Webtoons", "webtoons"),
|
|
||||||
Genre("Wuxia", "wuxia"),
|
|
||||||
Genre("Yaoi", "yaoi"),
|
|
||||||
Genre("Yuri", "yuri")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DTO
|
|
||||||
@Serializable
|
|
||||||
data class GraphQLDataDto<T>(
|
|
||||||
val errors: List<ErrorDto>? = null,
|
|
||||||
val data: T? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ErrorDto(
|
|
||||||
val message: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ChapterDto(
|
|
||||||
val chapter: ChapterInnerDto?
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ChapterInnerDto(
|
|
||||||
val date: String,
|
|
||||||
val id: Int,
|
|
||||||
val manga: MangaInnerDto,
|
|
||||||
@SerialName("mangaID") val mangaId: Int,
|
|
||||||
val noAd: Boolean,
|
|
||||||
val number: Float,
|
|
||||||
val pages: String,
|
|
||||||
val slug: String,
|
|
||||||
val title: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MangaInnerDto(
|
|
||||||
val author: String,
|
|
||||||
val id: Int,
|
|
||||||
val isLicensed: Boolean,
|
|
||||||
val isPorn: Boolean,
|
|
||||||
val isSoftPorn: Boolean,
|
|
||||||
val isWebtoon: Boolean,
|
|
||||||
val isYaoi: Boolean,
|
|
||||||
val mainSlug: String,
|
|
||||||
val slug: String,
|
|
||||||
val title: String,
|
|
||||||
val unauthFile: Boolean
|
|
||||||
)
|
|
|
@ -1,36 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.mangahub
|
|
||||||
|
|
||||||
import generator.ThemeSourceData.SingleLang
|
|
||||||
import generator.ThemeSourceGenerator
|
|
||||||
|
|
||||||
class MangaHubGenerator : ThemeSourceGenerator {
|
|
||||||
|
|
||||||
override val themePkg = "mangahub"
|
|
||||||
|
|
||||||
override val themeClass = "MangaHub"
|
|
||||||
|
|
||||||
override val baseVersionCode: Int = 10
|
|
||||||
|
|
||||||
override val sources = listOf(
|
|
||||||
SingleLang("1Manga.co", "https://1manga.co", "en", isNsfw = true, className = "OneMangaCo"),
|
|
||||||
SingleLang("MangaFox.fun", "https://mangafox.fun", "en", isNsfw = true, className = "MangaFoxFun"),
|
|
||||||
SingleLang("MangaHere.onl", "https://mangahere.onl", "en", isNsfw = true, className = "MangaHereOnl"),
|
|
||||||
SingleLang("MangaHub", "https://mangahub.io", "en", isNsfw = true, overrideVersionCode = 10, className = "MangaHubIo"),
|
|
||||||
SingleLang("Mangakakalot.fun", "https://mangakakalot.fun", "en", isNsfw = true, className = "MangakakalotFun"),
|
|
||||||
SingleLang("MangaNel", "https://manganel.me", "en", isNsfw = true),
|
|
||||||
SingleLang("MangaOnline.fun", "https://mangaonline.fun", "en", isNsfw = true, className = "MangaOnlineFun"),
|
|
||||||
SingleLang("MangaPanda.onl", "https://mangapanda.onl", "en", className = "MangaPandaOnl"),
|
|
||||||
SingleLang("MangaReader.site", "https://mangareader.site", "en", className = "MangaReaderSite"),
|
|
||||||
SingleLang("MangaToday", "https://mangatoday.fun", "en", isNsfw = true),
|
|
||||||
SingleLang("MangaTown (unoriginal)", "https://manga.town", "en", isNsfw = true, className = "MangaTownHub"),
|
|
||||||
// SingleLang("MF Read Online", "https://mangafreereadonline.com", "en", isNsfw = true), // different pageListParse logic
|
|
||||||
// SingleLang("OneManga.info", "https://onemanga.info", "en", isNsfw = true, className = "OneMangaInfo"), // Some chapters link to 1manga.co, hard to filter
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
MangaHubGenerator().createAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|