Add MyAnimeList Free Manga (#2339)

* Add MyAnimeList Free Manga.

* Remove unused imports.

* Use native method to decrypt the images.

* Fix login warning not hiding when login is done.
This commit is contained in:
Alessandro Jean 2020-03-03 22:19:00 -03:00 committed by GitHub
parent e0fbcc6ce0
commit c3ea208356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 0 deletions

View File

@ -0,0 +1,17 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
appName = 'Tachiyomi: MyAnimeList Free Manga'
pkgNameSuffix = 'en.myanimelist'
extClass = '.MyAnimeList'
extVersionCode = 1
libVersion = '1.2'
}
dependencies {
compileOnly 'com.google.code.gson:gson:2.8.2'
compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,170 @@
package eu.kanade.tachiyomi.extension.en.myanimelist
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.*
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class MyAnimeList : ParsedHttpSource() {
override val name = "MyAnimeList Free Manga"
override val baseUrl = "https://myanimelist.net"
override val lang = "en"
override val supportsLatest = false
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", USER_AGENT)
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/read/manga")
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor { imageIntercept(it) }
.build()
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/read/manga", headers)
override fun popularMangaSelector(): String = "table.top-ranking-table tr.ranking-list td.title"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
val malId = element.select("div.detail a.fs14").first().attr("href").substringAfterLast("/")
title = element.select("div.detail a.fs14").first().text().substringBefore(" (")
thumbnail_url = element.select("a.fl-l img.lazyloaded").first()?.attr("src")
url = "/read/manga/detail/$malId"
}
override fun popularMangaNextPageSelector(): String? = null
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return super.fetchSearchManga(page, query, filters)
.map { MangasPage(it.mangas.filter { m -> m.title.contains(query, true) }, it.hasNextPage) }
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(page)
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun mangaDetailsRequest(manga: SManga): Request
= GET(baseUrl + manga.url, headers, CacheControl.FORCE_NETWORK)
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
val infoElement = document.select("div#content div.membership-manager table[width='100%'] tr").first()
val people = infoElement.select("td:nth-child(2) table span.information.studio.author").first().text().split("), ")
val requireLogin = document.select("div.search_all div.content-left table tr:contains(Sign-in)").first() != null
title = infoElement.select("td:nth-child(2) h1.comic-detail-title").first().text()
author = people
.filter { it.contains("Story") }
.joinToString("; ") { it.substringBefore(" (") }
artist = people
.filter { it.contains("Art") }
.joinToString("; ") { it.substringBefore(" (") }
genre = infoElement.select("td:nth-child(2) table + div section a").joinToString { it.text() }
description = (if (requireLogin) LOGIN_WARNING else "") +
document.select("div.search_all div.content-right span[itemprop=description]").first().text()
thumbnail_url = infoElement.select("td:nth-child(1) a img.lazyload").first().attr("data-src")
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return super.fetchChapterList(manga)
.map { it.reversed() }
}
override fun chapterListSelector() = "div.search_all div.content-left table.free-manga-detail-table " +
"tr:not(:contains(Sign-in)):not(:contains(Buy)):not(:contains(Coming))"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
val info = element.select("td:nth-child(2) div.clearfix a.fs15").first()
name = info.text()
setUrlWithoutDomain(info.attr("href"))
}
override fun pageListParse(document: Document): List<Page> {
val chapterData = document.select("div.v-manga-store-viewer").first()
?: throw Exception("You must login to read this chapter.")
val chapterJson = chapterData.attr("data-state")
val chapterInfo = JSON_PARSER.parse(chapterJson).obj
val imageBaseUrl = chapterInfo["manuscript"]["image_base_url"].string
val queryParamsPart = chapterInfo["manuscript"]["query_params_part"].string
return chapterInfo["manuscript"]["filenames"].array
.mapIndexed { i, fileName -> Page(i, "", "$imageBaseUrl/${fileName.string}?$queryParamsPart") }
}
private fun imageIntercept(chain: Interceptor.Chain) : Response {
val request = chain.request()
val response = chain.proceed(request)
if (!request.url().queryParameterNames().contains("Key-Pair-Id")) {
return response
}
val contentType = response.header("Content-Type", "image/jpeg")!!
val image = decodeImage(response.body()!!.bytes())
val body = ResponseBody.create(MediaType.parse(contentType), image)
return response.newBuilder().body(body).build()
}
/**
* Decodes the image of chapter page using the XOR Cipher.
*
* The [image] comes in a [ByteArray], with the following properties:
*
* image[0] = 1
* image[1] = n (key size)
* image[2 ~ 2 + n - 1] = i (key)
* image[2 + n ~ image.length - 1] = r (image contents)
*
* To decrypt, for each byte b in the index o in r,
* b needs to be replaced with b xor i[o % n].
*/
private fun decodeImage(image: ByteArray): ByteArray {
val n = image[1].toPositiveInt()
val i = image.slice(2 until 2 + n).map{ it.toPositiveInt() }
val r = image.drop(2 + n).map { it.toPositiveInt() }.toMutableList()
for ((o, b) in r.iterator().withIndex()) {
r[o] = b xor i[o % n]
}
return ByteArray(r.size) { pos -> r[pos].toByte() }
}
private fun Byte.toPositiveInt() = toInt() and 0xFF
override fun imageUrlParse(document: Document) = ""
override fun latestUpdatesRequest(page: Int): Request = throw Exception("This method should not be called!")
override fun latestUpdatesSelector() = throw Exception("This method should not be called!")
override fun latestUpdatesFromElement(element: Element): SManga = throw Exception("This method should not be called!")
override fun latestUpdatesNextPageSelector() = throw Exception("This method should not be called!")
override fun searchMangaSelector() = throw Exception("This method should not be called!")
override fun searchMangaFromElement(element: Element): SManga = throw Exception("This method should not be called!")
override fun searchMangaNextPageSelector() = throw Exception("This method should not be called!")
companion object {
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36"
private val JSON_PARSER by lazy { JsonParser() }
private const val LOGIN_WARNING = "This manga has chapters that require login. Use the WebView to login into your MyAnimeList account.\n\n"
}
}