From a084909507a0d6fb7ccd70686405a40024687213 Mon Sep 17 00:00:00 2001 From: epikaigle444 <61425652+epikaigle444@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:30:54 +0200 Subject: [PATCH] MangaMoins: Add source (#2284) Co-authored-by: epikaigle444 Co-authored-by: Koitharu Co-authored-by: dragonx943 --- .github/summary.yaml | 2 +- .../kotatsu/parsers/site/fr/MangaMoins.kt | 230 ++++++++++++++++++ 2 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt diff --git a/.github/summary.yaml b/.github/summary.yaml index 192d6487..d082d2bf 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1253 \ No newline at end of file +total: 1254 \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt new file mode 100644 index 00000000..5215a367 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/MangaMoins.kt @@ -0,0 +1,230 @@ +package org.koitharu.kotatsu.parsers.site.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.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities +import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.generateUid +import org.koitharu.kotatsu.parsers.util.parseHtml +import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.core.SinglePageMangaParser +import java.util.EnumSet +import java.util.Locale + +@Broken +@MangaSourceParser("MANGAMOINS", "MangaMoins", "fr") +internal class MangaMoins(context: MangaLoaderContext) : + SinglePageMangaParser(context, MangaParserSource.MANGAMOINS) { + + override val configKeyDomain = ConfigKey.Domain("mangamoins.com") + override val availableSortOrders: Set = EnumSet.of(SortOrder.UPDATED) + override val filterCapabilities = MangaListFilterCapabilities() + + override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() + + override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + return listOf( + Manga( + id = generateUid("OP"), + title = "One Piece", + altTitles = emptySet(), + url = "OP", + publicUrl = "https://mangamoins.com/", + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = "https://mangamoins.com/files/scans/OP1161/thumbnail.png", + tags = emptySet(), + state = MangaState.ONGOING, + authors = setOf("Eiichiro Oda"), + source = source, + ), + Manga( + id = generateUid("LCDL"), + title = "Les Carnets de l'Apothicaire", + altTitles = emptySet(), + url = "LCDL", + publicUrl = "https://mangamoins.com/", + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = "https://mangamoins.com/files/scans/LCDL76.2/thumbnail.png", + tags = emptySet(), + state = MangaState.ONGOING, + authors = setOf("Itsuki Nanao", "Nekokurage"), + source = source, + ), + Manga( + id = generateUid("JKM"), + title = "Jujutsu Kaisen Modulo", + altTitles = emptySet(), + url = "JKM", + publicUrl = "https://mangamoins.com/", + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = "https://mangamoins.com/files/scans/JKM1/thumbnail.png", + tags = emptySet(), + state = MangaState.ONGOING, + authors = setOf("Gege Akutami", "Yuji Iwasaki"), + source = source, + ), + Manga( + id = generateUid("OPC"), + title = "One Piece Colo", + altTitles = emptySet(), + url = "OPC", + publicUrl = "https://mangamoins.com/", + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = "https://mangamoins.com/files/scans/OPC1160/thumbnail.png", + tags = emptySet(), + state = MangaState.ONGOING, + authors = setOf("Eiichiro Oda"), + source = source, + ), + Manga( + id = generateUid("LDS"), + title = "L'Atelier des Sorciers", + altTitles = emptySet(), + url = "LDS", + publicUrl = "https://mangamoins.com/", + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = "https://sceneario.com/wp-content/uploads/2023/05/9782811641344-1.jpg", + tags = emptySet(), + state = MangaState.ONGOING, + authors = setOf("Kamome Shirahama"), + source = source, + ) + ) + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet("https://$domain").parseHtml() + val prefix = manga.url + val latestSiteChapterNumber = doc.select("div.sortie a").mapNotNull { a -> + val href = a.attr("href") + if (href.startsWith("?scan=$prefix")) { + href.substringAfter(prefix).toFloatOrNull() + } else { + null + } + }.maxOrNull() + + if (latestSiteChapterNumber == null) return manga + + val cachedChapters = manga.chapters + val lastKnownChapterNumber = cachedChapters?.maxByOrNull { it.number }?.number + + if (lastKnownChapterNumber != null && cachedChapters.isNotEmpty()) { + // INCREMENTAL SCAN (CACHE EXISTS) + if (latestSiteChapterNumber <= lastKnownChapterNumber) { + return manga.copy(chapters = cachedChapters.sortedBy { it.number }) + } + + val newChapters = mutableListOf() + var currentChapterInt = latestSiteChapterNumber.toInt() + + while (currentChapterInt > lastKnownChapterNumber.toInt()) { + val chaptersForGroup = doChecks(prefix, currentChapterInt) + newChapters.addAll(chaptersForGroup) + currentChapterInt-- + } + + val combinedChapters = (cachedChapters + newChapters).distinctBy { it.number } + return manga.copy(chapters = combinedChapters.sortedBy { it.number }) + + } else { + // FULL SCAN (NO CACHE) + val allChapters = mutableListOf() + var currentChapterInt = latestSiteChapterNumber.toInt() + var misses = 0 + + while (currentChapterInt >= 1 && misses < 4) { + val chaptersForGroup = doChecks(prefix, currentChapterInt) + if (chaptersForGroup.isNotEmpty()) { + misses = 0 + allChapters.addAll(chaptersForGroup) + } else { + misses++ + } + currentChapterInt-- + } + return manga.copy(chapters = allChapters.sortedBy { it.number }) + } + } + + private suspend fun doChecks(prefix: String, chapterInt: Int): List { + val foundChapters = mutableListOf() + + // Check for integer chapter + val integerChapter = checkChapter(prefix, chapterInt.toString(), chapterInt.toFloat()) + if (integerChapter != null) { + foundChapters.add(integerChapter) + } + + // Conditionally and sequentially check for decimal chapters + if (prefix == "LCDL") { + // Start from 2 because .1 never exists + for (i in 2..9) { + val decimalNum = chapterInt + (i / 10.0f) + val decimalNumStr = String.format(Locale.US, "%.1f", decimalNum) + val decimalChapter = checkChapter(prefix, decimalNumStr, decimalNum) + if (decimalChapter != null) { + foundChapters.add(decimalChapter) + } else { + break // Stop if there's a gap + } + } + } + return foundChapters + } + + private suspend fun checkChapter(prefix: String, chapterNumStr: String, chapterNumFloat: Float): MangaChapter? { + val thumbUrl = "https://mangamoins.com/files/scans/$prefix$chapterNumStr/thumbnail.png" + return try { + val response = webClient.httpHead(thumbUrl) + if (response.isSuccessful) { + response.close() + val chapterUrl = "https://mangamoins.com/?scan=$prefix$chapterNumStr" + MangaChapter( + id = generateUid(chapterUrl), + title = null, + number = chapterNumFloat, + volume = 0, + url = chapterUrl, + scanlator = null, + uploadDate = 0, + branch = null, + source = source + ) + } else { + response.close() + null + } + } catch (_: Exception) { + null + } + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url).parseHtml() + return doc.select("link[rel=preload][as=image]").map { element -> + val url = element.attr("href").toAbsoluteUrl(domain) + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source + ) + } + } +}