Migration of PhoenixScans (#8191)
* PhenixScans: Add support for new site * Search, Filter, Genres * Cleaning Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com> Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Status * Variable formatting * Move Filters to a separate file --------- Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com> Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
parent
09d9b33080
commit
9efc599e9c
@ -1,9 +1,8 @@
|
||||
ext {
|
||||
extName = 'PhenixScans'
|
||||
extClass = '.PhenixScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://phenixscans.fr'
|
||||
overrideVersionCode = 2
|
||||
baseUrl = 'https://phenix-scans.com'
|
||||
extVersionCode = 33
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
@ -1,24 +1,198 @@
|
||||
package eu.kanade.tachiyomi.extension.fr.phenixscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 org.jsoup.nodes.Document
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import kotlinx.serialization.json.float
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class PhenixScans : MangaThemesia("PhenixScans", "https://phenixscans.fr", "fr", dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.FRENCH)) {
|
||||
override val seriesAuthorSelector = ".imptdt:contains(Auteur) i, .fmed b:contains(Auteur)+span"
|
||||
override val seriesStatusSelector = ".imptdt:contains(Statut) i"
|
||||
class PhenixScans : HttpSource() {
|
||||
override val baseUrl = "https://phenix-scans.com"
|
||||
private val apiBaseUrl = "https://api.phenix-scans.com"
|
||||
override val lang = "fr"
|
||||
override val name = "Phenix Scans"
|
||||
override val supportsLatest = true
|
||||
override val versionId = 2
|
||||
|
||||
override fun String?.parseStatus(): Int = when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
this.contains("En Cours", ignoreCase = true) -> SManga.ONGOING
|
||||
this.contains("Terminé", ignoreCase = true) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.FRENCH)
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl/front/homepage?section=top", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<TopMangaDto>()
|
||||
|
||||
val mangas = data.top.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
thumbnail_url = "$apiBaseUrl/${it.coverImage}" // Possibility of using ?width=75 and cdn.[...]/?url=
|
||||
url = it.slug
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga =
|
||||
super.mangaDetailsParse(document).apply {
|
||||
status = document.select(seriesStatusSelector).text().parseStatus()
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val apiUrl = "$apiBaseUrl/front/homepage?page=$page§ion=latest&limit=12"
|
||||
|
||||
return GET(apiUrl, headers)
|
||||
}
|
||||
|
||||
private fun parseMangaList(mangaList: List<LatestMangaItemDto>): List<SManga> {
|
||||
return mangaList.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
thumbnail_url = "$apiBaseUrl/${it.coverImage}" // Possibility of using ?width=75
|
||||
url = it.slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<LatestMangaDto>()
|
||||
|
||||
val mangas = parseMangaList(data.latest)
|
||||
|
||||
val hasNextPage = data.pagination.currentPage < data.pagination.totalPages
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isNotEmpty()) {
|
||||
// No limits here
|
||||
val apiUrl = "$apiBaseUrl/front/manga/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("query", query)
|
||||
.build()
|
||||
return GET(apiUrl, headers)
|
||||
}
|
||||
|
||||
val url = "$apiBaseUrl/front/manga".toHttpUrl().newBuilder()
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> {
|
||||
url.addQueryParameter("sort", filter.toUriPart())
|
||||
}
|
||||
is GenreFilter -> {
|
||||
val genres = filter.state
|
||||
.filter { it.state }
|
||||
.map { it.id }
|
||||
|
||||
url.addQueryParameter("genre", genres.joinToString(","))
|
||||
}
|
||||
is TypeFilter -> {
|
||||
url.addQueryParameter("type", filter.toUriPart())
|
||||
}
|
||||
is StatusFilter -> {
|
||||
url.addQueryParameter("status", filter.toUriPart())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("limit", "18") // Be cool on the API
|
||||
url.addQueryParameter("page", page.toString())
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<SearchResultsDto>()
|
||||
|
||||
val hasNextPage = (data.pagination?.page ?: 0) < (data.pagination?.totalPages ?: 0)
|
||||
|
||||
val mangas = parseMangaList(data.mangas)
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList = getGlobalFilterList(apiBaseUrl, client, headers)
|
||||
|
||||
// =============================== Manga ==================================
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val apiUrl = "$apiBaseUrl/front/manga/${manga.url}"
|
||||
|
||||
return GET(apiUrl, headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val data = response.parseAs<MangaDetailDto>()
|
||||
|
||||
return SManga.create().apply {
|
||||
title = data.manga.title
|
||||
thumbnail_url = "$apiBaseUrl/${data.manga.coverImage}"
|
||||
url = data.manga.slug
|
||||
description = data.manga.synopsis
|
||||
status = when (data.manga.status) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Hiatus" -> SManga.ON_HIATUS
|
||||
"Completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
return "$baseUrl/manga/${manga.url}"
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = response.parseAs<MangaDetailDto>()
|
||||
|
||||
return data.chapters.map {
|
||||
SChapter.create().apply {
|
||||
chapter_number = it.number.float
|
||||
date_upload = simpleDateFormat.tryParse(it.createdAt)
|
||||
name = "Chapter ${it.number}"
|
||||
url = "${data.manga.slug}/${it.number}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
val slug = chapter.url.substringBeforeLast("/")
|
||||
val chapterNumber = chapter.url.substringAfterLast("/")
|
||||
return "$baseUrl/manga/$slug/chapitre/$chapterNumber"
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val slug = chapter.url.substringBeforeLast("/")
|
||||
val chapterNumber = chapter.url.substringAfterLast("/")
|
||||
|
||||
val apiUrl = "$apiBaseUrl/front/manga/$slug/chapter/$chapterNumber"
|
||||
|
||||
return GET(apiUrl, headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val data = response.parseAs<ChapterContentDto>()
|
||||
|
||||
return data.chapter.images.mapIndexed { index, url ->
|
||||
Page(index, imageUrl = "$apiBaseUrl/$url")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,98 @@
|
||||
package eu.kanade.tachiyomi.extension.fr.phenixscans
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
// ---------------------------
|
||||
// 1. SEARCH & PAGINATION DTOs
|
||||
// ---------------------------
|
||||
@Serializable
|
||||
class SearchResultsDto(
|
||||
val mangas: List<LatestMangaItemDto>,
|
||||
val pagination: PaginationFilterDto? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PaginationFilterDto(
|
||||
val page: Int,
|
||||
val totalPages: Int,
|
||||
)
|
||||
|
||||
// ---------------------------
|
||||
// 2. MANGA DETAILS & CHAPTER DTOs
|
||||
// ---------------------------
|
||||
@Serializable
|
||||
class MangaInfoDto(
|
||||
val title: String,
|
||||
val coverImage: String? = null,
|
||||
val slug: String,
|
||||
val synopsis: String? = "",
|
||||
val status: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ChapterInfoDto(
|
||||
val number: JsonPrimitive,
|
||||
val createdAt: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaDetailDto(
|
||||
val manga: MangaInfoDto,
|
||||
val chapters: List<ChapterInfoDto>,
|
||||
)
|
||||
|
||||
// ---------------------------
|
||||
// 3. LATEST & TOP MANGA DTOs
|
||||
// ---------------------------
|
||||
@Serializable
|
||||
class LatestMangaItemDto(
|
||||
val title: String,
|
||||
val coverImage: String,
|
||||
val slug: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PaginationDto(
|
||||
val currentPage: Int,
|
||||
val totalPages: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class LatestMangaDto(
|
||||
val pagination: PaginationDto,
|
||||
val latest: List<LatestMangaItemDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class TopMangaDto(
|
||||
val top: List<MangaInfoDto>,
|
||||
)
|
||||
|
||||
// ---------------------------
|
||||
// 4. CHAPTER READING DTOs
|
||||
// ---------------------------
|
||||
@Serializable
|
||||
class ChapterImagesDto(
|
||||
val images: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ChapterContentDto(
|
||||
val chapter: ChapterImagesDto,
|
||||
)
|
||||
|
||||
// ---------------------------
|
||||
// 5. GENRE DTOs
|
||||
// ---------------------------
|
||||
@Serializable
|
||||
class GenreDto(
|
||||
@SerialName("_id") val id: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class GenreListDto(
|
||||
val data: List<GenreDto>,
|
||||
)
|
@ -0,0 +1,112 @@
|
||||
package eu.kanade.tachiyomi.extension.fr.phenixscans
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import keiyoushi.utils.parseAs
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
// ========================= Sorting & Filtering ==========================
|
||||
|
||||
class SortFilter : UriPartFilter(
|
||||
"Sort by",
|
||||
arrayOf(
|
||||
Pair("Alphabetic", "title"),
|
||||
Pair("Rating", "rating"),
|
||||
Pair("Last updated", "updatedAt"),
|
||||
Pair("Chapter number", "chapters"),
|
||||
),
|
||||
)
|
||||
|
||||
class Tag(name: String, val id: String) : Filter.CheckBox(name)
|
||||
|
||||
class GenreFilter(genres: List<Tag>) : Filter.Group<Tag>(
|
||||
"Genres",
|
||||
genres,
|
||||
)
|
||||
|
||||
class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("All status", ""),
|
||||
Pair("Ongoing", "Ongoing"),
|
||||
Pair("On Hiatus", "Hiatus"),
|
||||
Pair("Completed", "Completed"),
|
||||
),
|
||||
)
|
||||
|
||||
class TypeFilter : UriPartFilter(
|
||||
"Type",
|
||||
arrayOf(
|
||||
Pair("Any type", ""),
|
||||
Pair("Manga", "Manga"),
|
||||
Pair("Manhwa", "Manhwa"),
|
||||
Pair("Manhua", "Manhua"),
|
||||
),
|
||||
)
|
||||
|
||||
fun getGlobalFilterList(apiBaseUrl: String, client: OkHttpClient, headers: Headers): FilterList {
|
||||
fetchFilters(apiBaseUrl, client, headers)
|
||||
val filters = mutableListOf<Filter<*>>(
|
||||
Filter.Header("Filters are not compatible with text-based search"),
|
||||
Filter.Separator(),
|
||||
|
||||
Filter.Header("Type"),
|
||||
TypeFilter(),
|
||||
Filter.Separator(),
|
||||
|
||||
Filter.Header("Sort by"),
|
||||
SortFilter(),
|
||||
Filter.Separator(),
|
||||
|
||||
Filter.Header("Status"),
|
||||
StatusFilter(),
|
||||
Filter.Separator(),
|
||||
)
|
||||
|
||||
if (filtersState == FiltersState.FETCHED) {
|
||||
filters += listOf(
|
||||
Filter.Separator(),
|
||||
Filter.Header("Filter by genres"),
|
||||
GenreFilter(genresList),
|
||||
)
|
||||
} else {
|
||||
filters += listOf(
|
||||
Filter.Separator(),
|
||||
Filter.Header("Click on 'Reset' to load missing filters"),
|
||||
)
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
private var genresList: List<Tag> = emptyList()
|
||||
private var fetchFiltersAttempts = 0
|
||||
private var filtersState = FiltersState.NOT_FETCHED
|
||||
|
||||
private fun fetchFilters(apiBaseUrl: String, client: OkHttpClient, headers: Headers) {
|
||||
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
|
||||
filtersState = FiltersState.FETCHING
|
||||
fetchFiltersAttempts++
|
||||
thread {
|
||||
try {
|
||||
val response = client.newCall(GET("$apiBaseUrl/genres", headers)).execute()
|
||||
val filters = response.parseAs<GenreListDto>()
|
||||
|
||||
genresList = filters.data.map { Tag(it.name, it.id) }
|
||||
|
||||
filtersState = FiltersState.FETCHED
|
||||
} catch (e: Throwable) {
|
||||
filtersState = FiltersState.NOT_FETCHED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
|
Loading…
x
Reference in New Issue
Block a user