Add toptoon.net (#10160)
* Add toptoon.net * Use HttpSource * Use parseAs from core * Use SimpleDateFormat.tryParse * Use fragment for search query * Add "lock emoji" comment * Use plain class for dto * Fix MangaDto * Fix ThumbnailDto * Fix name * Fix extName * Update comment
This commit is contained in:
parent
36eb58e893
commit
4587ac2c1d
8
src/zh/toptoon/build.gradle
Normal file
8
src/zh/toptoon/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'Toptoon.net'
|
||||
extClass = '.Toptoon'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/zh/toptoon/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/zh/toptoon/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
src/zh/toptoon/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/zh/toptoon/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
src/zh/toptoon/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/zh/toptoon/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
src/zh/toptoon/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/zh/toptoon/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/zh/toptoon/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/zh/toptoon/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -0,0 +1,134 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.toptoon
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 keiyoushi.utils.tryParse
|
||||
import okhttp3.Response
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Toptoon : HttpSource() {
|
||||
override val name: String = "TOPTOON頂通"
|
||||
override val lang: String = "zh"
|
||||
override val supportsLatest = true
|
||||
override val baseUrl = "https://www.toptoon.net"
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/ranking", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val jsonUrl = response.body.string()
|
||||
.substringAfter("jsonFileUrl: [\"")
|
||||
.substringBefore("\"")
|
||||
.replace("\\/", "/")
|
||||
val jsonResponse = client.newCall(GET("https:$jsonUrl", headers)).execute()
|
||||
val mangas = jsonResponse.parseAs<PopularResponseDto>().adult.map {
|
||||
it.toSManga()
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/search", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val jsonUrl = response.body.string()
|
||||
.substringAfter("var jsonFileUrl = '")
|
||||
.substringBefore("'")
|
||||
val jsonResponse = client.newCall(GET("https:$jsonUrl", headers)).execute()
|
||||
val mangas = jsonResponse.parseAs<Map<String, MangaDto>>().values
|
||||
.sortedByDescending { it.lastUpdated.pubDate }
|
||||
.map {
|
||||
it.toSManga()
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = GET("$baseUrl/search#$query", headers)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val query = response.request.url.fragment!!
|
||||
val jsonUrl = response.body.string()
|
||||
.substringAfter("var jsonFileUrl = '")
|
||||
.substringBefore("'")
|
||||
val jsonResponse = client.newCall(GET("https:$jsonUrl", headers)).execute()
|
||||
val mangas = jsonResponse.parseAs<Map<String, MangaDto>>().values
|
||||
.map {
|
||||
it.toSManga()
|
||||
}
|
||||
.filter { it.title.contains(query, true) || it.author!!.contains(query, true) }
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
title = document.selectFirst("section.infoContent div.title")!!.text()
|
||||
thumbnail_url = document.selectFirst("div.comicThumb img")!!.absUrl("src")
|
||||
author = document.selectFirst("section.infoContent div.etc")!!.text()
|
||||
.substringAfter("作家 : ").substringBefore("|")
|
||||
description = document.selectFirst("div.comic_story div.desc")!!.text()
|
||||
genre = document.selectFirst("section.infoContent div.hashTag")?.text()
|
||||
?.replace("#", ", ")
|
||||
if (document.selectFirst("div.etc span.comicDayBox") != null) {
|
||||
status = SManga.ONGOING
|
||||
} else if (document.selectFirst("div.hashTag a[href=/search/keyword/79]") != null) {
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
if (response.request.url.pathSegments[0].isEmpty()) {
|
||||
throw Exception("请到WebView确认年满18岁")
|
||||
}
|
||||
val document = response.asJsoup()
|
||||
return document.select("section.episode_area ul.list_area li.episodeBox").map {
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(it.selectFirst("a")!!.absUrl("href"))
|
||||
name = if (it.selectFirst("button.coin, button.gift, button.waitFree") != null) {
|
||||
"\uD83D\uDD12" // lock emoji
|
||||
} else {
|
||||
""
|
||||
} + it.selectFirst("div.title")!!.text() + " " +
|
||||
it.selectFirst("div.subTitle")!!.text()
|
||||
date_upload = dateFormat.tryParse(it.selectFirst("div.pubDate")?.text())
|
||||
}
|
||||
}.asReversed()
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pathSegments = response.request.url.pathSegments
|
||||
if (pathSegments[0].isEmpty()) {
|
||||
throw Exception("请到WebView确认年满18岁")
|
||||
} else if (pathSegments.size < 2 || pathSegments[1] != "epView") {
|
||||
throw Exception("请确认是否已登录解锁")
|
||||
}
|
||||
val document = response.asJsoup()
|
||||
val images = document.select("article.epContent section.imgWrap div.cImg img")
|
||||
return images.mapIndexed { index, img ->
|
||||
Page(index, imageUrl = img.absUrl("data-src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.toptoon
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
class PopularResponseDto(val adult: List<MangaDto>)
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
private val meta: MetaDto,
|
||||
private val thumbnail: ThumbnailDto,
|
||||
private val id: String,
|
||||
val lastUpdated: LastUpdatedDto,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = "/comic/epList/$id"
|
||||
title = meta.title
|
||||
author = meta.author.authorString
|
||||
thumbnail_url = thumbnail.url
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MetaDto(val title: String, val author: AuthorDto)
|
||||
|
||||
@Serializable
|
||||
class AuthorDto(val authorString: String)
|
||||
|
||||
@Serializable
|
||||
class ThumbnailDto(private val standard: JsonElement) {
|
||||
// "standard" in json can be either string or array
|
||||
val url get() = when (standard) {
|
||||
is JsonPrimitive -> "https://tw-contents-image.toptoon.net${standard.content}"
|
||||
is JsonArray -> "https://tw-contents-image.toptoon.net${standard[0].jsonPrimitive.content}"
|
||||
else -> throw Exception("Unexpected JSON type")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class LastUpdatedDto(val pubDate: String)
|
Loading…
x
Reference in New Issue
Block a user