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
This commit is contained in:
Aurel 2025-06-14 04:49:36 -04:00 committed by Draff
parent fcc13a63ed
commit 0c3f9f2736
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 215 additions and 6 deletions

View File

@ -1,9 +1,8 @@
ext {
extName = 'Raijin Scans'
extClass = '.RaijinScans'
themePkg = 'madara'
baseUrl = 'https://raijinscan.fr'
overrideVersionCode = 4
extVersionCode = 47
isNsfw = false
}

View File

@ -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<LatestUpdatesDto>()
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<SChapter> {
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<Page> {
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.")
}

View File

@ -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,
)