Fix PerfScan : Complete rewrite for new site and API (#9310)

* Refactor PerfScan extension: update base URL, remove unused theme package, and implement new API response models

* Fix Review

* Fix consistency on URL
This commit is contained in:
Aurel 2025-06-19 22:25:57 -04:00 committed by Draff
parent 8a9231c5af
commit 68df5d3b69
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 215 additions and 15 deletions

View File

@ -1,9 +1,8 @@
ext { ext {
extName = 'Perf Scan' extName = 'Perf Scan'
extClass = '.PerfScan' extClass = '.PerfScan'
themePkg = 'heancms' baseUrl = 'https://perf-scan.net'
baseUrl = 'https://perf-scan.fr' extVersionCode = 30
overrideVersionCode = 0
isNsfw = true isNsfw = true
} }

View File

@ -1,24 +1,157 @@
package eu.kanade.tachiyomi.extension.fr.perfscan package eu.kanade.tachiyomi.extension.fr.perfscan
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost 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 keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.Locale
class PerfScan : HeanCms("Perf Scan", "https://perf-scan.fr", "fr") { class PerfScan : HttpSource() {
override val name = "Perf Scan"
override val baseUrl = "https://perf-scan.net"
private val apiUrl = "https://api.perf-scan.net"
override val lang = "fr"
override val supportsLatest = true
override val versionId = 2
override val client: OkHttpClient = super.client.newBuilder() override val client: OkHttpClient = network.cloudflareClient
.rateLimitHost(apiUrl.toHttpUrl(), 1, 2)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
private val chapterNumberFormat = DecimalFormat("#.##")
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/series".toHttpUrl().newBuilder()
.addQueryParameter("ranking", "POPULAR")
.addQueryParameter("rankingType", "YEARLY")
.addQueryParameter("type", "COMIC")
.addQueryParameter("page", page.toString())
.addQueryParameter("take", "24")
.build() .build()
return GET(url, headers)
}
init { override fun popularMangaParse(response: Response): MangasPage {
preferences.run { val result = response.parseAs<PerfScanResponse<List<PerfScanSeries>>>()
if (contains("pref_url_map")) { val mangaList = result.data.map { series ->
edit().remove("pref_url_map").apply() SManga.create().apply {
url = "/series/${series.slug}"
title = series.title
thumbnail_url = "$apiUrl/cdn/${series.thumbnail}"
}
}
val hasNextPage = result.data.size == 24
return MangasPage(mangaList, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/series".toHttpUrl().newBuilder()
.addQueryParameter("type", "COMIC")
.addQueryParameter("page", page.toString())
.addQueryParameter("take", "24")
.addQueryParameter("latestUpdate", "true")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/series".toHttpUrl().newBuilder()
.addQueryParameter("type", "COMIC")
.addQueryParameter("title", query)
.addQueryParameter("page", page.toString())
.addQueryParameter("take", "24")
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun getMangaUrl(manga: SManga): String {
return baseUrl + manga.url
}
// =========================== Manga Details ============================
override fun mangaDetailsRequest(manga: SManga): Request {
val slug = manga.url.substringAfterLast("/")
return GET("$apiUrl/series/$slug", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<PerfScanResponse<PerfScanSeriesDetails>>()
val series = result.data
return SManga.create().apply {
url = "/series/${series.slug}"
title = series.title
author = series.author
artist = series.artist
description = series.description
status = parseStatus(series.statusObject?.name)
genre = series.seriesGenre.joinToString { it.genre.name }
thumbnail_url = "$apiUrl/cdn/${series.thumbnail}"
initialized = true
}
}
private fun parseStatus(status: String?): Int {
return when (status?.lowercase()) {
"en cours" -> SManga.ONGOING
"terminé" -> SManga.COMPLETED
"en pause" -> SManga.ON_HIATUS
"annulé" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ==============================
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<PerfScanResponse<PerfScanSeriesDetails>>()
val seriesSlug = result.data.slug
return result.data.chapters
.sortedByDescending { it.index }
.map { chapter ->
SChapter.create().apply {
val chapterIndex = chapterNumberFormat.format(chapter.index)
url = "/series/$seriesSlug/chapter/$chapterIndex"
name = "Chapitre $chapterIndex" + if (!chapter.title.isNullOrEmpty() && chapter.title != "-") " - ${chapter.title}" else ""
date_upload = dateFormat.tryParse(chapter.createdAt)
scanlator = chapter.season.name
} }
} }
} }
override val useNewQueryEndpoint = true // =============================== Pages ================================
override val useNewChapterEndpoint = true override fun pageListRequest(chapter: SChapter): Request {
return GET(apiUrl + chapter.url, headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<PerfScanResponse<PerfScanPageList>>()
return result.data.content.mapIndexed { index, image ->
Page(index, imageUrl = "$apiUrl/cdn/${image.value}")
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used.")
} }

View File

@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.extension.fr.perfscan
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class PerfScanResponse<T>(
val data: T,
)
@Serializable
class PerfScanSeries(
val slug: String,
val title: String,
val thumbnail: String,
)
@Serializable
class PerfScanSeriesDetails(
val title: String,
val slug: String,
val thumbnail: String,
val description: String? = null,
val author: String? = null,
val artist: String? = null,
@SerialName("SeriesGenre") val seriesGenre: List<PerfScanGenreObject> = emptyList(),
@SerialName("Status") val statusObject: PerfScanStatusObject? = null,
@SerialName("Chapter") val chapters: List<PerfScanChapter> = emptyList(),
)
@Serializable
class PerfScanGenreObject(
@SerialName("Genre") val genre: PerfScanGenre,
)
@Serializable
class PerfScanGenre(
val name: String,
)
@Serializable
class PerfScanStatusObject(
val name: String,
)
@Serializable
class PerfScanChapter(
val id: String,
val index: Float,
val title: String?,
val createdAt: String,
@SerialName("Season") val season: PerfScanSeason,
)
@Serializable
class PerfScanSeason(
val name: String,
)
@Serializable
class PerfScanPageList(
val content: List<PerfScanImage>,
)
@Serializable
class PerfScanImage(
val value: String,
)