[VioletScans] Add source (#1798)

Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
master
Darwin 11 months ago committed by GitHub
parent 1e942c650b
commit 6854164e5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1 +1 @@
total: 1229
total: 1230

@ -0,0 +1,270 @@
package org.koitharu.kotatsu.parsers.site.en
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import org.koitharu.kotatsu.parsers.model.search.SearchCapability
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
import org.koitharu.kotatsu.parsers.model.search.SearchableField
import org.koitharu.kotatsu.parsers.model.search.SearchableField.*
import org.koitharu.kotatsu.parsers.util.generateUid
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.urlEncoded
import org.koitharu.kotatsu.parsers.util.tryParse
import org.koitharu.kotatsu.parsers.util.mapChapters
import java.text.SimpleDateFormat
import java.util.Locale
@MangaSourceParser("VIOLETSCANS", "VioletScans", "en")
internal class VioletScans(context: MangaLoaderContext):
PagedMangaParser(context, MangaParserSource.VIOLETSCANS, 12) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("violetscans.com")
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
super.onCreateConfig(keys)
keys.add(userAgentKey)
}
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
override val searchQueryCapabilities: MangaSearchQueryCapabilities
get() = MangaSearchQueryCapabilities(
SearchCapability(
field = TITLE_NAME,
criteriaTypes = setOf(Match::class),
isMultiple = false,
),
)
override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions()
override suspend fun getListPage(query: MangaSearchQuery, page: Int): List<Manga> {
var searchParameter = ""
query.criteria.forEach { criterion ->
when (criterion) {
is QueryCriteria.Match<*> -> {
if (criterion.field == SearchableField.TITLE_NAME) {
searchParameter = criterion.value.toString()
}
}
is QueryCriteria.Exclude<*> -> null
is QueryCriteria.Range<*> -> null
is QueryCriteria.Include<*> -> null
}
}
// scrapeNonSearchList has considerable less payload as response so this is a optimization
return when {
!searchParameter.isNullOrEmpty() -> scrapeSearchList(searchParameter, page)
else -> scrapeNonSearchList(page)
}
}
private suspend fun scrapeNonSearchList(page: Int): List<Manga> {
val url = buildString {
append("https://")
append(domain)
append("/wp-admin/admin-ajax.php")
}
val payload = buildString {
append("action=load_more_manga_posts&page=")
append((page - 1).toString())
}
val doc = webClient.httpPost(url, payload).parseHtml().body()
return doc.select("div.bsx").map { li ->
val href = li.selectFirstOrThrow(".info a").attr("href")
Manga(
id = generateUid(href),
title = li.selectFirstOrThrow(".info a .tt").text(),
altTitles = emptySet(),
url = href,
publicUrl = href,
rating = li.selectFirstOrThrow(".numscore").text().toFloat() / 10f,
contentRating = null,
coverUrl = li.selectFirstOrThrow("img").attr("src"),
tags = emptySet(),
state = null,
authors = emptySet(),
source = source,
)
}
}
private suspend fun scrapeSearchList(searchParameter: String, page: Int): List<Manga> {
val url = buildString {
append("https://")
append(domain)
if (page > 1) {
append("/page/")
append(page)
}
append("/?s=")
append(searchParameter.urlEncoded())
}
val doc = webClient.httpGet(url).parseHtml().body()
return doc.select("div.bsx").map { li ->
val href = li.selectFirstOrThrow("a").attr("href")
Manga(
id = generateUid(href),
title = li.selectFirstOrThrow(".bigor .tt").text(),
altTitles = emptySet(),
url = href,
publicUrl = href,
rating = li.selectFirstOrThrow("div .numscore").text().toFloat() / 10f,
contentRating = null,
coverUrl = li.selectFirstOrThrow("img").attr("src"),
tags = emptySet(),
state = null,
authors = emptySet(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url).parseHtml().body()
val root = doc.selectFirstOrThrow(".main-info")
val coverUrl = root.selectFirstOrThrow(".first-half .thumb img").attr("src")
val tags = root.selectFirstOrThrow("div .wd-full").map { tag ->
val tagName = tag.selectFirstOrThrow("a").text()
val tagKey = tag.selectFirstOrThrow("a").attr("href")
MangaTag(
title = tagName,
key = tagKey,
source = source,
)
}.toSet()
val description = StringBuilder()
val descriptionParagraphs = root.select(".summary p")
descriptionParagraphs.forEach { p ->
description.append(p.text())
}
var scanlator: String? = null
var status: MangaState? = null
var dateString: String? = null
val infoRoot = root.selectFirstOrThrow(".left-side")
val infos = infoRoot.select(".imptdt")
infos.forEach { info ->
//this website is pretty inconsistent thats why we have to do this ugly code
val data = info.selectFirst("h1")
if (data != null) {
when (data.text()) {
"Status" -> {
when (info.selectFirstOrThrow("i").text()) {
"Ongoing" -> {
status = MangaState.ONGOING
}
"Paused" -> {
status = MangaState.PAUSED
}
"Completed" -> {
status = MangaState.FINISHED
}
"Abandoned" -> {
status = MangaState.ABANDONED
}
}
}
"Serialization" -> {
scanlator = info.selectFirstOrThrow("i").text()
}
}
}
info.let {
if (info?.text() == "Posted On") dateString = info.text()
}
}
val dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
val date = dateFormat.tryParse(dateString) ?: 0L
val chaptersList = root.selectFirstOrThrow("#chapterlist ul")
val chapters = chaptersList.select("li")
val mangaChapters = chapters.mapNotNull { li ->
val url = li.getElementsByTag("a").attr("href")
// if url is empty it means the manga is paid
if (url.isEmpty()) null else {
val title = li.selectFirstOrThrow(".chapternum").text()
val regex = Regex("""\d+""")
val matchResult = regex.find(title)
val chapterNumber = matchResult?.value?.toFloat() ?: 0f
MangaChapter(
id = generateUid(url),
title = title,
number = chapterNumber,
volume = 0,
url = url,
scanlator = scanlator,
uploadDate = date,
branch = null,
source = source,
)
}
}
return manga.copy(
coverUrl = coverUrl,
chapters = mangaChapters.reversed(),
state = status,
description = description.toString(),
tags = tags,
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val url = chapter.url
val doc = webClient.httpGet(url).parseHtml()
val scriptTags = doc.select("script")
val pattern = Regex("""ts_reader\.run\((\{.*?\})\);""", RegexOption.DOT_MATCHES_ALL)
val jsonString = scriptTags.firstNotNullOfOrNull { script ->
val scriptText = script.data()
val match = pattern.find(scriptText)
match?.groups?.get(1)?.value
} ?: return emptyList()
val json = JSONObject(jsonString)
val sources = json.getJSONArray("sources")
if (sources.length() == 0) return emptyList()
val images = sources.getJSONObject(0).getJSONArray("images")
return (0 until images.length()).map { index ->
val src = images.getString(index)
MangaPage(
id = generateUid(src),
url = src,
preview = src,
source = source,
)
}
}
}
Loading…
Cancel
Save