parent
16d776056b
commit
4aa62d9b06
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'AgiToon'
|
||||||
|
extClass = '.AgiToon'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,218 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ko.agitoon
|
||||||
|
|
||||||
|
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 AgiToon : HttpSource() {
|
||||||
|
|
||||||
|
override val name = "아지툰"
|
||||||
|
|
||||||
|
override val lang = "ko"
|
||||||
|
|
||||||
|
private var currentBaseUrlHost = ""
|
||||||
|
override val baseUrl = "https://agitoon.in"
|
||||||
|
|
||||||
|
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/azi_toon/${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("/azi_toon/")
|
||||||
|
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("/azi_toons/")
|
||||||
|
append(chapter.url)
|
||||||
|
append(".html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
return GET("$baseUrl/azi_toons/${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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ko.agitoon
|
||||||
|
|
||||||
|
val platformsMap = mapOf(
|
||||||
|
Pair(1, "네이버"),
|
||||||
|
Pair(2, "다음"),
|
||||||
|
Pair(3, "카카오"),
|
||||||
|
Pair(4, "레진"),
|
||||||
|
Pair(5, "투믹스"),
|
||||||
|
Pair(6, "탑툰"),
|
||||||
|
Pair(7, "코미카"),
|
||||||
|
Pair(8, "배틀코믹"),
|
||||||
|
Pair(9, "코믹GT"),
|
||||||
|
Pair(10, "케이툰"),
|
||||||
|
Pair(11, "애니툰"),
|
||||||
|
Pair(12, "폭스툰"),
|
||||||
|
Pair(13, "피너툰"),
|
||||||
|
Pair(14, "봄툰"),
|
||||||
|
Pair(15, "코미코"),
|
||||||
|
Pair(16, "무툰"),
|
||||||
|
Pair(17, "지존신마"),
|
||||||
|
Pair(99, "기타"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val tagsMap = mapOf(
|
||||||
|
Pair(1, "학원"),
|
||||||
|
Pair(2, "액션"),
|
||||||
|
Pair(3, "SF"),
|
||||||
|
Pair(4, "스토리"),
|
||||||
|
Pair(5, "판타지"),
|
||||||
|
Pair(6, "BL/백합"),
|
||||||
|
Pair(7, "개그/코미디"),
|
||||||
|
Pair(8, "연애/순정"),
|
||||||
|
Pair(9, "드라마"),
|
||||||
|
Pair(10, "로맨스"),
|
||||||
|
Pair(11, "시대극"),
|
||||||
|
Pair(12, "스포츠"),
|
||||||
|
Pair(13, "일상"),
|
||||||
|
Pair(14, "추리/미스터리"),
|
||||||
|
Pair(15, "공포/스릴러"),
|
||||||
|
Pair(16, "성인"),
|
||||||
|
Pair(17, "옴니버스"),
|
||||||
|
Pair(18, "에피소드"),
|
||||||
|
Pair(19, "무협"),
|
||||||
|
Pair(20, "소년"),
|
||||||
|
Pair(99, "기타"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val publishDayMap = mapOf(
|
||||||
|
Pair(1, "월"),
|
||||||
|
Pair(2, "화"),
|
||||||
|
Pair(3, "수"),
|
||||||
|
Pair(4, "목"),
|
||||||
|
Pair(5, "금"),
|
||||||
|
Pair(6, "토"),
|
||||||
|
Pair(7, "일"),
|
||||||
|
Pair(10, "열흘"),
|
||||||
|
)
|
|
@ -0,0 +1,84 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ko.agitoon
|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1,122 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ko.agitoon
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
Loading…
Reference in New Issue