MangaRock.es extension (#2794)

* [WIP} MangaRock.es extension

* Search, filters

* Pages

* More imports

* Icons
This commit is contained in:
Mike 2020-05-15 17:56:46 -04:00 committed by GitHub
parent 58a311af12
commit 77ebca8d59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 332 additions and 0 deletions

View File

@ -0,0 +1,17 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
appName = 'Tachiyomi: MangaRock.es'
pkgNameSuffix = 'en.mangarockes'
extClass = '.MangaRockEs'
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: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -0,0 +1,315 @@
package eu.kanade.tachiyomi.extension.en.mangarockes
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.MediaType
import okhttp3.ResponseBody
import okhttp3.HttpUrl
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Calendar
import kotlin.experimental.and
import kotlin.experimental.xor
class MangaRockEs : ParsedHttpSource() {
override val name = "MangaRock.es"
override val baseUrl = "https://mangarock.es"
override val lang = "en"
override val supportsLatest = true
// Handles the page decoding
override val client: OkHttpClient = network.cloudflareClient.newBuilder().addInterceptor(fun(chain): Response {
val url = chain.request().url().toString()
val response = chain.proceed(chain.request())
if (!url.endsWith(".mri")) return response
val decoded: ByteArray = decodeMri(response)
val mediaType = MediaType.parse("image/webp")
val rb = ResponseBody.create(mediaType, decoded)
return response.newBuilder().body(rb).build()
}).build()
// Popular
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/$page?sort=rank", headers)
override fun popularMangaSelector() = "div.col-five"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
element.select("h3 a").let {
title = it.text()
setUrlWithoutDomain(it.attr("href"))
}
thumbnail_url = element.select("img").attr("abs:src")
}
}
override fun popularMangaNextPageSelector() = "a.page-link:contains(next)"
// Latest
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga/latest/$page?sort=date", headers)
override fun latestUpdatesSelector() = "div.product-item-detail"
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/search/${query.replace(" ", "+")}/$page", headers)
} else {
val url = HttpUrl.parse("$baseUrl/manga" + if (page > 1) "/$page" else "")!!.newBuilder()
filters.forEach { filter ->
when (filter) {
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
is RankFilter -> url.addQueryParameter("rank", filter.toUriPart())
is SortBy -> url.addQueryParameter("sort", filter.toUriPart())
is GenreList -> {
val genres = filter.state
.filter { it.state }
.joinToString(".") { it.uriPart }
url.addQueryParameter("genres", genres)
}
}
}
GET(url.toString(), headers)
}
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
return SManga.create().apply {
document.select("div.block-info-manga").let { info ->
thumbnail_url = info.select("div.thumb_inner").attr("style")
.substringAfter("'").substringBefore("'")
title = info.select("h1").text()
author = info.select("div.author_item").text()
status = info.select("div.status_chapter_item").text().substringBefore(" ").let {
when {
it.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
it.contains("Completed", ignoreCase = true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
}
genre = document.select("div.tags a").joinToString { it.text() }
description = document.select("div.full.summary p").filterNot { it.text().isNullOrEmpty() }.joinToString("\n")
}
}
// Chapters
override fun chapterListSelector(): String = "tbody[data-test=chapter-table] tr"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.select("a").let {
name = it.text()
setUrlWithoutDomain(it.attr("href"))
}
date_upload = element.select("td").lastOrNull()?.text()?.let { date ->
if (date.contains("ago", ignoreCase = true)) {
val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ")
val calendar = Calendar.getInstance()
when (trimmedDate[1]) {
"day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
"hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
"minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
"second" -> calendar.apply { add(Calendar.SECOND, -trimmedDate[0].toInt()) }
}
calendar.timeInMillis
} else {
SimpleDateFormat("MMM d, yyyy", Locale.US).parse(date).time
}
} ?: 0
}
}
// Pages
private val gson by lazy { Gson() }
override fun pageListParse(response: Response): List<Page> {
val responseString = response.body()!!.string()
return Regex("""mangaData = (\[.*]);""", RegexOption.IGNORE_CASE).find(responseString)?.groupValues?.get(1)?.let { array ->
gson.fromJson<JsonArray>(array)
.mapIndexed { i, jsonElement -> Page(i, "", jsonElement.asJsonObject["url"].asString) }
}
?: Regex("""getManga\(\d+, '(http.*)',""").findAll(responseString).toList()
.mapIndexed { i, mr -> Page(i, "", mr.groupValues[1]) }
}
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException("This method should not be called!")
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("This method should not be called!")
// Filters
// See drawWebpToCanvas function in the site's client.js file
// Extracted code: https://jsfiddle.net/6h2sLcs4/30/
private fun decodeMri(response: Response): ByteArray {
val data = response.body()!!.bytes()
// Decode file if it starts with "E" (space when XOR-ed later)
if (data[0] != 69.toByte()) return data
// Reconstruct WEBP header
// Doc: https://developers.google.com/speed/webp/docs/riff_container#webp_file_header
val buffer = ByteArray(data.size + 15)
val size = data.size + 7
buffer[0] = 82 // R
buffer[1] = 73 // I
buffer[2] = 70 // F
buffer[3] = 70 // F
buffer[4] = (255.toByte() and size.toByte())
buffer[5] = (size ushr 8).toByte() and 255.toByte()
buffer[6] = (size ushr 16).toByte() and 255.toByte()
buffer[7] = (size ushr 24).toByte() and 255.toByte()
buffer[8] = 87 // W
buffer[9] = 69 // E
buffer[10] = 66 // B
buffer[11] = 80 // P
buffer[12] = 86 // V
buffer[13] = 80 // P
buffer[14] = 56 // 8
// Decrypt file content using XOR cipher with 101 as the key
val cipherKey = 101.toByte()
for (r in data.indices) {
buffer[r + 15] = cipherKey xor data[r]
}
return buffer
}
// Filters
private class StatusFilter : UriPartFilter("Status", arrayOf(
Pair("All", "all"),
Pair("Completed", "completed"),
Pair("Ongoing", "ongoing")
))
private class RankFilter : UriPartFilter("Rank", arrayOf(
Pair("All", "all"),
Pair("1 - 999", "1-999"),
Pair("1k - 2k", "1000-2000"),
Pair("2k - 3k", "2000-3000"),
Pair("3k - 4k", "3000-4000"),
Pair("4k - 5k", "4000-5000"),
Pair("5k - 6k", "5000-6000"),
Pair("6k - 7k", "6000-7000"),
Pair("7k - 8k", "7000-8000"),
Pair("8k - 9k", "8000-9000"),
Pair("9k - 19k", "9000-10000"),
Pair("10k - 11k", "10000-11000")
))
private class SortBy : UriPartFilter("Sort by", arrayOf(
Pair("Name", "name"),
Pair("Rank", "rank")
))
private class Genre(name: String, val uriPart: String) : Filter.CheckBox(name)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList(
// Search and filter don't work at the same time
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
StatusFilter(),
RankFilter(),
SortBy(),
GenreList(getGenreList())
)
// [...document.querySelectorAll('._2DMqI .mdl-checkbox')].map(n => `Genre("${n.querySelector('.mdl-checkbox__label').innerText}", "${n.querySelector('input').dataset.oid}")`).sort().join(',\n')
// on https://mangarock.com/manga
private fun getGenreList() = listOf(
Genre("4-koma", "4-koma"),
Genre("Action", "action"),
Genre("Adult", "adult"),
Genre("Adventure", "adventure"),
Genre("Comedy", "comedy"),
Genre("Demons", "demons"),
Genre("Doujinshi", "doujinshi"),
Genre("Drama", "drama"),
Genre("Ecchi", "ecchi"),
Genre("Fantasy", "fantasy"),
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("Martial Arts", "martial-arts"),
Genre("Mature", "mature"),
Genre("Mecha", "mecha"),
Genre("Military", "military"),
Genre("Music", "music"),
Genre("Mystery", "mystery"),
Genre("One Shot", "one-shot"),
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("Shoujo Ai", "shoujo-ai"),
Genre("Shoujo", "shoujo"),
Genre("Shounen Ai", "shounen-ai"),
Genre("Shounen", "shounen"),
Genre("Slice of Life", "slice-of-life"),
Genre("Smut", "smut"),
Genre("Space", "space"),
Genre("Sports", "sports"),
Genre("Super Power", "super-power"),
Genre("Supernatural", "supernatural"),
Genre("Tragedy", "tragedy"),
Genre("Vampire", "vampire"),
Genre("Webtoons", "webtoons"),
Genre("Yaoi", "yaoi"),
Genre("Yuri", "yuri")
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
}