-
Notifications
You must be signed in to change notification settings - Fork 550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add KunMangaTo source #6192
base: main
Are you sure you want to change the base?
Add KunMangaTo source #6192
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
ext { | ||
extName = 'KunMangaTo' | ||
extClass = '.KunMangaTo' | ||
extVersionCode = 1 | ||
} | ||
|
||
apply from: "$rootDir/common.gradle" |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,213 @@ | ||||||
package eu.kanade.tachiyomi.extension.en.kunmangato | ||||||
|
||||||
import eu.kanade.tachiyomi.network.GET | ||||||
import eu.kanade.tachiyomi.network.POST | ||||||
import eu.kanade.tachiyomi.source.model.Filter | ||||||
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.decodeFromString | ||||||
import kotlinx.serialization.json.Json | ||||||
import okhttp3.FormBody | ||||||
import okhttp3.HttpUrl.Companion.toHttpUrl | ||||||
import okhttp3.Request | ||||||
import okhttp3.Response | ||||||
import org.jsoup.Jsoup | ||||||
import org.jsoup.nodes.Document | ||||||
import org.jsoup.nodes.Element | ||||||
import uy.kohesive.injekt.injectLazy | ||||||
import java.text.ParseException | ||||||
import java.text.SimpleDateFormat | ||||||
import java.util.Locale | ||||||
import kotlin.concurrent.thread | ||||||
|
||||||
class KunMangaTo : ParsedHttpSource() { | ||||||
override val name = "KunMangaTo" | ||||||
override val baseUrl = "https://kunmanga.to" | ||||||
override val lang = "en" | ||||||
override val supportsLatest = true | ||||||
|
||||||
private val json: Json by injectLazy() | ||||||
|
||||||
private val dateFormat by lazy { | ||||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH) | ||||||
} | ||||||
|
||||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers) | ||||||
|
||||||
override fun popularMangaParse(response: Response): MangasPage { | ||||||
val mangas = super.popularMangaParse(response).mangas.distinctBy { it.url } | ||||||
return MangasPage(mangas, false) | ||||||
} | ||||||
|
||||||
override fun popularMangaSelector(): String = ".sidebar-box-popular article" | ||||||
|
||||||
override fun popularMangaFromElement(element: Element): SManga { | ||||||
val a = element.selectFirst("a.manga")!! | ||||||
return SManga.create().apply { | ||||||
title = a.text() | ||||||
url = a.attr("href").removePrefix(baseUrl) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
thumbnail_url = element.selectFirst("img")!!.attr("src") | ||||||
} | ||||||
} | ||||||
|
||||||
override fun popularMangaNextPageSelector(): String? = null | ||||||
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||||
val urlBuilder = "$baseUrl/search".toHttpUrl().newBuilder() | ||||||
.addQueryParameter("term", query.trim()) | ||||||
.addQueryParameter("page", page.toString()) | ||||||
|
||||||
filters.filterIsInstance<UriPartFilter>().forEach { filter -> | ||||||
urlBuilder.addQueryParameter( | ||||||
filter.toQueryParam(), | ||||||
filter.toUriPart(), | ||||||
) | ||||||
} | ||||||
|
||||||
return GET(urlBuilder.build(), headers) | ||||||
} | ||||||
|
||||||
override fun searchMangaSelector(): String = "article .card-manga" | ||||||
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) | ||||||
|
||||||
override fun searchMangaNextPageSelector(): String = "ul.pagination-primary a[rel=next]" | ||||||
|
||||||
override fun latestUpdatesRequest(page: Int): Request { | ||||||
val urlBuilder = "$baseUrl/latest".toHttpUrl().newBuilder() | ||||||
.addQueryParameter("page", page.toString()) | ||||||
|
||||||
return GET(urlBuilder.build(), headers) | ||||||
} | ||||||
|
||||||
override fun latestUpdatesSelector(): String = searchMangaSelector() | ||||||
|
||||||
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element) | ||||||
|
||||||
override fun latestUpdatesNextPageSelector(): String = searchMangaNextPageSelector() | ||||||
|
||||||
override fun mangaDetailsParse(document: Document): SManga { | ||||||
return SManga.create().apply { | ||||||
title = document.selectFirst(".page-heading")!!.text() | ||||||
author = document.selectFirst("p.mb-1:nth-child(1)")!!.text().drop(9) | ||||||
description = document.getElementById("manga-description")!!.text() | ||||||
genre = | ||||||
document.select("a.manga-genre").joinToString(", ") { element -> element.text() } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ", " is already the default join operation, so no need to specify |
||||||
status = | ||||||
if (document.select(".text-success").size > 0) SManga.COMPLETED else SManga.ONGOING | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is somewhat flaky. Since it is so generic it will cause issues if the layout changes slightly. It would be preferable if it's The document is a bit difficult to navigate, but you can rely on Jsoup's selectors: *:has(~ #manga-description) p span:contains(Status:) ~ span |
||||||
thumbnail_url = document.selectFirst("img.text-end")!!.attr("src") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
} | ||||||
} | ||||||
|
||||||
override fun chapterListParse(response: Response): List<SChapter> { | ||||||
val document = response.asJsoup() | ||||||
val chaptersDocument = | ||||||
Jsoup.parse("<div>" + document.selectFirst(chapterListSelector())!!.attr("value") + "</div>") | ||||||
Comment on lines
+109
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||||||
val chapterItems = chaptersDocument.select(".chapter-item") | ||||||
return chapterItems.map { element -> | ||||||
chapterFromElement(element) | ||||||
} | ||||||
} | ||||||
|
||||||
override fun chapterListSelector(): String = "#chapterList" | ||||||
|
||||||
override fun chapterFromElement(element: Element): SChapter { | ||||||
return SChapter.create().apply { | ||||||
url = element.getElementsByTag("a").first()!!.attr("href").removePrefix(baseUrl) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
also use |
||||||
name = element.getElementsByTag("h3").first()!!.text() | ||||||
date_upload = element.selectFirst(".text-muted")!!.text().parseChapterDate() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
} | ||||||
} | ||||||
|
||||||
private fun String?.parseChapterDate(): Long { | ||||||
if (this == null) return 0L | ||||||
return try { | ||||||
dateFormat.parse(this)!!.time | ||||||
} catch (_: ParseException) { | ||||||
0L | ||||||
} | ||||||
} | ||||||
|
||||||
override fun pageListParse(response: Response): List<Page> { | ||||||
val document = response.asJsoup() | ||||||
val form = FormBody.Builder() | ||||||
.add( | ||||||
"chapterIdx", | ||||||
response.request.url.toString().substringBefore(".html").substringAfterLast("-"), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extract to variable. Consider using regex. val document = response.asJsoup()
val chapterIdx = response.request.url.toString().substringBefore(".html").substringAfterLast("-")
val form = FormBody.Builder()
.add("chapterIdx", chapterIdx)
.build()
val chaptersRequest = POST(
// ... |
||||||
) | ||||||
.build() | ||||||
|
||||||
val chaptersRequest = POST( | ||||||
"$baseUrl/chapter-resources", | ||||||
headers.newBuilder().add( | ||||||
"X-CSRF-TOKEN", | ||||||
document.selectFirst("head > meta[name=\"csrf-token\"]")!!.attr("content"), | ||||||
).add( | ||||||
"Cookie", | ||||||
response.headers.values("set-cookie") | ||||||
.find { value -> value.startsWith("kunmanga_session") }!!, | ||||||
).build(), | ||||||
form, | ||||||
) | ||||||
val chaptersResponse = client.newCall(chaptersRequest).execute() | ||||||
val chapterResources = | ||||||
json.decodeFromString<KunMangaToChapterResourcesDto>(chaptersResponse.body.string()) | ||||||
return chapterResources.data.resources.map { resource -> | ||||||
Page(resource.id, imageUrl = resource.thumb) | ||||||
} | ||||||
} | ||||||
|
||||||
private var filterValues: Map<KunMangaToFilter, List<OptionValueOptionNamePair>>? = null | ||||||
|
||||||
private var fetchFilterValuesAttempts: Int = 0 | ||||||
|
||||||
private fun parseFilters(document: Document): Map<KunMangaToFilter, List<OptionValueOptionNamePair>> { | ||||||
return KunMangaToFilter.values().associateWith { | ||||||
document.select("[name=\"${it.queryParam}\"] option") | ||||||
.map { option -> option.attr("value") to option.text() } | ||||||
} | ||||||
} | ||||||
|
||||||
private fun fetchFiltersValues() { | ||||||
if (fetchFilterValuesAttempts < 3 && filterValues == null) { | ||||||
thread { | ||||||
try { | ||||||
filterValues = parseFilters( | ||||||
client.newCall(searchMangaRequest(1, "", FilterList())).execute().asJsoup(), | ||||||
) | ||||||
} finally { | ||||||
fetchFilterValuesAttempts++ | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
override fun getFilterList(): FilterList { | ||||||
fetchFiltersValues() | ||||||
return if (filterValues != null) { | ||||||
FilterList( | ||||||
filterValues!!.map { filterValue -> | ||||||
UriPartFilter( | ||||||
filterValue.key, | ||||||
filterValue.value, | ||||||
) | ||||||
}, | ||||||
) | ||||||
} else { | ||||||
FilterList(Filter.Header("Press 'Reset' to attempt to fetch the filters")) | ||||||
} | ||||||
} | ||||||
|
||||||
override fun imageUrlParse(document: Document): String { | ||||||
throw UnsupportedOperationException() | ||||||
} | ||||||
|
||||||
override fun pageListParse(document: Document): List<Page> { | ||||||
throw UnsupportedOperationException() | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package eu.kanade.tachiyomi.extension.en.kunmangato | ||
|
||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
class KunMangaToChapterResourcesDto( | ||
val status: Int, | ||
val data: KunMangaToDataDto, | ||
) | ||
|
||
@Serializable | ||
class KunMangaToDataDto( | ||
val resources: List<KunMangaToResourceDto>, | ||
) | ||
|
||
@Serializable | ||
class KunMangaToResourceDto( | ||
val id: Int, | ||
val name: Int, | ||
val thumb: String, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package eu.kanade.tachiyomi.extension.en.kunmangato | ||
|
||
enum class KunMangaToFilter(val queryParam: String) { | ||
Genre("manga_genre_id"), | ||
Type("manga_type_id"), | ||
Status("status"), | ||
} | ||
|
||
typealias OptionName = String | ||
|
||
typealias OptionValue = String | ||
|
||
typealias OptionValueOptionNamePair = Pair<OptionValue, OptionName> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package eu.kanade.tachiyomi.extension.en.kunmangato | ||
|
||
import eu.kanade.tachiyomi.source.model.Filter | ||
|
||
open class UriPartFilter( | ||
private val filter: KunMangaToFilter, | ||
private val vals: List<OptionValueOptionNamePair>, | ||
) : Filter.Select<String>(filter.name, vals.map { it.second }.toTypedArray()) { | ||
fun toQueryParam() = filter.queryParam | ||
|
||
fun toUriPart() = vals[state].first | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably be marked as NSFW since they carry 'He Does a Body Good'.