Webtoons.com: refactor and fix for site changes (#9245)
* Webtoons Translate: move out of multisrc & rework it basically override everything from the main webtoons class, so split it off * DongmanManhua: move to individual * Webtoons: fix and make individual * remove old multisrc * use meta og:image * deeplink fix * fix deeplink crash & old details thumbnails
@ -1,5 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("lib-multisrc")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseVersionCode = 4
|
|
@ -1,308 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.Header
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.Select
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.Separator
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
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.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.net.SocketException
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
open class Webtoons(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
open val langCode: String = lang,
|
|
||||||
open val localeForCookie: String = lang,
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH),
|
|
||||||
) : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
||||||
.cookieJar(
|
|
||||||
object : CookieJar {
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
|
||||||
return listOf<Cookie>(
|
|
||||||
Cookie.Builder()
|
|
||||||
.domain("www.webtoons.com")
|
|
||||||
.path("/")
|
|
||||||
.name("ageGatePass")
|
|
||||||
.value("true")
|
|
||||||
.name("locale")
|
|
||||||
.value(localeForCookie)
|
|
||||||
.name("needGDPR")
|
|
||||||
.value("false")
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.addInterceptor(::sslRetryInterceptor)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// m.webtoons.com throws an SSL error that can be solved by a simple retry
|
|
||||||
private fun sslRetryInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
return try {
|
|
||||||
chain.proceed(chain.request())
|
|
||||||
} catch (e: SocketException) {
|
|
||||||
chain.proceed(chain.request())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val day: String
|
|
||||||
get() {
|
|
||||||
return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
|
|
||||||
Calendar.SUNDAY -> "div._list_SUNDAY"
|
|
||||||
Calendar.MONDAY -> "div._list_MONDAY"
|
|
||||||
Calendar.TUESDAY -> "div._list_TUESDAY"
|
|
||||||
Calendar.WEDNESDAY -> "div._list_WEDNESDAY"
|
|
||||||
Calendar.THURSDAY -> "div._list_THURSDAY"
|
|
||||||
Calendar.FRIDAY -> "div._list_FRIDAY"
|
|
||||||
Calendar.SATURDAY -> "div._list_SATURDAY"
|
|
||||||
else -> {
|
|
||||||
"div"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "not using"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div#dailyList > $day li > a"
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
|
||||||
.add("Referer", "https://www.webtoons.com/$langCode/")
|
|
||||||
|
|
||||||
protected val mobileHeaders: Headers = super.headersBuilder()
|
|
||||||
.add("Referer", "https://m.webtoons.com")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule", headers)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val mangas = mutableListOf<SManga>()
|
|
||||||
val document = response.asJsoup()
|
|
||||||
var maxChild = 0
|
|
||||||
|
|
||||||
// For ongoing webtoons rows are ordered by descending popularity, count how many rows there are
|
|
||||||
document.select("div#dailyList .daily_section").forEach { day ->
|
|
||||||
day.select("li").count().let { rowCount ->
|
|
||||||
if (rowCount > maxChild) maxChild = rowCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each row
|
|
||||||
for (i in 1..maxChild) {
|
|
||||||
document.select("div#dailyList .daily_section li:nth-child($i) a").map { mangas.add(popularMangaFromElement(it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add completed webtoons, no sorting needed
|
|
||||||
document.select("div.daily_lst.comp li a").map { mangas.add(popularMangaFromElement(it)) }
|
|
||||||
|
|
||||||
return MangasPage(mangas.distinctBy { it.url }, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule?sortOrder=UPDATE&webtoonCompleteType=ONGOING", headers)
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
val manga = SManga.create()
|
|
||||||
|
|
||||||
manga.setUrlWithoutDomain(element.attr("href"))
|
|
||||||
manga.title = element.select("p.subj").text()
|
|
||||||
manga.thumbnail_url = element.select("img").attr("abs:src")
|
|
||||||
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (!query.startsWith(URL_SEARCH_PREFIX)) {
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
val emptyResult = Observable.just(MangasPage(emptyList(), false))
|
|
||||||
|
|
||||||
// given a url to either a webtoon or an episode, returns a url path to corresponding webtoon
|
|
||||||
fun webtoonPath(u: HttpUrl) = when {
|
|
||||||
langCode == u.pathSegments[0] -> "/${u.pathSegments[0]}/${u.pathSegments[1]}/${u.pathSegments[2]}/list"
|
|
||||||
else -> "/${u.pathSegments[0]}/${u.pathSegments[1]}/list" // dongmanmanhua doesn't include langCode
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.substringAfter(URL_SEARCH_PREFIX).toHttpUrlOrNull()?.let { url ->
|
|
||||||
val title_no = url.queryParameter("title_no")
|
|
||||||
val couldBeWebtoonOrEpisode = title_no != null && (url.pathSegments.size >= 3 && url.pathSegments.last().isNotEmpty())
|
|
||||||
val isThisLang = "$url".startsWith("$baseUrl/$langCode")
|
|
||||||
if (!(couldBeWebtoonOrEpisode && isThisLang)) {
|
|
||||||
emptyResult
|
|
||||||
} else {
|
|
||||||
val potentialUrl = "${webtoonPath(url)}?title_no=$title_no"
|
|
||||||
fetchMangaDetails(SManga.create().apply { this.url = potentialUrl }).map {
|
|
||||||
it.url = potentialUrl
|
|
||||||
MangasPage(listOf(it), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: emptyResult
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/$langCode/search?keyword=$query".toHttpUrl().newBuilder()
|
|
||||||
val uriPart = (filters.find { it is SearchType } as? SearchType)?.toUriPart() ?: ""
|
|
||||||
|
|
||||||
url.addQueryParameter("searchType", uriPart)
|
|
||||||
if (uriPart != "WEBTOON" && page > 1) url.addQueryParameter("page", page.toString())
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "#content > div.card_wrap.search ul:not(#filterLayer) li a"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "div.more_area, div.paginate a[onclick] + a"
|
|
||||||
|
|
||||||
open fun parseDetailsThumbnail(document: Document): String? {
|
|
||||||
val picElement = document.select("#content > div.cont_box > div.detail_body")
|
|
||||||
val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb")
|
|
||||||
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")").removeSurrounding("\"").removeSurrounding("'")
|
|
||||||
.ifBlank { discoverPic.select("img").not("[alt='Representative image']").first()?.attr("src") }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
val detailElement = document.select("#content > div.cont_box > div.detail_header > div.info")
|
|
||||||
val infoElement = document.select("#_asideDetail")
|
|
||||||
|
|
||||||
val manga = SManga.create()
|
|
||||||
manga.title = document.selectFirst("h1.subj, h3.subj")!!.text()
|
|
||||||
manga.author = detailElement.select(".author:nth-of-type(1)").first()?.ownText()
|
|
||||||
?: detailElement.select(".author_area").first()?.ownText()
|
|
||||||
manga.artist = detailElement.select(".author:nth-of-type(2)").first()?.ownText()
|
|
||||||
?: detailElement.select(".author_area").first()?.ownText() ?: manga.author
|
|
||||||
manga.genre = detailElement.select(".genre").joinToString(", ") { it.text() }
|
|
||||||
manga.description = infoElement.select("p.summary").text()
|
|
||||||
manga.status = infoElement.select("p.day_info").firstOrNull()?.text().orEmpty().toStatus()
|
|
||||||
manga.thumbnail_url = parseDetailsThumbnail(document)
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun String.toStatus(): Int = when {
|
|
||||||
contains("UP") -> SManga.ONGOING
|
|
||||||
contains("COMPLETED") -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = document.select("img").first()!!.attr("src")
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
return FilterList(
|
|
||||||
Header("Query can not be blank"),
|
|
||||||
Separator(),
|
|
||||||
SearchType(getOfficialList()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "ul#_episodeList li[id*=episode]"
|
|
||||||
|
|
||||||
private class SearchType(vals: Array<Pair<String, String>>) : UriPartFilter("Official or Challenge", vals)
|
|
||||||
|
|
||||||
private fun getOfficialList() = arrayOf(
|
|
||||||
Pair("Any", ""),
|
|
||||||
Pair("Official only", "WEBTOON"),
|
|
||||||
Pair("Challenge only", "CHALLENGE"),
|
|
||||||
)
|
|
||||||
|
|
||||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
|
||||||
Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
|
||||||
fun toUriPart() = vals[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
val urlElement = element.select("a")
|
|
||||||
|
|
||||||
val chapter = SChapter.create()
|
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
|
||||||
chapter.name = element.select("a > div.row > div.info > p.sub_title > span.ellipsis").text()
|
|
||||||
val select = element.select("a > div.row > div.num")
|
|
||||||
if (select.isNotEmpty()) {
|
|
||||||
chapter.name += " Ch. " + select.text().substringAfter("#")
|
|
||||||
}
|
|
||||||
if (element.select(".ico_bgm").isNotEmpty()) {
|
|
||||||
chapter.name += " ♫"
|
|
||||||
}
|
|
||||||
chapter.date_upload = element.select("a > div.row > div.col > div.sub_info > span.date").text().let { chapterParseDate(it) } ?: 0
|
|
||||||
return chapter
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun chapterParseDate(date: String): Long {
|
|
||||||
return try {
|
|
||||||
dateFormat.parse(date)?.time ?: 0
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) = GET("https://m.webtoons.com" + manga.url, mobileHeaders)
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
|
|
||||||
|
|
||||||
if (pages.isNotEmpty()) { return pages }
|
|
||||||
|
|
||||||
val docString = document.toString()
|
|
||||||
|
|
||||||
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
|
|
||||||
val motiontoonPathRegex = Regex("jpg:.*?'(.*?)\\{")
|
|
||||||
|
|
||||||
val docUrl = docUrlRegex.find(docString)!!.destructured.toList()[0]
|
|
||||||
val motiontoonPath = motiontoonPathRegex.find(docString)!!.destructured.toList()[0]
|
|
||||||
val motiontoonResponse = client.newCall(GET(docUrl, headers)).execute()
|
|
||||||
|
|
||||||
val motiontoonJson = json.parseToJsonElement(motiontoonResponse.body.string()).jsonObject
|
|
||||||
val motiontoonImages = motiontoonJson["assets"]!!.jsonObject["image"]!!.jsonObject
|
|
||||||
|
|
||||||
return motiontoonImages.entries
|
|
||||||
.filter { it.key.contains("layer") }
|
|
||||||
.mapIndexed { i, entry ->
|
|
||||||
Page(i, "", motiontoonPath + entry.value.jsonPrimitive.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val URL_SEARCH_PREFIX = "url:"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,226 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.boolean
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
open class WebtoonsTranslate(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
private val translateLangCode: String,
|
|
||||||
) : Webtoons(name, baseUrl, lang) {
|
|
||||||
|
|
||||||
// popularMangaRequest already returns manga sorted by latest update
|
|
||||||
override val supportsLatest = false
|
|
||||||
|
|
||||||
private val apiBaseUrl = "https://global.apis.naver.com".toHttpUrl()
|
|
||||||
private val mobileBaseUrl = "https://m.webtoons.com".toHttpUrl()
|
|
||||||
private val thumbnailBaseUrl = "https://mwebtoon-phinf.pstatic.net"
|
|
||||||
|
|
||||||
private val pageSize = 24
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
|
||||||
.removeAll("Referer")
|
|
||||||
.add("Referer", mobileBaseUrl.toString())
|
|
||||||
|
|
||||||
private fun mangaRequest(page: Int, requeztSize: Int): Request {
|
|
||||||
val url = apiBaseUrl
|
|
||||||
.resolve("/lineWebtoon/ctrans/translatedWebtoons_jsonp.json")!!
|
|
||||||
.newBuilder()
|
|
||||||
.addQueryParameter("orderType", "UPDATE")
|
|
||||||
.addQueryParameter("offset", "${(page - 1) * requeztSize}")
|
|
||||||
.addQueryParameter("size", "$requeztSize")
|
|
||||||
.addQueryParameter("languageCode", translateLangCode)
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webtoons translations doesn't really have a "popular" sort; just "UPDATE", "TITLE_ASC",
|
|
||||||
// and "TITLE_DESC". Pick UPDATE as the most useful sort.
|
|
||||||
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, pageSize)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val offset = response.request.url.queryParameter("offset")!!.toInt()
|
|
||||||
val result = json.parseToJsonElement(response.body.string()).jsonObject
|
|
||||||
val responseCode = result["code"]!!.jsonPrimitive.content
|
|
||||||
|
|
||||||
if (responseCode != "000") {
|
|
||||||
throw Exception("Error getting popular manga: error code $responseCode")
|
|
||||||
}
|
|
||||||
|
|
||||||
val titles = result["result"]!!.jsonObject
|
|
||||||
val totalCount = titles["totalCount"]!!.jsonPrimitive.int
|
|
||||||
|
|
||||||
val mangaList = titles["titleList"]!!.jsonArray
|
|
||||||
.map { mangaFromJson(it.jsonObject) }
|
|
||||||
|
|
||||||
return MangasPage(mangaList, hasNextPage = totalCount > pageSize + offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mangaFromJson(manga: JsonObject): SManga {
|
|
||||||
val relativeThumnailURL = manga["thumbnailIPadUrl"]?.jsonPrimitive?.contentOrNull
|
|
||||||
?: manga["thumbnailMobileUrl"]?.jsonPrimitive?.contentOrNull
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = manga["representTitle"]!!.jsonPrimitive.content
|
|
||||||
author = manga["writeAuthorName"]!!.jsonPrimitive.content
|
|
||||||
artist = manga["pictureAuthorName"]?.jsonPrimitive?.contentOrNull ?: author
|
|
||||||
thumbnail_url = if (relativeThumnailURL != null) "$thumbnailBaseUrl$relativeThumnailURL" else null
|
|
||||||
status = SManga.UNKNOWN
|
|
||||||
url = mobileBaseUrl
|
|
||||||
.resolve("/translate/episodeList")!!
|
|
||||||
.newBuilder()
|
|
||||||
.addQueryParameter("titleNo", manga["titleNo"]!!.jsonPrimitive.int.toString())
|
|
||||||
.addQueryParameter("languageCode", translateLangCode)
|
|
||||||
.addQueryParameter("teamVersion", (manga["teamVersion"]?.jsonPrimitive?.intOrNull ?: 0).toString())
|
|
||||||
.build()
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return client.newCall(searchMangaRequest(page, query, filters))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
searchMangaParse(response, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Don't see a search function for Fan Translations, so let's do it client side.
|
|
||||||
* There's 75 webtoons as of 2019/11/21, a hardcoded request of 200 should be a sufficient request
|
|
||||||
* to get all titles, in 1 request, for quite a while
|
|
||||||
*/
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = mangaRequest(page, 200)
|
|
||||||
|
|
||||||
private fun searchMangaParse(response: Response, query: String): MangasPage {
|
|
||||||
val result = json.parseToJsonElement(response.body.string()).jsonObject
|
|
||||||
val responseCode = result["code"]!!.jsonPrimitive.content
|
|
||||||
|
|
||||||
if (responseCode != "000") {
|
|
||||||
throw Exception("Error getting manga: error code $responseCode")
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangaList = result["result"]!!.jsonObject["titleList"]!!.jsonArray
|
|
||||||
.map { mangaFromJson(it.jsonObject) }
|
|
||||||
.filter { it.title.contains(query, ignoreCase = true) }
|
|
||||||
|
|
||||||
return MangasPage(mangaList, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
return GET(manga.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
val getMetaProp = fun(property: String): String =
|
|
||||||
document.head().select("meta[property=\"$property\"]").attr("content")
|
|
||||||
var parsedAuthor = getMetaProp("com-linewebtoon:webtoon:author")
|
|
||||||
var parsedArtist = parsedAuthor
|
|
||||||
val authorSplit = parsedAuthor.split(" / ", limit = 2)
|
|
||||||
if (authorSplit.count() > 1) {
|
|
||||||
parsedAuthor = authorSplit[0]
|
|
||||||
parsedArtist = authorSplit[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = getMetaProp("og:title")
|
|
||||||
artist = parsedArtist
|
|
||||||
author = parsedAuthor
|
|
||||||
description = getMetaProp("og:description")
|
|
||||||
status = SManga.UNKNOWN
|
|
||||||
thumbnail_url = getMetaProp("og:image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector(): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
|
||||||
val mangaUrl = manga.url.toHttpUrl()
|
|
||||||
val titleNo = mangaUrl.queryParameter("titleNo")
|
|
||||||
val teamVersion = mangaUrl.queryParameter("teamVersion")
|
|
||||||
val chapterListUrl = apiBaseUrl
|
|
||||||
.resolve("/lineWebtoon/ctrans/translatedEpisodes_jsonp.json")!!
|
|
||||||
.newBuilder()
|
|
||||||
.addQueryParameter("titleNo", titleNo)
|
|
||||||
.addQueryParameter("languageCode", translateLangCode)
|
|
||||||
.addQueryParameter("offset", "0")
|
|
||||||
.addQueryParameter("limit", "10000")
|
|
||||||
.addQueryParameter("teamVersion", teamVersion)
|
|
||||||
.toString()
|
|
||||||
return GET(chapterListUrl, mobileHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val result = json.parseToJsonElement(response.body.string()).jsonObject
|
|
||||||
val responseCode = result["code"]!!.jsonPrimitive.content
|
|
||||||
|
|
||||||
if (responseCode != "000") {
|
|
||||||
val message = result["message"]?.jsonPrimitive?.content ?: "error code $responseCode"
|
|
||||||
throw Exception("Error getting chapter list: $message")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result["result"]!!.jsonObject["episodes"]!!.jsonArray
|
|
||||||
.filter { it.jsonObject["translateCompleted"]!!.jsonPrimitive.boolean }
|
|
||||||
.map { parseChapterJson(it.jsonObject) }
|
|
||||||
.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterJson(obj: JsonObject): SChapter = SChapter.create().apply {
|
|
||||||
name = obj["title"]!!.jsonPrimitive.content + " #" + obj["episodeSeq"]!!.jsonPrimitive.int
|
|
||||||
chapter_number = obj["episodeSeq"]!!.jsonPrimitive.int.toFloat()
|
|
||||||
date_upload = obj["updateYmdt"]!!.jsonPrimitive.long
|
|
||||||
scanlator = obj["teamVersion"]!!.jsonPrimitive.int.takeIf { it != 0 }?.toString() ?: "(wiki)"
|
|
||||||
|
|
||||||
val chapterUrl = apiBaseUrl
|
|
||||||
.resolve("/lineWebtoon/ctrans/translatedEpisodeDetail_jsonp.json")!!
|
|
||||||
.newBuilder()
|
|
||||||
.addQueryParameter("titleNo", obj["titleNo"]!!.jsonPrimitive.int.toString())
|
|
||||||
.addQueryParameter("episodeNo", obj["episodeNo"]!!.jsonPrimitive.int.toString())
|
|
||||||
.addQueryParameter("languageCode", obj["languageCode"]!!.jsonPrimitive.content)
|
|
||||||
.addQueryParameter("teamVersion", obj["teamVersion"]!!.jsonPrimitive.int.toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
setUrlWithoutDomain(chapterUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
return GET(apiBaseUrl.resolve(chapter.url)!!, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val result = json.parseToJsonElement(response.body.string()).jsonObject
|
|
||||||
|
|
||||||
return result["result"]!!.jsonObject["imageInfo"]!!.jsonArray
|
|
||||||
.mapIndexed { i, jsonEl ->
|
|
||||||
Page(i, "", jsonEl.jsonObject["imageUrl"]!!.jsonPrimitive.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList()
|
|
||||||
}
|
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.webtoons.WebtoonsUrlActivity"
|
android:name="eu.kanade.tachiyomi.extension.all.webtoons.WebtoonsUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Webtoons.com'
|
extName = 'Webtoons.com'
|
||||||
extClass = '.WebtoonsFactory'
|
extClass = '.WebtoonsFactory'
|
||||||
themePkg = 'webtoons'
|
extVersionCode = 46
|
||||||
baseUrl = 'https://www.webtoons.com'
|
|
||||||
overrideVersionCode = 41
|
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(project(':lib:cookieinterceptor'))
|
||||||
implementation(project(':lib:textinterceptor'))
|
implementation(project(':lib:textinterceptor'))
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.webtoons
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MotionToonResponse(
|
||||||
|
val assets: MotionToonAssets,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MotionToonAssets(
|
||||||
|
val images: Map<String, String>,
|
||||||
|
)
|
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.webtoons
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<Pair<String, String?>>,
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
val selected get() = options[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchType : SelectFilter(
|
||||||
|
name = "Search Type",
|
||||||
|
options = listOf(
|
||||||
|
"ALL" to null,
|
||||||
|
"Originals" to "originals",
|
||||||
|
"Canvas" to "canvas",
|
||||||
|
),
|
||||||
|
)
|
@ -0,0 +1,337 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.webtoons
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.lib.cookieinterceptor.CookieInterceptor
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
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.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.firstInstanceOrNull
|
||||||
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import java.net.SocketException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
open class Webtoons(
|
||||||
|
override val lang: String,
|
||||||
|
private val langCode: String = lang,
|
||||||
|
localeForCookie: String = lang,
|
||||||
|
private val dateFormat: SimpleDateFormat,
|
||||||
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
override val name = "Webtoons.com"
|
||||||
|
override val baseUrl = "https://www.webtoons.com"
|
||||||
|
private val mobileUrl = "https://m.webtoons.com"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private val mobileHeaders = super.headersBuilder()
|
||||||
|
.set("Referer", "$mobileUrl/")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.addNetworkInterceptor(
|
||||||
|
CookieInterceptor(
|
||||||
|
domain = "webtoons.com",
|
||||||
|
cookies = listOf(
|
||||||
|
"ageGatePass" to "true",
|
||||||
|
"locale" to localeForCookie,
|
||||||
|
"needGDPR" to "false",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
// m.webtoons.com throws an SSL error that can be solved by a simple retry
|
||||||
|
try {
|
||||||
|
chain.proceed(chain.request())
|
||||||
|
} catch (e: SocketException) {
|
||||||
|
chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addInterceptor(TextInterceptor())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val preferences by getPreferencesLazy()
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val ranking = when (page) {
|
||||||
|
1 -> "trending"
|
||||||
|
2 -> "popular"
|
||||||
|
3 -> "originals"
|
||||||
|
4 -> "canvas"
|
||||||
|
else -> throw Exception("page > 4 not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET("$baseUrl/$langCode/ranking/$ranking", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val entries = document.select(".webtoon_list li a")
|
||||||
|
.map(::mangaFromElement)
|
||||||
|
val hasNextPage = response.request.url.pathSegments.last() != "canvas"
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaFromElement(element: Element): SManga {
|
||||||
|
return SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.absUrl("href"))
|
||||||
|
title = element.selectFirst(".title")!!.text()
|
||||||
|
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val day = when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
|
||||||
|
Calendar.MONDAY -> "monday"
|
||||||
|
Calendar.TUESDAY -> "tuesday"
|
||||||
|
Calendar.WEDNESDAY -> "wednesday"
|
||||||
|
Calendar.THURSDAY -> "thursday"
|
||||||
|
Calendar.FRIDAY -> "friday"
|
||||||
|
Calendar.SATURDAY -> "saturday"
|
||||||
|
Calendar.SUNDAY -> "sunday"
|
||||||
|
else -> throw Exception("Unknown day of week")
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET("$baseUrl/$langCode/originals/$day?sortOrder=UPDATE", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val entries = document.select(".webtoon_list li a")
|
||||||
|
.map(::mangaFromElement)
|
||||||
|
|
||||||
|
return MangasPage(entries, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
if (query.startsWith(ID_SEARCH_PREFIX)) {
|
||||||
|
val (_, titleLang, titleNo) = query.split(":", limit = 3)
|
||||||
|
val tmpManga = SManga.create().apply {
|
||||||
|
url = "/episodeList?titleNo=$titleNo"
|
||||||
|
}
|
||||||
|
return if (titleLang == langCode) {
|
||||||
|
fetchMangaDetails(tmpManga).map {
|
||||||
|
MangasPage(listOf(it), false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Observable.just(
|
||||||
|
MangasPage(emptyList(), false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
return FilterList(
|
||||||
|
SearchType(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
var searchTypeAdded = false
|
||||||
|
addPathSegment(langCode)
|
||||||
|
addPathSegment("search")
|
||||||
|
filters.firstInstanceOrNull<SearchType>()?.selected?.also {
|
||||||
|
searchTypeAdded = true
|
||||||
|
addPathSegment(it)
|
||||||
|
}
|
||||||
|
addQueryParameter("keyword", query)
|
||||||
|
if (page > 1 && searchTypeAdded) {
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val entries = document.select(".webtoon_list li a").map(::mangaFromElement)
|
||||||
|
val hasNextPage = document.selectFirst("a.pagination[aria-current=true] + a") != null
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { mangaDetailsParse(it, manga) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaDetailsParse(response: Response, oldManga: SManga): SManga {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val detailElement = document.selectFirst(".detail_header .info")
|
||||||
|
val infoElement = document.selectFirst("#_asideDetail")
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(document.location())
|
||||||
|
title = document.selectFirst("h1.subj, h3.subj")!!.text()
|
||||||
|
author = detailElement?.selectFirst(".author:nth-of-type(1)")?.ownText()
|
||||||
|
?: detailElement?.selectFirst(".author_area")?.ownText()
|
||||||
|
artist = detailElement?.selectFirst(".author:nth-of-type(2)")?.ownText()
|
||||||
|
?: detailElement?.selectFirst(".author_area")?.ownText() ?: author
|
||||||
|
genre = detailElement?.select(".genre").orEmpty().joinToString { it.text() }
|
||||||
|
description = infoElement?.selectFirst("p.summary")?.text()
|
||||||
|
status = with(infoElement?.selectFirst("p.day_info")?.text().orEmpty()) {
|
||||||
|
when {
|
||||||
|
contains("UP") || contains("EVERY") || contains("NOUVEAU") -> SManga.ONGOING
|
||||||
|
contains("END") || contains("TERMINÉ") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnail_url = run {
|
||||||
|
val bannerFile = document.selectFirst(".detail_header .thmb img")
|
||||||
|
?.absUrl("src")
|
||||||
|
?.toHttpUrl()
|
||||||
|
?.pathSegments
|
||||||
|
?.lastOrNull()
|
||||||
|
val oldThumbFile = oldManga.thumbnail_url
|
||||||
|
?.toHttpUrl()
|
||||||
|
?.pathSegments
|
||||||
|
?.lastOrNull()
|
||||||
|
val thumbnail = document.selectFirst("head meta[property=\"og:image\"]")
|
||||||
|
?.attr("content")
|
||||||
|
|
||||||
|
// replace banner image for toons in library
|
||||||
|
if (oldThumbFile != null && oldThumbFile != bannerFile) {
|
||||||
|
oldManga.thumbnail_url
|
||||||
|
} else {
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) = GET(mobileUrl + manga.url, mobileHeaders)
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
return document.select("ul#_episodeList li[id*=episode] a").map { element ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain(element.absUrl("href"))
|
||||||
|
name = element.selectFirst(".sub_title > span.ellipsis")!!.text()
|
||||||
|
element.selectFirst("a > div.row > div.num")?.let {
|
||||||
|
name += " Ch. " + it.text().substringAfter("#")
|
||||||
|
}
|
||||||
|
element.selectFirst(".ico_bgm")?.also {
|
||||||
|
name += " ♫"
|
||||||
|
}
|
||||||
|
date_upload = dateFormat.tryParse(element.selectFirst(".sub_info .date")?.text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val useMaxQuality = useMaxQualityPref()
|
||||||
|
|
||||||
|
val pages = document.select("div#_imageList > img").mapIndexed { i, element ->
|
||||||
|
val imageUrl = element.attr("data-url").toHttpUrl()
|
||||||
|
|
||||||
|
if (useMaxQuality && imageUrl.queryParameter("type") == "q90") {
|
||||||
|
val newImageUrl = imageUrl.newBuilder().apply {
|
||||||
|
removeAllQueryParameters("type")
|
||||||
|
}.build()
|
||||||
|
Page(i, imageUrl = newImageUrl.toString())
|
||||||
|
} else {
|
||||||
|
Page(i, imageUrl = imageUrl.toString())
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
if (pages.isEmpty()) {
|
||||||
|
pages.addAll(
|
||||||
|
fetchMotionToonPages(document),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAuthorsNotesPref()) {
|
||||||
|
val note = document.select("div.creator_note p.author_text").text()
|
||||||
|
|
||||||
|
if (note.isNotEmpty()) {
|
||||||
|
val creator = document.select("div.creator_note a.author_name span").text().trim()
|
||||||
|
|
||||||
|
pages += Page(
|
||||||
|
pages.size,
|
||||||
|
imageUrl = TextInterceptorHelper.createUrl("Author's Notes from $creator", note),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchMotionToonPages(document: Document): List<Page> {
|
||||||
|
val docString = document.toString()
|
||||||
|
|
||||||
|
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
|
||||||
|
val motionToonPathRegex = Regex("jpg:.*?'(.*?)\\{")
|
||||||
|
|
||||||
|
val docUrl = docUrlRegex.find(docString)!!.groupValues[1]
|
||||||
|
val motionToonPath = motionToonPathRegex.find(docString)!!.groupValues[1]
|
||||||
|
val motionToonResponse = client.newCall(GET(docUrl, headers)).execute()
|
||||||
|
val motionToonImages = motionToonResponse.parseAs<MotionToonResponse>().assets.images
|
||||||
|
|
||||||
|
return motionToonImages.entries
|
||||||
|
.filter { it.key.contains("layer") }
|
||||||
|
.mapIndexed { i, entry ->
|
||||||
|
Page(i, imageUrl = motionToonPath + entry.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAuthorsNotesPref() = preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
|
||||||
|
private fun useMaxQualityPref() = preferences.getBoolean(USE_MAX_QUALITY_KEY, false)
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = SHOW_AUTHORS_NOTES_KEY
|
||||||
|
title = "Show author's notes"
|
||||||
|
summary = "Enable to see the author's notes at the end of chapters (if they're there)."
|
||||||
|
setDefaultValue(false)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = USE_MAX_QUALITY_KEY
|
||||||
|
title = "Use maximum quality images"
|
||||||
|
summary = "Enable to load images in maximum quality."
|
||||||
|
setDefaultValue(false)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
||||||
|
private const val USE_MAX_QUALITY_KEY = "useMaxQuality"
|
||||||
|
const val ID_SEARCH_PREFIX = "id:"
|
@ -1,66 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.webtoons
|
package eu.kanade.tachiyomi.extension.all.webtoons
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.GregorianCalendar
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class WebtoonsFactory : SourceFactory {
|
class WebtoonsFactory : SourceFactory {
|
||||||
override fun createSources(): List<Source> = listOf(
|
override fun createSources() = listOf(
|
||||||
WebtoonsEN(),
|
Webtoons("en", dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)),
|
||||||
WebtoonsID(),
|
Webtoons("id", dateFormat = SimpleDateFormat("d MMMM yyyy", Locale("id"))),
|
||||||
WebtoonsTH(),
|
Webtoons("th", dateFormat = SimpleDateFormat("d MMM yyyy", Locale("th"))),
|
||||||
WebtoonsES(),
|
Webtoons("es", dateFormat = SimpleDateFormat("d MMMM. yyyy", Locale("es"))),
|
||||||
WebtoonsFR(),
|
Webtoons("fr", dateFormat = SimpleDateFormat("d MMM yyyy", Locale.FRENCH)),
|
||||||
WebtoonsZH(),
|
object : Webtoons("zh-Hant", "zh-hant", "zh_TW", SimpleDateFormat("yyyy/MM/dd", Locale.TRADITIONAL_CHINESE)) {
|
||||||
WebtoonsDE(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
class WebtoonsEN : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "en")
|
|
||||||
class WebtoonsID : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "id") {
|
|
||||||
// Override ID as part of the name was removed to be more consiten with other enteries
|
|
||||||
override val id: Long = 8749627068478740298
|
|
||||||
|
|
||||||
// Android seems to be unable to parse Indonesian dates; we'll use a short hard-coded table
|
|
||||||
// instead.
|
|
||||||
private val dateMap: Array<String> = arrayOf(
|
|
||||||
"Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des",
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun chapterParseDate(date: String): Long {
|
|
||||||
val expr = Regex("""(\d{4}) ([A-Z][a-z]{2}) (\d+)""").find(date) ?: return 0
|
|
||||||
val (_, year, monthString, day) = expr.groupValues
|
|
||||||
val monthIndex = dateMap.indexOf(monthString)
|
|
||||||
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class WebtoonsTH : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "th", dateFormat = SimpleDateFormat("d MMM yyyy", Locale("th")))
|
|
||||||
class WebtoonsES : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "es") {
|
|
||||||
// Android seems to be unable to parse es dates like Indonesian; we'll use a short hard-coded table instead.
|
|
||||||
private val dateMap: Array<String> = arrayOf(
|
|
||||||
"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic",
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun chapterParseDate(date: String): Long {
|
|
||||||
val expr = Regex("""(\d+)-([A-Za-z]{3})-(\d{4})""").find(date) ?: return 0
|
|
||||||
val (_, day, monthString, year) = expr.groupValues
|
|
||||||
val monthIndex = dateMap.indexOf(monthString.lowercase(Locale("es")))
|
|
||||||
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebtoonsFR : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "fr", dateFormat = SimpleDateFormat("d MMM yyyy", Locale.FRENCH)) {
|
|
||||||
override fun String.toStatus(): Int = when {
|
|
||||||
contains("NOUVEAU") -> SManga.ONGOING
|
|
||||||
contains("TERMINÉ") -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebtoonsZH : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "zh-Hant", "zh-hant", "zh_TW", SimpleDateFormat("yyyy/MM/dd", Locale.TRADITIONAL_CHINESE)) {
|
|
||||||
// Due to lang code getting more specific
|
// Due to lang code getting more specific
|
||||||
override val id: Long = 2959982438613576472
|
override val id: Long = 2959982438613576472
|
||||||
|
},
|
||||||
|
Webtoons("de", dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
class WebtoonsDE : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "de", dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN))
|
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.webtoons
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
|
|
||||||
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
|
||||||
import eu.kanade.tachiyomi.multisrc.webtoons.Webtoons
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
open class WebtoonsSrc(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
langCode: String = lang,
|
|
||||||
override val localeForCookie: String = lang,
|
|
||||||
dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH),
|
|
||||||
) : ConfigurableSource, Webtoons(name, baseUrl, lang, langCode, localeForCookie, dateFormat) {
|
|
||||||
|
|
||||||
override val client: OkHttpClient = super.client.newBuilder()
|
|
||||||
.addInterceptor(TextInterceptor())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = SHOW_AUTHORS_NOTES_KEY
|
|
||||||
title = "Show author's notes"
|
|
||||||
summary = "Enable to see the author's notes at the end of chapters (if they're there)."
|
|
||||||
setDefaultValue(false)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val checkValue = newValue as Boolean
|
|
||||||
preferences.edit().putBoolean(SHOW_AUTHORS_NOTES_KEY, checkValue).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
screen.addPreference(authorsNotesPref)
|
|
||||||
|
|
||||||
val maxQualityPref = SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = USE_MAX_QUALITY_KEY
|
|
||||||
title = "Use maximum quality images"
|
|
||||||
summary = "Enable to load images in maximum quality."
|
|
||||||
setDefaultValue(false)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val checkValue = newValue as Boolean
|
|
||||||
preferences.edit().putBoolean(USE_MAX_QUALITY_KEY, checkValue).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
screen.addPreference(maxQualityPref)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showAuthorsNotesPref() = preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
|
|
||||||
private fun useMaxQualityPref() = preferences.getBoolean(USE_MAX_QUALITY_KEY, false)
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val useMaxQuality = useMaxQualityPref()
|
|
||||||
var pages = document.select("div#_imageList > img").mapIndexed { i, element ->
|
|
||||||
val imageUrl = element.attr("data-url").toHttpUrl()
|
|
||||||
|
|
||||||
if (useMaxQuality && imageUrl.queryParameter("type") == "q90") {
|
|
||||||
val newImageUrl = imageUrl.newBuilder().apply {
|
|
||||||
removeAllQueryParameters("type")
|
|
||||||
}.build()
|
|
||||||
Page(i, "", newImageUrl.toString())
|
|
||||||
} else {
|
|
||||||
Page(i, "", imageUrl.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showAuthorsNotesPref()) {
|
|
||||||
val note = document.select("div.creator_note p.author_text").text()
|
|
||||||
|
|
||||||
if (note.isNotEmpty()) {
|
|
||||||
val creator = document.select("div.creator_note a.author_name span").text().trim()
|
|
||||||
|
|
||||||
pages = pages + Page(
|
|
||||||
pages.size,
|
|
||||||
"",
|
|
||||||
TextInterceptorHelper.createUrl("Author's Notes from $creator", note),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pages.isNotEmpty()) { return pages }
|
|
||||||
|
|
||||||
val docString = document.toString()
|
|
||||||
|
|
||||||
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
|
|
||||||
val motiontoonPathRegex = Regex("jpg:.*?'(.*?)\\{")
|
|
||||||
|
|
||||||
val docUrl = docUrlRegex.find(docString)!!.destructured.toList()[0]
|
|
||||||
val motiontoonPath = motiontoonPathRegex.find(docString)!!.destructured.toList()[0]
|
|
||||||
val motiontoonResponse = client.newCall(GET(docUrl, headers)).execute()
|
|
||||||
|
|
||||||
val motiontoonJson = json.parseToJsonElement(motiontoonResponse.body.string()).jsonObject
|
|
||||||
val motiontoonImages = motiontoonJson["assets"]!!.jsonObject["image"]!!.jsonObject
|
|
||||||
|
|
||||||
return motiontoonImages.entries
|
|
||||||
.filter { it.key.contains("layer") }
|
|
||||||
.mapIndexed { i, entry ->
|
|
||||||
Page(i, "", motiontoonPath + entry.value.jsonPrimitive.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
|
||||||
private const val USE_MAX_QUALITY_KEY = "useMaxQuality"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
package eu.kanade.tachiyomi.extension.all.webtoons
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
@ -18,24 +18,26 @@ import kotlin.system.exitProcess
|
|||||||
*/
|
*/
|
||||||
class WebtoonsUrlActivity : Activity() {
|
class WebtoonsUrlActivity : Activity() {
|
||||||
|
|
||||||
|
private val name = javaClass.simpleName
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val titleNo = intent?.data?.getQueryParameter("title_no")
|
||||||
val title_no = intent?.data?.getQueryParameter("title_no")
|
val lang = intent?.data?.pathSegments?.get(0)
|
||||||
if (pathSegments != null && pathSegments.size >= 3 && title_no != null) {
|
if (titleNo != null) {
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${Webtoons.URL_SEARCH_PREFIX}${intent?.data?.toString()}")
|
putExtra("query", "$ID_SEARCH_PREFIX$lang:$titleNo")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Log.e("WebtoonsUrlActivity", e.toString())
|
Log.e(name, e.toString())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("WebtoonsUrlActivity", "could not parse uri from intent $intent")
|
Log.e(name, "could not parse uri from intent $intent")
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
finish()
|
@ -1,9 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Webtoons.com Translations'
|
extName = 'Webtoons.com Translations'
|
||||||
extClass = '.WebtoonsTranslateFactory'
|
extClass = '.WebtoonsTranslateFactory'
|
||||||
themePkg = 'webtoons'
|
extVersionCode = 9
|
||||||
baseUrl = 'https://translate.webtoons.com'
|
|
||||||
overrideVersionCode = 4
|
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
src/all/webtoonstranslate/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/all/webtoonstranslate/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/all/webtoonstranslate/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/all/webtoonstranslate/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/all/webtoonstranslate/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,83 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.webtoonstranslate
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Result<T>(
|
||||||
|
val result: T?,
|
||||||
|
val code: String,
|
||||||
|
val message: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class TitleList(
|
||||||
|
val totalCount: Int,
|
||||||
|
val titleList: List<Title>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Title(
|
||||||
|
private val titleNo: Int,
|
||||||
|
private val teamVersion: Int,
|
||||||
|
private val representTitle: String,
|
||||||
|
private val writeAuthorName: String?,
|
||||||
|
private val pictureAuthorName: String?,
|
||||||
|
private val thumbnailIPadUrl: String?,
|
||||||
|
private val thumbnailMobileUrl: String?,
|
||||||
|
) {
|
||||||
|
private val thumbnailUrl: String?
|
||||||
|
get() = (thumbnailIPadUrl ?: thumbnailMobileUrl)
|
||||||
|
?.let { "https://mwebtoon-phinf.pstatic.net$it" }
|
||||||
|
|
||||||
|
fun toSManga(baseUrl: String, translateLangCode: String) = SManga.create().apply {
|
||||||
|
url = baseUrl.toHttpUrl().newBuilder()
|
||||||
|
.addPathSegments("translate/episodeList")
|
||||||
|
.addQueryParameter("titleNo", titleNo.toString())
|
||||||
|
.addQueryParameter("languageCode", translateLangCode)
|
||||||
|
.addQueryParameter("teamVersion", teamVersion.toString())
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
title = representTitle
|
||||||
|
author = writeAuthorName
|
||||||
|
artist = pictureAuthorName ?: writeAuthorName
|
||||||
|
thumbnail_url = thumbnailUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EpisodeList(
|
||||||
|
val episodes: List<Episode>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Episode(
|
||||||
|
val translateCompleted: Boolean,
|
||||||
|
private val titleNo: Int,
|
||||||
|
private val episodeNo: Int,
|
||||||
|
private val languageCode: String,
|
||||||
|
private val teamVersion: Int,
|
||||||
|
private val title: String,
|
||||||
|
private val episodeSeq: Int,
|
||||||
|
private val updateYmdt: Long,
|
||||||
|
) {
|
||||||
|
fun toSChapter() = SChapter.create().apply {
|
||||||
|
url = "/lineWebtoon/ctrans/translatedEpisodeDetail_jsonp.json?titleNo=$titleNo&episodeNo=$episodeNo&languageCode=$languageCode&teamVersion=$teamVersion"
|
||||||
|
name = "$title #$episodeSeq"
|
||||||
|
chapter_number = episodeSeq.toFloat()
|
||||||
|
date_upload = updateYmdt
|
||||||
|
scanlator = teamVersion.takeIf { it != 0 }?.toString() ?: "(wiki)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ImageList(
|
||||||
|
val imageInfo: List<Image>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Image(
|
||||||
|
val imageUrl: String,
|
||||||
|
)
|
@ -0,0 +1,183 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.webtoonstranslate
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
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.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class WebtoonsTranslate(
|
||||||
|
override val lang: String,
|
||||||
|
private val translateLangCode: String,
|
||||||
|
private val extensionId: Long? = null,
|
||||||
|
) : HttpSource() {
|
||||||
|
|
||||||
|
override val name = "Webtoons.com Translations"
|
||||||
|
|
||||||
|
override val baseUrl = "https://translate.webtoons.com"
|
||||||
|
|
||||||
|
override val id get() = extensionId ?: super.id
|
||||||
|
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
private val apiBaseUrl = "https://global.apis.naver.com"
|
||||||
|
private val mobileBaseUrl = "https://m.webtoons.com"
|
||||||
|
|
||||||
|
private val pageSize = 24
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", mobileBaseUrl)
|
||||||
|
|
||||||
|
private fun mangaRequest(page: Int, requestSize: Int): Request {
|
||||||
|
val url = apiBaseUrl.toHttpUrl().newBuilder()
|
||||||
|
.addPathSegments("lineWebtoon/ctrans/translatedWebtoons_jsonp.json")
|
||||||
|
.addQueryParameter("orderType", "UPDATE")
|
||||||
|
.addQueryParameter("offset", "${(page - 1) * requestSize}")
|
||||||
|
.addQueryParameter("size", "$requestSize")
|
||||||
|
.addQueryParameter("languageCode", translateLangCode)
|
||||||
|
.build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webtoons translations doesn't really have a "popular" sort; just "UPDATE", "TITLE_ASC",
|
||||||
|
// and "TITLE_DESC". Pick UPDATE as the most useful sort.
|
||||||
|
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, pageSize)
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val offset = response.request.url.queryParameter("offset")!!.toInt()
|
||||||
|
val result = response.parseAs<Result<TitleList>>()
|
||||||
|
|
||||||
|
assert(result.code == "000") {
|
||||||
|
"Error getting popular manga: error code ${result.code}"
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaList = result.result!!.titleList
|
||||||
|
.map { it.toSManga(mobileBaseUrl, translateLangCode) }
|
||||||
|
|
||||||
|
return MangasPage(mangaList, hasNextPage = result.result.totalCount > pageSize + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return client.newCall(searchMangaRequest(page, query, filters))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
searchMangaParse(response, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't see a search function for Fan Translations, so let's do it client side.
|
||||||
|
* There's 75 webtoons as of 2019/11/21, a hardcoded request of 200 should be a sufficient request
|
||||||
|
* to get all titles, in 1 request, for quite a while
|
||||||
|
*/
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = mangaRequest(page, 200)
|
||||||
|
|
||||||
|
private fun searchMangaParse(response: Response, query: String): MangasPage {
|
||||||
|
val mangas = popularMangaParse(response).mangas
|
||||||
|
.filter {
|
||||||
|
it.title.contains(query, ignoreCase = true) ||
|
||||||
|
it.author?.contains(query, ignoreCase = true) == true ||
|
||||||
|
it.artist?.contains(query, ignoreCase = true) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
return GET(manga.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val (webtoonAuthor, webtoonArtist) = document.getMetaProp("com-linewebtoon:webtoon:author").let {
|
||||||
|
val split = it.split(" / ", limit = 2)
|
||||||
|
if (split.count() > 1) {
|
||||||
|
split[0] to split[1]
|
||||||
|
} else {
|
||||||
|
it to it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = document.getMetaProp("og:title")
|
||||||
|
artist = webtoonAuthor
|
||||||
|
author = webtoonArtist
|
||||||
|
description = document.getMetaProp("og:description")
|
||||||
|
thumbnail_url = document.getMetaProp("og:image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Document.getMetaProp(property: String): String =
|
||||||
|
head().select("meta[property=\"$property\"]").attr("content")
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val (titleNo, teamVersion) = manga.url.toHttpUrl().let {
|
||||||
|
it.queryParameter("titleNo") to it.queryParameter("teamVersion")
|
||||||
|
}
|
||||||
|
val chapterListUrl = apiBaseUrl.toHttpUrl().newBuilder()
|
||||||
|
.addPathSegments("lineWebtoon/ctrans/translatedEpisodes_jsonp.json")
|
||||||
|
.addQueryParameter("titleNo", titleNo)
|
||||||
|
.addQueryParameter("languageCode", translateLangCode)
|
||||||
|
.addQueryParameter("offset", "0")
|
||||||
|
.addQueryParameter("limit", "10000")
|
||||||
|
.addQueryParameter("teamVersion", teamVersion)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(chapterListUrl, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val result = response.parseAs<Result<EpisodeList>>()
|
||||||
|
|
||||||
|
assert(result.code == "000") {
|
||||||
|
val message = result.message ?: "error ${result.code}"
|
||||||
|
throw Exception("Error getting chapter list: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.result!!.episodes
|
||||||
|
.filter { it.translateCompleted }
|
||||||
|
.map { it.toSChapter() }
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
return GET("$apiBaseUrl${chapter.url}", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val result = response.parseAs<Result<ImageList>>()
|
||||||
|
|
||||||
|
return result.result!!.imageInfo.mapIndexed { i, img ->
|
||||||
|
Page(i, imageUrl = img.imageUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
}
|
@ -1,83 +1,40 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.webtoonstranslate
|
package eu.kanade.tachiyomi.extension.all.webtoonstranslate
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.webtoons.WebtoonsTranslate
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
class WebtoonsTranslateFactory : SourceFactory {
|
class WebtoonsTranslateFactory : SourceFactory {
|
||||||
override fun createSources(): List<Source> = listOf(
|
override fun createSources() = listOf(
|
||||||
WebtoonsTranslateEN(),
|
WebtoonsTranslate("en", "ENG"),
|
||||||
WebtoonsTranslateZH_CMN(),
|
WebtoonsTranslate("zh-Hans", "CMN", 5196522547754842244),
|
||||||
WebtoonsTranslateZH_CMY(),
|
WebtoonsTranslate("zh-Hant", "CMT", 1016181401146312893),
|
||||||
WebtoonsTranslateTH(),
|
WebtoonsTranslate("th", "THA"),
|
||||||
WebtoonsTranslateID(),
|
WebtoonsTranslate("id", "IND"),
|
||||||
WebtoonsTranslateFR(),
|
WebtoonsTranslate("fr", "FRA"),
|
||||||
WebtoonsTranslateVI(),
|
WebtoonsTranslate("vi", "VIE"),
|
||||||
WebtoonsTranslateRU(),
|
WebtoonsTranslate("ru", "RUS"),
|
||||||
WebtoonsTranslateAR(),
|
WebtoonsTranslate("ar", "ARA"),
|
||||||
WebtoonsTranslateFIL(),
|
WebtoonsTranslate("fil", "FIL"),
|
||||||
WebtoonsTranslateDE(),
|
WebtoonsTranslate("de", "DEU"),
|
||||||
WebtoonsTranslateHI(),
|
WebtoonsTranslate("hi", "HIN"),
|
||||||
WebtoonsTranslateIT(),
|
WebtoonsTranslate("it", "ITA"),
|
||||||
WebtoonsTranslateJA(),
|
WebtoonsTranslate("ja", "JPN"),
|
||||||
WebtoonsTranslatePT_POR(),
|
WebtoonsTranslate("pt-BR", "POR", 275670196689829558),
|
||||||
WebtoonsTranslateTR(),
|
WebtoonsTranslate("tr", "TUR"),
|
||||||
WebtoonsTranslateMS(),
|
WebtoonsTranslate("ms", "MAY"),
|
||||||
WebtoonsTranslatePL(),
|
WebtoonsTranslate("pl", "POL"),
|
||||||
WebtoonsTranslatePT_POT(),
|
WebtoonsTranslate("pt", "POT", 9219933036054791613),
|
||||||
WebtoonsTranslateBG(),
|
WebtoonsTranslate("bg", "BUL"),
|
||||||
WebtoonsTranslateDA(),
|
WebtoonsTranslate("da", "DAN"),
|
||||||
WebtoonsTranslateNL(),
|
WebtoonsTranslate("nl", "NLD"),
|
||||||
WebtoonsTranslateRO(),
|
WebtoonsTranslate("ro", "RON"),
|
||||||
WebtoonsTranslateMN(),
|
WebtoonsTranslate("mn", "MON"),
|
||||||
WebtoonsTranslateEL(),
|
WebtoonsTranslate("el", "GRE"),
|
||||||
WebtoonsTranslateLT(),
|
WebtoonsTranslate("lt", "LIT"),
|
||||||
WebtoonsTranslateCS(),
|
WebtoonsTranslate("cs", "CES"),
|
||||||
WebtoonsTranslateSV(),
|
WebtoonsTranslate("sv", "SWE"),
|
||||||
WebtoonsTranslateBN(),
|
WebtoonsTranslate("bn", "BEN"),
|
||||||
WebtoonsTranslateFA(),
|
WebtoonsTranslate("fa", "PER"),
|
||||||
WebtoonsTranslateUK(),
|
WebtoonsTranslate("uk", "UKR"),
|
||||||
WebtoonsTranslateES(),
|
WebtoonsTranslate("es", "SPA"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
class WebtoonsTranslateEN : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "en", "ENG")
|
|
||||||
class WebtoonsTranslateZH_CMN : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "zh-Hans", "CMN") {
|
|
||||||
override val id: Long = 5196522547754842244
|
|
||||||
}
|
|
||||||
class WebtoonsTranslateZH_CMY : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "zh-Hant", "CMT") {
|
|
||||||
override val id: Long = 1016181401146312893
|
|
||||||
}
|
|
||||||
class WebtoonsTranslateTH : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "th", "THA")
|
|
||||||
class WebtoonsTranslateID : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "id", "IND")
|
|
||||||
class WebtoonsTranslateFR : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "fr", "FRA")
|
|
||||||
class WebtoonsTranslateVI : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "vi", "VIE")
|
|
||||||
class WebtoonsTranslateRU : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "ru", "RUS")
|
|
||||||
class WebtoonsTranslateAR : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "ar", "ARA")
|
|
||||||
class WebtoonsTranslateFIL : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "fil", "FIL")
|
|
||||||
class WebtoonsTranslateDE : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "de", "DEU")
|
|
||||||
class WebtoonsTranslateHI : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "hi", "HIN")
|
|
||||||
class WebtoonsTranslateIT : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "it", "ITA")
|
|
||||||
class WebtoonsTranslateJA : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "ja", "JPN")
|
|
||||||
class WebtoonsTranslatePT_POR : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "pt-BR", "POR") {
|
|
||||||
// Hardcode the id because the language code was wrong.
|
|
||||||
override val id: Long = 275670196689829558
|
|
||||||
}
|
|
||||||
class WebtoonsTranslateTR : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "tr", "TUR")
|
|
||||||
class WebtoonsTranslateMS : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "ms", "MAY")
|
|
||||||
class WebtoonsTranslatePL : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "pl", "POL")
|
|
||||||
class WebtoonsTranslatePT_POT : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "pt", "POT") {
|
|
||||||
override val id: Long = 9219933036054791613
|
|
||||||
}
|
|
||||||
class WebtoonsTranslateBG : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "bg", "BUL")
|
|
||||||
class WebtoonsTranslateDA : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "da", "DAN")
|
|
||||||
class WebtoonsTranslateNL : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "nl", "NLD")
|
|
||||||
class WebtoonsTranslateRO : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "ro", "RON")
|
|
||||||
class WebtoonsTranslateMN : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "mn", "MON")
|
|
||||||
class WebtoonsTranslateEL : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "el", "GRE")
|
|
||||||
class WebtoonsTranslateLT : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "lt", "LIT")
|
|
||||||
class WebtoonsTranslateCS : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "cs", "CES")
|
|
||||||
class WebtoonsTranslateSV : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "sv", "SWE")
|
|
||||||
class WebtoonsTranslateBN : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "bn", "BEN")
|
|
||||||
class WebtoonsTranslateFA : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "fa", "PER")
|
|
||||||
class WebtoonsTranslateUK : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "uk", "UKR")
|
|
||||||
class WebtoonsTranslateES : WebtoonsTranslate("Webtoons.com Translations", "https://translate.webtoons.com", "es", "SPA")
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Dongman Manhua'
|
extName = 'Dongman Manhua'
|
||||||
extClass = '.DongmanManhua'
|
extClass = '.DongmanManhua'
|
||||||
themePkg = 'webtoons'
|
extVersionCode = 5
|
||||||
baseUrl = 'https://www.dongmanmanhua.cn'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +1,132 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.dongmanmanhua
|
package eu.kanade.tachiyomi.extension.zh.dongmanmanhua
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.webtoons.Webtoons
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.Headers
|
import keiyoushi.utils.tryParse
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class DongmanManhua : Webtoons("Dongman Manhua", "https://www.dongmanmanhua.cn", "zh", "", dateFormat = SimpleDateFormat("yyyy-M-d", Locale.ENGLISH)) {
|
class DongmanManhua : HttpSource() {
|
||||||
|
override val name = "Dongman Manhua"
|
||||||
|
override val lang = "zh"
|
||||||
|
override val baseUrl = "https://www.dongmanmanhua.cn"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.removeAll("Referer")
|
.set("Referer", "$baseUrl/")
|
||||||
.add("Referer", baseUrl)
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/dailySchedule", headers)
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/dailySchedule", headers)
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/dailySchedule?sortOrder=UPDATE&webtoonCompleteType=ONGOING", headers)
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
override fun parseDetailsThumbnail(document: Document): String? {
|
val entries = document.select("div#dailyList .daily_section li a, div.daily_lst.comp li a")
|
||||||
return document.select("div.detail_body").attr("style").substringAfter("(").substringBefore(")")
|
.map(::mangaFromElement)
|
||||||
|
.distinctBy { it.url }
|
||||||
|
|
||||||
|
return MangasPage(entries, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
private fun mangaFromElement(element: Element): SManga {
|
||||||
|
return SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
title = element.selectFirst("p.subj")!!.text()
|
||||||
|
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListSelector() = "ul#_listUl li"
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
GET("$baseUrl/dailySchedule?sortOrder=UPDATE&webtoonCompleteType=ONGOING", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val day = when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
|
||||||
|
Calendar.SUNDAY -> "div._list_SUNDAY"
|
||||||
|
Calendar.MONDAY -> "div._list_MONDAY"
|
||||||
|
Calendar.TUESDAY -> "div._list_TUESDAY"
|
||||||
|
Calendar.WEDNESDAY -> "div._list_WEDNESDAY"
|
||||||
|
Calendar.THURSDAY -> "div._list_THURSDAY"
|
||||||
|
Calendar.FRIDAY -> "div._list_FRIDAY"
|
||||||
|
Calendar.SATURDAY -> "div._list_SATURDAY"
|
||||||
|
else -> "div"
|
||||||
|
}
|
||||||
|
|
||||||
|
val entries = document.select("div#dailyList > $day li > a")
|
||||||
|
.map(::mangaFromElement)
|
||||||
|
.distinctBy { it.url }
|
||||||
|
|
||||||
|
return MangasPage(entries, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegment("search")
|
||||||
|
addQueryParameter("keyword", query)
|
||||||
|
if (page > 1) {
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val entries = document.select("#content > div.card_wrap.search ul:not(#filterLayer) li a")
|
||||||
|
.map(::mangaFromElement)
|
||||||
|
val hasNextPage = document.selectFirst("div.more_area, div.paginate a[onclick] + a") != null
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val detailElement = document.selectFirst(".detail_header .info")
|
||||||
|
val infoElement = document.selectFirst("#_asideDetail")
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = document.selectFirst("h1.subj, h3.subj")!!.text()
|
||||||
|
author = detailElement?.selectFirst(".author:nth-of-type(1)")?.ownText()
|
||||||
|
?: detailElement?.selectFirst(".author_area")?.ownText()
|
||||||
|
artist = detailElement?.selectFirst(".author:nth-of-type(2)")?.ownText()
|
||||||
|
?: detailElement?.selectFirst(".author_area")?.ownText() ?: author
|
||||||
|
genre = detailElement?.select(".genre").orEmpty().joinToString { it.text() }
|
||||||
|
description = infoElement?.selectFirst("p.summary")?.text()
|
||||||
|
status = with(infoElement?.selectFirst("p.day_info")?.text().orEmpty()) {
|
||||||
|
when {
|
||||||
|
contains("更新") -> SManga.ONGOING
|
||||||
|
contains("完结") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbnail_url = run {
|
||||||
|
val picElement = document.selectFirst("#content > div.cont_box > div.detail_body")
|
||||||
|
val discoverPic = document.selectFirst("#content > div.cont_box > div.detail_header > span.thmb")
|
||||||
|
picElement?.attr("style")
|
||||||
|
?.substringAfter("url(")
|
||||||
|
?.substringBeforeLast(")")
|
||||||
|
?.removeSurrounding("\"")
|
||||||
|
?.removeSurrounding("'")
|
||||||
|
?.takeUnless { it.isBlank() }
|
||||||
|
?: discoverPic?.selectFirst("img:not([alt='Representative image'])")
|
||||||
|
?.attr("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
var document = response.asJsoup()
|
var document = response.asJsoup()
|
||||||
@ -38,7 +134,7 @@ class DongmanManhua : Webtoons("Dongman Manhua", "https://www.dongmanmanhua.cn",
|
|||||||
val chapters = mutableListOf<SChapter>()
|
val chapters = mutableListOf<SChapter>()
|
||||||
|
|
||||||
while (continueParsing) {
|
while (continueParsing) {
|
||||||
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
document.select("ul#_listUl li").map { chapters.add(chapterFromElement(it)) }
|
||||||
document.select("div.paginate a[onclick] + a").let { element ->
|
document.select("div.paginate a[onclick] + a").let { element ->
|
||||||
if (element.isNotEmpty()) {
|
if (element.isNotEmpty()) {
|
||||||
document = client.newCall(GET(element.attr("abs:href"), headers)).execute().asJsoup()
|
document = client.newCall(GET(element.attr("abs:href"), headers)).execute().asJsoup()
|
||||||
@ -50,13 +146,25 @@ class DongmanManhua : Webtoons("Dongman Manhua", "https://www.dongmanmanhua.cn",
|
|||||||
return chapters
|
return chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
private fun chapterFromElement(element: Element): SChapter {
|
||||||
return SChapter.create().apply {
|
return SChapter.create().apply {
|
||||||
name = element.select("span.subj span").text()
|
name = element.selectFirst("span.subj span")!!.text()
|
||||||
url = element.select("a").attr("href").substringAfter(".cn")
|
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||||
date_upload = chapterParseDate(element.select("span.date").text())
|
date_upload = dateFormat.tryParse(element.selectFirst("span.date")?.text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList()
|
private val dateFormat = SimpleDateFormat("yyyy-M-d", Locale.ENGLISH)
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
return document.select("div#_imageList > img").mapIndexed { i, element ->
|
||||||
|
Page(i, imageUrl = element.attr("data-url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|