diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt index 79c25bdf..1eee2f15 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/ja/NicovideoSeigaParser.kt @@ -48,7 +48,7 @@ class NicovideoSeigaParser(context: MangaLoaderContext) : !query.isNullOrEmpty() -> return if (offset == 0) getSearchList(query, page) else emptyList() tags.isNullOrEmpty() -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}" tags.size == 1 -> "https://$domain/manga/list?category=${tags.first().key}&page=$page" + - "&sort=${getSortKey(sortOrder)}" + "&sort=${getSortKey(sortOrder)}" tags.size > 1 -> throw IllegalArgumentException("This source supports only 1 category") else -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}" 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 365ff64d..c8d767be 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 @@ -438,31 +438,31 @@ internal abstract class MadaraParser( val d = date?.lowercase() ?: return 0 return when { d.endsWith(" ago") || d.endsWith(" atrás") || // Handle translated 'ago' in Portuguese. - d.startsWith("há ") || // other translated 'ago' in Portuguese. - d.endsWith(" hace") || // other translated 'ago' in Spanish - d.endsWith(" назад") || // other translated 'ago' in Russian - d.endsWith(" önce") || // Handle translated 'ago' in Turkish. - d.endsWith(" trước") || // Handle translated 'ago' in Viêt Nam. - d.startsWith("il y a") || // Handle translated 'ago' in French. - //If there is no ago but just a motion of time - // short Hours - d.endsWith(" h") || - // short Day - d.endsWith(" d") || - // Day in Portuguese - d.endsWith(" días") || d.endsWith(" día") || - // Day in French - d.endsWith(" jour") || d.endsWith(" jours") || - // Hours in Portuguese - d.endsWith(" horas") || d.endsWith(" hora") || - // Hours in french - d.endsWith(" heure") || d.endsWith(" heures") || - // Minutes in English - d.endsWith(" mins") || - // Minutes in Portuguese - d.endsWith(" minutos") || d.endsWith(" minuto") || - //Minutes in French - d.endsWith(" minute") || d.endsWith(" minutes") -> parseRelativeDate(date) + d.startsWith("há ") || // other translated 'ago' in Portuguese. + d.endsWith(" hace") || // other translated 'ago' in Spanish + d.endsWith(" назад") || // other translated 'ago' in Russian + d.endsWith(" önce") || // Handle translated 'ago' in Turkish. + d.endsWith(" trước") || // Handle translated 'ago' in Viêt Nam. + d.startsWith("il y a") || // Handle translated 'ago' in French. + //If there is no ago but just a motion of time + // short Hours + d.endsWith(" h") || + // short Day + d.endsWith(" d") || + // Day in Portuguese + d.endsWith(" días") || d.endsWith(" día") || + // Day in French + d.endsWith(" jour") || d.endsWith(" jours") || + // Hours in Portuguese + d.endsWith(" horas") || d.endsWith(" hora") || + // Hours in french + d.endsWith(" heure") || d.endsWith(" heures") || + // Minutes in English + d.endsWith(" mins") || + // Minutes in Portuguese + d.endsWith(" minutos") || d.endsWith(" minuto") || + //Minutes in French + d.endsWith(" minute") || d.endsWith(" minutes") -> parseRelativeDate(date) // Handle 'yesterday' and 'today', using midnight d.startsWith("year") -> Calendar.getInstance().apply { @@ -559,10 +559,10 @@ internal abstract class MadaraParser( private fun createRequestTemplate() = ("action=madara_load_more&page=1&template=madara-core%2Fcontent%2Fcontent-search&vars%5Bs%5D=&vars%5B" + - "orderby%5D=meta_value_num&vars%5Bpaged%5D=1&vars%5Btemplate%5D=search&vars%5Bmeta_query" + - "%5D%5B0%5D%5Brelation%5D=AND&vars%5Bmeta_query%5D%5Brelation%5D=OR&vars%5Bpost_type" + - "%5D=wp-manga&vars%5Bpost_status%5D=publish&vars%5Bmeta_key%5D=_latest_update&vars%5Border" + - "%5D=desc&vars%5Bmanga_archives_item_layout%5D=default").split( + "orderby%5D=meta_value_num&vars%5Bpaged%5D=1&vars%5Btemplate%5D=search&vars%5Bmeta_query" + + "%5D%5B0%5D%5Brelation%5D=AND&vars%5Bmeta_query%5D%5Brelation%5D=OR&vars%5Bpost_type" + + "%5D=wp-manga&vars%5Bpost_status%5D=publish&vars%5Bmeta_key%5D=_latest_update&vars%5Border" + + "%5D=desc&vars%5Bmanga_archives_item_layout%5D=default").split( '&', ).map { val pos = it.indexOf('=') diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt new file mode 100644 index 00000000..cc17baa0 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/Manga18Parser.kt @@ -0,0 +1,216 @@ +package org.koitharu.kotatsu.parsers.site.manga18 + +import kotlinx.coroutines.async +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.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.* + +internal abstract class Manga18Parser( + context: MangaLoaderContext, + source: MangaSource, + domain: String, + pageSize: Int = 20, +) : PagedMangaParser(context, source, pageSize) { + + override val configKeyDomain = ConfigKey.Domain(domain) + + override val sortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.ALPHABETICAL, + ) + + protected open val listeurl = "list-manga/" + protected open val tagUrl = "manga-list/" + protected open val datePattern = "dd-MM-yyyy" + + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + + @JvmField + protected val ongoing: Set = setOf( + "On Going", + ) + + @JvmField + protected val finished: Set = setOf( + "Completed", + ) + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + val url = buildString { + append("https://") + append(domain) + when { + !query.isNullOrEmpty() -> { + append("/$listeurl") + append(page.toString()) + append("?search=") + append(query.urlEncoded()) + append("&") + } + + !tags.isNullOrEmpty() -> { + append("/$tagUrl") + for (tag in tags) { + append(tag.key) + } + append("/") + append(page.toString()) + append("?") + } + + else -> { + append("/$listeurl") + append(page.toString()) + append("?") + } + } + append("order_by=") + when (sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("lastest") + SortOrder.ALPHABETICAL -> append("name") + else -> append("latest") + } + } + val doc = webClient.httpGet(url).parseHtml() + + return doc.select("div.story_item").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + title = div.selectFirstOrThrow("div.mg_info").selectFirst("div.mg_name a")?.text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } + + override suspend fun getTags(): Set { + val doc = webClient.httpGet("https://$domain/$listeurl/").parseHtml() + return doc.select("div.grid_cate li").mapNotNullToSet { li -> + val a = li.selectFirst("a") ?: return@mapNotNullToSet null + val href = a.attr("href").removeSuffix('/').substringAfterLast('/') + MangaTag( + key = href, + title = a.text(), + source = source, + ) + } + } + + protected open val selectdesc = "div.detail_reviewContent" + protected open val selectdate = "div.item p" + protected open val selectchapter = "div.chapter_box li" + protected open val selectState = "div.item:contains(Status) div.info_value" + protected open val selectAlt = "div.item:contains(Other name) div.info_value" + protected open val selectTag = "div.item:contains(Categories) div.info_value a" + + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val fullUrl = manga.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val body = doc.body().selectFirstOrThrow("div.detail_listInfo") + + val chaptersDeferred = async { getChapters(manga, doc) } + + val desc = doc.selectFirstOrThrow(selectdesc).html() + + val stateDiv = body.selectFirst(selectState) + + val state = stateDiv?.let { + when (it.text()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + else -> null + } + } + + val alt = doc.body().select(selectAlt).text() + + manga.copy( + tags = doc.body().select(selectTag).mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + title = a.text().toTitleCase(), + source = source, + ) + }, + description = desc, + altTitle = alt, + state = state, + chapters = chaptersDeferred.await(), + ) + } + + + protected open suspend fun getChapters(manga: Manga, doc: Document): List { + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + return doc.body().select(selectchapter).mapChapters(reversed = true) { i, li -> + val a = li.selectFirstOrThrow("a") + val href = a.attrAsRelativeUrl("href") + val dateText = li.selectFirst(selectdate)?.text() + MangaChapter( + id = generateUid(href), + name = a.text(), + number = i + 1, + url = href, + uploadDate = dateFormat.tryParse(dateText), + source = source, + scanlator = null, + branch = null, + ) + } + } + + 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(slides_p_path)") + val urlencoed = script.data().substringAfter('[').substringBefore(",]").replace("\"", "").split(",") + return urlencoed.map { url -> + val img = context.decodeBase64(url).toString(Charsets.UTF_8) + + MangaPage( + id = generateUid(img), + url = img, + preview = null, + source = source, + ) + } + } + + + protected fun Element.src(): String? { + var result = absUrl("data-src") + if (result.isEmpty()) result = absUrl("data-cfsrc") + if (result.isEmpty()) result = absUrl("src") + return result.ifEmpty { null } + } + +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Hentai3zCc.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Hentai3zCc.kt new file mode 100644 index 00000000..fe77fddc --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Hentai3zCc.kt @@ -0,0 +1,94 @@ +package org.koitharu.kotatsu.parsers.site.madara.en + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser +import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl +import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.generateUid +import org.koitharu.kotatsu.parsers.util.host +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("HENTAI3ZCC", "Hentai3z Cc", "en", ContentType.HENTAI) +internal class Hentai3zCc(context: MangaLoaderContext) : + Manga18Parser(context, MangaSource.HENTAI3ZCC, "hentai3z.cc") { + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + val url = buildString { + append("https://") + append(domain) + val pages = page + 1 + when { + !query.isNullOrEmpty() -> { + append("/$listeurl/") + append(pages.toString()) + append("?search=") + append(query.urlEncoded()) + append("&") + } + + !tags.isNullOrEmpty() -> { + append("/$tagUrl/") + for (tag in tags) { + append(tag.key) + } + append("/") + append(pages.toString()) + append("?") + } + + else -> { + append("/$listeurl/") + append(pages.toString()) + append("?") + } + } + append("order_by=") + when (sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.UPDATED -> append("lastest") + SortOrder.ALPHABETICAL -> append("name") + else -> append("latest") + } + } + val doc = webClient.httpGet(url).parseHtml() + + + return doc.select("div.story_item").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src() + ?.replace("cover_thumb_2.webp", "cover_250x350.jpg") + ?.replace("admin.manga18.us", "bk.18porncomic.com") + .orEmpty(), + title = div.selectFirstOrThrow("div.mg_info").selectFirst("div.mg_name a")?.text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Manga18.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Manga18.kt new file mode 100644 index 00000000..75714c1c --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/Manga18.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.parsers.site.madara.en + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser + + +@MangaSourceParser("MANGA18", "Manga18", "en", ContentType.HENTAI) +internal class Manga18(context: MangaLoaderContext) : + Manga18Parser(context, MangaSource.MANGA18, "manga18.club") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/PornComic18.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/PornComic18.kt new file mode 100644 index 00000000..617e9621 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/en/PornComic18.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.parsers.site.madara.en + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser + + +@MangaSourceParser("PORNCOMIC18", "18 Porn Comic", "en", ContentType.HENTAI) +internal class PornComic18(context: MangaLoaderContext) : + Manga18Parser(context, MangaSource.PORNCOMIC18, "18porncomic.com") { + + override val selectTag = "div.item:not(.info_label) div.info_value a" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/es/Tumanhwas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/es/Tumanhwas.kt new file mode 100644 index 00000000..b9ebecfa --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/manga18/es/Tumanhwas.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.parsers.site.madara.es + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser + + +@MangaSourceParser("TUMANHWAS", "Tumanhwas", "es", ContentType.HENTAI) +internal class Tumanhwas(context: MangaLoaderContext) : + Manga18Parser(context, MangaSource.TUMANHWAS, "tumanhwas.club") { + + override val selectTag = "div.item:contains(Géneros) div.info_value a" + override val selectAlt = "div.item:contains(Títulos alternativos) div.info_value" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/LynxScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/LynxScans.kt new file mode 100644 index 00000000..44e65d8d --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/LynxScans.kt @@ -0,0 +1,15 @@ +package org.koitharu.kotatsu.parsers.site.mangareader.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser + + +@MangaSourceParser("LYNXSCANS", "LynxScans", "en") +internal class LynxScans(context: MangaLoaderContext) : + MangaReaderParser(context, MangaSource.LYNXSCANS, "lynxscans.com", pageSize = 25, searchPageSize = 10) { + + override val listUrl = "/comics" + +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/ManhuaScanUs.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/ManhuaScanUs.kt new file mode 100644 index 00000000..797fcb25 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/ManhuaScanUs.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.parsers.site.mangareader.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser + + +@MangaSourceParser("MANHUASCANUS", "Manhua Scan Us", "en", ContentType.HENTAI) +internal class ManhuaScanUs(context: MangaLoaderContext) : + MangaReaderParser(context, MangaSource.MANHUASCANUS, "manhuascan.us", pageSize = 30, searchPageSize = 30) { + + override val datePattern = "dd-MM-yyyy" + override val listUrl = "/manga-list" + +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt new file mode 100644 index 00000000..37948641 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/MmrcmsParser.kt @@ -0,0 +1,260 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms + +import kotlinx.coroutines.async +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.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.* + +internal abstract class MmrcmsParser( + context: MangaLoaderContext, + source: MangaSource, + domain: String, + pageSize: Int = 20, +) : PagedMangaParser(context, source, pageSize) { + + override val configKeyDomain = ConfigKey.Domain(domain) + + override val sortOrders: Set = EnumSet.of( + SortOrder.POPULARITY, + SortOrder.ALPHABETICAL, + SortOrder.UPDATED, + ) + + protected open val listeurl = "filterList" + protected open val tagUrl = "manga-list" + protected open val datePattern = "dd MMM. yyyy" + + + init { + paginator.firstPage = 1 + searchPaginator.firstPage = 1 + } + + + @JvmField + protected val ongoing: Set = hashSetOf( + "On Going", + "Ongoing", + "En cours", + "En curso", + ) + + @JvmField + protected val finished: Set = hashSetOf( + "Completed", + "Completo", + "Complete", + "Terminé", + ) + + protected open val imgUpdated = "/cover/cover_250x350.jpg" + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + + val url = if (sortOrder == SortOrder.UPDATED) { + //the Updated page doesn't really exist, we just use the home page to weight the latest chapters, so it doesn't include tag and page management. + buildString { + append("https://") + append(domain) + if (page == 2) { + append("/STOP") + } + } + } else { + buildString { + append("https://") + append(domain) + + append("/$listeurl/") + append("?page=") + append(page.toString()) + append("&asc=true&author=&tag=") + + append("&alpha=") + if (!query.isNullOrEmpty()) { + append(query.urlEncoded()) + } + + append("&cat=") + if (!tags.isNullOrEmpty()) { + + for (tag in tags) { + append(tag.key) + } + } + + append("&sortBy=") + when (sortOrder) { + SortOrder.POPULARITY -> append("views") + SortOrder.ALPHABETICAL -> append("name") + else -> append("views") + } + } + } + + val doc = webClient.httpGet(url).parseHtml() + + if (sortOrder == SortOrder.UPDATED) { + + return doc.select("div.manga-item").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + val deeplink = href.substringAfterLast("/") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = "https://$domain/uploads/manga/$deeplink$imgUpdated", + title = div.selectFirstOrThrow("a").text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } else { + return doc.select("div.media").map { div -> + val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + title = div.selectFirstOrThrow("div.media-body h5").text().orEmpty(), + altTitle = null, + rating = div.selectFirstOrThrow("span").ownText().toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } + + } + + override suspend fun getTags(): Set { + val doc = webClient.httpGet("https://$domain/$tagUrl/").parseHtml() + return doc.select("ul.list-category li").mapNotNullToSet { li -> + val a = li.selectFirst("a") ?: return@mapNotNullToSet null + val href = a.attr("href").substringAfterLast("cat=") + MangaTag( + key = href, + title = a.text(), + source = source, + ) + } + } + + protected open val selectdesc = "div.well" + protected open val selectState = "dt:contains(Statut)" + protected open val selectAlt = "dt:contains(Autres noms)" + protected open val selectAut = "dt:contains(Auteur(s))" + protected open val selectTag = "dt:contains(Catégories)" + + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val fullUrl = manga.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val body = doc.body().selectFirstOrThrow("dl.dl-horizontal") + + val chaptersDeferred = async { getChapters(manga, doc) } + + val desc = doc.selectFirstOrThrow(selectdesc).text() + + val stateDiv = body.selectFirst(selectState)?.nextElementSibling() + + val state = stateDiv?.let { + when (it.text()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + else -> null + } + } + + val alt = doc.body().selectFirst(selectAlt)?.nextElementSibling()?.text() + val auth = doc.body().selectFirst(selectAut)?.nextElementSibling()?.text() + + val tags = doc.body().selectFirst(selectTag)?.nextElementSibling()?.select("a") ?: emptySet() + + manga.copy( + tags = tags.mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + title = a.text().toTitleCase(), + source = source, + ) + }, + author = auth, + description = desc, + altTitle = alt, + state = state, + chapters = chaptersDeferred.await(), + ) + } + + + protected open val selectdate = "div.date-chapter-title-rtl" + protected open val selectchapter = "ul.chapters > li:not(.btn)" + + protected open suspend fun getChapters(manga: Manga, doc: Document): List { + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + + return doc.body().select(selectchapter).mapChapters(reversed = true) { i, li -> + val a = li.selectFirstOrThrow("a") + val href = a.attrAsRelativeUrl("href") + val dateText = li.selectFirst(selectdate)?.text() + MangaChapter( + id = generateUid(href), + name = li.selectFirstOrThrow("h5").text(), + number = i + 1, + url = href, + uploadDate = dateFormat.tryParse(dateText), + source = source, + scanlator = null, + branch = null, + ) + } + } + + protected open val selectPage = "div#all img" + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + + + return doc.select(selectPage).map { url -> + val img = url.src()?.toRelativeUrl(domain) ?: url.parseFailed("Image src not found") + MangaPage( + id = generateUid(img), + url = img, + preview = null, + source = source, + ) + } + } + + + protected fun Element.src(): String? { + var result = absUrl("data-src") + if (result.isEmpty()) result = absUrl("data-cfsrc") + if (result.isEmpty()) result = absUrl("src") + return result.ifEmpty { null } + } + +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/en/ReadComicsOnline.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/en/ReadComicsOnline.kt new file mode 100644 index 00000000..4e87357e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/en/ReadComicsOnline.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.en + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser + + +@MangaSourceParser("READCOMICSONLINE", "Read Comics Online", "en") +internal class ReadComicsOnline(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.READCOMICSONLINE, "readcomicsonline.ru") { + + override val selectState = "dt:contains(Status)" + override val selectTag = "dt:contains(Categories)" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/es/MangaDoor.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/es/MangaDoor.kt new file mode 100644 index 00000000..1366220e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/es/MangaDoor.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.es + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("MANGADOOR", "Manga Door", "es") +internal class MangaDoor(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.MANGADOOR, "mangadoor.com") { + + + override val sourceLocale: Locale = Locale.ENGLISH + + override val selectState = "dt:contains(Estado)" + override val selectAlt = "dt:contains(Otros nombres)" + override val selectAut = "dt:contains(Autor(es))" + override val selectTag = "dt:contains(Categorías)" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/JpMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/JpMangas.kt new file mode 100644 index 00000000..9f8f64e5 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/JpMangas.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.fr + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("JPMANGAS", "JpMangas", "fr") +internal class JpMangas(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.JPMANGAS, "jpmangas.xyz") { + + + override val sourceLocale: Locale = Locale.ENGLISH +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/LelScanVf.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/LelScanVf.kt new file mode 100644 index 00000000..ea68767f --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/LelScanVf.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.fr + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("LELSCANVF", "Lel Scan Vf", "fr") +internal class LelScanVf(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.LELSCANVF, "lelscanvf.cc") { + + + override val sourceLocale: Locale = Locale.ENGLISH +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/MangaFr.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/MangaFr.kt new file mode 100644 index 00000000..8c887cf2 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/MangaFr.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.fr + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("MANGAFR", "Manga Fr", "fr") +internal class MangaFr(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.MANGAFR, "manga-fr.me") { + + + override val imgUpdated = ".jpg" + override val sourceLocale: Locale = Locale.ENGLISH +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/MangaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/MangaScan.kt new file mode 100644 index 00000000..67d008e2 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/MangaScan.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.fr + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("MANGA_SCAN", "Manga-Scan", "fr") +internal class MangaScan(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.MANGA_SCAN, "manga-scan.co") { + + override val imgUpdated = ".jpg" + override val sourceLocale: Locale = Locale.ENGLISH +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/ScanFrOrg.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/ScanFrOrg.kt new file mode 100644 index 00000000..069412cd --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/ScanFrOrg.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.fr + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("SCAN_FR_ORG", "Scan-Fr Org", "fr") +internal class ScanFrOrg(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.SCAN_FR_ORG, "www.scan-fr.org") { + + + override val sourceLocale: Locale = Locale.ENGLISH + override val selectchapter = "ul.chapterszozo li" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/ScanVf.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/ScanVf.kt new file mode 100644 index 00000000..6d8bbfed --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/ScanVf.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.fr + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("SCANVF", "Scan Vf", "fr") +internal class ScanVf(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.SCANVF, "www.scan-vf.net") { + + + override val sourceLocale: Locale = Locale.ENGLISH +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/KomikId.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/KomikId.kt new file mode 100644 index 00000000..b2365740 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/KomikId.kt @@ -0,0 +1,21 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.id + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("KOMIKID", "KomikId", "id") +internal class KomikId(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.KOMIKID, "komikid.com") { + + + override val selectState = "dt:contains(Status)" + override val selectAlt = "dt:contains(Other names)" + override val selectAut = "dt:contains(Author(s))" + override val selectTag = "dt:contains(Categories)" + override val sourceLocale: Locale = Locale.ENGLISH +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/Mangaid.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/Mangaid.kt new file mode 100644 index 00000000..a72541ca --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/id/Mangaid.kt @@ -0,0 +1,21 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.id + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import java.util.Locale + + +@MangaSourceParser("MANGAID", "Mangaid", "id") +internal class Mangaid(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.MANGAID, "mangaid.click") { + + + override val selectState = "dt:contains(Status)" + override val selectAlt = "dt:contains(Other names)" + override val selectAut = "dt:contains(Author(s))" + override val selectTag = "dt:contains(Categories)" + override val sourceLocale: Locale = Locale.ENGLISH +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/pt/Animaregia.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/pt/Animaregia.kt new file mode 100644 index 00000000..2ab9221a --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/pt/Animaregia.kt @@ -0,0 +1,69 @@ +package org.koitharu.kotatsu.parsers.site.mmrcms.pt + + +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser +import org.koitharu.kotatsu.parsers.util.* +import java.util.EnumSet +import java.util.Locale + + +@MangaSourceParser("ANIMAREGIA", "Animaregia", "pt") +internal class Animaregia(context: MangaLoaderContext) : + MmrcmsParser(context, MangaSource.ANIMAREGIA, "animaregia.net") { + + override val selectdate = "div.col-md-4" + override val sourceLocale: Locale = Locale.ENGLISH + + //temporary + override val sortOrders: Set = EnumSet.of( + SortOrder.POPULARITY, + SortOrder.ALPHABETICAL, + ) + + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val fullUrl = manga.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val body = doc.body().selectFirstOrThrow("ul.list-group") + + val chaptersDeferred = async { getChapters(manga, doc) } + + val desc = doc.select(selectdesc).text() + + val stateDiv = body.selectFirst("li.list-group-item:contains(Status)")?.lastElementChild() + + val state = stateDiv?.let { + when (it.text()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + else -> null + } + } + val auth = doc.body().selectFirst("li.list-group-item:contains(Autor(es)) a")?.text() + + val tags = doc.body().select("li.list-group-item:contains(Autor(es)) a") ?: emptySet() + + manga.copy( + tags = tags.mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), + title = a.text().toTitleCase(), + source = source, + ) + }, + author = auth, + description = desc, + altTitle = null, + state = state, + chapters = chaptersDeferred.await(), + ) + } +}