From 0c3f9f27364f853007d9a766b2ab6fad198a8e96 Mon Sep 17 00:00:00 2001 From: Aurel <68382673+Nyantad@users.noreply.github.com> Date: Sat, 14 Jun 2025 04:49:36 -0400 Subject: [PATCH] Fix (Raijin Scans) : Update for site changes (#9172) * Refactor RaijinScans extension: update to HttpSource and add LatestUpdatesDto class for new site * Fix VersionCode * Fix review * Fix version Code --- src/fr/raijinscans/build.gradle | 3 +- .../extension/fr/raijinscans/RaijinScans.kt | 201 +++++++++++++++++- .../fr/raijinscans/RaijinScansDto.kt | 17 ++ 3 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScansDto.kt diff --git a/src/fr/raijinscans/build.gradle b/src/fr/raijinscans/build.gradle index 345aad7c5..bd07a9a3a 100644 --- a/src/fr/raijinscans/build.gradle +++ b/src/fr/raijinscans/build.gradle @@ -1,9 +1,8 @@ ext { extName = 'Raijin Scans' extClass = '.RaijinScans' - themePkg = 'madara' baseUrl = 'https://raijinscan.fr' - overrideVersionCode = 4 + extVersionCode = 47 isNsfw = false } diff --git a/src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScans.kt b/src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScans.kt index e58811eb0..4cc5b3cde 100644 --- a/src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScans.kt +++ b/src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScans.kt @@ -1,9 +1,202 @@ package eu.kanade.tachiyomi.extension.fr.raijinscans -import eu.kanade.tachiyomi.multisrc.madara.Madara -import java.text.SimpleDateFormat +import android.util.Base64 +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.utils.parseAs +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.util.Calendar import java.util.Locale -class RaijinScans : Madara("Raijin Scans", "https://raijinscan.fr", "fr", dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.FRENCH)) { - override val useNewChapterEndpoint = true +class RaijinScans : HttpSource() { + + override val name = "Raijin Scans" + override val baseUrl = "https://raijinscan.fr" + override val lang = "fr" + override val supportsLatest = true + + override val client = network.cloudflareClient + + private var nonce: String? = null + + private val nonceRegex = """"nonce"\s*:\s*"([^"]+)"""".toRegex() + private val numberRegex = """(\d+)""".toRegex() + private val descriptionScriptRegex = """content\.innerHTML = `([\s\S]+?)`;""".toRegex() + + override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/") + + // ============================== Popular =============================== + override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers) + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + val mangas = document.select("section#most-viewed div.swiper-slide.unit").map(::popularMangaFromElement) + return MangasPage(mangas, false) + } + + private fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + val titleElement = element.selectFirst("a.c-title")!! + setUrlWithoutDomain(titleElement.attr("abs:href")) + title = titleElement.text() + thumbnail_url = element.selectFirst("a.poster div.poster-image-wrapper > img")?.attr("abs:src") + } + + // ================================ Recent ================================ + override fun latestUpdatesRequest(page: Int): Request { + if (page == 1) { + return GET(baseUrl, headers) + } + + val currentNonce = nonce + ?: throw Exception("Nonce not found. Please try refreshing by pulling down on the 'Recent' page.") + + val formBody = FormBody.Builder() + .add("action", "load_manga") + .add("page", (page - 1).toString()) + .add("nonce", currentNonce) + .build() + + val xhrHeaders = headersBuilder() + .set("X-Requested-With", "XMLHttpRequest") + .set("Accept", "*/*") + .add("Origin", baseUrl) + .build() + + return POST("$baseUrl/wp-admin/admin-ajax.php", xhrHeaders, formBody) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + if (response.request.method == "GET") { + val document = response.asJsoup() + val scriptElement = document.selectFirst("script#ajax-sh-js-extra")?.data() + nonce = scriptElement?.let { nonceRegex.find(it)?.groupValues?.get(1) } + + val mangas = document.select("section.recently-updated div.unit").map(::searchMangaFromElement) + val hasNextPage = document.selectFirst("a#load-more-manga") != null + return MangasPage(mangas, hasNextPage) + } + + val data = response.parseAs() + + if (!data.success) { + return MangasPage(emptyList(), false) + } + + val documentFragment = Jsoup.parseBodyFragment(data.data.mangaHtml, baseUrl) + val mangas = documentFragment.select("div.unit").map(::searchMangaFromElement) + val hasNextPage = data.data.currentPage < data.data.totalPages + + return MangasPage(mangas, hasNextPage) + } + + // =============================== Search ============================== + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + if (page > 1) addPathSegment("page").addPathSegment(page.toString()) + addQueryParameter("s", query) + addQueryParameter("post_type", "wp-manga") + }.build() + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + val mangas = document.select("div.original.card-lg div.unit").map(::searchMangaFromElement) + val hasNextPage = document.selectFirst("li.page-item:not(.disabled) a[rel=next]") != null + return MangasPage(mangas, hasNextPage) + } + + private fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { + val linkElement = element.selectFirst("div.info > a")!! + setUrlWithoutDomain(linkElement.attr("abs:href")) + title = linkElement.text() + thumbnail_url = element.selectFirst("div.poster-image-wrapper > img")?.attr("abs:src") + } + + // =========================== Manga Details =========================== + private fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + title = document.selectFirst("h1.serie-title")!!.text() + author = document.selectFirst("div.stat-item:has(span:contains(Auteur)) span.stat-value")?.text() + artist = document.selectFirst("div.stat-item:has(span:contains(Artiste)) span.stat-value")?.text() + + val scriptDescription = document.select("script:containsData(content.innerHTML)") + .firstNotNullOfOrNull { descriptionScriptRegex.find(it.data())?.groupValues?.get(1)?.trim() } + description = scriptDescription ?: document.selectFirst("div.description-content")?.text() + + genre = document.select("div.genre-list div.genre-link").joinToString { it.text() } + + thumbnail_url = document.selectFirst("img.cover")?.attr("abs:src") + status = parseStatus(document.selectFirst("div.stat-item:has(span:contains(État du titre)) span.manga")?.text()) + } + + override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup()) + + private fun parseStatus(status: String?): Int { + return when { + status == null -> SManga.UNKNOWN + status.contains("En cours", ignoreCase = true) -> SManga.ONGOING + status.contains("Terminé", ignoreCase = true) -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + } + + // ========================= Chapter List ========================== + override fun chapterListParse(response: Response): List { + return response.asJsoup().select("ul.scroll-sm li.item").map(::chapterFromElement) + } + + private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + val link = element.selectFirst("a")!! + setUrlWithoutDomain(link.attr("abs:href")) + name = link.attr("title").trim() + + date_upload = parseRelativeDateString(link.selectFirst("> span:nth-of-type(2)")?.text()) + } + + private fun parseRelativeDateString(date: String?): Long { + if (date == null) return 0L + + val lcDate = date.lowercase(Locale.FRENCH).trim() + val cal = Calendar.getInstance() + val number = numberRegex.find(lcDate)?.value?.toIntOrNull() + + return when { + "aujourd'hui" in lcDate -> cal.timeInMillis + "hier" in lcDate -> cal.apply { add(Calendar.DAY_OF_MONTH, -1) }.timeInMillis + number != null -> when { + ("h" in lcDate || "heure" in lcDate) && "chapitre" !in lcDate -> cal.apply { add(Calendar.HOUR_OF_DAY, -number) }.timeInMillis + "min" in lcDate -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis + "jour" in lcDate || lcDate.endsWith("j") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis + "semaine" in lcDate -> cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis + "mois" in lcDate || (lcDate.endsWith("m") && "min" !in lcDate) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis + "an" in lcDate -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis + else -> 0L + } + else -> 0L + } + } + + // ========================== Page List ============================= + override fun pageListParse(response: Response): List { + return response.asJsoup().select("div.protected-image-data").mapIndexed { index, element -> + val encodedUrl = element.attr("data-src") + val imageUrl = String(Base64.decode(encodedUrl, Base64.DEFAULT)) + Page(index, imageUrl = imageUrl) + } + } + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used.") } diff --git a/src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScansDto.kt b/src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScansDto.kt new file mode 100644 index 000000000..96b11ac3b --- /dev/null +++ b/src/fr/raijinscans/src/eu/kanade/tachiyomi/extension/fr/raijinscans/RaijinScansDto.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.extension.fr.raijinscans + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class LatestUpdatesDto( + val success: Boolean, + val data: LatestUpdatesDataDto, +) + +@Serializable +class LatestUpdatesDataDto( + @SerialName("manga_html") val mangaHtml: String, + @SerialName("current_page") val currentPage: Int, + @SerialName("total_pages") val totalPages: Int, +)