diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/fr/HniScantrad.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/fr/HniScantrad.kt deleted file mode 100644 index da23c547..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/foolslide/fr/HniScantrad.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.koitharu.kotatsu.parsers.site.foolslide.fr - -import org.koitharu.kotatsu.parsers.Broken -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser - -@Broken // Not dead, changed template -@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr") -internal class HniScantrad(context: MangaLoaderContext) : - FoolSlideParser(context, MangaParserSource.HNISCANTRAD, "hni-scantrad.net") { - - override val pagination = false - override val searchUrl = "lel/search/" - override val listUrl = "lel/directory/" -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt new file mode 100644 index 00000000..9db88295 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/FuzzyDoodleParser.kt @@ -0,0 +1,307 @@ +package org.koitharu.kotatsu.parsers.site.fuzzydoodle + +import androidx.collection.scatterSetOf +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.PagedMangaParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.util.* +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +internal abstract class FuzzyDoodleParser( + context: MangaLoaderContext, + source: MangaParserSource, + domain: String, + pageSize: Int = 24, +) : PagedMangaParser(context, source, pageSize) { + + override val availableSortOrders: Set = EnumSet.of(SortOrder.NEWEST) + + override val availableStates: Set = + EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED) + + override val configKeyDomain = ConfigKey.Domain(domain) + + override val isMultipleTagsSupported = true + + + @JvmField + protected val ongoing = scatterSetOf( + "en cours", + "ongoing", + "مستمر", + ) + + @JvmField + protected val finished = scatterSetOf( + "terminé", + "dropped", + "cancelled", + "متوقف", + ) + + @JvmField + protected val abandoned = scatterSetOf( + "canceled", + "cancelled", + "dropped", + "abandonné", + ) + + @JvmField + protected val paused = scatterSetOf( + "hiatus", + "on Hold", + "en pause", + "en attente", + ) + + protected open val ongoingValue = "ongoing" + protected open val finishedValue = "completed" + protected open val pausedValue = "haitus" + protected open val abandonedValue = "dropped" + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { + append("https://") + append(domain) + append("/manga?page=") + append(page) + + when (filter) { + + is MangaListFilter.Search -> { + append("&title=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + append("&type=") + + append("&status=") + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> ongoingValue + MangaState.FINISHED -> finishedValue + MangaState.PAUSED -> pausedValue + MangaState.ABANDONED -> abandonedValue + else -> "" + }, + ) + } + + filter.tags.forEach { + append("&") + append("genre[]".urlEncoded()) + append("=") + append(it.key) + } + } + + null -> {} + } + } + + return parseMangaList(webClient.httpGet(url).parseHtml()) + } + + protected open val selectMangas = "div#card-real" + + protected open fun parseMangaList(doc: Document): List { + return doc.select(selectMangas).mapNotNull { div -> + val href = div.selectFirst("a")?.attr("href") ?: return@mapNotNull null + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src().orEmpty(), + title = div.selectFirst("h2")?.text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } + + protected open val selectAltTitle = "div.flex gap-1:contains(Alternative Titles:) span" + protected open val selectState = "a[href*=status] span" + protected open val selectAuthor = + "div#buttons + div.hidden p:contains(Auteur) span, div#buttons + div.hidden p:contains(Author) span, div#buttons + div.hidden p:contains(المؤلف) span" + protected open val selectDescription = "div:has(> p#description) p" + protected open val selectTagManga = "div.flex > a.inline-block" + + override suspend fun getDetails(manga: Manga): Manga = coroutineScope { + val mangaUrl = manga.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(mangaUrl).parseHtml() + + val maxPageChapterSelect = doc.select("ul.pagination li[onclick]") + var maxPageChapter = 1 + if (!maxPageChapterSelect.isNullOrEmpty()) { + maxPageChapterSelect.map { + val i = it.attr("onclick").substringAfterLast("=").substringBefore("'").toInt() + if (i > maxPageChapter) { + maxPageChapter = i + } + } + } + + manga.copy( + altTitle = doc.selectLast(selectAltTitle)?.text(), + state = when (doc.selectFirst(selectState)?.text()?.lowercase().orEmpty()) { + in ongoing -> MangaState.ONGOING + in finished -> MangaState.FINISHED + in abandoned -> MangaState.ABANDONED + in paused -> MangaState.PAUSED + else -> null + }, + author = doc.selectFirst(selectAuthor)?.text().orEmpty(), + description = doc.select(selectDescription).text(), + tags = doc.select(selectTagManga).mapNotNullToSet { + val key = it.attr("href").substringAfterLast('=') + MangaTag( + key = key, + title = it.text(), + source = source, + ) + }, + chapters = run { + if (maxPageChapter == 1) { + parseChapters(doc) + } else { + coroutineScope { + val result = ArrayList(parseChapters(doc)) + result.ensureCapacity(result.size * maxPageChapter) + (2..maxPageChapter).map { i -> + async { + loadChapters(mangaUrl, i) + } + }.awaitAll() + .flattenTo(result) + result + } + } + }.reversed(), + ) + } + + + private suspend fun loadChapters(baseUrl: String, page: Int): List { + return parseChapters(webClient.httpGet("$baseUrl?page=$page").parseHtml().body()) + } + + protected open val datePattern = "MMMM d, yyyy" + protected open val selectChapters = "div#chapters-list > a[href]" + + private fun parseChapters(doc: Element): List { + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + return doc.select(selectChapters) + .mapChapters { _, a -> + val href = a.attrAsRelativeUrl("href") + val name = a.selectFirst("div.gap-2, #item-title")?.text().orEmpty() + val dateText = a.selectFirst("div.gap-3 span, div:has( #item-title) span.mt-1")?.text() + val chapterN = href.substringAfterLast('/').replace("-", ".").replace("[^0-9.]".toRegex(), "").toFloat() + MangaChapter( + id = generateUid(href), + name = name, + number = chapterN, + volume = 0, + url = href, + scanlator = null, + uploadDate = parseChapterDate( + dateFormat, + dateText, + ), + branch = null, + source = source, + ) + } + } + + protected open val selectPages = "div#chapter-container > img" + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return doc.select(selectPages).map { img -> + val url = img.src() ?: img.parseFailed("Image src not found") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + + protected open val selectTagsList = "div.mt-1 div.items-center:has(label)" + + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/manga").parseHtml() + return doc.select(selectTagsList).mapNotNullToSet { + val key = it.selectFirst("input")?.attr("value") ?: return@mapNotNullToSet null + MangaTag( + key = key, + title = it.selectFirst("label")?.text() ?: key, + source = source, + ) + } + } + + private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { + val d = date?.lowercase() ?: return 0 + return when { + d.endsWith(" ago") || + d.endsWith("مضت") || d.startsWith("منذ") || + d.startsWith("il y a") -> parseRelativeDate(date) + + date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { + if (it.contains(Regex("""\d\D\D"""))) { + it.replace(Regex("""\D"""), "") + } else { + it + } + }.let { dateFormat.tryParse(it.joinToString(" ")) } + + else -> dateFormat.tryParse(date) + } + } + + private fun parseRelativeDate(date: String): Long { + val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 + val cal = Calendar.getInstance() + return when { + WordSet("detik", "segundo", "second", "ثوان") + .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis + + WordSet("menit", "dakika", "min", "minute", "minutes", "minuto", "mins", "phút", "минут", "دقيقة") + .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis + + WordSet("jam", "saat", "heure", "hora", "horas", "hour", "hours", "h", "ساعات", "ساعة") + .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis + + WordSet("hari", "gün", "jour", "día", "dia", "day", "days", "d", "день") + .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis + + WordSet("month", "months", "أشهر", "mois") + .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis + + WordSet("week", "weeks", "semana", "semanas") + .anyWordIn(date) -> cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis + + WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis + else -> 0 + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/ar/HentaiSlayer.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/ar/HentaiSlayer.kt new file mode 100644 index 00000000..91fb00e0 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/ar/HentaiSlayer.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.parsers.site.fuzzydoodle.ar + +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.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser +import java.util.EnumSet + +@MangaSourceParser("HENTAISLAYER", "HentaiSlayer", "ar", ContentType.HENTAI) +internal class HentaiSlayer(context: MangaLoaderContext) : + FuzzyDoodleParser(context, MangaParserSource.HENTAISLAYER, "hentaislayer.net") { + override val availableStates: Set = + EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED) + override val ongoingValue = "مستمر" + override val finishedValue = "مكتمل" + override val abandonedValue = "متوقف" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/en/ScyllaComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/en/ScyllaComics.kt new file mode 100644 index 00000000..142165a9 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/en/ScyllaComics.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.parsers.site.fuzzydoodle.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser + +@MangaSourceParser("SCYLLACOMICS", "ScyllaComics", "en") +internal class ScyllaComics(context: MangaLoaderContext) : + FuzzyDoodleParser(context, MangaParserSource.SCYLLACOMICS, "scyllacomics.xyz") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/fr/LelScanVf.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/fr/LelScanVf.kt new file mode 100644 index 00000000..55b906f2 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fuzzydoodle/fr/LelScanVf.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.parsers.site.fuzzydoodle.fr + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser +import java.util.EnumSet + +@MangaSourceParser("LELSCANVF", "LelScanFr", "fr") +internal class LelScanVf(context: MangaLoaderContext) : + FuzzyDoodleParser(context, MangaParserSource.LELSCANVF, "lelscanfr.com") { + override val availableStates: Set = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED) + override val ongoingValue = "en-cours" + override val finishedValue = "termin" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt index 6f46a52a..d81cad20 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/heancms/HeanCms.kt @@ -9,7 +9,7 @@ import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault import org.koitharu.kotatsu.parsers.util.json.mapJSON -import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed +import org.koitharu.kotatsu.parsers.util.json.toJSONList import org.koitharu.kotatsu.parsers.util.json.unescapeJson import java.text.SimpleDateFormat import java.util.* @@ -143,16 +143,16 @@ internal abstract class HeanCms( val seriesId = manga.id val url = "https://$apiPath/chapter/query?page=1&perPage=9999&series_id=$seriesId" val response = webClient.httpGet(url).parseJson() - val data = response.getJSONArray("data") + val data = response.getJSONArray("data").toJSONList() val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH) return manga.copy( - chapters = data.mapJSONIndexed { index, it -> + chapters = data.mapChapters(reversed = true) { i, it -> val chapterUrl = "/series/${it.getJSONObject("series").getString("series_slug")}/${it.getString("chapter_slug")}" MangaChapter( id = it.getLong("id"), name = it.getString("chapter_name"), - number = (data.length() - index).toFloat(), + number = i + 1f, volume = 0, url = chapterUrl, scanlator = null, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt new file mode 100644 index 00000000..33d027ad --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/HotComicsParser.kt @@ -0,0 +1,201 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics + +import androidx.collection.ArrayMap +import androidx.collection.ArraySet +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okhttp3.Headers +import org.jsoup.nodes.Document +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.network.UserAgents +import org.koitharu.kotatsu.parsers.util.* +import java.text.SimpleDateFormat +import java.util.* + +internal abstract class HotComicsParser( + context: MangaLoaderContext, + source: MangaParserSource, + domain: String, + pageSize: Int = 24, +) : PagedMangaParser(context, source, pageSize) { + + override val availableSortOrders: Set = EnumSet.of(SortOrder.NEWEST) + + override val configKeyDomain = ConfigKey.Domain(domain) + + override val isMultipleTagsSupported = false + + protected open val mangasUrl = "/genres" + + protected open val onePage = false + + override val headers: Headers = Headers.Builder() + .add("User-Agent", UserAgents.CHROME_DESKTOP) + .build() + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + if (onePage && page > 1) { + return emptyList() + } + + val url = buildString { + append("https://") + append(domain) + when (filter) { + + is MangaListFilter.Search -> { + append("/search?keyword=") + append(filter.query.urlEncoded()) + append("&page=") + append(page) + } + + is MangaListFilter.Advanced -> { + append(mangasUrl) + filter.tags.oneOrThrowIfMany()?.let { + append('/') + append(it.key) + } + + if (!onePage) { + append("?page=") + append(page) + } + } + + null -> { + append("/genres?page=") + append(page) + } + } + } + val tagMap = getOrCreateTagMap() + return parseMangaList(webClient.httpGet(url).parseHtml(), tagMap) + } + + protected open val selectMangas = "li[itemtype*=ComicSeries]:not(.no-comic)" + + protected open fun parseMangaList(doc: Document, tagMap: ArrayMap): List { + + return doc.select(selectMangas).mapNotNull { li -> + val a = li.selectFirstOrThrow("a") + val href = a.attr("href") + + val url = if (href.startsWith("/")) { + "/" + href.removePrefix("/").substringAfter('/') // remove /$lang/url + } else { + href + } + + val tags = li.select(".etc span").mapNotNullToSet { tagMap[it.text()] } + + Manga( + id = generateUid(url), + url = url, + publicUrl = url.toAbsoluteUrl(domain), + coverUrl = li.selectFirst("img")?.src().orEmpty(), + title = li.selectFirst(".title")?.text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + description = li.selectFirst("p[itemprop*=description]")?.text().orEmpty(), + tags = tags, + author = li.selectFirst(".writer")?.text().orEmpty(), + state = if (doc.selectFirst(".ico_fin") != null) { + MangaState.FINISHED + } else { + MangaState.ONGOING + }, + source = source, + isNsfw = a.selectFirst(".ico-18plus") != null, + ) + } + } + + protected open val selectMangaChapters = "#tab-chapter li" + protected open val datePattern = "MMM dd, yyyy" + + override suspend fun getDetails(manga: Manga): Manga { + val mangaUrl = manga.url.toAbsoluteUrl(domain) + val redirectHeaders = Headers.Builder().set("Referer", mangaUrl).build() + val doc = webClient.httpGet(mangaUrl, redirectHeaders).parseHtml() + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + return manga.copy( + description = doc.selectFirst("div.title_content_box h2")?.text() ?: manga.description, + chapters = doc.select(selectMangaChapters) + .mapChapters { i, li -> + val a = li.selectFirstOrThrow("a") + val href = a.attr("href") + val url = if (href.startsWith("/")) { + "/" + href.removePrefix("/").substringAfter('/') // remove /$lang/url + } else if (href.startsWith("javascript")) { + val h = a.attr("onclick").substringAfterLast("href='").substringBefore("'") + "/" + h.removePrefix("/").substringAfter('/') // remove /$lang/url + } else { + href + } + val chapterNum = li.selectFirst(".num")?.text()?.toFloat() ?: (i + 1f) + MangaChapter( + id = generateUid(url), + name = "Chapter : $chapterNum", + number = chapterNum, + volume = 0, + url = url, + scanlator = null, + uploadDate = dateFormat.tryParse(li.selectFirst("time")?.attr("datetime")), + branch = null, + source = source, + ) + }, + ) + } + + + protected open val selectPages = "#viewer-img img" + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return doc.select(selectPages).map { img -> + val url = img.src() ?: img.parseFailed("Image src not found") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + + override suspend fun getAvailableTags(): Set { + val map = getOrCreateTagMap() + val tagSet = ArraySet(map.size) + for (entry in map) { + tagSet.add(entry.value) + } + return tagSet + } + + protected open val mutex = Mutex() + protected open var tagCache: ArrayMap? = null + + protected open val selectTagsList = ".genres-list li:not(.on) a" + + protected open suspend fun getOrCreateTagMap(): ArrayMap = mutex.withLock { + tagCache?.let { return@withLock it } + val doc = webClient.httpGet("https://$domain$mangasUrl").parseHtml() + val tagItems = doc.select(selectTagsList) + val result = ArrayMap(tagItems.size) + for (item in tagItems) { + val title = item.text() + val key = item.attr("href").substringAfterLast('/') + if (key.isNotEmpty() && title.isNotEmpty()) { + result[title] = MangaTag(title = title, key = key, source = source) + } + } + tagCache = result + result + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/de/TooMicsDe.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/de/TooMicsDe.kt new file mode 100644 index 00000000..deca8691 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/de/TooMicsDe.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.de + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSDE", "TooMicsDe", "de") +internal class TooMicsDe(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSDE, "toomics.com/de") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/de/Toomics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/de/Toomics.kt new file mode 100644 index 00000000..63932a1e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/de/Toomics.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.de + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser +import java.util.Locale + +@MangaSourceParser("TOOMICS", "Toomics", "de") +internal class Toomics(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICS, "toomics.top/de") { + override val sourceLocale: Locale = Locale.ENGLISH + override val isSearchSupported = false +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/DayComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/DayComics.kt new file mode 100644 index 00000000..34748c60 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/DayComics.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("DAYCOMICS", "DayComics", "en") +internal class DayComics(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.DAYCOMICS, "daycomics.me/en") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/HotComics.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/HotComics.kt new file mode 100644 index 00000000..5db1ae06 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/HotComics.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("HOTCOMICS", "HotComics", "en") +internal class HotComics(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.HOTCOMICS, "hotcomics.me/en") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/TooMicsEn.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/TooMicsEn.kt new file mode 100644 index 00000000..97b98d43 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/en/TooMicsEn.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSEN", "TooMicsEn", "en") +internal class TooMicsEn(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSEN, "toomics.com/en") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/es/TooMicsEs.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/es/TooMicsEs.kt new file mode 100644 index 00000000..ed1a7d73 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/es/TooMicsEs.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.es + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSES", "TooMicsEs", "es") +internal class TooMicsEs(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSES, "toomics.com/es") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/es/TooMicsEsLa.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/es/TooMicsEsLa.kt new file mode 100644 index 00000000..ca66ab88 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/es/TooMicsEsLa.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.es + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSESLA", "TooMicsEsLa", "es") +internal class TooMicsEsLa(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSESLA, "toomics.com/mx") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/fr/TooMicsFr.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/fr/TooMicsFr.kt new file mode 100644 index 00000000..db308a3b --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/fr/TooMicsFr.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.fr + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSFR", "TooMicsFr", "fr") +internal class TooMicsFr(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSFR, "toomics.com/fr") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/it/TooMicsIt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/it/TooMicsIt.kt new file mode 100644 index 00000000..9e6b24ab --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/it/TooMicsIt.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.it + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSIT", "TooMicsIt", "it") +internal class TooMicsIt(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSIT, "toomics.com/it") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/ja/TooMicsJa.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/ja/TooMicsJa.kt new file mode 100644 index 00000000..c0281c86 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/ja/TooMicsJa.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.ja + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSJA", "TooMicsJa", "ja") +internal class TooMicsJa(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSJA, "toomics.com/ja") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/pt/TooMicsPt.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/pt/TooMicsPt.kt new file mode 100644 index 00000000..53a4bed8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/pt/TooMicsPt.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.pt + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSPT", "TooMicsPt", "pt") +internal class TooMicsPt(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSPT, "toomics.com/por") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/zh/TooMicsSc.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/zh/TooMicsSc.kt new file mode 100644 index 00000000..eeae4221 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/zh/TooMicsSc.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.zh + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSSC", "TooMicsSc", "zh") +internal class TooMicsSc(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSSC, "toomics.com/sc") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/zh/TooMicsTc.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/zh/TooMicsTc.kt new file mode 100644 index 00000000..7c928f0b --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/hotcomics/zh/TooMicsTc.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.parsers.site.hotcomics.zh + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser + +@MangaSourceParser("TOOMICSTC", "TooMicsTc", "zh") +internal class TooMicsTc(context: MangaLoaderContext) : + HotComicsParser(context, MangaParserSource.TOOMICSTC, "toomics.com/tc") { + override val isSearchSupported = false + override val mangasUrl = "/webtoon/ranking/genre" + override val selectMangas = "li > div.visual" + override val selectMangaChapters = "li.normal_ep:has(.coin-type1)" + override val selectTagsList = "div.genre_list li:not(.on) a" + override val selectPages = "div[id^=load_image_] img" + override val onePage = true +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt new file mode 100644 index 00000000..a40ae373 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/HentaiCrot.kt @@ -0,0 +1,126 @@ +package org.koitharu.kotatsu.parsers.site.id + +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("HENTAICROT", "HentaiCrot", "id", ContentType.HENTAI) +internal class HentaiCrot(context: MangaLoaderContext) : + PagedMangaParser(context, MangaParserSource.HENTAICROT, 8) { + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + ) + override val configKeyDomain = ConfigKey.Domain("hentaicrot.com") + + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = buildString { + append("https://") + append(domain) + when (filter) { + is MangaListFilter.Search -> { + append("/page/") + append(page) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + filter.tags.oneOrThrowIfMany()?.let { + append("/category/") + append(it.key) + append('/') + } + + append("/page/") + append(page) + append('/') + } + + null -> { + append("/page/") + append(page) + append('/') + } + } + } + + val doc = webClient.httpGet(url).parseHtml() + return doc.select("div#content article").mapNotNull { div -> + val href = div.selectFirst("a")?.attr("href") ?: return@mapNotNull null + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src()?.replace("-200x285", "").orEmpty(), + title = div.selectFirst("h2")?.text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } + + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain").parseHtml() + return doc.select("ul.megamenu li").mapNotNullToSet { li -> + val key = li.selectFirstOrThrow("a").attr("href").removeSuffix('/').substringAfterLast('/') + val name = li.selectFirstOrThrow("a").text() + MangaTag( + key = key, + title = name, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val fullUrl = manga.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return manga.copy( + description = doc.selectFirst("div.entry-content p")?.text().orEmpty(), + altTitle = doc.selectFirst("div.entry-content ul li:contains(Alternative Name(s) :) em")?.text().orEmpty(), + author = doc.selectFirst("div.entry-content ul li:contains(Artists :) em")?.text().orEmpty(), + state = null, + chapters = listOf( + MangaChapter( + id = manga.id, + name = manga.title, + number = 1f, + volume = 0, + url = fullUrl, + scanlator = null, + uploadDate = 0, + branch = null, + source = source, + ), + ), + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return doc.select(".thumbnail img, figure.gallery-item img").map { img -> + val url = img.src() ?: img.parseFailed("Image src not found") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt new file mode 100644 index 00000000..41c4d25c --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/PixHentai.kt @@ -0,0 +1,126 @@ +package org.koitharu.kotatsu.parsers.site.id + +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("PIXHENTAI", "PixHentai", "id", ContentType.HENTAI) +internal class PixHentai(context: MangaLoaderContext) : + PagedMangaParser(context, MangaParserSource.PIXHENTAI, 8) { + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + ) + override val configKeyDomain = ConfigKey.Domain("pixhentai.com") + + override val isMultipleTagsSupported = false + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + + val url = buildString { + append("https://") + append(domain) + when (filter) { + is MangaListFilter.Search -> { + append("/page/") + append(page) + append("/?s=") + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + filter.tags.oneOrThrowIfMany()?.let { + append("/genre/") + append(it.key) + append('/') + } + + append("/page/") + append(page) + append('/') + } + + null -> { + append("/page/") + append(page) + append('/') + } + } + } + + val doc = webClient.httpGet(url).parseHtml() + return doc.select("div#content article").mapNotNull { div -> + val href = div.selectFirst("a")?.attr("href") ?: return@mapNotNull null + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(div.host ?: domain), + coverUrl = div.selectFirst("img")?.src()?.replace("-200x285", "").orEmpty(), + title = div.selectFirst("h2")?.text().orEmpty(), + altTitle = null, + rating = RATING_UNKNOWN, + tags = emptySet(), + author = null, + state = null, + source = source, + isNsfw = isNsfwSource, + ) + } + } + + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain").parseHtml() + return doc.select("ul.megamenu li").mapNotNullToSet { li -> + val key = li.selectFirstOrThrow("a").attr("href").removeSuffix('/').substringAfterLast('/') + val name = li.selectFirstOrThrow("a").text() + MangaTag( + key = key, + title = name, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val fullUrl = manga.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return manga.copy( + description = doc.selectFirst("div.entry-content p")?.text().orEmpty(), + altTitle = doc.selectFirst("div.entry-content ul li:contains(Alternative Name(s) :) em")?.text().orEmpty(), + author = doc.selectFirst("div.entry-content ul li:contains(Artists :) em")?.text().orEmpty(), + state = null, + chapters = listOf( + MangaChapter( + id = manga.id, + name = manga.title, + number = 1f, + volume = 0, + url = fullUrl, + scanlator = null, + uploadDate = 0, + branch = null, + source = source, + ), + ), + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return doc.select(".thumbnail img, figure.gallery-item img").map { img -> + val url = img.src() ?: img.parseFailed("Image src not found") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt new file mode 100644 index 00000000..a1db55c6 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/IkenParser.kt @@ -0,0 +1,154 @@ +package org.koitharu.kotatsu.parsers.site.iken + +import org.json.JSONObject +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 org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault +import org.koitharu.kotatsu.parsers.util.json.mapJSON +import org.koitharu.kotatsu.parsers.util.json.toJSONList +import java.text.SimpleDateFormat +import java.util.* + +internal abstract class IkenParser( + context: MangaLoaderContext, + source: MangaParserSource, + domain: String, + pageSize: Int = 18, +) : PagedMangaParser(context, source, pageSize) { + + override val availableSortOrders: Set = EnumSet.of(SortOrder.POPULARITY) + + override val availableStates: Set = + EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.UPCOMING) + + override val configKeyDomain = ConfigKey.Domain(domain) + + override val isMultipleTagsSupported = true + + override suspend fun getListPage(page: Int, filter: MangaListFilter?): List { + val url = buildString { + append("https://") + append(domain) + append("/api/query?page=") + append(page) + append("&perPage=18&searchTerm=") + when (filter) { + + is MangaListFilter.Search -> { + append(filter.query.urlEncoded()) + } + + is MangaListFilter.Advanced -> { + + if (filter.tags.isNotEmpty()) { + append("&genreIds=") + appendAll(filter.tags, ",") { it.key } + } + + append("&seriesType=&seriesStatus=") + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "ONGOING" + MangaState.FINISHED -> "COMPLETED" + MangaState.UPCOMING -> "COMING_SOON" + MangaState.ABANDONED -> "DROPPED" + else -> "" + }, + ) + } + } + + null -> {} + } + } + return parseMangaList(webClient.httpGet(url).parseJson()) + } + + protected open fun parseMangaList(json: JSONObject): List { + return json.getJSONArray("posts").mapJSON { + val url = "/series/${it.getString("slug")}" + Manga( + id = it.getLong("id"), + url = url, + publicUrl = url.toAbsoluteUrl(domain), + coverUrl = it.getString("featuredImage").orEmpty(), + title = it.getString("postTitle"), + altTitle = it.getString("alternativeTitles"), + description = it.getString("postContent"), + rating = RATING_UNKNOWN, + tags = emptySet(), + author = it.getString("author"), + state = when (it.getString("seriesStatus")) { + "ONGOING" -> MangaState.ONGOING + "COMPLETED" -> MangaState.FINISHED + "DROPPED", "CANCELLED" -> MangaState.ABANDONED + "COMING_SOON" -> MangaState.UPCOMING + else -> null + }, + source = source, + isNsfw = it.getBooleanOrDefault("hot", false), + ) + } + } + + + protected open val datePattern = "yyyy-MM-dd" + + override suspend fun getDetails(manga: Manga): Manga { + val seriesId = manga.id + val url = "https://$domain/api/chapters?postId=$seriesId&skip=0&take=1000&order=desc&userid=" + val json = webClient.httpGet(url).parseJson().getJSONObject("post") + val slug = json.getString("slug") + val data = json.getJSONArray("chapters").toJSONList() + val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH) + return manga.copy( + chapters = data.mapChapters(reversed = true) { i, it -> + val chapterUrl = + "/series/$slug/${it.getString("slug")}" + MangaChapter( + id = it.getLong("id"), + name = "Chapter : ${it.getInt("number")}", + number = it.getInt("number").toFloat(), + volume = 0, + url = chapterUrl, + scanlator = null, + uploadDate = dateFormat.tryParse(it.getString("createdAt").substringBefore("T")), + branch = null, + source = source, + ) + }, + ) + } + + protected open val selectPages = "main section > img" + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + return doc.select(selectPages).map { img -> + val url = img.src() ?: img.parseFailed("Image src not found") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + + override suspend fun getAvailableTags(): Set { + val doc = webClient.httpGet("https://$domain/series").parseHtml() + return doc.selectLastOrThrow("select").select("option[value]").mapNotNullToSet { + val key = it.attr("value") ?: return@mapNotNullToSet null + MangaTag( + key = key, + title = it.text() ?: key, + source = source, + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/en/MangaGalaxyParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/en/MangaGalaxyParser.kt new file mode 100644 index 00000000..9bb19c95 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/en/MangaGalaxyParser.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.parsers.site.iken.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.iken.IkenParser + +@MangaSourceParser("MANGAGALAXY", "MangaGalaxy", "en") +internal class MangaGalaxyParser(context: MangaLoaderContext) : + IkenParser(context, MangaParserSource.MANGAGALAXY, "mangagalaxy.org") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/en/VortexScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/en/VortexScans.kt new file mode 100644 index 00000000..b0a72b7e --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/iken/en/VortexScans.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.parsers.site.iken.en + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.iken.IkenParser + +@MangaSourceParser("VORTEXSCANS", "VortexScans", "en") +internal class VortexScans(context: MangaLoaderContext) : + IkenParser(context, MangaParserSource.VORTEXSCANS, "vortexscans.org") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/MangaGalaxyParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/MangaGalaxyParser.kt deleted file mode 100644 index ca89c2f5..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/MangaGalaxyParser.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.koitharu.kotatsu.parsers.site.mangareader.en - - -import org.koitharu.kotatsu.parsers.Broken -import org.koitharu.kotatsu.parsers.MangaLoaderContext -import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser - -@Broken // Not dead, changed template -@MangaSourceParser("MANGAGALAXY", "MangaGalaxy", "en") -internal class MangaGalaxyParser(context: MangaLoaderContext) : - MangaReaderParser(context, MangaParserSource.MANGAGALAXY, "mangagalaxy.org", 20, 10) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorld.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorld.kt similarity index 74% rename from src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorld.kt rename to src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorld.kt index 6d8a155c..18e6c87c 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorld.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/MangaWorld.kt @@ -1,8 +1,9 @@ -package org.koitharu.kotatsu.parsers.site.it.mangaworld +package org.koitharu.kotatsu.parsers.site.mangaworld import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.mangaworld.it.MangaWorldParser @MangaSourceParser("MANGAWORLD", "MangaWorld", "it") internal class MangaWorld( diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorldAdult.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/it/MangaWorldAdult.kt similarity index 87% rename from src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorldAdult.kt rename to src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/it/MangaWorldAdult.kt index 3fc1cd40..1ccc4ef2 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorldAdult.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/it/MangaWorldAdult.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.parsers.site.it.mangaworld +package org.koitharu.kotatsu.parsers.site.mangaworld.it import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorldParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/it/MangaWorldParser.kt similarity index 99% rename from src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorldParser.kt rename to src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/it/MangaWorldParser.kt index 7fbfe235..96b252ac 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/it/mangaworld/MangaWorldParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangaworld/it/MangaWorldParser.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.parsers.site.it.mangaworld +package org.koitharu.kotatsu.parsers.site.mangaworld.it import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext 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 deleted file mode 100644 index 2c5e94d9..00000000 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mmrcms/fr/LelScanVf.kt +++ /dev/null @@ -1,13 +0,0 @@ -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.MangaParserSource -import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser -import java.util.* - -@MangaSourceParser("LELSCANVF", "LelScanVf", "fr") -internal class LelScanVf(context: MangaLoaderContext) : - MmrcmsParser(context, MangaParserSource.LELSCANVF, "lelscanfr.com") { - override val sourceLocale: Locale = Locale.ENGLISH -} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/fr/HniScantrad.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/fr/HniScantrad.kt new file mode 100644 index 00000000..bd8c8c58 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/fr/HniScantrad.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.parsers.site.pizzareader.fr + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.pizzareader.PizzaReaderParser + +@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr") +internal class HniScantrad(context: MangaLoaderContext) : + PizzaReaderParser(context, MangaParserSource.HNISCANTRAD, "hni-scantrad.net") { + override val ongoingFilter = "en cours" + override val completedFilter = "terminé" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/it/HastaTeam.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/it/HastaTeam.kt new file mode 100644 index 00000000..9440d29c --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pizzareader/it/HastaTeam.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.parsers.site.pizzareader.it + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.site.pizzareader.PizzaReaderParser + +@MangaSourceParser("HASTATEAM", "HastaTeam", "it") +internal class HastaTeam(context: MangaLoaderContext) : + PizzaReaderParser(context, MangaParserSource.HASTATEAM, "ddt.hastateam.com")