diff --git a/src/en/mangasail/build.gradle b/src/en/mangasail/build.gradle index 37a25fc94..4606a3626 100644 --- a/src/en/mangasail/build.gradle +++ b/src/en/mangasail/build.gradle @@ -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' } diff --git a/src/en/mangasail/src/eu/kanade/tachiyomi/extension/en/mangasail/Mangasail.kt b/src/en/mangasail/src/eu/kanade/tachiyomi/extension/en/mangasail/Mangasail.kt index 0751c42ce..2ca7ffdc7 100644 --- a/src/en/mangasail/src/eu/kanade/tachiyomi/extension/en/mangasail/Mangasail.kt +++ b/src/en/mangasail/src/eu/kanade/tachiyomi/extension/en/mangasail/Mangasail.kt @@ -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") }