|
|
|
@ -6,15 +6,33 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
|
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
|
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
|
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
|
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
|
|
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
|
|
|
|
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
|
|
|
|
import org.koitharu.kotatsu.parsers.model.*
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
|
|
import org.koitharu.kotatsu.parsers.util.*
|
|
|
|
import org.koitharu.kotatsu.parsers.model.Manga
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.WordSet
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.generateUid
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.parseSafe
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
|
|
|
import java.text.DateFormat
|
|
|
|
import java.text.DateFormat
|
|
|
|
import java.text.SimpleDateFormat
|
|
|
|
import java.text.SimpleDateFormat
|
|
|
|
import java.util.*
|
|
|
|
import java.util.Calendar
|
|
|
|
|
|
|
|
import java.util.EnumSet
|
|
|
|
|
|
|
|
|
|
|
|
@MangaSourceParser("PHENIXSCANS", "PhenixScans", "fr")
|
|
|
|
@MangaSourceParser("PHENIXSCANS", "PhenixScans", "fr")
|
|
|
|
internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
@ -22,6 +40,8 @@ internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
|
|
|
|
|
|
|
|
override val configKeyDomain = ConfigKey.Domain("phenix-scans.com")
|
|
|
|
override val configKeyDomain = ConfigKey.Domain("phenix-scans.com")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private val apiBaseUrl = "https://phenix-scans.com/api"
|
|
|
|
|
|
|
|
|
|
|
|
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
|
|
|
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
|
|
|
super.onCreateConfig(keys)
|
|
|
|
super.onCreateConfig(keys)
|
|
|
|
keys.add(userAgentKey)
|
|
|
|
keys.add(userAgentKey)
|
|
|
|
@ -42,7 +62,7 @@ internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
|
|
|
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
|
|
|
availableTags = fetchAvailableTags(),
|
|
|
|
availableTags = fetchAvailableTags(),
|
|
|
|
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED),
|
|
|
|
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED),
|
|
|
|
availableContentTypes = EnumSet.of(
|
|
|
|
availableContentTypes = EnumSet.of(
|
|
|
|
ContentType.MANGA,
|
|
|
|
ContentType.MANGA,
|
|
|
|
ContentType.MANHWA,
|
|
|
|
ContentType.MANHWA,
|
|
|
|
@ -69,12 +89,13 @@ internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
append("?page=")
|
|
|
|
append("?page=")
|
|
|
|
append(page.toString())
|
|
|
|
append(page.toString())
|
|
|
|
append("&limit=18&sort=")
|
|
|
|
append("&limit=18&sort=")
|
|
|
|
|
|
|
|
append(
|
|
|
|
when (order) {
|
|
|
|
when (order) {
|
|
|
|
SortOrder.POPULARITY -> append("rating")
|
|
|
|
SortOrder.POPULARITY -> "rating"
|
|
|
|
SortOrder.UPDATED -> append("updatedAt")
|
|
|
|
SortOrder.ALPHABETICAL -> "title"
|
|
|
|
SortOrder.ALPHABETICAL -> append("title")
|
|
|
|
else -> "updatedAt"
|
|
|
|
else -> append("updatedAt")
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (filter.tags.isNotEmpty()) {
|
|
|
|
if (filter.tags.isNotEmpty()) {
|
|
|
|
append("&genre=")
|
|
|
|
append("&genre=")
|
|
|
|
@ -108,7 +129,6 @@ internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return parseMangaList(webClient.httpGet(url).parseJson().getJSONArray("mangas"))
|
|
|
|
return parseMangaList(webClient.httpGet(url).parseJson().getJSONArray("mangas"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -119,17 +139,17 @@ internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
id = generateUid(j.getString("_id")),
|
|
|
|
id = generateUid(j.getString("_id")),
|
|
|
|
title = j.getString("title"),
|
|
|
|
title = j.getString("title"),
|
|
|
|
altTitles = emptySet(),
|
|
|
|
altTitles = emptySet(),
|
|
|
|
url = slug,
|
|
|
|
url = "/manga/$slug",
|
|
|
|
publicUrl = "https://$domain/manga/$slug",
|
|
|
|
publicUrl = "https://$domain/manga/$slug",
|
|
|
|
rating = j.getFloatOrDefault("averageRating", RATING_UNKNOWN * 10f) / 10f,
|
|
|
|
rating = j.getFloatOrDefault("averageRating", RATING_UNKNOWN * 10f) / 10f,
|
|
|
|
contentRating = null,
|
|
|
|
contentRating = null,
|
|
|
|
description = j.getStringOrNull("synopsis"),
|
|
|
|
description = j.getStringOrNull("synopsis"),
|
|
|
|
coverUrl = "https://cdn.phenix-scans.com/?url=https://api.phenix-scans.com/" + j.getString("coverImage") + "&output=webp&w=400&ll",
|
|
|
|
coverUrl = "$apiBaseUrl/${j.getString("coverImage")}",
|
|
|
|
tags = emptySet(),
|
|
|
|
tags = emptySet(),
|
|
|
|
state = when (j.getStringOrNull("status")) {
|
|
|
|
state = when (j.getStringOrNull("status")) {
|
|
|
|
"Ongoing" -> MangaState.ONGOING
|
|
|
|
"Ongoing" -> MangaState.ONGOING
|
|
|
|
"Completed" -> MangaState.FINISHED
|
|
|
|
"Completed" -> MangaState.FINISHED
|
|
|
|
"Hiatus" -> MangaState.FINISHED
|
|
|
|
"Hiatus" -> MangaState.PAUSED
|
|
|
|
else -> null
|
|
|
|
else -> null
|
|
|
|
},
|
|
|
|
},
|
|
|
|
authors = emptySet(),
|
|
|
|
authors = emptySet(),
|
|
|
|
@ -138,48 +158,89 @@ internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private val dateFormat = SimpleDateFormat("d MMM yyyy", sourceLocale)
|
|
|
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", sourceLocale)
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
|
|
|
val mangaUrl = "https://$domain/manga/${manga.url}"
|
|
|
|
val apiUrl = "$apiBaseUrl/front${manga.url}"
|
|
|
|
val doc = webClient.httpGet(mangaUrl).parseHtml()
|
|
|
|
val response = webClient.httpGet(apiUrl).parseJson()
|
|
|
|
|
|
|
|
val mangaData = response.getJSONObject("manga")
|
|
|
|
|
|
|
|
val chaptersArray = response.optJSONArray("chapters") ?: JSONArray()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val coverImage = mangaData.getString("coverImage")
|
|
|
|
|
|
|
|
val coverUrl = "$apiBaseUrl/$coverImage"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val chaptersMap = LinkedHashMap<String, Pair<Long, MangaChapter>>(chaptersArray.length())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (i in 0 until chaptersArray.length()) {
|
|
|
|
|
|
|
|
val chapterJson = chaptersArray.getJSONObject(i)
|
|
|
|
|
|
|
|
val number = chapterJson.optString("number")
|
|
|
|
|
|
|
|
val createdAt = chapterJson.optString("createdAt")
|
|
|
|
|
|
|
|
val uploadDate = parseChapterDate(dateFormat, createdAt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val chapter = MangaChapter(
|
|
|
|
|
|
|
|
id = generateUid(chapterJson.getString("_id")),
|
|
|
|
|
|
|
|
title = "Chapitre $number",
|
|
|
|
|
|
|
|
number = number.toFloatOrNull() ?: (i + 1f),
|
|
|
|
|
|
|
|
volume = 0,
|
|
|
|
|
|
|
|
url = "${manga.url}/$number",
|
|
|
|
|
|
|
|
scanlator = null,
|
|
|
|
|
|
|
|
uploadDate = uploadDate,
|
|
|
|
|
|
|
|
branch = null,
|
|
|
|
|
|
|
|
source = source,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Keep the most recent version of duplicate chapters
|
|
|
|
|
|
|
|
val existing = chaptersMap[number]
|
|
|
|
|
|
|
|
if (existing == null || uploadDate > existing.first) {
|
|
|
|
|
|
|
|
chaptersMap[number] = uploadDate to chapter
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val uniqueChapters = chaptersMap.values
|
|
|
|
|
|
|
|
.map { it.second }
|
|
|
|
|
|
|
|
.sortedBy { it.number }
|
|
|
|
|
|
|
|
|
|
|
|
manga.copy(
|
|
|
|
manga.copy(
|
|
|
|
tags = doc.select("div.project__content-tags a").mapToSet { a ->
|
|
|
|
title = mangaData.getString("title"),
|
|
|
|
|
|
|
|
altTitles = mangaData.optJSONArray("alternativeTitles")?.let { altArray ->
|
|
|
|
|
|
|
|
(0 until altArray.length()).mapTo(mutableSetOf()) { altArray.getString(it) }
|
|
|
|
|
|
|
|
} ?: emptySet(),
|
|
|
|
|
|
|
|
tags = mangaData.optJSONArray("genres").mapJSONToSet { genreJson ->
|
|
|
|
MangaTag(
|
|
|
|
MangaTag(
|
|
|
|
key = a.attr("href").removeSuffix('/').substringAfterLast("tag="),
|
|
|
|
key = genreJson.getString("_id"),
|
|
|
|
title = a.text().toTitleCase(),
|
|
|
|
title = genreJson.getString("name").toTitleCase(),
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
chapters = doc.select(" div.project__chapters a.project__chapter")
|
|
|
|
description = mangaData.getStringOrNull("synopsis"),
|
|
|
|
.mapChapters(reversed = true) { i, a ->
|
|
|
|
rating = mangaData.getFloatOrDefault("averageRating", RATING_UNKNOWN * 10f) / 10f,
|
|
|
|
val href = a.attrAsRelativeUrl("href")
|
|
|
|
coverUrl = coverUrl,
|
|
|
|
val name = a.selectFirst(".project__chapter-title")?.textOrNull()
|
|
|
|
state = when (mangaData.getStringOrNull("status")) {
|
|
|
|
val dateText = a.selectFirst(".project__chapter-date")?.textOrNull()
|
|
|
|
"Ongoing" -> MangaState.ONGOING
|
|
|
|
MangaChapter(
|
|
|
|
"Completed" -> MangaState.FINISHED
|
|
|
|
id = generateUid(href),
|
|
|
|
"Hiatus" -> MangaState.PAUSED
|
|
|
|
title = name,
|
|
|
|
else -> null
|
|
|
|
number = i.toFloat(),
|
|
|
|
|
|
|
|
volume = 0,
|
|
|
|
|
|
|
|
url = href,
|
|
|
|
|
|
|
|
scanlator = null,
|
|
|
|
|
|
|
|
uploadDate = parseChapterDate(
|
|
|
|
|
|
|
|
dateFormat,
|
|
|
|
|
|
|
|
dateText,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
branch = null,
|
|
|
|
|
|
|
|
source = source,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
authors = mangaData.optJSONArray("authors")?.let { authorsArray ->
|
|
|
|
|
|
|
|
(0 until authorsArray.length()).mapTo(mutableSetOf()) { authorsArray.getString(it) }
|
|
|
|
|
|
|
|
} ?: emptySet(),
|
|
|
|
|
|
|
|
chapters = uniqueChapters,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
|
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
|
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
|
|
|
val slug = chapter.url.substringAfterLast("manga/").substringBeforeLast("/")
|
|
|
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
|
|
|
val chapterNumber = chapter.url.substringAfterLast("/")
|
|
|
|
return doc.select("div.chapter-images img.chapter-image").map { img ->
|
|
|
|
|
|
|
|
val url = img.requireSrc()
|
|
|
|
val apiUrl = "$apiBaseUrl/front/manga/$slug/chapter/$chapterNumber"
|
|
|
|
|
|
|
|
val response = webClient.httpGet(apiUrl).parseJson()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val chapterData = response.getJSONObject("chapter")
|
|
|
|
|
|
|
|
val imagesArray = chapterData.getJSONArray("images")
|
|
|
|
|
|
|
|
println(imagesArray)
|
|
|
|
|
|
|
|
return (0 until imagesArray.length()).map { i ->
|
|
|
|
|
|
|
|
val imageUrl = imagesArray.getString(i)
|
|
|
|
|
|
|
|
val url = "/api/$imageUrl"
|
|
|
|
MangaPage(
|
|
|
|
MangaPage(
|
|
|
|
id = generateUid(url),
|
|
|
|
id = generateUid(url),
|
|
|
|
url = url,
|
|
|
|
url = url,
|
|
|
|
@ -190,8 +251,8 @@ internal class PhenixscansParser(context: MangaLoaderContext) :
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun fetchAvailableTags(): Set<MangaTag> {
|
|
|
|
private suspend fun fetchAvailableTags(): Set<MangaTag> {
|
|
|
|
val json = webClient.httpGet("https://api.$domain/front/manga?page=1&limit=18&sort=updatedAt").parseJson()
|
|
|
|
val json = webClient.httpGet("$apiBaseUrl/genres").parseJson()
|
|
|
|
.getJSONArray("genres")
|
|
|
|
.getJSONArray("data")
|
|
|
|
return json.mapJSONToSet {
|
|
|
|
return json.mapJSONToSet {
|
|
|
|
MangaTag(
|
|
|
|
MangaTag(
|
|
|
|
key = it.getString("_id"),
|
|
|
|
key = it.getString("_id"),
|
|
|
|
|