diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt index 44144c45..3d15b85b 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt @@ -34,6 +34,7 @@ internal abstract class MadaraParser( // Change these values only if the site does not support manga listings via ajax protected open val withoutAjax = false + protected open val authorSearchSupported = false override val availableSortOrders: Set = setupAvailableSortOrders() @@ -71,6 +72,7 @@ internal abstract class MadaraParser( isSearchSupported = true, isSearchWithFiltersSupported = true, isYearSupported = true, + isAuthorSearchSupported = authorSearchSupported ) override suspend fun getFilterOptions() = MangaListFilterOptions( @@ -273,11 +275,13 @@ internal abstract class MadaraParser( append(filter.year.toString()) } - // Support author - //filter.author?.let { - // append("&author=") - // append(filter.author) - //} + if (!filter.author.isNullOrEmpty()) { + filter.author.let { + append("&author=") + // should be like "minamida-usuke" + append(it.lowercase().replace(" ", "-")) + } + } // Support artist //filter.artist?.let { @@ -454,9 +458,16 @@ internal abstract class MadaraParser( } protected open fun parseMangaList(doc: Document): List { - return doc.select("div.row.c-tabs-item__content").ifEmpty { + val elements = doc.select("div.row.c-tabs-item__content").ifEmpty { doc.select("div.page-item-detail") - }.map { div -> + } + + // Avoid "Content not found or removed" errors + if (elements.isEmpty()) { + return emptyList() + } + + return elements.map { div -> val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") val summary = div.selectFirst(".tab-summary") ?: div.selectFirst(".item-summary") val author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText() @@ -534,7 +545,7 @@ internal abstract class MadaraParser( protected open val selectAlt = ".post-content_item:contains(Alt) .summary-content, .post-content_item:contains(Nomes alternativos: ) .summary-content" - protected open fun createMangaTag(a: Element): MangaTag? { + protected open suspend fun createMangaTag(a: Element): MangaTag? { return MangaTag( key = a.attr("href").removeSuffix("/").substringAfterLast('/'), title = a.text().toTitleCase(), 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 24291f8b..bc6fa5c0 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,34 +8,136 @@ import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.site.madara.MadaraParser import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull +import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import java.util.* +// Do not use "hentaicb.sbs" domain, may cause duplicate tags! @MangaSourceParser("HENTAICUBE", "CBHentai", "vi", ContentType.HENTAI) internal class HentaiCube(context: MangaLoaderContext) : MadaraParser(context, MangaParserSource.HENTAICUBE, "hentaicube.xyz") { - override val configKeyDomain = ConfigKey.Domain("hentaicube.xyz", "hentaicb.sbs") - override val datePattern = "dd/MM/yyyy" override val postReq = true + override val authorSearchSupported = true override val postDataReq = "action=manga_views&manga=" + + private val availableTags = suspendLazy(initializer = ::fetchTags) override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = fetchTags(), + availableTags = availableTags.get(), ) - override fun createMangaTag(a: Element): MangaTag? { - return MangaTag( - title = a.text().replace(Regex("\\(\\d+\\)"), ""), - key = a.attr("href").substringAfter("/theloai/").removeSuffix("/"), - source = source, - ) - } + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val pages = page + 1 + + val url = buildString { + if (!filter.author.isNullOrEmpty()) { + append("https://") + append(domain) + append("/tacgia/") + append(filter.author.lowercase().replace(" ", "-")) + + if (pages > 1) { + append("/page/") + append(pages.toString()) + } + + append("/?m_orderby=") + when (order) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("new-manga") + SortOrder.ALPHABETICAL -> {} + SortOrder.RATING -> append("trending") + SortOrder.RELEVANCE -> {} + else -> append("latest") // default + } + return@buildString + } + + append("https://") + append(domain) + + if (pages > 1) { + append("/page/") + append(pages.toString()) + } + + append("/?s=") + + filter.query?.let { + append(filter.query.urlEncoded()) + } + + append("&post_type=wp-manga") + + if (filter.tags.isNotEmpty()) { + filter.tags.forEach { + append("&genre[]=") + append(it.key) + } + } + + filter.states.forEach { + append("&status[]=") + when (it) { + MangaState.ONGOING -> append("on-going") + MangaState.FINISHED -> append("end") + MangaState.ABANDONED -> append("canceled") + MangaState.PAUSED -> append("on-hold") + MangaState.UPCOMING -> append("upcoming") + } + } + + filter.contentRating.oneOrThrowIfMany()?.let { + append("&adult=") + append( + when (it) { + ContentRating.SAFE -> "0" + ContentRating.ADULT -> "1" + else -> "" + }, + ) + } + + if (filter.year != 0) { + append("&release=") + append(filter.year.toString()) + } + + append("&m_orderby=") + when (order) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("latest") + SortOrder.NEWEST -> append("new-manga") + SortOrder.ALPHABETICAL -> append("alphabet") + SortOrder.RATING -> append("rating") + SortOrder.RELEVANCE -> {} + else -> {} + } + } + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + override suspend fun createMangaTag(a: Element): MangaTag? { + val allTags = availableTags.getOrNull().orEmpty() + val title = a.text().replace(Regex("\\(\\d+\\)"), "").trim() // force trim to remove space + // compare to avoid duplicate tags with the same title + return allTags.find { + it.title.trim().equals(title, ignoreCase = true) // try to search with trim + } + } override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) @@ -54,17 +156,17 @@ internal class HentaiCube(context: MangaLoaderContext) : } private suspend fun fetchTags(): Set { - val doc = webClient.httpGet("https://$domain/the-loai-genres").parseHtml() - val elements = doc.select("ul.list-unstyled li a") - return elements.mapToSet { element -> - val href = element.attr("href") - val key = href.substringAfter("/theloai/").removeSuffix("/") - val title = element.text().replace(Regex("\\(\\d+\\)"), "") - MangaTag( - key = key, - title = title, - source = source, - ) - }.toSet() - } + val doc = webClient.httpGet("https://$domain/the-loai-genres").parseHtml() + val elements = doc.select("ul.list-unstyled li a") + return elements.mapToSet { element -> + val href = element.attr("href") + val key = href.substringAfter("/theloai/").removeSuffix("/") + val title = element.text().replace(Regex("\\(\\d+\\)"), "").trim() // force trim + MangaTag( + key = key, + title = title, + source = source, + ) + }.toSet() + } }