Add base url to ParseException

pull/38/head
Koitharu 4 years ago
parent 37fda4bd9e
commit 30071709af
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import okhttp3.Headers import okhttp3.Headers
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
@ -177,8 +178,8 @@ abstract class MangaParser @InternalParsersApi constructor(val source: MangaSour
} }
@InternalParsersApi @InternalParsersApi
protected fun parseFailed(message: String? = null): Nothing { protected fun Element.parseFailed(message: String? = null): Nothing {
throw ParseException(message, null) throw ParseException(message, ownerDocument()?.location() ?: baseUri(), null)
} }
@InternalParsersApi @InternalParsersApi

@ -0,0 +1,3 @@
package org.koitharu.kotatsu.parsers.exception
class ContentUnavailableException(message: String) : RuntimeException(message)

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.exception
import org.koitharu.kotatsu.parsers.InternalParsersApi import org.koitharu.kotatsu.parsers.InternalParsersApi
class ParseException @InternalParsersApi @JvmOverloads constructor( class ParseException @InternalParsersApi @JvmOverloads constructor(
message: String?, val shortMessage: String?,
val url: String,
cause: Throwable? = null, cause: Throwable? = null,
) : RuntimeException(message, cause) ) : RuntimeException("$shortMessage at $url", cause)

@ -8,6 +8,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@ -70,8 +71,8 @@ internal class BatoToParser(override val context: MangaLoaderContext) : PagedMan
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val root = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml() val root = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
.getElementById("mainer") ?: parseFailed("Cannot find root") .requireElementById("mainer")
val details = root.selectFirst(".detail-set") ?: parseFailed("Cannot find detail-set") val details = root.selectFirstOrThrow(".detail-set")
val attrs = details.selectFirst(".attr-main")?.select(".attr-item")?.associate { val attrs = details.selectFirst(".attr-main")?.select(".attr-item")?.associate {
it.child(0).text().trim() to it.child(1) it.child(0).text().trim() to it.child(1)
}.orEmpty() }.orEmpty()
@ -113,11 +114,11 @@ internal class BatoToParser(override val context: MangaLoaderContext) : PagedMan
} }
val images = JSONArray(scriptSrc.substring(start, end)) val images = JSONArray(scriptSrc.substring(start, end))
val batoPass = scriptSrc.substringBetweenFirst("batoPass =", ";")?.trim(' ', '"', '\n') val batoPass = scriptSrc.substringBetweenFirst("batoPass =", ";")?.trim(' ', '"', '\n')
?: parseFailed("Cannot find batoPass") ?: script.parseFailed("Cannot find batoPass")
val batoWord = scriptSrc.substringBetweenFirst("batoWord =", ";")?.trim(' ', '"', '\n') val batoWord = scriptSrc.substringBetweenFirst("batoWord =", ";")?.trim(' ', '"', '\n')
?: parseFailed("Cannot find batoWord") ?: script.parseFailed("Cannot find batoWord")
val password = context.evaluateJs(batoPass)?.removeSurrounding('"') val password = context.evaluateJs(batoPass)?.removeSurrounding('"')
?: parseFailed("Cannot evaluate batoPass") ?: script.parseFailed("Cannot evaluate batoPass")
val args = JSONArray(decryptAES(batoWord, password)) val args = JSONArray(decryptAES(batoWord, password))
val result = ArrayList<MangaPage>(images.length()) val result = ArrayList<MangaPage>(images.length())
repeat(images.length()) { i -> repeat(images.length()) { i ->
@ -132,13 +133,13 @@ internal class BatoToParser(override val context: MangaLoaderContext) : PagedMan
} }
return result return result
} }
parseFailed("Cannot find images list") throw ParseException("Cannot find images list", fullUrl)
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val scripts = context.httpGet( val scripts = context.httpGet(
"https://${getDomain()}/browse", "https://${getDomain()}/browse",
).parseHtml().select("script") ).parseHtml().selectOrThrow("script")
for (script in scripts) { for (script in scripts) {
val genres = script.html().substringBetweenFirst("const _genres =", ";") ?: continue val genres = script.html().substringBetweenFirst("const _genres =", ";") ?: continue
val jo = JSONObject(genres) val jo = JSONObject(genres)
@ -153,7 +154,7 @@ internal class BatoToParser(override val context: MangaLoaderContext) : PagedMan
} }
return result return result
} }
parseFailed("Cannot find gernes list") throw ParseException("Cannot find gernes list", scripts[0].baseUri())
} }
override fun getFaviconUrl(): String = "https://styles.amarkcdn.com/img/batoto/favicon.ico?v0" override fun getFaviconUrl(): String = "https://styles.amarkcdn.com/img/batoto/favicon.ico?v0"
@ -173,7 +174,7 @@ internal class BatoToParser(override val context: MangaLoaderContext) : PagedMan
private fun getActivePage(body: Element): Int = body.select("nav ul.pagination > li.page-item.active") private fun getActivePage(body: Element): Int = body.select("nav ul.pagination > li.page-item.active")
.lastOrNull() .lastOrNull()
?.text() ?.text()
?.toIntOrNull() ?: parseFailed("Cannot determine current page") ?.toIntOrNull() ?: body.parseFailed("Cannot determine current page")
private suspend fun parseList(url: String, page: Int): List<Manga> { private suspend fun parseList(url: String, page: Int): List<Manga> {
val body = context.httpGet(url).parseHtml().body() val body = context.httpGet(url).parseHtml().body()
@ -184,11 +185,11 @@ internal class BatoToParser(override val context: MangaLoaderContext) : PagedMan
if (activePage != page) { if (activePage != page) {
return emptyList() return emptyList()
} }
val root = body.getElementById("series-list") ?: parseFailed("Cannot find root") val root = body.requireElementById("series-list")
return root.children().map { div -> return root.children().map { div ->
val a = div.selectFirst("a") ?: parseFailed() val a = div.selectFirstOrThrow("a")
val href = a.attrAsRelativeUrl("href") val href = a.attrAsRelativeUrl("href")
val title = div.selectFirst(".item-title")?.text() ?: parseFailed("Title not found") val title = div.selectFirstOrThrow(".item-title").text()
Manga( Manga(
id = generateUid(href), id = generateUid(href),
title = title, title = title,

@ -50,7 +50,8 @@ internal class DesuMeParser(override val context: MangaLoaderContext) : PagedMan
append(query) append(query)
} }
} }
val json = context.httpGet(url).parseJson().getJSONArray("response") ?: parseFailed("Invalid response") val json = context.httpGet(url).parseJson().getJSONArray("response")
?: throw ParseException("Invalid response", url)
val total = json.length() val total = json.length()
val list = ArrayList<Manga>(total) val list = ArrayList<Manga>(total)
for (i in 0 until total) { for (i in 0 until total) {
@ -83,7 +84,7 @@ internal class DesuMeParser(override val context: MangaLoaderContext) : PagedMan
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val url = manga.url.toAbsoluteUrl(getDomain()) val url = manga.url.toAbsoluteUrl(getDomain())
val json = context.httpGet(url).parseJson().getJSONObject("response") val json = context.httpGet(url).parseJson().getJSONObject("response")
?: throw ParseException("Invalid response") ?: throw ParseException("Invalid response", url)
val baseChapterUrl = manga.url + "/chapter/" val baseChapterUrl = manga.url + "/chapter/"
val chaptersList = json.getJSONObject("chapters").getJSONArray("list") val chaptersList = json.getJSONObject("chapters").getJSONArray("list")
val totalChapters = chaptersList.length() val totalChapters = chaptersList.length()
@ -119,7 +120,7 @@ internal class DesuMeParser(override val context: MangaLoaderContext) : PagedMan
val fullUrl = chapter.url.toAbsoluteUrl(getDomain()) val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
val json = context.httpGet(fullUrl) val json = context.httpGet(fullUrl)
.parseJson() .parseJson()
.getJSONObject("response") ?: throw ParseException("Invalid response") .getJSONObject("response") ?: throw ParseException("Invalid response", fullUrl)
return json.getJSONObject("pages").getJSONArray("list").mapJSON { jo -> return json.getJSONObject("pages").getJSONArray("list").mapJSON { jo ->
MangaPage( MangaPage(
id = generateUid(jo.getLong("id")), id = generateUid(jo.getLong("id")),
@ -133,17 +134,17 @@ internal class DesuMeParser(override val context: MangaLoaderContext) : PagedMan
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val doc = context.httpGet("https://${getDomain()}/manga/").parseHtml() val doc = context.httpGet("https://${getDomain()}/manga/").parseHtml()
val root = doc.body().getElementById("animeFilter") val root = doc.body().requireElementById("animeFilter")
?.selectFirst(".catalog-genres") ?: throw ParseException("Root not found") .selectFirstOrThrow(".catalog-genres")
return root.select("li").mapToSet { return root.select("li").mapToSet {
val input = it.selectFirst("input") ?: parseFailed() val input = it.selectFirstOrThrow("input")
MangaTag( MangaTag(
source = source, source = source,
key = input.attr("data-genre-slug").ifEmpty { key = input.attr("data-genre-slug").ifEmpty {
parseFailed("data-genre-slug is empty") it.parseFailed("data-genre-slug is empty")
}, },
title = input.attr("data-genre-name").toTitleCase().ifEmpty { title = input.attr("data-genre-name").toTitleCase().ifEmpty {
parseFailed("data-genre-name is empty") it.parseFailed("data-genre-name is empty")
}, },
) )
} }

@ -7,7 +7,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.util.* import java.util.*
@ -94,7 +93,7 @@ internal class ExHentaiParser(
val root = body.selectFirst("table.itg") val root = body.selectFirst("table.itg")
?.selectFirst("tbody") ?.selectFirst("tbody")
?: if (updateDm) { ?: if (updateDm) {
parseFailed("Cannot find root") body.parseFailed("Cannot find root")
} else { } else {
updateDm = true updateDm = true
return getListPage(page, query, tags, sortOrder) return getListPage(page, query, tags, sortOrder)
@ -103,10 +102,10 @@ internal class ExHentaiParser(
return root.children().mapNotNull { tr -> return root.children().mapNotNull { tr ->
if (tr.childrenSize() != 2) return@mapNotNull null if (tr.childrenSize() != 2) return@mapNotNull null
val (td1, td2) = tr.children() val (td1, td2) = tr.children()
val glink = td2.selectFirst("div.glink") ?: parseFailed("glink not found") val glink = td2.selectFirstOrThrow("div.glink")
val a = glink.parents().select("a").first() ?: parseFailed("link not found") val a = glink.parents().select("a").first() ?: glink.parseFailed("link not found")
val href = a.attrAsRelativeUrl("href") val href = a.attrAsRelativeUrl("href")
val tagsDiv = glink.nextElementSibling() ?: parseFailed("tags div not found") val tagsDiv = glink.nextElementSibling() ?: glink.parseFailed("tags div not found")
val mainTag = td2.selectFirst("div.cn")?.let { div -> val mainTag = td2.selectFirst("div.cn")?.let { div ->
MangaTag( MangaTag(
title = div.text().toTitleCase(), title = div.text().toTitleCase(),
@ -134,7 +133,7 @@ internal class ExHentaiParser(
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
val root = doc.body().selectFirst("div.gm") ?: parseFailed("Cannot find root") val root = doc.body().selectFirstOrThrow("div.gm")
val cover = root.getElementById("gd1")?.children()?.first() val cover = root.getElementById("gd1")?.children()?.first()
val title = root.getElementById("gd2") val title = root.getElementById("gd2")
val taglist = root.getElementById("taglist") val taglist = root.getElementById("taglist")
@ -178,7 +177,7 @@ internal class ExHentaiParser(
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml()
val root = doc.body().getElementById("gdt") ?: parseFailed("Root not found") val root = doc.body().requireElementById("gdt")
return root.select("a").map { a -> return root.select("a").map { a ->
val url = a.attrAsRelativeUrl("href") val url = a.attrAsRelativeUrl("href")
MangaPage( MangaPage(
@ -193,14 +192,12 @@ internal class ExHentaiParser(
override suspend fun getPageUrl(page: MangaPage): String { override suspend fun getPageUrl(page: MangaPage): String {
val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain())).parseHtml()
return doc.body().getElementById("img")?.absUrl("src") return doc.body().requireElementById("img").attrAsAbsoluteUrl("src")
?: parseFailed("Image not found")
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val doc = context.httpGet("https://${getDomain()}").parseHtml() val doc = context.httpGet("https://${getDomain()}").parseHtml()
val root = doc.body().getElementById("searchbox")?.selectFirst("table") val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table")
?: parseFailed("Root not found")
return root.select("div.cs").mapNotNullToSet { div -> return root.select("div.cs").mapNotNullToSet { div ->
val id = div.id().substringAfterLast('_').toIntOrNull() val id = div.id().substringAfterLast('_').toIntOrNull()
?: return@mapNotNullToSet null ?: return@mapNotNullToSet null
@ -221,7 +218,7 @@ internal class ExHentaiParser(
?: if (doc.getElementById("userlinksguest") != null) { ?: if (doc.getElementById("userlinksguest") != null) {
throw AuthRequiredException(source) throw AuthRequiredException(source)
} else { } else {
throw ParseException(null) doc.parseFailed()
} }
return username return username
} }

@ -45,7 +45,7 @@ class MangaInUaParser(override val context: MangaLoaderContext) : PagedMangaPars
else -> "/mangas/page/$page".toAbsoluteUrl(getDomain()) else -> "/mangas/page/$page".toAbsoluteUrl(getDomain())
} }
val doc = context.httpGet(url).parseHtml() val doc = context.httpGet(url).parseHtml()
val container = doc.body().getElementById("dle-content") ?: parseFailed("Container not found") val container = doc.body().requireElementById("dle-content")
val items = container.select("div.col-6") val items = container.select("div.col-6")
return items.mapNotNull { item -> return items.mapNotNull { item ->
val href = item.selectFirst("a")?.attrAsRelativeUrl("href") ?: return@mapNotNull null val href = item.selectFirst("a")?.attrAsRelativeUrl("href") ?: return@mapNotNull null
@ -81,8 +81,7 @@ class MangaInUaParser(override val context: MangaLoaderContext) : PagedMangaPars
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
val root = val root = doc.body().requireElementById("dle-content")
doc.body().getElementById("dle-content") ?: parseFailed("Cannot find root")
val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US) val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
val chapterNodes = root.selectFirstOrThrow(".linkstocomics") val chapterNodes = root.selectFirstOrThrow(".linkstocomics")
.select(".ltcitems") .select(".ltcitems")
@ -125,10 +124,9 @@ class MangaInUaParser(override val context: MangaLoaderContext) : PagedMangaPars
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(getDomain()) val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
val doc = context.httpGet(fullUrl).parseHtml() val doc = context.httpGet(fullUrl).parseHtml()
val root = val root = doc.body().requireElementById("comics").selectFirstOrThrow("ul.xfieldimagegallery")
doc.body().getElementById("comics")?.selectFirst("ul.xfieldimagegallery") ?: parseFailed("Root not found")
return root.select("li").map { ul -> return root.select("li").map { ul ->
val img = ul.selectFirst("img") ?: parseFailed("Page image not found") val img = ul.selectFirstOrThrow("img")
val url = img.attrAsAbsoluteUrl("data-src") val url = img.attrAsAbsoluteUrl("data-src")
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
@ -143,8 +141,7 @@ class MangaInUaParser(override val context: MangaLoaderContext) : PagedMangaPars
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val domain = getDomain() val domain = getDomain()
val doc = context.httpGet("https://$domain/mangas").parseHtml() val doc = context.httpGet("https://$domain/mangas").parseHtml()
val root = val root = doc.body().requireElementById("menu_1").selectFirstOrThrow("div.menu__wrapper")
doc.body().getElementById("menu_1")?.selectFirst("div.menu__wrapper") ?: parseFailed("Cannot find root")
return root.select("li").mapNotNullToSet { li -> return root.select("li").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null val a = li.selectFirst("a") ?: return@mapNotNullToSet null
MangaTag( MangaTag(

@ -4,7 +4,6 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -52,7 +51,7 @@ internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaP
} }
} }
val doc = context.httpGet(link).parseHtml() val doc = context.httpGet(link).parseHtml()
val slides = doc.body().select("ul.slides") ?: parseFailed("An error occurred while parsing") val slides = doc.body().selectOrThrow("ul.slides")
val items = slides.select("div.col-md-2") val items = slides.select("div.col-md-2")
return items.mapNotNull { item -> return items.mapNotNull { item ->
val href = item.selectFirst("h6 a")?.attrAsRelativeUrlOrNull("href") ?: return@mapNotNull null val href = item.selectFirst("h6 a")?.attrAsRelativeUrlOrNull("href") ?: return@mapNotNull null
@ -80,12 +79,12 @@ internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaP
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.publicUrl).parseHtml() val doc = context.httpGet(manga.publicUrl).parseHtml()
val info = doc.body().selectFirst("div.single_detail") ?: parseFailed("An error occurred while parsing") val info = doc.body().selectFirstOrThrow("div.single_detail")
val table = doc.body().selectFirst("div.single-grid-right") ?: parseFailed("An error occurred while parsing") val table = doc.body().selectFirstOrThrow("div.single-grid-right")
val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US) val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US)
val trRegex = "window\\['tr'] = '([^']*)';".toRegex(RegexOption.IGNORE_CASE) val trRegex = "window\\['tr'] = '([^']*)';".toRegex(RegexOption.IGNORE_CASE)
val trElement = val trElement = doc.getElementsByTag("script").find { trRegex.find(it.data()) != null }
doc.getElementsByTag("script").find { trRegex.find(it.data()) != null } ?: parseFailed("Oops, tr not found") ?: doc.parseFailed("Oops, tr not found")
val tr = trRegex.find(trElement.data())!!.groups[1]!!.value val tr = trRegex.find(trElement.data())!!.groups[1]!!.value
val s = context.encodeBase64(getDomain().toByteArray()) val s = context.encodeBase64(getDomain().toByteArray())
var isNsfw = manga.isNsfw var isNsfw = manga.isNsfw
@ -115,7 +114,7 @@ internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaP
.asReversed().mapChapters { i, li -> .asReversed().mapChapters { i, li ->
val a = li.select("a") val a = li.select("a")
val href = a.attr("data-href").ifEmpty { val href = a.attr("data-href").ifEmpty {
parseFailed("Link is missing") li.parseFailed("Link is missing")
} }
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
@ -134,9 +133,9 @@ internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaP
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(getDomain()) val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
val doc = context.httpGet(fullUrl).parseHtml() val doc = context.httpGet(fullUrl).parseHtml()
val root = doc.body().select("div.item img.owl-lazy") ?: throw ParseException("Root not found") val root = doc.body().selectOrThrow("div.item img.owl-lazy")
return root.map { div -> return root.map { div ->
val url = div?.attrAsRelativeUrlOrNull("data-src") ?: parseFailed("Page image not found") val url = div?.attrAsRelativeUrlOrNull("data-src") ?: doc.parseFailed("Page image not found")
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,
@ -158,7 +157,7 @@ internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaP
val doc = context.httpGet("https://${getDomain()}/").parseHtml() val doc = context.httpGet("https://${getDomain()}/").parseHtml()
val root = doc.body().select("ul.dropdown-menu.multi-column.columns-3").select("li") val root = doc.body().select("ul.dropdown-menu.multi-column.columns-3").select("li")
return root.mapToSet { p -> return root.mapToSet { p ->
val a = p.selectFirst("a") ?: parseFailed("a is null") val a = p.selectFirstOrThrow("a")
MangaTag( MangaTag(
title = a.text().toTitleCase(), title = a.text().toTitleCase(),
key = a.attr("href"), key = a.attr("href"),

@ -4,7 +4,6 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.DateFormat import java.text.DateFormat
@ -55,8 +54,7 @@ internal class MangaTownParser(override val context: MangaLoaderContext) : Manga
} }
} }
val doc = context.httpGet(url).parseHtml() val doc = context.httpGet(url).parseHtml()
val root = doc.body().selectFirst("ul.manga_pic_list") val root = doc.body().selectFirstOrThrow("ul.manga_pic_list")
?: throw ParseException("Root not found")
return root.select("li").mapNotNull { li -> return root.select("li").mapNotNull { li ->
val a = li.selectFirst("a.manga_cover") val a = li.selectFirst("a.manga_cover")
val href = a?.attrAsRelativeUrlOrNull("href") val href = a?.attrAsRelativeUrlOrNull("href")
@ -96,8 +94,8 @@ internal class MangaTownParser(override val context: MangaLoaderContext) : Manga
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
val root = doc.body().selectFirst("section.main") val root = doc.body().selectFirstOrThrow("section.main")
?.selectFirst("div.article_content") ?: throw ParseException("Cannot find root") .selectFirstOrThrow("div.article_content")
val info = root.selectFirst("div.detail_info")?.selectFirst("ul") val info = root.selectFirst("div.detail_info")?.selectFirst("ul")
val chaptersList = root.selectFirst("div.chapter_content") val chaptersList = root.selectFirst("div.chapter_content")
?.selectFirst("ul.chapter_list")?.select("li")?.asReversed() ?.selectFirst("ul.chapter_list")?.select("li")?.asReversed()
@ -139,9 +137,8 @@ internal class MangaTownParser(override val context: MangaLoaderContext) : Manga
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(getDomain()) val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
val doc = context.httpGet(fullUrl).parseHtml() val doc = context.httpGet(fullUrl).parseHtml()
val root = doc.body().selectFirst("div.page_select") val root = doc.body().selectFirstOrThrow("div.page_select")
?: throw ParseException("Cannot find root") return root.selectFirstOrThrow("select").selectOrThrow("option").mapNotNull {
return root.selectFirst("select")?.select("option")?.mapNotNull {
val href = it.attrAsRelativeUrlOrNull("value") val href = it.attrAsRelativeUrlOrNull("value")
if (href == null || href.endsWith("featured.html")) { if (href == null || href.endsWith("featured.html")) {
return@mapNotNull null return@mapNotNull null
@ -153,12 +150,12 @@ internal class MangaTownParser(override val context: MangaLoaderContext) : Manga
referer = fullUrl, referer = fullUrl,
source = MangaSource.MANGATOWN, source = MangaSource.MANGATOWN,
) )
} ?: parseFailed("Pages list not found") }
} }
override suspend fun getPageUrl(page: MangaPage): String { override suspend fun getPageUrl(page: MangaPage): String {
val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain())).parseHtml()
return doc.getElementById("image")?.absUrl("src") ?: parseFailed("Image not found") return doc.requireElementById("image").attrAsAbsoluteUrl("src")
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
@ -166,7 +163,7 @@ internal class MangaTownParser(override val context: MangaLoaderContext) : Manga
val root = doc.body().selectFirst("aside.right") val root = doc.body().selectFirst("aside.right")
?.getElementsContainingOwnText("Genres") ?.getElementsContainingOwnText("Genres")
?.first() ?.first()
?.nextElementSibling() ?: parseFailed("Root not found") ?.nextElementSibling() ?: doc.parseFailed("Root not found")
return root.select("li").mapNotNullToSet { li -> return root.select("li").mapNotNullToSet { li ->
val a = li.selectFirst("a") ?: return@mapNotNullToSet null val a = li.selectFirst("a") ?: return@mapNotNullToSet null
val key = a.attr("href").parseTagKey() val key = a.attr("href").parseTagKey()

@ -66,8 +66,8 @@ class NHentaiParser(override val context: MangaLoaderContext) : PagedMangaParser
} }
} }
} }
val root = context.httpGet(url).parseHtml().body().getElementById("content") val root = context.httpGet(url).parseHtml().body().requireElementById("content")
?.selectLast("div.index-container") ?: parseFailed("Root not found") .selectLastOrThrow("div.index-container")
val regexBrackets = Regex("\\[[^]]+]|\\([^)]+\\)") val regexBrackets = Regex("\\[[^]]+]|\\([^)]+\\)")
val regexSpaces = Regex("\\s+") val regexSpaces = Regex("\\s+")
return root.select(".gallery").map { div -> return root.select(".gallery").map { div ->

@ -54,7 +54,7 @@ class NicovideoSeigaParser(override val context: MangaLoaderContext) :
else -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}" else -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}"
} }
val doc = context.httpGet(url).parseHtml() val doc = context.httpGet(url).parseHtml()
val comicList = doc.body().select("#comic_list > ul > li") ?: parseFailed("Container not found") val comicList = doc.body().select("#comic_list > ul > li") ?: doc.parseFailed("Container not found")
val items = comicList.select("div > .description > div > div") val items = comicList.select("div > .description > div > div")
return items.mapNotNull { item -> return items.mapNotNull { item ->
val href = val href =
@ -89,7 +89,7 @@ class NicovideoSeigaParser(override val context: MangaLoaderContext) :
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain("seiga"))).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain("seiga"))).parseHtml()
val contents = doc.body().selectFirst("#contents") ?: parseFailed("Cannot find root") val contents = doc.body().selectFirstOrThrow("#contents")
val statusText = contents val statusText = contents
.select("div.mg_work_detail > div > div:nth-child(2) > div.tip.content_status.status_series > span") .select("div.mg_work_detail > div > div:nth-child(2) > div.tip.content_status.status_series > span")
.text() .text()
@ -105,7 +105,7 @@ class NicovideoSeigaParser(override val context: MangaLoaderContext) :
isNsfw = contents.select(".icon_adult").isNotEmpty(), isNsfw = contents.select(".icon_adult").isNotEmpty(),
chapters = contents.select("#episode_list > ul > li").mapChapters { i, li -> chapters = contents.select("#episode_list > ul > li").mapChapters { i, li ->
val href = li.selectFirst("div > div.description > div.title > a") val href = li.selectFirst("div > div.description > div.title > a")
?.attrAsRelativeUrl("href") ?: parseFailed() ?.attrAsRelativeUrl("href") ?: li.parseFailed()
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
name = li.select("div > div.description > div.title > a").text(), name = li.select("div > div.description > div.title > a").text(),
@ -140,9 +140,9 @@ class NicovideoSeigaParser(override val context: MangaLoaderContext) :
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val doc = context.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml() val doc = context.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml()
val root = doc.body().select("#mg_category_list > ul > li") ?: parseFailed("Cannot find tags") val root = doc.body().selectOrThrow("#mg_category_list > ul > li")
return root.mapToSet { li -> return root.mapToSet { li ->
val a = li.selectFirst("a") ?: parseFailed("a is null") val a = li.selectFirstOrThrow("a")
MangaTag( MangaTag(
title = a.text(), title = a.text(),
key = a.attrAsRelativeUrlOrNull("href").orEmpty(), key = a.attrAsRelativeUrlOrNull("href").orEmpty(),
@ -157,7 +157,7 @@ class NicovideoSeigaParser(override val context: MangaLoaderContext) :
val root = doc.body().select(".search_result__item") val root = doc.body().select(".search_result__item")
return root.mapNotNull { item -> return root.mapNotNull { item ->
val href = item.selectFirst(".search_result__item__thumbnail > a") val href = item.selectFirst(".search_result__item__thumbnail > a")
?.attrAsRelativeUrl("href") ?: parseFailed() ?.attrAsRelativeUrl("href") ?: doc.parseFailed()
Manga( Manga(
id = generateUid(href), id = generateUid(href),
url = href, url = href,

@ -6,7 +6,6 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -66,11 +65,11 @@ internal abstract class NineMangaParser(
} }
val doc = context.httpGet(url, headers).parseHtml() val doc = context.httpGet(url, headers).parseHtml()
val root = doc.body().selectFirst("ul.direlist") val root = doc.body().selectFirst("ul.direlist")
?: throw ParseException("Cannot find root") ?: doc.parseFailed("Cannot find root")
val baseHost = root.baseUri().toHttpUrl().host val baseHost = root.baseUri().toHttpUrl().host
return root.select("li").map { node -> return root.select("li").map { node ->
val href = node.selectFirst("a")?.absUrl("href") val href = node.selectFirst("a")?.absUrl("href")
?: parseFailed("Link not found") ?: node.parseFailed("Link not found")
val relUrl = href.toRelativeUrl(baseHost) val relUrl = href.toRelativeUrl(baseHost)
val dd = node.selectFirst("dd") val dd = node.selectFirst("dd")
Manga( Manga(
@ -96,10 +95,8 @@ internal abstract class NineMangaParser(
manga.url.toAbsoluteUrl(getDomain()) + "?waring=1", manga.url.toAbsoluteUrl(getDomain()) + "?waring=1",
headers, headers,
).parseHtml() ).parseHtml()
val root = doc.body().selectFirst("div.manga") val root = doc.body().selectFirstOrThrow("div.manga")
?: throw ParseException("Cannot find root") val infoRoot = root.selectFirstOrThrow("div.bookintro")
val infoRoot = root.selectFirst("div.bookintro")
?: throw ParseException("Cannot find info")
return manga.copy( return manga.copy(
tags = infoRoot.getElementsByAttributeValue("itemprop", "genre").first() tags = infoRoot.getElementsByAttributeValue("itemprop", "genre").first()
?.select("a")?.mapToSet { a -> ?.select("a")?.mapToSet { a ->
@ -117,7 +114,7 @@ internal abstract class NineMangaParser(
?.asReversed()?.mapChapters { i, li -> ?.asReversed()?.mapChapters { i, li ->
val a = li.selectFirst("a.chapter_list_a") val a = li.selectFirst("a.chapter_list_a")
val href = a?.attrAsRelativeUrlOrNull("href") val href = a?.attrAsRelativeUrlOrNull("href")
?.replace("%20", " ") ?: parseFailed("Link not found") ?.replace("%20", " ") ?: li.parseFailed("Link not found")
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
name = a.text(), name = a.text(),
@ -143,14 +140,14 @@ internal abstract class NineMangaParser(
preview = null, preview = null,
source = source, source = source,
) )
} ?: throw ParseException("Pages list not found at ${chapter.url}") } ?: doc.parseFailed("Pages list not found")
} }
override suspend fun getPageUrl(page: MangaPage): String { override suspend fun getPageUrl(page: MangaPage): String {
val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain()), headers).parseHtml() val doc = context.httpGet(page.url.toAbsoluteUrl(getDomain()), headers).parseHtml()
val root = doc.body() val root = doc.body()
return root.selectFirst("a.pic_download")?.absUrl("href") return root.selectFirst("a.pic_download")?.absUrl("href")
?: throw ParseException("Page image not found") ?: doc.parseFailed("Page image not found")
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
@ -165,7 +162,7 @@ internal abstract class NineMangaParser(
key = cateId, key = cateId,
source = source, source = source,
) )
} ?: parseFailed("Root not found") } ?: doc.parseFailed("Root not found")
} }
private fun parseStatus(status: String) = when { private fun parseStatus(status: String) = when {

@ -6,7 +6,6 @@ import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -59,12 +58,13 @@ internal class NudeMoonParser(
postfix = "&rowstart=$offset", postfix = "&rowstart=$offset",
transform = { it.key.urlEncoded() }, transform = { it.key.urlEncoded() },
) )
else -> "https://$domain/all_manga?${getSortKey(sortOrder)}&rowstart=$offset" else -> "https://$domain/all_manga?${getSortKey(sortOrder)}&rowstart=$offset"
} }
val doc = context.httpGet(url).parseHtml() val doc = context.httpGet(url).parseHtml()
val root = doc.body().run { val root = doc.body().run {
selectFirst("td.main-bg") ?: selectFirst("td.main-body") selectFirst("td.main-bg") ?: selectFirst("td.main-body")
} ?: parseFailed("Cannot find root") } ?: doc.parseFailed("Cannot find root")
return root.select("table.news_pic2").mapNotNull { row -> return root.select("table.news_pic2").mapNotNull { row ->
val a = row.selectFirst("td.bg_style1")?.selectFirst("a") val a = row.selectFirst("td.bg_style1")?.selectFirst("a")
?: return@mapNotNull null ?: return@mapNotNull null
@ -102,7 +102,7 @@ internal class NudeMoonParser(
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val body = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml().body() val body = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml().body()
val root = body.selectFirst("table.shoutbox") val root = body.selectFirst("table.shoutbox")
?: parseFailed("Cannot find root") ?: body.parseFailed("Cannot find root")
val info = root.select("div.tbl2") val info = root.select("div.tbl2")
val lastInfo = info.last() val lastInfo = info.last()
return manga.copy( return manga.copy(
@ -143,7 +143,7 @@ internal class NudeMoonParser(
val script = doc.select("script").firstNotNullOfOrNull { val script = doc.select("script").firstNotNullOfOrNull {
it.html().takeIf { x -> x.contains(" images = new ") } it.html().takeIf { x -> x.contains(" images = new ") }
} ?: if (isAuthorized) { } ?: if (isAuthorized) {
parseFailed("Cannot find pages list") doc.parseFailed("Cannot find pages list")
} else { } else {
throw AuthRequiredException(source) throw AuthRequiredException(source)
} }
@ -174,7 +174,7 @@ internal class NudeMoonParser(
val root = doc.body().getElementsContainingOwnText("Поиск манги по тегам") val root = doc.body().getElementsContainingOwnText("Поиск манги по тегам")
.firstOrNull()?.parents()?.find { it.tag().normalName() == "tbody" } .firstOrNull()?.parents()?.find { it.tag().normalName() == "tbody" }
?.selectFirst("td.textbox")?.selectFirst("td.small") ?.selectFirst("td.textbox")?.selectFirst("td.small")
?: parseFailed("Tags root not found") ?: doc.parseFailed("Tags root not found")
return root.select("a").mapToSet { return root.select("a").mapToSet {
MangaTag( MangaTag(
title = it.text().toTitleCase(), title = it.text().toTitleCase(),
@ -197,7 +197,7 @@ internal class NudeMoonParser(
throw if (body.selectFirst("form[name=\"loginform\"]") != null) { throw if (body.selectFirst("form[name=\"loginform\"]") != null) {
AuthRequiredException(source) AuthRequiredException(source)
} else { } else {
ParseException("Cannot find username") body.parseFailed("Cannot find username")
} }
} }
} }

@ -11,6 +11,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
@ -113,7 +114,7 @@ internal class RemangaParser(
copyCookies() copyCookies()
val domain = getDomain() val domain = getDomain()
val slug = manga.url.find(regexLastUrlPath) val slug = manga.url.find(regexLastUrlPath)
?: throw ParseException("Cannot obtain slug from ${manga.url}") ?: throw ParseException("Cannot obtain slug from ${manga.url}", manga.publicUrl)
val data = context.httpGet( val data = context.httpGet(
url = "https://api.$domain/api/titles/$slug/", url = "https://api.$domain/api/titles/$slug/",
headers = getApiHeaders(), headers = getApiHeaders(),
@ -121,10 +122,10 @@ internal class RemangaParser(
val content = try { val content = try {
data.getJSONObject("content") data.getJSONObject("content")
} catch (e: JSONException) { } catch (e: JSONException) {
throw ParseException(data.optString("msg"), e) throw ParseException(data.optString("msg"), manga.publicUrl, e)
} }
val branchId = content.getJSONArray("branches").optJSONObject(0) val branchId = content.getJSONArray("branches").optJSONObject(0)
?.getLong("id") ?: throw ParseException("No branches found") ?.getLong("id") ?: throw ParseException("No branches found", manga.publicUrl)
val chapters = grabChapters(domain, branchId) val chapters = grabChapters(domain, branchId)
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US) val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
return manga.copy( return manga.copy(
@ -182,9 +183,9 @@ internal class RemangaParser(
} }
if (pubDate != null && pubDate > System.currentTimeMillis()) { if (pubDate != null && pubDate > System.currentTimeMillis()) {
val at = SimpleDateFormat.getDateInstance(DateFormat.LONG).format(Date(pubDate)) val at = SimpleDateFormat.getDateInstance(DateFormat.LONG).format(Date(pubDate))
parseFailed("Глава станет доступной $at") throw ContentUnavailableException("Глава станет доступной $at")
} else { } else {
parseFailed("Глава недоступна") throw ContentUnavailableException("Глава недоступна")
} }
} }
val result = ArrayList<MangaPage>(pages.length()) val result = ArrayList<MangaPage>(pages.length())
@ -192,7 +193,7 @@ internal class RemangaParser(
when (val item = pages.get(i)) { when (val item = pages.get(i)) {
is JSONObject -> result += parsePage(item, referer) is JSONObject -> result += parsePage(item, referer)
is JSONArray -> item.mapJSONTo(result) { parsePage(it, referer) } is JSONArray -> item.mapJSONTo(result) { parsePage(it, referer) }
else -> throw ParseException("Unknown json item $item") else -> throw ParseException("Unknown json item $item", chapter.url)
} }
} }
return result return result

@ -7,6 +7,7 @@ import org.json.JSONArray
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.json.mapJSON
@ -79,13 +80,13 @@ internal abstract class GroupleParser(
else -> advancedSearch(domain, tags) else -> advancedSearch(domain, tags)
}.parseHtml().body() }.parseHtml().body()
val root = (doc.getElementById("mangaBox") ?: doc.getElementById("mangaResults")) val root = (doc.getElementById("mangaBox") ?: doc.getElementById("mangaResults"))
?: parseFailed("Cannot find root") ?: doc.parseFailed("Cannot find root")
val tiles = root.selectFirst("div.tiles.row") ?: if ( val tiles = root.selectFirst("div.tiles.row") ?: if (
root.select(".alert").any { it.ownText() == NOTHING_FOUND } root.select(".alert").any { it.ownText() == NOTHING_FOUND }
) { ) {
return emptyList() return emptyList()
} else { } else {
parseFailed("No tiles found") doc.parseFailed("No tiles found")
} }
val baseHost = root.baseUri().toHttpUrl().host val baseHost = root.baseUri().toHttpUrl().host
return tiles.select("div.tile").mapNotNull { node -> return tiles.select("div.tile").mapNotNull { node ->
@ -142,7 +143,7 @@ internal abstract class GroupleParser(
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain()), headers).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain()), headers).parseHtml()
val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent") val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent")
?: parseFailed("Cannot find root") ?: doc.parseFailed("Cannot find root")
val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US) val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US)
val coverImg = root.selectFirst("div.subject-cover")?.selectFirst("img") val coverImg = root.selectFirst("div.subject-cover")?.selectFirst("img")
return manga.copy( return manga.copy(
@ -218,7 +219,7 @@ internal abstract class GroupleParser(
) )
} }
} }
parseFailed("Pages list not found at ${chapter.url}") doc.parseFailed("Pages list not found at ${chapter.url}")
} }
override suspend fun getPageUrl(page: MangaPage): String { override suspend fun getPageUrl(page: MangaPage): String {
@ -232,14 +233,14 @@ internal abstract class GroupleParser(
return url return url
} }
} }
val fallbackServer = servers.firstOrNull() ?: parseFailed("Cannot find any page url") val fallbackServer = servers.firstOrNull() ?: throw ParseException("Cannot find any page url", page.url)
return fallbackServer + path return fallbackServer + path
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val doc = context.httpGet("https://${getDomain()}/list/genres/sort_name", headers).parseHtml() val doc = context.httpGet("https://${getDomain()}/list/genres/sort_name", headers).parseHtml()
val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent") val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent")
?.selectFirst("table.table") ?: parseFailed("Cannot find root") ?.selectFirst("table.table") ?: doc.parseFailed("Cannot find root")
return root.select("a.element-link").mapToSet { a -> return root.select("a.element-link").mapToSet { a ->
MangaTag( MangaTag(
title = a.text().toTitleCase(), title = a.text().toTitleCase(),
@ -254,7 +255,7 @@ internal abstract class GroupleParser(
val element = root.selectFirst("img.user-avatar") ?: throw AuthRequiredException(source) val element = root.selectFirst("img.user-avatar") ?: throw AuthRequiredException(source)
val res = element.parent()?.text() val res = element.parent()?.text()
return if (res.isNullOrEmpty()) { return if (res.isNullOrEmpty()) {
parseFailed("Cannot find username") root.parseFailed("Cannot find username")
} else res } else res
} }
@ -273,21 +274,21 @@ internal abstract class GroupleParser(
val tagsIndex = context.httpGet(url, headers).parseHtml() val tagsIndex = context.httpGet(url, headers).parseHtml()
.body().selectFirst("form.search-form") .body().selectFirst("form.search-form")
?.select("div.form-group") ?.select("div.form-group")
?.get(1) ?: parseFailed("Genres filter element not found") ?.get(1) ?: throw ParseException("Genres filter element not found", url)
val tagNames = tags.map { it.title.lowercase() } val tagNames = tags.map { it.title.lowercase() }
val payload = HashMap<String, String>() val payload = HashMap<String, String>()
var foundGenres = 0 var foundGenres = 0
tagsIndex.select("li.property").forEach { li -> tagsIndex.select("li.property").forEach { li ->
val name = li.text().trim().lowercase() val name = li.text().trim().lowercase()
val id = li.selectFirst("input")?.id() val id = li.selectFirst("input")?.id()
?: parseFailed("Id for tag $name not found") ?: li.parseFailed("Id for tag $name not found")
payload[id] = if (name in tagNames) { payload[id] = if (name in tagNames) {
foundGenres++ foundGenres++
"in" "in"
} else "" } else ""
} }
if (foundGenres != tags.size) { if (foundGenres != tags.size) {
parseFailed("Some genres are not found") tagsIndex.parseFailed("Some genres are not found")
} }
// Step 2: advanced search // Step 2: advanced search
payload["q"] = "" payload["q"] = ""

@ -89,7 +89,7 @@ abstract class Madara5Parser @InternalParsersApi constructor(
?.getElementsByAttributeValueContaining("href", tagPrefix) ?.getElementsByAttributeValueContaining("href", tagPrefix)
?.mapToSet { a -> a.asMangaTag() } ?: manga.tags ?.mapToSet { a -> a.asMangaTag() } ?: manga.tags
val mangaId = root.getElementById("manga-chapters-holder")?.attr("data-id")?.toLongOrNull() val mangaId = root.getElementById("manga-chapters-holder")?.attr("data-id")?.toLongOrNull()
?: parseFailed("Cannot find mangaId") ?: root.parseFailed("Cannot find mangaId")
return manga.copy( return manga.copy(
description = (root.selectFirst(".detail-content") description = (root.selectFirst(".detail-content")
?: root.selectFirstOrThrow(".description-summary")).html(), ?: root.selectFirstOrThrow(".description-summary")).html(),
@ -106,7 +106,7 @@ abstract class Madara5Parser @InternalParsersApi constructor(
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(getDomain()) val fullUrl = chapter.url.toAbsoluteUrl(getDomain())
val doc = context.httpGet(fullUrl).parseHtml() val doc = context.httpGet(fullUrl).parseHtml()
val arrayData = doc.getElementById("arraydata") ?: parseFailed("#arraydata not found") val arrayData = doc.getElementById("arraydata") ?: doc.parseFailed("#arraydata not found")
return arrayData.html().split(',').map { url -> return arrayData.html().split(',').map { url ->
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),

@ -55,7 +55,7 @@ internal abstract class MadaraParser(
).parseHtml() ).parseHtml()
return doc.select("div.row.c-tabs-item__content").map { div -> return doc.select("div.row.c-tabs-item__content").map { div ->
val href = div.selectFirst("a")?.attrAsRelativeUrlOrNull("href") val href = div.selectFirst("a")?.attrAsRelativeUrlOrNull("href")
?: parseFailed("Link not found") ?: div.parseFailed("Link not found")
val summary = div.selectFirst(".tab-summary") val summary = div.selectFirst(".tab-summary")
Manga( Manga(
id = generateUid(href), id = generateUid(href),
@ -94,7 +94,7 @@ internal abstract class MadaraParser(
val root1 = body.selectFirst("header")?.selectFirst("ul.second-menu") val root1 = body.selectFirst("header")?.selectFirst("ul.second-menu")
val root2 = body.selectFirst("div.genres_wrap")?.selectFirst("ul.list-unstyled") val root2 = body.selectFirst("div.genres_wrap")?.selectFirst("ul.list-unstyled")
if (root1 == null && root2 == null) { if (root1 == null && root2 == null) {
parseFailed("Root not found") doc.parseFailed("Root not found")
} }
val list = root1?.select("li").orEmpty() + root2?.select("li").orEmpty() val list = root1?.select("li").orEmpty() + root2?.select("li").orEmpty()
val keySet = HashSet<String>(list.size) val keySet = HashSet<String>(list.size)
@ -121,10 +121,10 @@ internal abstract class MadaraParser(
val root = doc.body().selectFirst("div.profile-manga") val root = doc.body().selectFirst("div.profile-manga")
?.selectFirst("div.summary_content") ?.selectFirst("div.summary_content")
?.selectFirst("div.post-content") ?.selectFirst("div.post-content")
?: throw ParseException("Root not found") ?: throw ParseException("Root not found", fullUrl)
val root2 = doc.body().selectFirst("div.content-area") val root2 = doc.body().selectFirst("div.content-area")
?.selectFirst("div.c-page") ?.selectFirst("div.c-page")
?: throw ParseException("Root2 not found") ?: throw ParseException("Root2 not found", fullUrl)
val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US) val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
return manga.copy( return manga.copy(
tags = root.selectFirst("div.genres-content")?.select("a") tags = root.selectFirst("div.genres-content")?.select("a")
@ -142,7 +142,7 @@ internal abstract class MadaraParser(
?.joinToString { it.html() }, ?.joinToString { it.html() },
chapters = root2.select("li").asReversed().mapChapters { i, li -> chapters = root2.select("li").asReversed().mapChapters { i, li ->
val a = li.selectFirst("a") val a = li.selectFirst("a")
val href = a?.attrAsRelativeUrlOrNull("href") ?: parseFailed("Link is missing") val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
name = a.ownText(), name = a.ownText(),
@ -165,10 +165,10 @@ internal abstract class MadaraParser(
val doc = context.httpGet(fullUrl).parseHtml() val doc = context.httpGet(fullUrl).parseHtml()
val root = doc.body().selectFirst("div.main-col-inner") val root = doc.body().selectFirst("div.main-col-inner")
?.selectFirst("div.reading-content") ?.selectFirst("div.reading-content")
?: throw ParseException("Root not found") ?: throw ParseException("Root not found", fullUrl)
return root.select("div.page-break").map { div -> return root.select("div.page-break").map { div ->
val img = div.selectFirst("img") ?: parseFailed("Page image not found") val img = div.selectFirst("img") ?: div.parseFailed("Page image not found")
val url = img.src()?.toRelativeUrl(getDomain()) ?: parseFailed("Image src not found") val url = img.src()?.toRelativeUrl(getDomain()) ?: div.parseFailed("Image src not found")
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.parsers.site.multichan
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -48,7 +47,7 @@ internal abstract class ChanParser(source: MangaSource) : MangaParser(source), M
} }
val doc = context.httpGet(url).parseHtml() val doc = context.httpGet(url).parseHtml()
val root = doc.body().selectFirst("div.main_fon")?.getElementById("content") val root = doc.body().selectFirst("div.main_fon")?.getElementById("content")
?: parseFailed("Cannot find root") ?: doc.parseFailed("Cannot find root")
return root.select("div.content_row").mapNotNull { row -> return root.select("div.content_row").mapNotNull { row ->
val a = row.selectFirst("div.manga_row1")?.selectFirst("h2")?.selectFirst("a") val a = row.selectFirst("div.manga_row1")?.selectFirst("h2")?.selectFirst("a")
?: return@mapNotNull null ?: return@mapNotNull null
@ -84,8 +83,7 @@ internal abstract class ChanParser(source: MangaSource) : MangaParser(source), M
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
val root = val root = doc.body().getElementById("dle-content") ?: doc.parseFailed("Cannot find root")
doc.body().getElementById("dle-content") ?: parseFailed("Cannot find root")
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
return manga.copy( return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"), description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
@ -135,16 +133,16 @@ internal abstract class ChanParser(source: MangaSource) : MangaParser(source), M
) )
} }
} }
parseFailed("Pages list not found at ${chapter.url}") doc.parseFailed("Pages list not found at ${chapter.url}")
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val domain = getDomain() val domain = getDomain()
val doc = context.httpGet("https://$domain/mostfavorites&sort=manga").parseHtml() val doc = context.httpGet("https://$domain/mostfavorites&sort=manga").parseHtml()
val root = doc.body().selectFirst("div.main_fon")?.getElementById("side") val root = doc.body().selectFirst("div.main_fon")?.getElementById("side")
?.select("ul")?.last() ?: throw ParseException("Cannot find root") ?.select("ul")?.last() ?: doc.parseFailed("Cannot find root")
return root.select("li.sidetag").mapToSet { li -> return root.select("li.sidetag").mapToSet { li ->
val a = li.children().last() ?: throw ParseException("a is null") val a = li.children().lastOrNull() ?: li.parseFailed("a is null")
MangaTag( MangaTag(
title = a.text().toTagName(), title = a.text().toTagName(),
key = a.attr("href").substringAfterLast('/'), key = a.attr("href").substringAfterLast('/'),

@ -3,12 +3,8 @@ package org.koitharu.kotatsu.parsers.site.multichan
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.toTitleCase
@MangaSourceParser("HENCHAN", "Хентай-тян", "ru") @MangaSourceParser("HENCHAN", "Хентай-тян", "ru")
internal class HenChanParser(override val context: MangaLoaderContext) : ChanParser(MangaSource.HENCHAN) { internal class HenChanParser(override val context: MangaLoaderContext) : ChanParser(MangaSource.HENCHAN) {
@ -34,13 +30,13 @@ internal class HenChanParser(override val context: MangaLoaderContext) : ChanPar
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
val root = doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root") val root = doc.body().requireElementById("dle-content")
val readLink = manga.url.replace("manga", "online") val readLink = manga.url.replace("manga", "online")
return manga.copy( return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"), description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.absUrl("src"), largeCoverUrl = root.getElementById("cover")?.absUrl("src"),
tags = root.selectFirst("div.sidetags")?.select("li.sidetag")?.mapToSet { tags = root.selectFirst("div.sidetags")?.select("li.sidetag")?.mapToSet {
val a = it.children().last() ?: parseFailed("Invalid tag") val a = it.children().last() ?: doc.parseFailed("Invalid tag")
MangaTag( MangaTag(
title = a.text().toTitleCase(), title = a.text().toTitleCase(),
key = a.attr("href").substringAfterLast('/'), key = a.attr("href").substringAfterLast('/'),

@ -3,14 +3,10 @@ package org.koitharu.kotatsu.parsers.site.multichan
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.mapChapters
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
@MangaSourceParser("YAOICHAN", "Яой-тян", "ru") @MangaSourceParser("YAOICHAN", "Яой-тян", "ru")
internal class YaoiChanParser(override val context: MangaLoaderContext) : ChanParser(MangaSource.YAOICHAN) { internal class YaoiChanParser(override val context: MangaLoaderContext) : ChanParser(MangaSource.YAOICHAN) {
@ -19,8 +15,7 @@ internal class YaoiChanParser(override val context: MangaLoaderContext) : ChanPa
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml() val doc = context.httpGet(manga.url.toAbsoluteUrl(getDomain())).parseHtml()
val root = val root = doc.body().requireElementById("dle-content")
doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root")
return manga.copy( return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"), description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.absUrl("src"), largeCoverUrl = root.getElementById("cover")?.absUrl("src"),

@ -59,7 +59,7 @@ internal open class MangaLibParser(
} }
} }
val doc = context.httpGet(url).parseHtml() val doc = context.httpGet(url).parseHtml()
val root = doc.body().getElementById("manga-list") ?: throw ParseException("Root not found") val root = doc.body().getElementById("manga-list") ?: doc.parseFailed("Root not found")
val items = root.selectFirst("div.media-cards-grid")?.select("div.media-card-wrap") val items = root.selectFirst("div.media-cards-grid")?.select("div.media-card-wrap")
?: return emptyList() ?: return emptyList()
return items.mapNotNull { card -> return items.mapNotNull { card ->
@ -85,7 +85,7 @@ internal open class MangaLibParser(
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val fullUrl = manga.url.toAbsoluteUrl(getDomain()) val fullUrl = manga.url.toAbsoluteUrl(getDomain())
val doc = context.httpGet("$fullUrl?section=info").parseHtml() val doc = context.httpGet("$fullUrl?section=info").parseHtml()
val root = doc.body().getElementById("main-page") ?: throw ParseException("Root not found") val root = doc.body().getElementById("main-page") ?: throw ParseException("Root not found", fullUrl)
val title = root.selectFirst("div.media-header__wrap")?.children() val title = root.selectFirst("div.media-header__wrap")?.children()
val info = root.selectFirst("div.media-content") val info = root.selectFirst("div.media-content")
val chaptersDoc = context.httpGet("$fullUrl?section=chapters").parseHtml() val chaptersDoc = context.httpGet("$fullUrl?section=chapters").parseHtml()
@ -169,7 +169,7 @@ internal open class MangaLibParser(
throw AuthRequiredException(source) throw AuthRequiredException(source)
} }
val scripts = doc.head().select("script") val scripts = doc.head().select("script")
val pg = (doc.body().getElementById("pg")?.html() ?: parseFailed("Element #pg not found")) val pg = (doc.body().getElementById("pg")?.html() ?: doc.parseFailed("Element #pg not found"))
.substringAfter('=') .substringAfter('=')
.substringBeforeLast(';') .substringBeforeLast(';')
val pages = JSONArray(pg) val pages = JSONArray(pg)
@ -199,7 +199,7 @@ internal open class MangaLibParser(
} }
} }
} }
throw ParseException("Script with info not found") throw ParseException("Script with info not found", fullUrl)
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
@ -222,7 +222,7 @@ internal open class MangaLibParser(
return result return result
} }
} }
throw ParseException("Script with genres not found") throw ParseException("Script with genres not found", url)
} }
override val isAuthorized: Boolean override val isAuthorized: Boolean
@ -237,13 +237,13 @@ internal open class MangaLibParser(
if (body.baseUri().endsWith("/login")) { if (body.baseUri().endsWith("/login")) {
throw AuthRequiredException(source) throw AuthRequiredException(source)
} }
return body.selectFirst(".profile-user__username")?.text() ?: parseFailed("Cannot find username") return body.selectFirst(".profile-user__username")?.text() ?: body.parseFailed("Cannot find username")
} }
protected open fun isNsfw(doc: Document): Boolean { protected open fun isNsfw(doc: Document): Boolean {
val sidebar = doc.body().run { val sidebar = doc.body().run {
selectFirst(".media-sidebar") ?: selectFirst(".media-info") selectFirst(".media-sidebar") ?: selectFirst(".media-info")
} ?: parseFailed("Sidebar not found") } ?: doc.parseFailed("Sidebar not found")
return sidebar.getElementsContainingOwnText("18+").isNotEmpty() return sidebar.getElementsContainingOwnText("18+").isNotEmpty()
} }

@ -4,6 +4,7 @@ package org.koitharu.kotatsu.parsers.util
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import org.jsoup.select.Selector import org.jsoup.select.Selector
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
@ -90,13 +91,21 @@ fun Element.styleValueOrNull(property: String): String? {
return css.substringAfter(':').removeSuffix(';').trim() return css.substringAfter(':').removeSuffix(';').trim()
} }
@Deprecated("Now implemented in Jsoup", ReplaceWith("expectFirst(cssQuery)")) /**
* Like a `expectFirst` but with detailed error message
*/
fun Element.selectFirstOrThrow(cssQuery: String): Element { fun Element.selectFirstOrThrow(cssQuery: String): Element {
return Selector.selectFirst(cssQuery, this) ?: throw ParseException("Cannot find \"$cssQuery\"") return Selector.selectFirst(cssQuery, this) ?: throw ParseException("Cannot find \"$cssQuery\"", baseUri())
}
fun Element.selectOrThrow(cssQuery: String): Elements {
return Selector.select(cssQuery, this).ifEmpty {
throw ParseException("Empty result for \"$cssQuery\"", baseUri())
}
} }
fun Element.requireElementById(id: String): Element { fun Element.requireElementById(id: String): Element {
return getElementById(id) ?: throw ParseException("Cannot find \"#$id\"") return getElementById(id) ?: throw ParseException("Cannot find \"#$id\"", baseUri())
} }
fun Element.selectLast(cssQuery: String): Element? { fun Element.selectLast(cssQuery: String): Element? {
@ -104,5 +113,5 @@ fun Element.selectLast(cssQuery: String): Element? {
} }
fun Element.selectLastOrThrow(cssQuery: String): Element { fun Element.selectLastOrThrow(cssQuery: String): Element {
return selectLast(cssQuery) ?: throw ParseException("Cannot find \"$cssQuery\"") return selectLast(cssQuery) ?: throw ParseException("Cannot find \"$cssQuery\"", baseUri())
} }
Loading…
Cancel
Save