MangaHere fix (#1029)
* mangahere fix * adult manga cookie, logs removed * status fix * version number update
This commit is contained in:
parent
b2c231eaea
commit
952656f6b6
@ -5,8 +5,12 @@ ext {
|
||||
appName = 'Tachiyomi: Mangahere'
|
||||
pkgNameSuffix = 'en.mangahere'
|
||||
extClass = '.Mangahere'
|
||||
extVersionCode = 7
|
||||
extVersionCode = 8
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':duktape-stub')
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,19 +1,16 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangahere
|
||||
|
||||
import com.squareup.duktape.Duktape
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.*
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class Mangahere : ParsedHttpSource() {
|
||||
|
||||
@ -27,233 +24,298 @@ class Mangahere : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val trustManager = object : X509TrustManager {
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private val sslContext = SSLContext.getInstance("SSL").apply {
|
||||
init(null, arrayOf(trustManager), SecureRandom())
|
||||
}
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.sslSocketFactory(sslContext.socketFactory, trustManager)
|
||||
.cookieJar(object : CookieJar{
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {}
|
||||
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
|
||||
return ArrayList<Cookie>().apply {
|
||||
add(Cookie.Builder()
|
||||
.domain("www.mangahere.cc")
|
||||
.path("/")
|
||||
.name("isAdult")
|
||||
.value("1")
|
||||
.build()) }
|
||||
}
|
||||
|
||||
})
|
||||
.build()
|
||||
|
||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||
override fun popularMangaSelector() = ".manga-list-1-list li"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||
override fun latestUpdatesSelector() = ".manga-list-1-list li"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?views.za", headers)
|
||||
return GET("$baseUrl/directory/$page.htm", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers)
|
||||
}
|
||||
|
||||
private fun mangaFromElement(query: String, element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select(query).first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
||||
}
|
||||
return manga
|
||||
return GET("$baseUrl/directory/$page.htm?latest", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("div.title > a", element)
|
||||
val manga = SManga.create()
|
||||
|
||||
val titleElement = element.select("a").first()
|
||||
manga.title = titleElement.attr("title")
|
||||
manga.setUrlWithoutDomain(titleElement.attr("href"))
|
||||
manga.thumbnail_url = element.select("img.manga-list-1-cover")
|
||||
?.first()?.attr("src")
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||
override fun popularMangaNextPageSelector() = "div.pager-list-left a:last-child"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||
override fun latestUpdatesNextPageSelector() = "div.pager-list-left a:last-child"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1")!!.newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state])
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
val url = HttpUrl.parse("$baseUrl/search")!!.newBuilder()
|
||||
|
||||
filters.forEach {
|
||||
when(it) {
|
||||
|
||||
is TypeList -> {
|
||||
url.addEncodedQueryParameter("type", types[it.values[it.state]].toString())
|
||||
}
|
||||
is CompletionList -> url.addEncodedQueryParameter("st", it.state.toString())
|
||||
is GenreList -> {
|
||||
|
||||
val genreFilter = filters.find { it is GenreList } as GenreList?
|
||||
val includeGenres = ArrayList<Int>()
|
||||
val excludeGenres = ArrayList<Int>()
|
||||
genreFilter?.state?.forEach { genre ->
|
||||
if (genre.isIncluded())
|
||||
includeGenres.add(genre.id)
|
||||
else if (genre.isExcluded())
|
||||
excludeGenres.add(genre.id)
|
||||
}
|
||||
|
||||
url.addEncodedQueryParameter("genres", includeGenres.joinToString(","))
|
||||
.addEncodedQueryParameter("nogenres", excludeGenres.joinToString(","))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
|
||||
url.addEncodedQueryParameter("page", page.toString())
|
||||
.addEncodedQueryParameter("title", query)
|
||||
.addEncodedQueryParameter("sort", null)
|
||||
.addEncodedQueryParameter("stype", 1.toString())
|
||||
.addEncodedQueryParameter("name", null)
|
||||
.addEncodedQueryParameter("author_method","cw")
|
||||
.addEncodedQueryParameter("author", null)
|
||||
.addEncodedQueryParameter("artist_method", "cw")
|
||||
.addEncodedQueryParameter("artist", null)
|
||||
.addEncodedQueryParameter("rating_method","eq")
|
||||
.addEncodedQueryParameter("rating",null)
|
||||
.addEncodedQueryParameter("released_method","eq")
|
||||
.addEncodedQueryParameter("released", null)
|
||||
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||
override fun searchMangaSelector() = ".manga-list-4-list > li"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("a.manga_info", element)
|
||||
val manga = SManga.create()
|
||||
val titleEl = element.select(".manga-list-4-item-title > a").first()
|
||||
manga.setUrlWithoutDomain(titleEl?.attr("href") ?: "")
|
||||
manga.title = titleEl?.attr("title") ?: ""
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
override fun searchMangaNextPageSelector() = "div.pager-list-left a:last-child"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select(".manga_detail_top").first()
|
||||
val infoElement = detailElement.select(".detail_topText").first()
|
||||
val licensedElement = document.select(".mt10.color_ff00.mb10").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("a[href*=author/]").first()?.text()
|
||||
manga.artist = infoElement.select("a[href*=artist/]").first()?.text()
|
||||
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
||||
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
||||
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
|
||||
manga.author = document.select(".detail-info-right-say > a")?.first()?.text()
|
||||
manga.artist = ""
|
||||
manga.genre = document.select(".detail-info-right-tag-list > a")?.joinToString { it.text() }
|
||||
manga.description = document.select(".fullcontent")?.first()?.text()
|
||||
manga.thumbnail_url = document.select("img.detail-info-cover-img")?.first()
|
||||
?.attr("src")
|
||||
|
||||
if (licensedElement?.text()?.contains("licensed") == true) {
|
||||
manga.status = SManga.LICENSED
|
||||
} else {
|
||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
document.select("span.detail-info-right-title-tip")?.first()?.text()?.also { statusText ->
|
||||
when {
|
||||
statusText.contains("ongoing", true) -> manga.status = SManga.ONGOING
|
||||
statusText.contains("completed", true) -> manga.status = SManga.COMPLETED
|
||||
else -> manga.status = SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
|
||||
override fun chapterListSelector() = "ul.detail-main-list > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val parentEl = element.select("span.left").first()
|
||||
|
||||
val urlElement = parentEl.select("a").first()
|
||||
|
||||
var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
|
||||
if (volume.length > 0) {
|
||||
volume = " - " + volume
|
||||
}
|
||||
|
||||
var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
|
||||
if (title.length > 0) {
|
||||
title = " - " + title
|
||||
}
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text() + volume + title
|
||||
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
chapter.setUrlWithoutDomain(element.select("a").first().attr("href"))
|
||||
chapter.name = element.select("a p.title3").first().text()
|
||||
chapter.date_upload = element.select("a p.title2").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if ("Today" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else if ("Yesterday" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
return try {
|
||||
SimpleDateFormat("MMM dd,yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val licensedError = document.select(".mangaread_error > .mt10").first()
|
||||
if (licensedError != null) {
|
||||
throw Exception(licensedError.text())
|
||||
}
|
||||
|
||||
val html = document.html()
|
||||
val link = document.location()
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||
if (!it.attr("value").contains("featured.html")) {
|
||||
pages.add(Page(pages.size, "http:" + it.attr("value")))
|
||||
|
||||
val duktape = Duktape.create()
|
||||
|
||||
var secretKey = extractSecretKey(html, duktape)
|
||||
|
||||
val chapterIdStartLoc = html.indexOf("chapterid")
|
||||
val chapterId = html.substring(
|
||||
chapterIdStartLoc + 11,
|
||||
html.indexOf(";", chapterIdStartLoc)).trim()
|
||||
|
||||
val chapterPagesElement = document.select(".pager-list-left > span").first()
|
||||
val pagesLinksElements = chapterPagesElement.select("a")
|
||||
val pagesNumber = pagesLinksElements.get(pagesLinksElements.size - 2).attr("data-page").toInt()
|
||||
|
||||
val pageBase = link.substring(0, link.lastIndexOf("/"))
|
||||
|
||||
for (i in 1..pagesNumber){
|
||||
|
||||
val pageLink = "${pageBase}/chapterfun.ashx?cid=$chapterId&page=$i&key=$secretKey"
|
||||
|
||||
var responseText = ""
|
||||
|
||||
for (tr in 1..3){
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(pageLink)
|
||||
.addHeader("Referer",link)
|
||||
.addHeader("Accept","*/*")
|
||||
.addHeader("Accept-Language","en-US,en;q=0.9")
|
||||
.addHeader("Connection","keep-alive")
|
||||
.addHeader("Host","www.mangahere.cc")
|
||||
.addHeader("User-Agent", System.getProperty("http.agent") ?: "")
|
||||
.addHeader("X-Requested-With","XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
responseText = response.body()!!.string()
|
||||
|
||||
if (responseText.isNotEmpty())
|
||||
break
|
||||
else
|
||||
secretKey = ""
|
||||
|
||||
}
|
||||
|
||||
val deobfuscatedScript = duktape.evaluate(responseText.removePrefix("eval")).toString()
|
||||
|
||||
val baseLinkStartPos = deobfuscatedScript.indexOf("pix=") + 5
|
||||
val baseLinkEndPos = deobfuscatedScript.indexOf(";", baseLinkStartPos) - 1
|
||||
val baseLink = deobfuscatedScript.substring(baseLinkStartPos, baseLinkEndPos)
|
||||
|
||||
val imageLinkStartPos = deobfuscatedScript.indexOf("pvalue=") + 9
|
||||
val imageLinkEndPos = deobfuscatedScript.indexOf("\"", imageLinkStartPos)
|
||||
val imageLink = deobfuscatedScript.substring(imageLinkStartPos, imageLinkEndPos)
|
||||
|
||||
pages.add(Page(i, "", "http:$baseLink$imageLink"))
|
||||
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
|
||||
duktape.close()
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
private fun extractSecretKey(html: String, duktape: Duktape): String {
|
||||
|
||||
val secretKeyScriptLocation = html.indexOf("eval(function(p,a,c,k,e,d)")
|
||||
val secretKeyScriptEndLocation = html.indexOf("</script>", secretKeyScriptLocation)
|
||||
val secretKeyScript = html.substring(secretKeyScriptLocation, secretKeyScriptEndLocation).removePrefix("eval")
|
||||
|
||||
val secretKeyDeobfuscatedScript = duktape.evaluate(secretKeyScript).toString()
|
||||
|
||||
val secretKeyStartLoc = secretKeyDeobfuscatedScript.indexOf("'")
|
||||
val secretKeyEndLoc = secretKeyDeobfuscatedScript.indexOf(";")
|
||||
|
||||
val secretKeyResultScript = secretKeyDeobfuscatedScript.substring(
|
||||
secretKeyStartLoc, secretKeyEndLoc)
|
||||
|
||||
return duktape.evaluate(secretKeyResultScript).toString()
|
||||
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
private class Genre(title: String, val id: Int) : Filter.TriState(title)
|
||||
|
||||
private class TypeList(types: Array<String>) : Filter.Select<String>("Type", types,0)
|
||||
private class CompletionList(completions: Array<String>) : Filter.Select<String>("Completed series", completions,0)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
Type(),
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
TypeList(types.keys.toList().sorted().toTypedArray()),
|
||||
CompletionList(completions),
|
||||
GenreList(genres)
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n')
|
||||
// http://www.mangahere.co/advsearch.htm
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("One Shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
private val types = hashMapOf(
|
||||
"Japanese Manga" to 1,
|
||||
"Korean Manhwa" to 2,
|
||||
"Other Manga" to 4,
|
||||
"Any" to 0
|
||||
)
|
||||
|
||||
private val completions = arrayOf("Either","No","Yes")
|
||||
|
||||
private val genres = arrayListOf(
|
||||
Genre("Action", 1),
|
||||
Genre("Adventure", 2),
|
||||
Genre("Comedy", 3),
|
||||
Genre("Fantasy", 4),
|
||||
Genre("Historical", 5),
|
||||
Genre("Horror", 6),
|
||||
Genre("Martial Arts", 7),
|
||||
Genre("Mystery", 8),
|
||||
Genre("Romance", 9),
|
||||
Genre("Shounen Ai", 10),
|
||||
Genre("Supernatural", 11),
|
||||
Genre("Drama", 12),
|
||||
Genre("Shounen", 13),
|
||||
Genre("School Life", 14),
|
||||
Genre("Shoujo", 15),
|
||||
Genre("Gender Bender", 16),
|
||||
Genre("Josei", 17),
|
||||
Genre("Psychological", 18),
|
||||
Genre("Seinen", 19),
|
||||
Genre("Slice of Life", 20),
|
||||
Genre("Sci-fi", 21),
|
||||
Genre("Ecchi", 22),
|
||||
Genre("Harem", 23),
|
||||
Genre("Shoujo Ai", 24),
|
||||
Genre("Yuri", 25),
|
||||
Genre("Mature", 26),
|
||||
Genre("Tragedy", 27),
|
||||
Genre("Yaoi", 28),
|
||||
Genre("Doujinshi", 29),
|
||||
Genre("Sports", 30),
|
||||
Genre("Adult", 31),
|
||||
Genre("One Shot", 32),
|
||||
Genre("Smut", 33),
|
||||
Genre("Mecha", 34),
|
||||
Genre("Shotacon", 35),
|
||||
Genre("Lolicon", 36)
|
||||
)
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user