Add filters and json migration for Mangasail (#8958)

* Added filters and some refactoring

* Migration to kotlinx-serialization
This commit is contained in:
Arraiment 2021-09-05 01:59:11 +08:00 committed by GitHub
parent 4d8f8a4db3
commit 025a1b9d39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 154 additions and 73 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Mangasail'
pkgNameSuffix = 'en.mangasail'
extClass = '.Mangasail'
extVersionCode = 2
extVersionCode = 3
libVersion = '1.2'
}

View File

@ -1,22 +1,23 @@
package eu.kanade.tachiyomi.extension.en.mangasail
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup.parse
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
@ -35,55 +36,64 @@ class Mangasail : ParsedHttpSource() {
/* Site loads some manga info (manga cover, author name, status, etc.) client side through JQuery
need to add this header for when we request these data fragments
Also necessary for latest updates request */
override fun headersBuilder() = super.headersBuilder().add("X-Authcache", "1")!!
override fun headersBuilder() = super.headersBuilder().add("X-Authcache", "1")
// Popular
override fun popularMangaRequest(page: Int) = GET("$baseUrl/directory/hot?page=${page - 1}", headers)
override fun popularMangaSelector() = "tbody tr"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/directory/hot" + if (page > 1) "/hot?page= + ${page - 1}" else "", headers)
}
override fun latestUpdatesSelector() = "ul#latest-list > li"
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/sites/all/modules/authcache/modules/authcache_p13n/frontcontroller/authcache.php?r=frag/block/showmanga-lastest_list&o[q]=node", headers)
}
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("td:first-of-type a").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst("td:first-of-type a").let {
setUrlWithoutDomain(it.attr("href"))
title = it.text()
}
manga.thumbnail_url = element.select("td img").first().attr("src")
return manga
}
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
manga.title = element.select("a strong").text()
element.select("a:has(img)").let {
manga.url = it.attr("href")
// Thumbnails are kind of low-res on latest updates page, transform the img url to get a better version
manga.thumbnail_url = it.select("img").first().attr("src").substringBefore("?").replace("styles/minicover/public/", "")
}
return manga
thumbnail_url = element.selectFirst("td img").attr("src")
}
override fun popularMangaNextPageSelector() = "table + div.text-center ul.pagination li.next a"
override fun latestUpdatesNextPageSelector(): String = "There is no next page"
// Latest
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/search/node/$query" + if (page > 1) "?page= + ${page - 1}" else "")
override fun latestUpdatesRequest(page: Int) =
GET(
"$baseUrl/sites/all/modules/authcache/modules/authcache_p13n/frontcontroller/authcache.php?r=frag/block/showmanga-lastest_list&o[q]=node",
headers
)
override fun latestUpdatesSelector() = "ul#latest-list > li"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
title = element.select("a strong").text()
element.select("a:has(img)").let {
url = it.attr("href")
// Thumbnails are kind of low-res on latest updates page, transform the img url to get a better version
thumbnail_url = it.select("img").first().attr("src").substringBefore("?").replace("styles/minicover/public/", "")
}
}
override fun searchMangaSelector() = "h3.title"
override fun latestUpdatesNextPageSelector(): String = "There is no next page"
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
return when {
query.isNotBlank() -> GET("$baseUrl/search/node/$query?page=${page - 1}")
genreFilter.state != 0 -> GET("$baseUrl/tags/${genreFilter.toUriPart()}?page=${page - 1}")
else -> GET("$baseUrl/directory/hot?page=${page - 1}", headers)
}
}
override fun searchMangaSelector() = "h3.title, div.region-content h2:has(a)"
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
element.selectFirst("a").let {
manga.setUrlWithoutDomain(it.attr("abs:href"))
manga.title = it.text()
// Search page doesn't contain cover images, have to get them from the manga's page; but first we need that page's node number
val node = getNodeNumber(client.newCall(GET(it.attr("href"), headers)).execute().asJsoup())
@ -92,29 +102,30 @@ class Mangasail : ParsedHttpSource() {
return manga
}
private val gson by lazy { Gson() }
override fun searchMangaNextPageSelector() = "li.next a"
private val json: Json by injectLazy()
// Function to get data fragments from website
private fun getNodeDetail(node: String, field: String): String? {
val requestUrl = "$baseUrl/sites/all/modules/authcache/modules/authcache_p13n/frontcontroller/authcache.php?a[field][0]=$node:full:en&r=asm/field/node/$field&o[q]=node/$node"
val requestUrl =
"$baseUrl/sites/all/modules/authcache/modules/authcache_p13n/frontcontroller/authcache.php?a[field][0]=$node:full:en&r=asm/field/node/$field&o[q]=node/$node"
val responseString = client.newCall(GET(requestUrl, headers)).execute().body?.string() ?: return null
return with(gson.fromJson<JsonObject>(responseString)) {
val htmlString = json.parseToJsonElement(responseString).jsonObject["field"]!!.jsonObject["$node:full:en"]!!.jsonPrimitive.content
return parse(htmlString).let {
when (field) {
"field_image2" -> this["field"]["$node:full:en"].asString.substringAfter("src=\"").substringBefore("\"")
"field_status", "field_author", "field_artist" -> this["field"]["$node:full:en"].asString.substringAfter("even\">").substringBefore("</div>")
"body" -> parse(this["field"]["$node:full:en"].asString, baseUrl).select("p").text().substringAfter("summary: ")
"field_genres" -> parse(this["field"]["$node:full:en"].asString, baseUrl).select("a").text()
"field_image2" -> it.selectFirst("img.img-responsive").attr("src")
"field_status", "field_author", "field_artist" -> it.selectFirst("div.field-item.even").text()
"body" -> it.selectFirst("div.field-item.even p").text().substringAfter("summary: ")
"field_genres" -> it.select("a").text()
else -> null
}
}
}
// Get a page's node number so we can get data fragments for that page
private fun getNodeNumber(document: Document): String {
return document.select("[rel=shortlink]").attr("href").split("/").last().replace("\"", "")
}
override fun searchMangaNextPageSelector() = "li.next a"
private fun getNodeNumber(document: Document): String =
document.select("[rel=shortlink]").attr("href").substringAfter("/node/")
// On source's website most of these details are loaded through JQuery
override fun mangaDetailsParse(document: Document): SManga {
@ -138,14 +149,97 @@ class Mangasail : ParsedHttpSource() {
else -> SManga.UNKNOWN
}
// Chapters
override fun chapterListSelector() = "tbody tr"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.select("a").first().attr("href"))
chapter.name = element.select("a").text()
chapter.date_upload = parseChapterDate(element.select("td + td").text())
return chapter
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.select("a").first().attr("href"))
name = element.select("a").text()
date_upload = parseChapterDate(element.select("td + td").text())
}
private fun parseChapterDate(string: String): Long {
return dateFormat.parse(string.substringAfter("on "))?.time ?: 0L
}
// Page List
override fun pageListParse(document: Document): List<Page> {
val imgUrlArray = document.selectFirst("script:containsData(paths)").data()
.substringAfter("paths\":").substringBefore(",\"count_p")
return json.parseToJsonElement(imgUrlArray).jsonArray.mapIndexed { i, el ->
Page(i, "", el.jsonPrimitive.content)
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Text search ignores filters"),
GenreFilter()
)
// From https://www.mangasail.co/tagclouds/chunk/1
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("4-Koma", "4-koma"),
Pair("Action", "action"),
Pair("Adult", "adult"),
Pair("Adventure", "adventure"),
Pair("Bender", "bender"),
Pair("Comedy", "comedy"),
Pair("Cooking", "cooking"),
Pair("Doujinshi", "doujinshi"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Fantasy", "fantasy"),
Pair("Fantsy", "fantsy"),
Pair("Game", "game"),
Pair("Gender", "gender"),
Pair("Gender Bender", "gender-bender"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Isekai", "isekai"),
Pair("Josei", "josei"),
Pair("Manhua", "manhua"),
Pair("Martial Arts", "martial-arts"),
Pair("Mature", "mature"),
Pair("Mecha", "mecha"),
Pair("Medical", "medical"),
Pair("Mystery", "mystery"),
Pair("One Shot", "one-shot"),
Pair("Psychological", "psychological"),
Pair("Romance", "romance"),
Pair("School Life", "school-life"),
Pair("Sci-fi", "sci-fi"),
Pair("Sci fi", "sci-fi-0"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Slice of Life", "slice-life"),
Pair("Smut", "smut"),
Pair("Sports", "sports"),
Pair("Supernatura", "supernatura"),
Pair("Supernatural", "supernatural"),
Pair("Supernaturaledit", "supernaturaledit"),
Pair("Tragedy", "tragedy"),
Pair("Webtoon", "webtoon"),
Pair("Webtoons", "webtoons"),
Pair("Yaoi", "yaoi")
)
)
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
}
companion object {
@ -153,18 +247,4 @@ class Mangasail : ParsedHttpSource() {
SimpleDateFormat("d MMM yyyy", Locale.US)
}
}
private fun parseChapterDate(string: String): Long {
return dateFormat.parse(string.substringAfter("on "))?.time ?: 0L
}
override fun pageListParse(document: Document): List<Page> {
val objectString = document.select("script:containsData(paths)").first().data()
.substringAfter(" ").substringBefore(");")
return gson.fromJson<JsonObject>(objectString)["showmanga"]["paths"].asJsonArray.mapIndexed { i, jsonElement ->
Page(i, "", jsonElement.string)
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
}