MangaHere fix (#1029)

* mangahere fix

* adult manga cookie, logs removed

* status fix

* version number update
This commit is contained in:
Yaroslav Shuliak 2019-04-15 18:46:31 +03:00 committed by Carlos
parent b2c231eaea
commit 952656f6b6
2 changed files with 234 additions and 168 deletions

View File

@ -5,8 +5,12 @@ ext {
appName = 'Tachiyomi: Mangahere' appName = 'Tachiyomi: Mangahere'
pkgNameSuffix = 'en.mangahere' pkgNameSuffix = 'en.mangahere'
extClass = '.Mangahere' extClass = '.Mangahere'
extVersionCode = 7 extVersionCode = 8
libVersion = '1.2' libVersion = '1.2'
} }
dependencies {
compileOnly project(':duktape-stub')
}
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,19 +1,16 @@
package eu.kanade.tachiyomi.extension.en.mangahere package eu.kanade.tachiyomi.extension.en.mangahere
import com.squareup.duktape.Duktape
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.HttpUrl import okhttp3.*
import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.net.ssl.SSLContext import kotlin.collections.ArrayList
import javax.net.ssl.X509TrustManager
class Mangahere : ParsedHttpSource() { class Mangahere : ParsedHttpSource() {
@ -27,233 +24,298 @@ class Mangahere : ParsedHttpSource() {
override val supportsLatest = true 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() 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() .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 { 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 { override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers) return GET("$baseUrl/directory/$page.htm?latest", 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
} }
override fun popularMangaFromElement(element: Element): SManga { 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 { override fun latestUpdatesFromElement(element: Element): SManga {
return popularMangaFromElement(element) 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 { 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) val url = HttpUrl.parse("$baseUrl/search")!!.newBuilder()
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) { filters.forEach {
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state]) when(it) {
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
is TextField -> url.addQueryParameter(filter.key, filter.state) is TypeList -> {
is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state]) url.addEncodedQueryParameter("type", types[it.values[it.state]].toString())
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")
} }
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) 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 { 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 { 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() val manga = SManga.create()
manga.author = infoElement.select("a[href*=author/]").first()?.text() manga.author = document.select(".detail-info-right-say > a")?.first()?.text()
manga.artist = infoElement.select("a[href*=artist/]").first()?.text() manga.artist = ""
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") manga.genre = document.select(".detail-info-right-tag-list > a")?.joinToString { it.text() }
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") manga.description = document.select(".fullcontent")?.first()?.text()
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src") manga.thumbnail_url = document.select("img.detail-info-cover-img")?.first()
?.attr("src")
if (licensedElement?.text()?.contains("licensed") == true) { document.select("span.detail-info-right-title-tip")?.first()?.text()?.also { statusText ->
manga.status = SManga.LICENSED when {
} else { statusText.contains("ongoing", true) -> manga.status = SManga.ONGOING
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } statusText.contains("completed", true) -> manga.status = SManga.COMPLETED
else -> manga.status = SManga.UNKNOWN
}
} }
return manga return manga
} }
private fun parseStatus(status: String) = when { override fun chapterListSelector() = "ul.detail-main-list > li"
status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
override fun chapterFromElement(element: Element): SChapter { 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() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(element.select("a").first().attr("href"))
chapter.name = urlElement.text() + volume + title chapter.name = element.select("a p.title3").first().text()
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("a p.title2").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter return chapter
} }
private fun parseChapterDate(date: String): Long { private fun parseChapterDate(date: String): Long {
return if ("Today" in date) { return try {
Calendar.getInstance().apply { SimpleDateFormat("MMM dd,yyyy", Locale.ENGLISH).parse(date).time
set(Calendar.HOUR_OF_DAY, 0) } catch (e: ParseException) {
set(Calendar.MINUTE, 0) 0L
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
}
} }
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val licensedError = document.select(".mangaread_error > .mt10").first()
if (licensedError != null) { val html = document.html()
throw Exception(licensedError.text()) val link = document.location()
}
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
if (!it.attr("value").contains("featured.html")) { val duktape = Duktape.create()
pages.add(Page(pages.size, "http:" + it.attr("value")))
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 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") override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
private class Status : Filter.TriState("Completed") private class Genre(title: String, val id: Int) : Filter.TriState(title)
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 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) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
TextField("Author", "author"), TypeList(types.keys.toList().sorted().toTypedArray()),
TextField("Artist", "artist"), CompletionList(completions),
Type(), GenreList(genres)
Status(),
OrderBy(),
GenreList(getGenreList())
) )
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n') private val types = hashMapOf(
// http://www.mangahere.co/advsearch.htm "Japanese Manga" to 1,
private fun getGenreList() = listOf( "Korean Manhwa" to 2,
Genre("Action"), "Other Manga" to 4,
Genre("Adventure"), "Any" to 0
Genre("Comedy"), )
Genre("Doujinshi"),
Genre("Drama"), private val completions = arrayOf("Either","No","Yes")
Genre("Ecchi"),
Genre("Fantasy"), private val genres = arrayListOf(
Genre("Gender Bender"), Genre("Action", 1),
Genre("Harem"), Genre("Adventure", 2),
Genre("Historical"), Genre("Comedy", 3),
Genre("Horror"), Genre("Fantasy", 4),
Genre("Josei"), Genre("Historical", 5),
Genre("Martial Arts"), Genre("Horror", 6),
Genre("Mature"), Genre("Martial Arts", 7),
Genre("Mecha"), Genre("Mystery", 8),
Genre("Mystery"), Genre("Romance", 9),
Genre("One Shot"), Genre("Shounen Ai", 10),
Genre("Psychological"), Genre("Supernatural", 11),
Genre("Romance"), Genre("Drama", 12),
Genre("School Life"), Genre("Shounen", 13),
Genre("Sci-fi"), Genre("School Life", 14),
Genre("Seinen"), Genre("Shoujo", 15),
Genre("Shoujo"), Genre("Gender Bender", 16),
Genre("Shoujo Ai"), Genre("Josei", 17),
Genre("Shounen"), Genre("Psychological", 18),
Genre("Shounen Ai"), Genre("Seinen", 19),
Genre("Slice of Life"), Genre("Slice of Life", 20),
Genre("Sports"), Genre("Sci-fi", 21),
Genre("Supernatural"), Genre("Ecchi", 22),
Genre("Tragedy"), Genre("Harem", 23),
Genre("Yaoi"), Genre("Shoujo Ai", 24),
Genre("Yuri") 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)
) )
} }