From c48ebb937be417e8d09fe82a9bb27cb513698a2f Mon Sep 17 00:00:00 2001 From: devi Date: Sun, 23 Jul 2023 12:29:50 +0200 Subject: [PATCH] add Lugnica Scans --- .../kotatsu/parsers/site/fr/LugnicaScans.kt | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt new file mode 100644 index 00000000..45e0e549 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/fr/LugnicaScans.kt @@ -0,0 +1,215 @@ +package org.koitharu.kotatsu.parsers.site.fr + +import okhttp3.Headers +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.exception.ParseException +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.network.UserAgents +import org.koitharu.kotatsu.parsers.util.* +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.EnumSet +import java.util.Locale + +@MangaSourceParser("LUGNICASCANS", "Lugnica Scans", "fr") +internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.LUGNICASCANS, 10) { + + override val sortOrders: Set = EnumSet.of( + SortOrder.ALPHABETICAL, + SortOrder.UPDATED, + ) + + override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com") + + override val headers: Headers = Headers.Builder() + .add("User-Agent", UserAgents.CHROME_DESKTOP) + .build() + + init { + context.cookieJar.insertCookies( + domain, + "reader_render=continue;", + ) + } + + override suspend fun getFavicons(): Favicons { + return Favicons( + listOf( + Favicon("https://$domain/favicon/favicon-32x32.png", 32, null), + ), + domain, + ) + } + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + + val url = buildString { + append("https://") + append(domain) + if (sortOrder == SortOrder.ALPHABETICAL) { + append("/mangas/") + // just to stop the search of the ALPHABETICAL page because it contains all the manga and has no page function ( to change if there is a better method to stop the search ) + if (page.toString() == "2") { + append(page.toString()) // juste for break + } + } + + if (sortOrder == SortOrder.UPDATED) { + append("/api/manga/home/getlast/") + append(page.toString()) + } + } + val doc = webClient.httpGet(url).parseHtml() + if (sortOrder == SortOrder.UPDATED) { + return doc.select(".last_chapters-element") + .map { div -> + val a = div.selectFirstOrThrow("a.last_chapters-title") + val href = a.attrAsAbsoluteUrl("href") + Manga( + id = generateUid(href), + title = a.text(), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = div.selectFirstOrThrow(".last_chapters-rate").ownText().toFloatOrNull()?.div(5f) + ?: -1f, + isNsfw = false, + coverUrl = div.selectFirstOrThrow(".last_chapters-image img").attrAsAbsoluteUrl("src"), + tags = setOf(), + state = null, + author = null, + source = source, + ) + } + } else { + val root = doc.selectFirstOrThrow(".catalog") + return root.select("div.element") + .map { div -> + val href = div.selectFirstOrThrow("a").attrAsAbsoluteUrl("href") + Manga( + id = generateUid(href), + title = div.select("a.title").text(), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = div.selectFirstOrThrow("div.stats").lastElementChild()?.ownText()?.toFloatOrNull() + ?.div(5f) ?: -1f, + isNsfw = false, + coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"), + tags = setOf(), + state = null, + author = null, + source = source, + ) + } + } + + + } + + override suspend fun getDetails(manga: Manga): Manga { + val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.FRANCE) + + return manga.copy( + altTitle = null, + state = when (root.select("div.manga-tags")[3].select("a").text()) { + "En Cours" -> MangaState.ONGOING + "Fini", "Abandonné", "Licencier" -> MangaState.FINISHED + else -> null + }, + + // Lists the tags but there is no search on the site so it will just come back to the a-z or last list. + tags = root.select("div.manga-tags")[1].select("a").mapNotNullToSet { a -> + MangaTag( + key = a.text(), + title = a.text().toTitleCase(), + source = source, + ) + }, + author = root.select("div.manga-staff").text(), + description = root.selectFirst("div.manga-description div")?.text(), + chapters = root.select("div.manga-chapters_wrapper div.manga-chapter") + .mapChapters(reversed = true) { i, div -> + + val a = div.selectFirstOrThrow("a") + val href = a.attr("href") + val name = a.text() + + val dateText = div.select("span").last()?.text() + MangaChapter( + id = generateUid(href), + name = name, + number = i, + url = href, + scanlator = null, + uploadDate = parseChapterDate( + dateFormat, + dateText, + ), + branch = null, + source = source, + ) + }, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val root = doc.body().selectFirst("#forgen_reader") + ?: throw ParseException("Root not found", fullUrl) + return root.select("img").map { img -> + val url = img.attrAsRelativeUrlOrNull("data-src") ?: img.attrAsRelativeUrlOrNull("src") + ?: img.parseFailed("Image src not found") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + + override suspend fun getTags(): Set = emptySet() + + protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { + val d = date?.lowercase() ?: return 0 + return when { + d.startsWith("il y a") -> parseRelativeDate(date) + + 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("jour", "jours").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis + WordSet("heure", "heures").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis + WordSet("minute", "minutes").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis + WordSet("seconde", "secondes").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis + WordSet("mois").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis + WordSet("année", "années").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis + WordSet("semaine", "semaines").anyWordIn(date) -> cal.apply { + add( + Calendar.WEEK_OF_MONTH, + -number, + ) + }.timeInMillis + + else -> 0 + } + } +}