commit
6316ac055c
@ -0,0 +1,341 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.jsoup.nodes.*
|
||||
import org.koitharu.kotatsu.parsers.*
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState.*
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType.*
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder.*
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
|
||||
@MangaSourceParser("WEEBCENTRAL", "Weeb Central", "en")
|
||||
internal class WeebCentral(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.WEEBCENTRAL),
|
||||
MangaParserAuthProvider {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("weebcentral.com")
|
||||
|
||||
override val authUrl: String
|
||||
get() = "https://$domain"
|
||||
|
||||
override val isAuthorized: Boolean
|
||||
get() = context.cookieJar.getCookies(domain).any { it.name == "access_token" }
|
||||
|
||||
override suspend fun getUsername(): String {
|
||||
return webClient.httpGet("https://$domain/users/me/profiles")
|
||||
.parseHtml()
|
||||
.selectFirstOrThrow("div:has(section > .avatar) .text-4xl")
|
||||
.text()
|
||||
}
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
RELEVANCE,
|
||||
ALPHABETICAL,
|
||||
ALPHABETICAL_DESC,
|
||||
POPULARITY,
|
||||
POPULARITY_ASC,
|
||||
RATING,
|
||||
RATING_ASC,
|
||||
ADDED,
|
||||
ADDED_ASC,
|
||||
UPDATED,
|
||||
UPDATED_ASC,
|
||||
)
|
||||
|
||||
override val filterCapabilities: MangaListFilterCapabilities =
|
||||
MangaListFilterCapabilities(
|
||||
isMultipleTagsSupported = true,
|
||||
isTagsExclusionSupported = true,
|
||||
isSearchSupported = true,
|
||||
isSearchWithFiltersSupported = true,
|
||||
)
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions {
|
||||
val document = webClient.httpGet("https://$domain/search")
|
||||
.parseHtml()
|
||||
|
||||
val tags = document.select("section[x-show=show_filter] div:contains(tags) ~ fieldset label").mapToSet {
|
||||
MangaTag(
|
||||
title = it.selectFirstOrThrow(".label-text").text(),
|
||||
key = it.selectFirstOrThrow("input[id$=value]").attr("value"),
|
||||
source = source
|
||||
)
|
||||
}
|
||||
|
||||
val states = EnumSet.of(
|
||||
ONGOING, FINISHED, ABANDONED, PAUSED
|
||||
)
|
||||
|
||||
val types = EnumSet.of(
|
||||
MANGA, MANHWA, MANHUA, COMICS
|
||||
)
|
||||
|
||||
val rating = EnumSet.of(
|
||||
SAFE, SUGGESTIVE
|
||||
)
|
||||
|
||||
return MangaListFilterOptions(
|
||||
availableTags = tags,
|
||||
availableStates = states,
|
||||
availableContentTypes = types,
|
||||
availableContentRating = rating
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
val url = "https://$domain/search/data".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("limit", "32")
|
||||
addQueryParameter("offset", offset.toString())
|
||||
filter.query?.let {
|
||||
val query = it
|
||||
.replace(Regex("""[^a-zA-Z0-9\s]"""), " ")
|
||||
.replace(Regex("""\s+"""), " ")
|
||||
.trim()
|
||||
addQueryParameter("text", query)
|
||||
}
|
||||
addQueryParameter(
|
||||
name = "sort",
|
||||
value = when (order) {
|
||||
RELEVANCE -> "Best Match"
|
||||
ALPHABETICAL, ALPHABETICAL_DESC -> "Alphabet"
|
||||
POPULARITY, POPULARITY_ASC -> "Popularity"
|
||||
RATING, RATING_ASC -> "Subscribers"
|
||||
ADDED, ADDED_ASC -> "Recently Added"
|
||||
UPDATED, UPDATED_ASC -> "Latest Updates"
|
||||
else -> throw UnsupportedOperationException("unsupported order: $order")
|
||||
}
|
||||
)
|
||||
addQueryParameter(
|
||||
name = "order",
|
||||
value = when (order) {
|
||||
RELEVANCE, ALPHABETICAL, POPULARITY_ASC, RATING_ASC, ADDED_ASC, UPDATED_ASC -> "Ascending"
|
||||
ALPHABETICAL_DESC, POPULARITY, RATING, ADDED, UPDATED -> "Descending"
|
||||
else -> throw UnsupportedOperationException("unsupported order: $order")
|
||||
}
|
||||
)
|
||||
addQueryParameter("official", "Any")
|
||||
addQueryParameter("anime", "Any")
|
||||
with (filter.contentRating) {
|
||||
addQueryParameter(
|
||||
name = "adult",
|
||||
value = when {
|
||||
isEmpty() -> "Any"
|
||||
SAFE in this && SUGGESTIVE in this -> "Any"
|
||||
SAFE in this -> "False"
|
||||
SUGGESTIVE in this -> "True"
|
||||
else -> throw UnsupportedOperationException("unsupported content rating: $this")
|
||||
}
|
||||
)
|
||||
}
|
||||
filter.states.forEach { state ->
|
||||
addQueryParameter(
|
||||
name = "included_status",
|
||||
value = when (state) {
|
||||
ONGOING -> "Ongoing"
|
||||
FINISHED -> "Complete"
|
||||
ABANDONED -> "Canceled"
|
||||
PAUSED -> "Hiatus"
|
||||
else -> throw UnsupportedOperationException("unsupported state: $state")
|
||||
}
|
||||
)
|
||||
}
|
||||
filter.types.forEach { type ->
|
||||
addQueryParameter(
|
||||
name = "included_type",
|
||||
value = when (type) {
|
||||
MANGA -> "Manga"
|
||||
MANHWA -> "Manhwa"
|
||||
MANHUA -> "Manhua"
|
||||
COMICS -> "OEL"
|
||||
else -> throw UnsupportedOperationException("unsupported type: $type")
|
||||
}
|
||||
)
|
||||
}
|
||||
filter.tags.forEach { tag ->
|
||||
addQueryParameter("included_tag", tag.key)
|
||||
}
|
||||
filter.tagsExclude.forEach { tag ->
|
||||
addQueryParameter("excluded_tag", tag.key)
|
||||
}
|
||||
addQueryParameter("display_mode", "Full Display")
|
||||
}.build()
|
||||
|
||||
val document = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return document.select("article:has(section)").map { element ->
|
||||
val mangaId = element.selectFirstOrThrow("div > a")
|
||||
.attrAsAbsoluteUrl("href")
|
||||
.toHttpUrl()
|
||||
.pathSegments[1]
|
||||
Manga(
|
||||
id = generateUid(mangaId),
|
||||
url = mangaId,
|
||||
publicUrl = "https://$domain/series/$mangaId",
|
||||
title = element.selectFirstOrThrow("div > a").text(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
contentRating = if (element.selectFirst("svg:has(style:containsData(ff0000))") == null) {
|
||||
SAFE
|
||||
} else {
|
||||
SUGGESTIVE
|
||||
},
|
||||
coverUrl = element.selectFirst("picture img")?.attrAsAbsoluteUrlOrNull("src"),
|
||||
tags = element.selectFirst("div:contains(Tag(s): )")?.text()
|
||||
?.substringAfter("Tag(s): ")
|
||||
?.split(", ")
|
||||
?.mapToSet {
|
||||
MangaTag(
|
||||
title = it,
|
||||
key = it,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
.orEmpty(),
|
||||
state = when(document.selectFirst("div:contains(status) span")?.text()) {
|
||||
"Ongoing" -> ONGOING
|
||||
"Complete" -> FINISHED
|
||||
"Canceled" -> ABANDONED
|
||||
"Hiatus" -> PAUSED
|
||||
else -> null
|
||||
},
|
||||
author = document.select("div:contains(author) a").eachText().joinToString(),
|
||||
largeCoverUrl = null,
|
||||
chapters = null,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val document = webClient.httpGet("https://$domain/series/${manga.url}")
|
||||
.parseHtml()
|
||||
|
||||
val chapters = async { getChapters(manga.url, document) }
|
||||
|
||||
val sectionLeft = document.select("section[x-data] > section")[0]
|
||||
val sectionRight = document.select("section[x-data] > section")[1]
|
||||
|
||||
manga.copy(
|
||||
title = sectionRight.selectFirstOrThrow("h1").text(),
|
||||
altTitle = sectionRight.select("li:has(strong:contains(Associated Name)) li")
|
||||
.eachText().joinToString(),
|
||||
publicUrl = "https://$domain/series/${manga.url}",
|
||||
rating = RATING_UNKNOWN,
|
||||
contentRating = if (sectionLeft.selectFirst("ul > li > strong:contains(Official Translation) + a:contains(Yes)") != null) {
|
||||
SUGGESTIVE
|
||||
} else {
|
||||
SAFE
|
||||
},
|
||||
coverUrl = sectionLeft.selectFirst("img")?.attrAsAbsoluteUrlOrNull("src"),
|
||||
tags = sectionRight.select("ul > li:has(strong:contains(Tag)) a").mapToSet {
|
||||
MangaTag(
|
||||
title = it.text(),
|
||||
key = it.text(),
|
||||
source = source
|
||||
)
|
||||
},
|
||||
state = when (sectionLeft.selectFirst("ul > li:has(strong:contains(Status)) > a")?.text()) {
|
||||
"Ongoing" -> ONGOING
|
||||
"Complete" -> FINISHED
|
||||
"Canceled" -> ABANDONED
|
||||
"Hiatus" -> PAUSED
|
||||
else -> null
|
||||
},
|
||||
author = sectionLeft.select("ul > li:has(strong:contains(Author)) > span > a")
|
||||
.eachText().joinToString(),
|
||||
description = Element("div").also { desc ->
|
||||
sectionRight.selectFirst("li:has(strong:contains(Description)) > p")?.let {
|
||||
desc.appendChild(it)
|
||||
}
|
||||
|
||||
val ul = Element("ul")
|
||||
sectionLeft.select("ul > li:has(strong:contains(Track)) abbr").stream().forEach { abbr ->
|
||||
abbr.selectFirst("a")?.attr("href")?.let { url ->
|
||||
val a = Element("a")
|
||||
.text(
|
||||
abbr.attr("title")
|
||||
)
|
||||
.attr("href", url)
|
||||
|
||||
ul.appendChild(
|
||||
Element("li").appendChild(a)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (ul.children().isNotEmpty()) {
|
||||
desc.append("<br><strong>Links:</strong>")
|
||||
desc.appendChild(ul)
|
||||
}
|
||||
|
||||
}.outerHtml(),
|
||||
chapters = chapters.await(),
|
||||
source = source
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getChapters(mangaId: String, mangaDocument: Document): List<MangaChapter> {
|
||||
val document = if (mangaDocument.selectFirst("#chapter-list > button[hx-get*=full-chapter-list]") != null) {
|
||||
webClient.httpGet("https://$domain/series/$mangaId/full-chapter-list").parseHtml()
|
||||
} else {
|
||||
mangaDocument
|
||||
}
|
||||
|
||||
return document.select("div[x-data] > a").mapChapters(reversed = true) { _, element ->
|
||||
val chapterId = element.attrAsAbsoluteUrl("href")
|
||||
.toHttpUrl()
|
||||
.pathSegments[1]
|
||||
|
||||
MangaChapter(
|
||||
id = generateUid(chapterId),
|
||||
url = chapterId,
|
||||
name = element.selectFirstOrThrow("span.flex > span").text(),
|
||||
number = element.selectFirstOrThrow("span.flex > span").text().let {
|
||||
Regex("""(\d+(\.\d+)?)""").find(it)!!.groupValues[1].toFloat()
|
||||
},
|
||||
volume = 0,
|
||||
scanlator = when (element.selectFirst("svg")?.attr("stroke")) {
|
||||
"#d8b4fe" -> "Official"
|
||||
"#4C4D54" -> "Unofficial"
|
||||
else -> null
|
||||
},
|
||||
uploadDate = dateFormat.tryParse(
|
||||
element.selectFirst("time[datetime]")?.attr("datetime")
|
||||
),
|
||||
branch = null,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val url = "https://$domain".toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("chapters")
|
||||
addPathSegment(chapter.url)
|
||||
addPathSegment("images")
|
||||
addQueryParameter("is_prev", "False")
|
||||
addQueryParameter("reading_style", "long_strip")
|
||||
}.build()
|
||||
|
||||
val document = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return document.select("section[x-data~=scroll] > img").map { element ->
|
||||
val pageUrl = element.attrAsAbsoluteUrl("src")
|
||||
|
||||
MangaPage(
|
||||
id = generateUid(pageUrl),
|
||||
url = pageUrl,
|
||||
preview = null,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.nepnep.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.Broken
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.nepnep.NepnepParser
|
||||
|
||||
// site closed in favour of weeb central
|
||||
@Broken
|
||||
@MangaSourceParser("MANGA4LIFE", "Manga4Life", "en")
|
||||
internal class Manga4Life(context: MangaLoaderContext) :
|
||||
NepnepParser(context, MangaParserSource.MANGA4LIFE, "manga4life.com")
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.nepnep.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.Broken
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.site.nepnep.NepnepParser
|
||||
|
||||
// site closed in favour of weeb central
|
||||
@Broken
|
||||
@MangaSourceParser("MANGASEE", "MangaSee", "en")
|
||||
internal class MangaSee(context: MangaLoaderContext) :
|
||||
NepnepParser(context, MangaParserSource.MANGASEE, "mangasee123.com")
|
||||
|
||||
Loading…
Reference in New Issue