diff --git a/.github/summary.yaml b/.github/summary.yaml index 713b196a1..89a6bae26 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1127 \ No newline at end of file +total: 1128 diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/vi/DocTruyen5s.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/vi/DocTruyen5s.kt index 01cf28340..f89a89026 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/vi/DocTruyen5s.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/liliana/vi/DocTruyen5s.kt @@ -1,12 +1,48 @@ package org.koitharu.kotatsu.parsers.site.liliana.vi +import org.jsoup.Jsoup import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.site.liliana.LilianaParser -import org.koitharu.kotatsu.parsers.Broken +import org.koitharu.kotatsu.parsers.util.* -@Broken @MangaSourceParser("DOCTRUYEN5S", "DocTruyen5s", "vi") internal class DocTruyen5s(context: MangaLoaderContext) : - LilianaParser(context, MangaParserSource.DOCTRUYEN5S, "dongmoe.com") + LilianaParser(context, MangaParserSource.DOCTRUYEN5S, "dongmoe.com") { + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val script = doc.selectFirstOrThrow("script:containsData(const CHAPTER_ID)")?.data() + val chapterId = script?.substringAfter("const CHAPTER_ID = ")?.substringBefore(';') + ?: throw IllegalStateException("Không thể tìm thấy CHAPTER_ID, hãy kiểm tra nguồn") + + val ajaxUrl = buildString { + append("https://") + append(domain) + append("/ajax/image/list/chap/") + append(chapterId) + } + + val responseJson = webClient.httpGet(ajaxUrl).parseJson() + + if (!responseJson.optBoolean("status", false)) { + throw IllegalStateException(responseJson.optString("msg")) + } + + val pageListDoc = Jsoup.parse(responseJson.getString("html")) + + return pageListDoc.select("div.separator a").map { element -> + val url = element.attr("href") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt index 2bd9a40f5..0bf661e3d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/vi/HentaiCube.kt @@ -8,12 +8,15 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.site.madara.MadaraParser +import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.util.* @MangaSourceParser("HENTAICUBE", "HentaiCube", "vi", ContentType.HENTAI) internal class HentaiCube(context: MangaLoaderContext) : MadaraParser(context, MangaParserSource.HENTAICUBE, "hentaicb.pro") { + override val configKeyDomain = ConfigKey.Domain("hentaicb.pro", "hentaicube.xyz") + override val datePattern = "dd/MM/yyyy" override val tagPrefix = "the-loai/" override val postReq = true diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt index 130e32bb9..34961d965 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/CuuTruyenParser.kt @@ -29,10 +29,10 @@ internal class CuuTruyenParser(context: MangaLoaderContext) : override val userAgentKey = ConfigKey.UserAgent(UserAgents.KOTATSU) override val configKeyDomain = ConfigKey.Domain( + "cuutruyenpip7z.site", "cuutruyen.net", "nettrom.com", - "hetcuutruyen.net", - "cuutruyenpip7z.site", + "hetcuutruyen.net" ) override fun onCreateConfig(keys: MutableCollection>) { diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/SayHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/SayHentai.kt new file mode 100644 index 000000000..5c483d6f8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/vi/SayHentai.kt @@ -0,0 +1,181 @@ +package org.koitharu.kotatsu.parsers.site.vi + +import java.util.concurrent.atomic.AtomicReference +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.PagedMangaParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.network.UserAgents +import java.text.SimpleDateFormat +import java.util.* + +@MangaSourceParser("SAYHENTAI", "SayHentai", "vi", ContentType.HENTAI) +internal class SayHentai(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.SAYHENTAI, 20) { + override val configKeyDomain = ConfigKey.Domain("sayhentai.one") + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + private val tagsCache = AtomicReference?>(null) + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.ALPHABETICAL, + SortOrder.RATING + ) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + ) + + override suspend fun getFilterOptions(): MangaListFilterOptions { + return MangaListFilterOptions( + availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), + availableTags = fetchTags(), + ) + } + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(domain) + if (!filter.query.isNullOrEmpty()) { + append("/search?s=") + append(filter.query.urlEncoded()) + append("&page=") + append(page.toString()) + } else { + if (filter.tags.isNotEmpty()) { + append("/genre/") + append(filter.tags.first().key) + append("/") + } else { + append("/") + } + append("?page=") + append(page.toString()) + val sortQuery = getSortOrderQuery(order, filter.tags.isNotEmpty()) + if (sortQuery.isNotEmpty()) { + append("&") + append(sortQuery) + } + } + } + + val doc = webClient.httpGet(url).parseHtml() + return doc.select(".page-item-detail").mapNotNull { element -> + val href = element.selectFirst(".item-summary a")?.attrAsRelativeUrl("href") ?: return@mapNotNull null + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + title = element.selectFirst(".item-summary a")?.text().orEmpty(), + coverUrl = element.selectFirst(".item-thumb img")?.src().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + return manga.copy( + altTitle = doc.selectFirst("h2.other-name")?.text(), + author = doc.selectFirst("div.summary-heading:contains(Tác giả) + div.summary-content")?.text(), + tags = doc.select("div.genres-content a[rel=tag]").mapToSet { a -> + MangaTag( + key = a.attr("href").substringAfterLast('/'), + title = a.text().toTitleCase(sourceLocale), + source = source + ) + }, + description = doc.selectFirst("div.summary__content")?.html(), + state = when (doc.selectFirst("div.summary-heading:contains(Trạng thái) + div.summary-content")?.text()?.lowercase()) { + "đang ra" -> MangaState.ONGOING + "hoàn thành" -> MangaState.FINISHED + else -> null + }, + chapters = doc.select("li.wp-manga-chapter").mapChapters(reversed = true) { i, element -> + val a = element.selectFirst("a") ?: return@mapChapters null + MangaChapter( + id = generateUid(a.attrAsRelativeUrl("href")), + name = a.text(), + number = i + 1f, + url = a.attrAsRelativeUrl("href"), + uploadDate = parseChapterDate(element.selectFirst("span.chapter-release-date")?.text()), + branch = null, + scanlator = null, + source = source, + volume = 0 + ) + } + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return doc.select("div.page-break img").mapIndexed { i, img -> + val url = img.src().orEmpty() + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source + ) + } + } + + private fun parseChapterDate(date: String?): Long { + if (date == null) return 0 + return when { + date.contains("giây trước") -> System.currentTimeMillis() - date.removeSuffix(" giây trước").toLong() * 1000 + date.contains("phút trước") -> System.currentTimeMillis() - date.removeSuffix(" phút trước").toLong() * 60 * 1000 + date.contains("giờ trước") -> System.currentTimeMillis() - date.removeSuffix(" giờ trước").toLong() * 60 * 60 * 1000 + date.contains("ngày trước") -> System.currentTimeMillis() - date.removeSuffix(" ngày trước").toLong() * 24 * 60 * 60 * 1000 + date.contains("tuần trước") -> System.currentTimeMillis() - date.removeSuffix(" tuần trước").toLong() * 7 * 24 * 60 * 60 * 1000 + date.contains("tháng trước") -> System.currentTimeMillis() - date.removeSuffix(" tháng trước").toLong() * 30 * 24 * 60 * 60 * 1000 + date.contains("năm trước") -> System.currentTimeMillis() - date.removeSuffix(" năm trước").toLong() * 365 * 24 * 60 * 60 * 1000 + else -> SimpleDateFormat("dd/MM/yyyy", Locale.US).parse(date)?.time ?: 0L + } + } + + private fun getSortOrderQuery(order: SortOrder, hasTags: Boolean): String { + if (!hasTags) return "" + return when (order) { + SortOrder.UPDATED -> "m_orderby=latest" + SortOrder.POPULARITY -> "m_orderby=views" + SortOrder.ALPHABETICAL -> "m_orderby=alphabet" + SortOrder.RATING -> "m_orderby=rating" + else -> "m_orderby=latest" + } + } + + private suspend fun fetchTags(): Set { + return tagsCache.get() ?: run { + val tags = webClient.httpGet("https://$domain/genre").parseHtml() + .select("ul.page-genres li a") + .mapToSet { a -> + val title = a.ownText().toTitleCase(sourceLocale) + MangaTag( + key = a.attr("href").substringAfterLast("/"), + title = title, + source = source + ) + } + tagsCache.set(tags) + tags + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt index e19d94f50..0bb2c5d4f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/DocTruyen3Q.kt @@ -14,7 +14,7 @@ import java.text.SimpleDateFormat @Broken @MangaSourceParser("DOCTRUYEN3Q", "DocTruyen3Q", "vi") internal class DocTruyen3Q(context: MangaLoaderContext) : - WpComicsParser(context, MangaParserSource.DOCTRUYEN3Q, "doctruyen3qmoi.pro", 36) { + WpComicsParser(context, MangaParserSource.DOCTRUYEN3Q, "doctruyen3qw.pro", 36) { override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyenViet.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyenViet.kt index 07a082707..7140c7ad3 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyenViet.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/wpcomics/vi/TopTruyenViet.kt @@ -16,7 +16,7 @@ import java.text.SimpleDateFormat @Broken @MangaSourceParser("TOPTRUYENVIET", "TopTruyen.pro", "vi") internal class TopTruyenViet(context: MangaLoaderContext) : - WpComicsParser(context, MangaParserSource.TOPTRUYENVIET, "www.toptruyen68.pro", 36) { + WpComicsParser(context, MangaParserSource.TOPTRUYENVIET, "www.toptruyenww.pro", 36) { override val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_DESKTOP)