diff --git a/.github/summary.yaml b/.github/summary.yaml index 9d9abed2..b97324b9 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1237 +total: 1237 \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt index 7ccafcac..7a016fcb 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/HentaiVnBuzz.kt @@ -1,135 +1,113 @@ package org.koitharu.kotatsu.parsers.site.vi -import org.jsoup.nodes.Document 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.LegacyPagedMangaParser import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.util.* import java.util.* @MangaSourceParser("HENTAIVNBUZZ", "HentaiVn.buzz", "vi", type = ContentType.HENTAI) internal class HentaiVnBuzz(context: MangaLoaderContext) : - LegacyPagedMangaParser(context, MangaParserSource.HENTAIVNBUZZ, 24) { + LegacyPagedMangaParser(context, MangaParserSource.HENTAIVNBUZZ, 60) { - override val configKeyDomain = ConfigKey.Domain("hentaivn.email") + override val configKeyDomain = ConfigKey.Domain("hentaivn.beer") - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } - - override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) - - override val availableSortOrders: Set = EnumSet.of( - SortOrder.NEWEST, - SortOrder.POPULARITY, - SortOrder.UPDATED, - ) + override val availableSortOrders: Set = + EnumSet.of( + SortOrder.UPDATED, + SortOrder.NEWEST, + SortOrder.POPULARITY, + SortOrder.RATING, + ) override val filterCapabilities: MangaListFilterCapabilities get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isTagsExclusionSupported = true, isSearchSupported = true, ) override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchTags(), + availableTags = fetchAvailableTags(), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val url = when { - !filter.query.isNullOrEmpty() -> { - buildString { - append("/tim-kiem?key_word=") - append(filter.query.urlEncoded()) - if (page > 1) { - append("&page=") - append(page) - } - } + val url = buildString { + append("https://") + append(domain) + append("/tim-kiem-nang-cao") + + append("?page=") + append(page) + + append("&sort=") + append( + when (order) { + SortOrder.UPDATED -> "0" + SortOrder.NEWEST -> "1" + SortOrder.POPULARITY -> "2" + SortOrder.RATING -> "6" + else -> "0" + }, + ) + + append("&status=") + append( + when (filter.states.oneOrThrowIfMany()) { + MangaState.ONGOING -> "1" + MangaState.FINISHED -> "2" + else -> "0" + }, + ) + + if (filter.tags.isNotEmpty()) { + append("&category=") + append(filter.tags.joinToString(",") { it.key }) } - filter.tags.isNotEmpty() -> { - val tag = filter.tags.first() - buildString { - append("/the-loai/") - append(tag.key) - append("?") - when (order) { - SortOrder.NEWEST -> append("sort=0") - SortOrder.UPDATED -> append("sort=1") - SortOrder.POPULARITY -> append("sort=2") - else -> append("sort=0") - } - if (filter.states.isNotEmpty()) { - filter.states.forEach { - when (it) { - MangaState.ONGOING -> append("&is_full=0") - MangaState.FINISHED -> append("&is_full=1") - else -> append("") - } - } - } - if (page > 1) { - append("&page=") - append(page) - } - } + if (filter.tagsExclude.isNotEmpty()) { + append("¬category=") + append(filter.tagsExclude.joinToString(",") { it.key }) } - else -> { - buildString { - append("/danh-sach/truyen-moi?") - when (order) { - SortOrder.NEWEST -> append("sort=0") - SortOrder.UPDATED -> append("sort=1") - SortOrder.POPULARITY -> append("sort=2") - else -> append("sort=0") - } - if (filter.states.isNotEmpty()) { - filter.states.forEach { - when (it) { - MangaState.ONGOING -> append("&is_full=0") - MangaState.FINISHED -> append("&is_full=1") - else -> append("") - } - } - } - if (page > 1) { - append("&page=") - append(page) - } - } + if (!filter.query.isNullOrEmpty()) { + clear() + + append("https://") + append(domain) + append("/tim-kiem?q=") + append(filter.query.urlEncoded()) + + return@buildString // end of buildString } } - val fullUrl = url.toAbsoluteUrl(domain) - val doc = webClient.httpGet(fullUrl).parseHtml() - return when { - !filter.query.isNullOrEmpty() -> parseSearchManga(doc) - filter.tags.isNotEmpty() -> parseSearchManga(doc) - else -> parseListManga(doc) - } - } + val doc = webClient.httpGet(url).parseHtml() + return doc.select("ul.list_grid.grid > li").map { element -> + val aTag = element.selectFirstOrThrow("h3 a") + val tags = element.select(".genre-item").mapToSet { + MangaTag( + key = it.attr("href").substringAfterLast('-').substringBeforeLast('.'), + title = it.text().toTitleCase(sourceLocale), + source = source, + ) + } + + val href = aTag.attrAsRelativeUrl("href") - private fun parseSearchManga(doc: Document): List { - return doc.select(".story-item-list.d-flex.align-items-center.position-relative.mb-1").map { div -> - val href = div.selectFirstOrThrow("a.story-item-list__image").attrAsRelativeUrl("href") - val coverUrl = div.selectFirst("img")?.attr("data-src") - val title = div.selectFirst("img")?.attr("alt").orEmpty() Manga( id = generateUid(href), - title = title, + title = aTag.text(), altTitles = emptySet(), url = href, - publicUrl = href.toAbsoluteUrl(domain), + publicUrl = aTag.attrAsAbsoluteUrl("href"), rating = RATING_UNKNOWN, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - coverUrl = coverUrl, - tags = emptySet(), + contentRating = ContentRating.ADULT, + coverUrl = element.selectFirst(".book_avatar a img")?.src(), + tags = tags, state = null, authors = emptySet(), source = source, @@ -137,71 +115,51 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : } } - private fun parseListManga(doc: Document): List { - return doc.select(".story-item-list.d-flex.align-items-center.position-relative.mb-1").map { div -> - val href = div.selectFirstOrThrow("a.story-item-list__image").attrAsRelativeUrl("href") - val coverUrl = div.selectFirst("img")?.attr("data-src").orEmpty() - val title = div.selectFirst("img")?.attr("alt").orEmpty() - Manga( - id = generateUid(href), - title = title, - altTitles = emptySet(), - url = href, - publicUrl = href.toAbsoluteUrl(domain), - rating = RATING_UNKNOWN, - contentRating = if (isNsfwSource) ContentRating.ADULT else null, - coverUrl = coverUrl, - tags = emptySet(), - state = null, - authors = emptySet(), + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val tags = doc.select("ul.list01 li").mapToSet { + MangaTag( + key = it.attr("href").substringAfterLast('-').substringBeforeLast('.'), + title = it.text().toTitleCase(sourceLocale), source = source, ) } - } + val author = doc.selectFirst("li.author a")?.textOrNull() - override suspend fun getDetails(manga: Manga): Manga { - val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() - val author = doc.select("p:contains(Tác giả:) a").text().nullIfEmpty() return manga.copy( + altTitles = setOfNotNull(doc.selectFirst("h2.other-name")?.textOrNull()), authors = setOfNotNull(author), - tags = doc.select("div.mb-1 span a").mapToSet { element -> - MangaTag( - key = element.attr("href").substringAfter("/the-loai/"), - title = element.text().substringBefore(',').trim(), // force trim before , symbol and space - source = source, - ) - }, - description = null, - state = when (doc.select("p:contains(Trạng thái:) span").text()) { - "Đang ra" -> MangaState.ONGOING + tags = tags, + description = doc.selectFirst("div.story-detail-info")?.html(), + state = when (doc.selectFirst(".status p.col-xs-9")?.text()) { + "Đang tiến hành" -> MangaState.ONGOING "Hoàn thành" -> MangaState.FINISHED else -> null }, - chapters = doc.select("div.story-detail__list-chapter--list ul.list-unstyled li a") - .mapIndexed { i, element -> - val href = element.attrAsRelativeUrl("href") - val name = element.text().removePrefix("- ") - MangaChapter( - id = generateUid(href), - title = name, - number = i + 1f, - volume = 0, - url = href, - scanlator = null, - uploadDate = 0, - branch = null, - source = source, - ) - }, + chapters = doc.select("div.list_chapter div.works-chapter-item").mapChapters(reversed = true) { i, div -> + val a = div.selectFirstOrThrow("a") + val href = a.attrAsRelativeUrl("href") + val name = a.text() + MangaChapter( + id = generateUid(href), + title = name, + number = i + 1f, + volume = 0, + url = href, + scanlator = null, + uploadDate = 0L, + branch = null, + source = source, + ) + }, ) } override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val imageUrls = doc.select("meta[property='og:image']").map { it.attr("content") } - val finalUrls = imageUrls.drop(1) - return finalUrls.map { url -> + return doc.select(".chapter_content img").map { img -> + val url = img.requireSrc() MangaPage( id = generateUid(url), url = url, @@ -211,18 +169,15 @@ internal class HentaiVnBuzz(context: MangaLoaderContext) : } } - private suspend fun fetchTags(): Set { - val doc = webClient.httpGet("https://$domain/").parseHtml() - val list = doc.select("ul.dropdown-menu.dropdown-menu-custom li a") - return list.mapToSet { tags -> - val href = tags.attr("href") - val key = href.substringAfter("/the-loai/").substringBefore("/") - val title = tags.text() + private suspend fun fetchAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/tim-kiem-nang-cao").parseHtml() + val elements = doc.select(".genre-item") + return elements.mapIndexed { i, element -> MangaTag( - key = key, - title = title, + key = (i + 1).toString(), + title = element.text().toTitleCase(sourceLocale), source = source, ) - } + }.toSet() } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/yurigarden/YuriGardenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/yurigarden/YuriGardenParser.kt index 25117d4f..b4a9a1b4 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/yurigarden/YuriGardenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/yurigarden/YuriGardenParser.kt @@ -42,6 +42,7 @@ internal abstract class YuriGardenParser( isSearchSupported = true, isMultipleTagsSupported = true, isSearchWithFiltersSupported = true, + isAuthorSearchSupported = true, ) override suspend fun getFilterOptions(): MangaListFilterOptions { @@ -99,6 +100,19 @@ internal abstract class YuriGardenParser( append("&genre=") append(filter.tags.joinToString(separator = ",") { it.key }) } + + if (!filter.author.isNullOrEmpty()) { + clear() + + append("https://") + append(apiSuffix) + append("/creators/authors/") + append( + filter.author.substringAfter("(").substringBefore(")") + ) + + return@buildString // end of buildString + } } val json = webClient.httpGet(url).parseJson() @@ -106,8 +120,10 @@ internal abstract class YuriGardenParser( return data.mapJSON { jo -> val id = jo.getLong("id") - val allTags = fetchTags().orEmpty() - val tags = allTags.let { allTags -> + val altTitles = setOf(jo.optString("anotherName", null)) + .filterNotNull() + .toSet() + val tags = fetchTags().let { allTags -> jo.optJSONArray("genres")?.asTypedList()?.mapNotNullToSet { g -> allTags.find { x -> x.key == g } } @@ -118,12 +134,12 @@ internal abstract class YuriGardenParser( url = "/comics/$id", publicUrl = "https://$domain/comic/$id", title = jo.getString("title"), - altTitles = setOf(jo.getString("anotherName")), + altTitles = altTitles, coverUrl = jo.getString("thumbnail"), largeCoverUrl = jo.getString("thumbnail"), authors = emptySet(), tags = tags, - state = when(jo.getString("status")) { + state = when(jo.optString("status")) { "ongoing" -> MangaState.ONGOING "completed" -> MangaState.FINISHED "hiatus" -> MangaState.PAUSED @@ -131,7 +147,7 @@ internal abstract class YuriGardenParser( "oncoming" -> MangaState.UPCOMING else -> null }, - description = jo.getString("description"), + description = jo.optString("description").orEmpty(), contentRating = if (jo.getBooleanOrDefault("r18", false)) ContentRating.ADULT else ContentRating.SUGGESTIVE, source = source, rating = RATING_UNKNOWN, @@ -144,9 +160,11 @@ internal abstract class YuriGardenParser( val json = webClient.httpGet("https://$apiSuffix/comics/${id}").parseJson() val authors = json.optJSONArray("authors")?.mapJSONToSet { jo -> - jo.getString("name") + jo.getString("name") + " (${jo.getLong("id")})" }.orEmpty() + val altTitles = setOf(json.getString("anotherName")) + val description = json.getString("description") val team = json.optJSONArray("teams")?.getJSONObject(0)?.getString("name") val chaptersDeferred = async { @@ -154,6 +172,7 @@ internal abstract class YuriGardenParser( } manga.copy( + altTitles = altTitles, authors = authors, chapters = chaptersDeferred.await().mapChapters() { _, jo -> val chapId = jo.getLong("id") @@ -168,7 +187,8 @@ internal abstract class YuriGardenParser( branch = null, source = source, ) - } + }, + description = description, ) }