Merge pull request #355 from KotatsuApp/GalleryAdultsParser

Add GalleryAdultsParser
pull/365/head
Koitharu 3 years ago committed by GitHub
commit face1d5b26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,187 +0,0 @@
package org.koitharu.kotatsu.parsers.site.all
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.text.SimpleDateFormat
import java.util.*
@MangaSourceParser("NHENTAI", "N-Hentai", type = ContentType.HENTAI)
class NHentaiParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.NHENTAI, pageSize = 25) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("nhentai.net")
override val sortOrders: Set<SortOrder>
get() = EnumSet.of(SortOrder.NEWEST, SortOrder.POPULARITY)
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
val domain = domain
val url = buildString {
append("https://")
append(domain)
if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
append("&page=")
append(page)
if (sortOrder == SortOrder.POPULARITY) {
append("&sort=popular")
}
} else {
append('/')
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
append("tag/")
append(tag.key)
append('/')
if (sortOrder == SortOrder.POPULARITY) {
append("popular")
}
append("?page=")
append(page)
} else {
if (sortOrder == SortOrder.POPULARITY) {
append("?sort=popular&page=")
} else {
append("?page=")
}
append(page)
}
}
}
val root = webClient.httpGet(url).parseHtml().body().requireElementById("content")
.selectLastOrThrow("div.index-container")
val regexBrackets = Regex("\\[[^]]+]|\\([^)]+\\)")
val regexSpaces = Regex("\\s+")
return root.select(".gallery").map { div ->
val a = div.selectFirstOrThrow("a.cover")
val href = a.attrAsRelativeUrl("href")
val img = div.selectFirstOrThrow("img")
val title = div.selectFirstOrThrow(".caption").text()
Manga(
id = generateUid(href),
title = title.replace(regexBrackets, "")
.replace(regexSpaces, " ")
.trim(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = true,
coverUrl = img.attrAsAbsoluteUrlOrNull("data-src")
?: img.attrAsAbsoluteUrl("src"),
tags = setOf(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
chapters = listOf(),
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val root = webClient.httpGet(
url = manga.url.toAbsoluteUrl(domain),
).parseHtml().body().requireElementById("bigcontainer")
val img = root.requireElementById("cover").selectFirstOrThrow("img")
val tagContainers = root.requireElementById("tags").select(".tag-container")
val dateFormat = SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSSSSS'+00:00'",
Locale.ROOT,
)
return manga.copy(
tags = tagContainers.find { x -> x.ownText() == "Tags:" }?.parseTags() ?: manga.tags,
author = tagContainers.find { x -> x.ownText() == "Artists:" }
?.selectFirst("span.name")?.text()?.toCamelCase(),
largeCoverUrl = img.attrAsAbsoluteUrlOrNull("data-src")
?: img.attrAsAbsoluteUrl("src"),
description = null,
chapters = listOf(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
url = manga.url,
scanlator = null,
uploadDate = dateFormat.tryParse(
tagContainers.find { x -> x.ownText() == "Uploaded:" }
?.selectFirst("time")
?.attr("datetime"),
),
branch = null,
source = source,
),
),
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val url = chapter.url.toAbsoluteUrl(domain)
val root = webClient.httpGet(url).parseHtml().requireElementById("thumbnail-container")
return root.select(".thumb-container").map { div ->
val a = div.selectFirstOrThrow("a")
val img = div.selectFirstOrThrow("img")
val href = a.attrAsRelativeUrl("href")
MangaPage(
id = generateUid(href),
url = href,
preview = img.attrAsAbsoluteUrlOrNull("data-src")
?: img.attrAsAbsoluteUrl("src"),
source = source,
)
}
}
override suspend fun getPageUrl(page: MangaPage): String {
val root = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml().body()
.requireElementById("image-container")
return root.selectFirstOrThrow("img").attrAsAbsoluteUrl("src")
}
override suspend fun getTags(): Set<MangaTag> {
return coroutineScope {
// parse first 3 pages of tags
(1..3).map { page ->
async { getTags(page) }
}
}.awaitAll().flattenTo(ArraySet(360))
}
private suspend fun getTags(page: Int): Set<MangaTag> {
val root = webClient.httpGet("https://${domain}/tags/popular?page=$page").parseHtml().body()
.getElementById("tag-container")
return root?.parseTags().orEmpty()
}
private fun Element.parseTags() = select("a.tag").mapToSet { a ->
val href = a.attr("href").removeSuffix('/')
MangaTag(
title = a.selectFirstOrThrow(".name").text().toTitleCase(),
key = href.substringAfterLast('/'),
source = source,
)
}
private fun buildQuery(tags: Collection<MangaTag>) = tags.joinToString(separator = " ") { tag ->
"tag:\"${tag.key}\""
}
}

@ -1,23 +1,27 @@
package org.koitharu.kotatsu.parsers.site.all
package org.koitharu.kotatsu.parsers.site.galleryadults
import androidx.collection.ArraySet
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import java.util.*
@MangaSourceParser("HENTAIFOX", "HentaiFox", type = ContentType.HENTAI)
internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.HENTAIFOX, 20) {
internal abstract class GalleryAdultsParser(
context: MangaLoaderContext,
source: MangaSource,
domain: String,
pageSize: Int = 20,
) : PagedMangaParser(context, source, pageSize) {
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
override val configKeyDomain = ConfigKey.Domain("hentaifox.com")
override val configKeyDomain = ConfigKey.Domain(domain)
override suspend fun getListPage(
page: Int,
@ -26,49 +30,48 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/tag/")
append(tag?.key.orEmpty())
if (page > 1) {
append("/pag/")
append(page)
append("/")
}
append("/?")
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
if (page > 1) {
append("&page=")
append(page)
}
append("&")
} else {
if (page > 2) {
append("/pag/")
append(page)
append("/")
} else if (page > 1) {
append("/page/")
append(page)
append("/")
}
append("/?")
}
append("page=")
append(page)
}
val doc = webClient.httpGet(url).parseHtml()
return doc.select(".lc_galleries .thumb").map { div ->
val href = div.selectFirstOrThrow(".inner_thumb a").attrAsRelativeUrl("href")
return parseMangaList(webClient.httpGet(url).parseHtml())
}
protected open val selectGallery = ".preview_item"
protected open val selectGalleryLink = ".inner_thumb a"
protected open val selectGalleryImg = ".inner_thumb img"
protected open val selectGalleryTitle = "h2"
protected open fun parseMangaList(doc: Document): List<Manga> {
val regexBrackets = Regex("\\[[^]]+]|\\([^)]+\\)")
val regexSpaces = Regex("\\s+")
return doc.select(selectGallery).map { div ->
val href = div.selectFirstOrThrow(selectGalleryLink).attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
title = div.select("h2.g_title").text(),
title = div.select(selectGalleryTitle).text().replace(regexBrackets, "")
.replace(regexSpaces, " ")
.trim(),
altTitle = null,
url = href,
publicUrl = href.toAbsoluteUrl(domain),
rating = RATING_UNKNOWN,
isNsfw = isNsfwSource,
coverUrl = div.selectFirstOrThrow("img").src().orEmpty(),
coverUrl = div.selectFirstOrThrow(selectGalleryImg).src().orEmpty(),
tags = emptySet(),
state = null,
author = null,
@ -87,37 +90,38 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context
}.awaitAll().flattenTo(ArraySet(360))
}
protected open val pathTagUrl = "/tags/popular/pag/"
protected open val selectTags = ".tags_page ul.tags li"
private suspend fun getTags(page: Int): Set<MangaTag> {
val url = "https://$domain/tags/popular/pag/$page/"
val root = webClient.httpGet(url).parseHtml()
val url = "https://$domain$pathTagUrl$page"
val root = webClient.httpGet(url).parseHtml().selectFirstOrThrow(selectTags)
return root.parseTags()
}
private fun Element.parseTags() = select(".list_tags a.tag_btn").mapToSet {
protected open fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
val name = it.selectFirst(".item_name")?.text() ?: it.text()
MangaTag(
key = key,
title = it.selectFirstOrThrow("h3").text(),
title = name,
source = source,
)
}
protected open val selectTag = "div.tags:contains(Tags:) .tag_list"
protected open val selectAuthor = "ul.artists a.tag_btn"
protected open val urlReplaceBefore = "/g/"
protected open val urlReplaceAfter = "/gallery/"
protected open val selectLanguageChapter = "div.tags:contains(Languages:) .tag_list a span.tag"
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val urlChapters = manga.url.replace("/gallery/", "/g/") + "1/"
val urlChapters = manga.url.replace(urlReplaceBefore, urlReplaceAfter) + "1/"
val tag = doc.selectFirstOrThrow(selectTag)
return manga.copy(
altTitle = null,
tags = doc.select("ul.tags a.tag_btn ").mapNotNullToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
MangaTag(
key = key,
title = it.html().substringBefore("<span"),
source = source,
)
},
author = doc.selectFirst("ul.artists a.tag_btn")?.html()?.substringBefore("<span"),
description = null,
tags = tag.parseTags(),
author = doc.selectFirst(selectAuthor)?.html()?.substringBefore("<span"),
chapters = listOf(
MangaChapter(
id = manga.id,
@ -126,7 +130,7 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context
url = urlChapters,
scanlator = null,
uploadDate = 0,
branch = doc.selectFirstOrThrow("ul.languages a.tag_btn").html().substringBefore("<span"),
branch = doc.selectFirst(selectLanguageChapter)?.html()?.substringBefore("<"),
source = source,
),
),
@ -134,31 +138,14 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context
}
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
val doc = webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body().selectFirstOrThrow(".related_galleries")
return root.select("div.thumb").mapNotNull { div ->
val a = div.selectFirst(".inner_thumb a") ?: return@mapNotNull null
val href = a.attrAsRelativeUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.toAbsoluteUrl(a.host ?: domain),
altTitle = null,
title = div.selectFirstOrThrow("h2.g_title").text(),
author = null,
coverUrl = div.selectFirst("img")?.src().orEmpty(),
tags = emptySet(),
rating = RATING_UNKNOWN,
state = null,
isNsfw = isNsfwSource,
source = source,
)
}
return parseMangaList(webClient.httpGet(seed.url.toAbsoluteUrl(domain)).parseHtml())
}
protected open val selectTotalPage = ".total_pages"
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val totalPages = doc.selectFirstOrThrow(".total_pages").text().toInt()
val totalPages = doc.selectFirstOrThrow(selectTotalPage).text().toInt()
val rawUrl = chapter.url.replace("/1/", "/")
return (1..totalPages).map {
val url = "$rawUrl$it/"
@ -171,9 +158,11 @@ internal class HentaiFox(context: MangaLoaderContext) : PagedMangaParser(context
}
}
protected open val idImg = "gimg"
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.requireElementById("gimg").attrAsAbsoluteUrl("data-src")
return root.requireElementById(idImg).src() ?: root.parseFailed("Image src not found")
}
}

@ -0,0 +1,30 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.removeSuffix
@MangaSourceParser("ASMHENTAI", "AsmHentai", type = ContentType.HENTAI)
internal class AsmHentai(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.ASMHENTAI, "asmhentai.com") {
override val selectGalleryLink = ".image a"
override val selectGalleryImg = ".image img"
override val pathTagUrl = "/tags/?page="
override val selectAuthor = "div.tags:contains(Artists:) .tag_list a span.tag"
override val selectTotalPage = ".tp"
override val idImg = "fimg"
override fun Element.parseTags() = select("a").mapToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
val name = it.selectFirst(".tag")?.html()?.substringBefore("<") ?: it.html().substringBefore("<")
MangaTag(
key = key,
title = name,
source = source,
)
}
}

@ -0,0 +1,75 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
import org.koitharu.kotatsu.parsers.util.urlEncoded
@MangaSourceParser("HENTAIERA", "HentaiEra", type = ContentType.HENTAI)
internal class HentaiEra(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIERA, "hentaiera.com", 25) {
override val selectGallery = ".thumb"
override val pathTagUrl = "/tags/popular?page="
override val selectTags = ".tags_section"
override val selectTag = ".galleries_info li:contains(Tags) div.info_tags"
override val selectAuthor = ".galleries_info li:contains(Artists) span.item_name"
override val urlReplaceBefore = "/gallery/"
override val urlReplaceAfter = "/view/"
override val selectLanguageChapter = ".galleries_info li:contains(Languages) div.info_tags .item_name"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/tag/")
append(tag?.key.orEmpty())
append("/?")
} else if (!query.isNullOrEmpty()) {
append("/search/?key=")
append(query.urlEncoded())
append("&")
} else {
append("/?")
}
append("page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val urlChapters = manga.url.replace(urlReplaceBefore, urlReplaceAfter) + "1/"
val tag = doc.selectFirstOrThrow(selectTag)
return manga.copy(
tags = tag.parseTags(),
author = doc.selectFirst(selectAuthor)?.text(),
chapters = listOf(
MangaChapter(
id = manga.id,
name = manga.title,
number = 1,
url = urlChapters,
scanlator = null,
uploadDate = 0,
branch = doc.selectFirst(selectLanguageChapter)?.text(),
source = source,
),
),
)
}
}

@ -0,0 +1,71 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("HENTAIFOX", "HentaiFox", type = ContentType.HENTAI)
internal class HentaiFox(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIFOX, "hentaifox.com") {
override val selectGallery = ".lc_galleries .thumb, .related_galleries .thumb"
override val selectTags = ".list_tags"
override val selectTag = "ul.tags"
override val urlReplaceBefore = "/gallery/"
override val urlReplaceAfter = "/g/"
override val selectLanguageChapter = "ul.languages a.tag_btn"
override val selectTotalPage = ".total_pages"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val tag = tags.oneOrThrowIfMany()
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
append("/tag/")
append(tag?.key.orEmpty())
if (page > 1) {
append("/pag/")
append(page)
append("/")
}
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
if (page > 1) {
append("&page=")
append(page)
}
} else {
if (page > 2) {
append("/pag/")
append(page)
append("/")
} else if (page > 1) {
append("/page/")
append(page)
append("/")
}
}
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override fun Element.parseTags() = select("a").mapToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
val name = it.html().substringBefore("<")
MangaTag(
key = key,
title = name,
source = source,
)
}
}

@ -0,0 +1,19 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
@MangaSourceParser("HENTAIROX", "HentaiRox", type = ContentType.HENTAI)
internal class HentaiRox(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.HENTAIROX, "hentairox.com") {
override val selectGallery = ".thumb"
override val pathTagUrl = "/tags/popular?page="
override val selectTags = ".gtags"
override val selectTag = "li:contains(Tags:)"
override val selectAuthor = "li:contains(Artists:) span.item_name"
override val urlReplaceBefore = "/gallery/"
override val urlReplaceAfter = "/view/"
override val selectLanguageChapter = "li:contains(Languages:) .item_name"
}

@ -0,0 +1,79 @@
package org.koitharu.kotatsu.parsers.site.galleryadults.all
import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
import org.koitharu.kotatsu.parsers.util.*
@MangaSourceParser("NHENTAI", "NHentai", type = ContentType.HENTAI)
internal class NHentaiParser(context: MangaLoaderContext) :
GalleryAdultsParser(context, MangaSource.NHENTAI, "nhentai.net", 25) {
override val selectGallery = "div.index-container:not(.index-popular) .gallery, #related-container .gallery"
override val selectGalleryLink = "a"
override val selectGalleryImg = "img"
override val selectGalleryTitle = ".caption"
override val pathTagUrl = "/tags/popular?page="
override val selectTags = "#tag-container a"
override val selectTag = ".tag-container:contains(Tags:) span.tags"
override val selectAuthor = "#tags div.tag-container:contains(Artists:) span.name"
override val urlReplaceBefore = "/g/"
override val urlReplaceAfter = "/g/"
override val selectLanguageChapter =
".tag-container:contains(Languages:) span.tags a:not(.tag-17249) span.name" // tag-17249 = translated
override val selectTotalPage = ".num-pages"
override val idImg = "image-container"
override suspend fun getListPage(
page: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
}
val url = buildString {
append("https://")
append(domain)
if (!tags.isNullOrEmpty()) {
val tag = tags.single()
append("/tag/")
append(tag.key)
append("/?")
} else if (!query.isNullOrEmpty()) {
append("/search/?q=")
append(query.urlEncoded())
append("&")
} else {
append("/?")
}
append("page=")
append(page)
}
return parseMangaList(webClient.httpGet(url).parseHtml())
}
override suspend fun getPageUrl(page: MangaPage): String {
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
val root = doc.body()
return root.requireElementById(idImg).selectFirstOrThrow("img").src() ?: root.parseFailed("Image src not found")
}
override fun Element.parseTags() = select("a").mapToSet {
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
val name = it.selectFirst(".name")?.text() ?: it.text()
MangaTag(
key = key,
title = name,
source = source,
)
}
private fun buildQuery(tags: Collection<MangaTag>) = tags.joinToString(separator = " ") { tag ->
"tag:\"${tag.key}\""
}
}
Loading…
Cancel
Save