add Blacktoon (#4617)

* add BlackToon

* newline
This commit is contained in:
AwkwardPeak7 2024-08-14 16:18:40 +05:00 committed by Draff
parent 9fd4ff010d
commit 7c8f692488
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
10 changed files with 489 additions and 0 deletions

View File

@ -0,0 +1,8 @@
ext {
extName = 'BlackToon'
extClass = '.BlackToon'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,218 @@
package eu.kanade.tachiyomi.extension.ko.blacktoon
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import rx.Observable
import uy.kohesive.injekt.injectLazy
import kotlin.math.min
import kotlin.random.Random
class BlackToon : HttpSource() {
override val name = "블랙툰"
override val lang = "ko"
private var currentBaseUrlHost = ""
override val baseUrl = "https://blacktoon.me"
private val cdnUrl = "https://blacktoonimg.com/"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder().addInterceptor { chain ->
if (currentBaseUrlHost.isBlank()) {
noRedirectClient.newCall(GET(baseUrl, headers)).execute().use {
currentBaseUrlHost = it.headers["location"]?.toHttpUrlOrNull()?.host
?: throw IOException("unable to get updated url")
}
}
val request = chain.request().newBuilder().apply {
if (chain.request().url.toString().startsWith(baseUrl)) {
url(
chain.request().url.newBuilder()
.host(currentBaseUrlHost)
.build(),
)
}
header("Referer", "https://$currentBaseUrlHost/")
header("Origin", "https://$currentBaseUrlHost")
}.build()
return@addInterceptor chain.proceed(request)
}.build()
private val noRedirectClient = network.cloudflareClient.newBuilder()
.followRedirects(false)
.build()
private val json by injectLazy<Json>()
private val db by lazy {
val doc = client.newCall(GET(baseUrl, headers)).execute().asJsoup()
doc.select("script[src*=data/webtoon]").flatMap { scriptEl ->
var listIdx: Int
client.newCall(GET(scriptEl.absUrl("src"), headers))
.execute().body.string()
.also {
listIdx = it.substringBefore(" = ")
.substringAfter("data")
.toInt()
}
.substringAfter(" = ")
.removeSuffix(";")
.let { json.decodeFromString<List<SeriesItem>>(it) }
.onEach { it.listIndex = listIdx }
}
}
private fun List<SeriesItem>.getPageChunk(page: Int): MangasPage {
return MangasPage(
mangas = subList((page - 1) * 24, min(page * 24, size))
.map { it.toSManga(cdnUrl) },
hasNextPage = (page + 1) * 24 <= size,
)
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.just(
db.sortedByDescending { it.hot }.getPageChunk(page),
)
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return Observable.just(
db.sortedByDescending { it.updatedAt }.getPageChunk(page),
)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
var list = db
if (query.isNotBlank()) {
val stdQuery = query.trim()
list = list.filter {
it.name.contains(stdQuery, true) ||
it.author.contains(stdQuery, true)
}
}
filters.filterIsInstance<ListFilter>().forEach {
list = it.applyFilter(list)
}
return Observable.just(
list.getPageChunk(page),
)
}
override fun getFilterList() = getFilters()
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl/webtoon/${manga.url}.html#${manga.status}", headers)
}
override fun getMangaUrl(manga: SManga): String {
return buildString {
if (currentBaseUrlHost.isBlank()) {
append(baseUrl)
} else {
append("https://")
append(currentBaseUrlHost)
}
append("/webtoon/")
append(manga.url)
append(".html")
}
}
override fun mangaDetailsParse(response: Response): SManga {
val doc = response.asJsoup()
return SManga.create().apply {
description = doc.select("p.mt-2").last()?.text()
thumbnail_url = doc.selectFirst("script:containsData(+img_domain+)")?.data()?.let {
cdnUrl + it.substringAfter("+'").substringBefore("'+")
}
status = response.request.url.fragment!!.toInt()
}
}
override fun chapterListRequest(manga: SManga): Request {
val url = "$baseUrl/data/toonlist/${manga.url}.js?v=${"%.17f".format(Random.nextDouble())}"
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val mangaId = response.request.url.pathSegments.last().removeSuffix(".js")
val data = response.body.string()
.substringAfter(" = ")
.removeSuffix(";")
.let { json.decodeFromString<List<Chapter>>(it) }
return data.map { it.toSChapter(mangaId) }.reversed()
}
override fun getChapterUrl(chapter: SChapter): String {
return buildString {
if (currentBaseUrlHost.isBlank()) {
append(baseUrl)
} else {
append("https://")
append(currentBaseUrlHost)
}
append("/webtoons/")
append(chapter.url)
append(".html")
}
}
override fun pageListRequest(chapter: SChapter): Request {
return GET("$baseUrl/webtoons/${chapter.url}.html", headers)
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("#toon_content_imgs img").map {
Page(0, imageUrl = cdnUrl + it.attr("o_src"))
}
}
// unused
override fun popularMangaRequest(page: Int): Request {
throw UnsupportedOperationException()
}
override fun popularMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException()
}
override fun latestUpdatesRequest(page: Int): Request {
throw UnsupportedOperationException()
}
override fun latestUpdatesParse(response: Response): MangasPage {
throw UnsupportedOperationException()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
throw UnsupportedOperationException()
}
override fun searchMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException()
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
}

View File

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.extension.ko.blacktoon
val platformsMap = mapOf(
1 to "네이버",
2 to "다음",
3 to "카카오",
4 to "레진",
5 to "투믹스",
6 to "탑툰",
7 to "코미카",
8 to "배틀코믹",
9 to "코믹GT",
10 to "케이툰",
11 to "애니툰",
12 to "폭스툰",
13 to "피너툰",
14 to "봄툰",
15 to "코미코",
16 to "무툰",
17 to "지존신마",
99 to "기타",
)
val tagsMap = mapOf(
1 to "학원",
2 to "액션",
3 to "SF",
4 to "스토리",
5 to "판타지",
6 to "BL/백합",
7 to "개그/코미디",
8 to "연애/순정",
9 to "드라마",
10 to "로맨스",
11 to "시대극",
12 to "스포츠",
13 to "일상",
14 to "추리/미스터리",
15 to "공포/스릴러",
16 to "성인",
17 to "옴니버스",
18 to "에피소드",
19 to "무협",
20 to "소년",
99 to "기타",
)
val publishDayMap = mapOf(
1 to "",
2 to "",
3 to "",
4 to "",
5 to "",
6 to "",
7 to "",
10 to "열흘",
)

View File

@ -0,0 +1,84 @@
package eu.kanade.tachiyomi.extension.ko.blacktoon
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class SeriesItem(
@SerialName("x")
private val id: String,
@SerialName("t")
val name: String,
@SerialName("p")
private val poster: String = "",
@SerialName("au")
val author: String = "",
@SerialName("g")
val updatedAt: Long = 0,
@SerialName("tag")
private val tagIds: String = "",
@SerialName("c")
private val platformId: String = "-1",
@SerialName("d")
private val publishDayId: String = "-1",
@SerialName("h")
val hot: Int = 0,
) {
val tag get() = tagIds.split(",")
.filter(String::isNotBlank)
.map(String::toInt)
val platform get() = platformId.toInt()
val publishDay get() = publishDayId.toInt()
var listIndex = -1
fun toSManga(cdnUrl: String) = SManga.create().apply {
url = id
title = name
thumbnail_url = poster.takeIf { it.isNotBlank() }?.let {
cdnUrl + it.replace("_x4", "").replace("_x3", "")
}
genre = buildList {
add(platformsMap[platform])
add(publishDayMap[publishDay])
tag.forEach {
add(tagsMap[it])
}
}.filterNotNull().joinToString()
author = this@SeriesItem.author
status = when (listIndex) {
0 -> SManga.COMPLETED
1 -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
}
@Serializable
class Chapter(
@SerialName("id")
val id: String,
@SerialName("t")
val title: String,
@SerialName("d")
val date: String = "",
) {
fun toSChapter(mangaId: String) = SChapter.create().apply {
url = "$mangaId/$id"
name = title
date_upload = try {
dateFormat.parse(date)!!.time
} catch (_: ParseException) {
0L
}
}
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)

View File

@ -0,0 +1,122 @@
package eu.kanade.tachiyomi.extension.ko.blacktoon
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
interface ListFilter {
fun applyFilter(list: List<SeriesItem>): List<SeriesItem>
}
class TriFilter(name: String, val id: Int) : Filter.TriState(name)
abstract class TriFilterGroup(
name: String,
values: Map<Int, String>,
) : Filter.Group<TriFilter>(name, values.map { TriFilter(it.value, it.key) }), ListFilter {
private val included get() = state.filter { it.isIncluded() }.map { it.id }
private val excluded get() = state.filter { it.isExcluded() }.map { it.id }
abstract fun SeriesItem.getAttribute(): List<Int>
override fun applyFilter(list: List<SeriesItem>): List<SeriesItem> {
return list.filter { series ->
included.all {
it in series.getAttribute()
} and excluded.all {
it !in series.getAttribute()
}
}
}
}
abstract class SelectFilter(
name: String,
private val options: List<Pair<Int, String>>,
) : Filter.Select<String>(
name,
options.map { it.second }.toTypedArray(),
) {
val selected get() = options[state].first
}
class TagFilter : TriFilterGroup("Tag", tagsMap) {
override fun SeriesItem.getAttribute(): List<Int> {
return tag
}
}
class PlatformFilter :
SelectFilter(
"Platform",
buildList {
add(-1 to "")
platformsMap.forEach {
add(it.key to it.value)
}
},
),
ListFilter {
override fun applyFilter(list: List<SeriesItem>): List<SeriesItem> {
return list.filter { selected == -1 || it.platform == selected }
}
}
class PublishDayFilter :
SelectFilter(
"Publishing Day",
buildList {
add(-1 to "")
publishDayMap.forEach {
add(it.key to it.value)
}
},
),
ListFilter {
override fun applyFilter(list: List<SeriesItem>): List<SeriesItem> {
return list.filter { selected == -1 || it.publishDay == state }
}
}
class Status :
SelectFilter(
"Status",
listOf(
-1 to "All",
1 to "연재",
0 to "완결",
),
),
ListFilter {
override fun applyFilter(list: List<SeriesItem>): List<SeriesItem> {
return when (selected) {
1, 0 -> list.filter { it.listIndex == selected }
else -> list
}
}
}
class Order :
SelectFilter(
"Order by",
listOf(
0 to "최신순",
1 to "인기순",
),
),
ListFilter {
override fun applyFilter(list: List<SeriesItem>): List<SeriesItem> {
return when (selected) {
0 -> list.sortedByDescending { it.updatedAt }
1 -> list.sortedByDescending { it.hot }
else -> list
}
}
}
fun getFilters() = FilterList(
Order(),
Status(),
PlatformFilter(),
PublishDayFilter(),
TagFilter(),
)