AnimeSama: Add source (#2080)
Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com>
This commit is contained in:
2
.github/summary.yaml
vendored
2
.github/summary.yaml
vendored
@@ -1 +1 @@
|
||||
total: 1248
|
||||
total: 1249
|
||||
@@ -0,0 +1,321 @@
|
||||
package org.koitharu.kotatsu.parsers.site.fr
|
||||
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
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.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.parsers.util.parseRaw
|
||||
import org.koitharu.kotatsu.parsers.util.toRelativeUrl
|
||||
import org.koitharu.kotatsu.parsers.util.urlDecode
|
||||
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||
import java.util.EnumSet
|
||||
|
||||
@MangaSourceParser("ANIMESAMA", "AnimeSama", "fr")
|
||||
internal class AnimeSama(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, source = MangaParserSource.ANIMESAMA, 96) {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("anime-sama.fr")
|
||||
private val baseUrl = "https://$domain"
|
||||
private val cdnUrl = "$baseUrl/s2/scans/"
|
||||
|
||||
override fun getRequestHeaders() = Headers.Builder()
|
||||
.add("Referer", baseUrl)
|
||||
.build()
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
override val filterCapabilities = MangaListFilterCapabilities(
|
||||
isSearchSupported = true,
|
||||
isSearchWithFiltersSupported = true,
|
||||
isMultipleTagsSupported = true,
|
||||
)
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions {
|
||||
val doc = webClient.httpGet("$baseUrl/catalogue").parseHtml()
|
||||
val genres = doc.select("#list_genres label").mapNotNull { labelElement ->
|
||||
val input = labelElement.selectFirst("input[name=genre[]]") ?: return@mapNotNull null
|
||||
val labelText = labelElement.ownText()
|
||||
val value = input.attr("value")
|
||||
MangaTag(
|
||||
key = value,
|
||||
title = labelText,
|
||||
source = source,
|
||||
)
|
||||
}.toSet()
|
||||
|
||||
return MangaListFilterOptions(
|
||||
availableTags = genres,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
val url = buildListUrl(page, order, filter)
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return if (url.toString() == "$baseUrl/") {
|
||||
parseHomePageScans(doc)
|
||||
} else {
|
||||
parseCataloguePage(doc)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildListUrl(page: Int, order: SortOrder, filter: MangaListFilter) = when {
|
||||
filter.query.isNullOrEmpty().not() || filter.tags.isNotEmpty() -> {
|
||||
"$baseUrl/catalogue".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("type[0]", "Scans")
|
||||
.apply {
|
||||
filter.query?.let { addQueryParameter("search", it) }
|
||||
filter.tags.forEach { tag ->
|
||||
addQueryParameter("genre[]", tag.key)
|
||||
}
|
||||
}
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
order == SortOrder.UPDATED && page == 1 -> baseUrl.toHttpUrl()
|
||||
else -> "$baseUrl/catalogue".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("type[0]", "Scans")
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun parseCataloguePage(doc: Document): List<Manga> {
|
||||
return doc.select("#list_catalog > div").mapNotNull { element ->
|
||||
val a = element.selectFirst("a") ?: return@mapNotNull null
|
||||
val title = element.selectFirst("h1")?.text() ?: return@mapNotNull null
|
||||
val cover = element.selectFirst("img")?.attr("src") ?: return@mapNotNull null
|
||||
val href = a.attr("href").removeSuffix("/")
|
||||
|
||||
createManga(
|
||||
href = href,
|
||||
title = title,
|
||||
cover = cover
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHomePageScans(doc: Document): List<Manga> {
|
||||
val mangaList = mutableListOf<Manga>()
|
||||
val seenUrls = mutableSetOf<String>()
|
||||
|
||||
doc.select("#containerAjoutsScans > div").forEach { element ->
|
||||
parseHomePageManga(element)?.let { manga ->
|
||||
if (seenUrls.add(manga.url)) {
|
||||
mangaList.add(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selectors = listOf("#containerSorties", "#containerClassiques", "#containerPepites")
|
||||
doc.select(selectors.joinToString(", ") { "$it > div" }).forEach { element ->
|
||||
parseHorizontalManga(element)?.let { manga ->
|
||||
if (manga.url.contains("/scan") && seenUrls.add(manga.url)) {
|
||||
mangaList.add(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mangaList
|
||||
}
|
||||
|
||||
private fun parseHorizontalManga(element: Element): Manga? {
|
||||
val a = element.selectFirst("a") ?: return null
|
||||
val title = element.selectFirst("h1")?.text() ?: return null
|
||||
val cover = element.selectFirst("img")?.attr("src") ?: return null
|
||||
|
||||
val href = a.attr("href")
|
||||
val types = element.select("p").getOrNull(2)?.text().orEmpty()
|
||||
if (!types.contains("Scans")) return null
|
||||
|
||||
return createManga(href, title, cover)
|
||||
}
|
||||
|
||||
private fun parseHomePageManga(element: Element): Manga? {
|
||||
val a = element.selectFirst("a") ?: return null
|
||||
val title = element.selectFirst("h1")?.text() ?: return null
|
||||
val cover = element.selectFirst("img")?.attr("src") ?: return null
|
||||
val href = a.attr("href").removeSuffix("/scan/vf/")
|
||||
|
||||
return createManga(href, title, cover)
|
||||
}
|
||||
|
||||
private fun createManga(href: String, title: String, cover: String) = Manga(
|
||||
id = generateUid(href),
|
||||
title = normalizeTitle(title),
|
||||
altTitles = emptySet(),
|
||||
url = href.toRelativeUrl(domain),
|
||||
publicUrl = href,
|
||||
rating = RATING_UNKNOWN,
|
||||
contentRating = ContentRating.SAFE,
|
||||
coverUrl = cover,
|
||||
largeCoverUrl = null,
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
authors = emptySet(),
|
||||
description = null,
|
||||
chapters = null,
|
||||
source = source,
|
||||
)
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.publicUrl.toHttpUrl()).parseHtml()
|
||||
|
||||
val description = doc.selectFirst("#sousBlocMiddle > div h2:contains(Synopsis)+p")?.text()
|
||||
val genreText = doc.select("#sousBlocMiddle > div h2:contains(Genres)+a").text()
|
||||
val title = doc.selectFirst("#titreOeuvre")?.text() ?: manga.title
|
||||
val cover = doc.selectFirst("#coverOeuvre")?.attr("src")
|
||||
|
||||
val genres = genreText.split("-", ",")
|
||||
.mapNotNull { genre ->
|
||||
genre.trim().takeIf { it.isNotEmpty() }?.let {
|
||||
MangaTag(key = it, title = it, source = source)
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
|
||||
val chapters = parseChapters(manga.url, title)
|
||||
return manga.copy(
|
||||
title = normalizeTitle(title),
|
||||
description = description,
|
||||
tags = genres,
|
||||
chapters = chapters,
|
||||
coverUrl = cover ?: manga.coverUrl,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun parseChapters(mangaUrl: String, mangaTitle: String): List<MangaChapter> {
|
||||
return try {
|
||||
val subDoc = webClient.httpGet("$baseUrl$mangaUrl/scan/vf").parseHtml()
|
||||
parseChapterListFromJs(subDoc).ifEmpty {
|
||||
parseChaptersFromJsonApi(mangaUrl, mangaTitle)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
parseChaptersFromJsonApi(mangaUrl, mangaTitle)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseChapterListFromJs(doc: Document): List<MangaChapter> {
|
||||
val title = doc.selectFirst("#titreOeuvre")?.text().orEmpty()
|
||||
val chapterUrlBuilder = doc.baseUri().toHttpUrl()
|
||||
.newBuilder()
|
||||
.query(null)
|
||||
.addPathSegment("episodes.js")
|
||||
.addQueryParameter("title", title)
|
||||
|
||||
val jsContent = webClient.httpGet(chapterUrlBuilder.build()).parseRaw()
|
||||
val episodeNumbers = Regex("""eps(\d+)""").findAll(jsContent)
|
||||
.mapNotNull { it.groupValues[1].toIntOrNull() }
|
||||
.distinct()
|
||||
.sorted()
|
||||
.toList()
|
||||
|
||||
return episodeNumbers.mapIndexed { index, number ->
|
||||
val chapterId = index + 1
|
||||
val url = chapterUrlBuilder
|
||||
.addQueryParameter("id", chapterId.toString())
|
||||
.build()
|
||||
.toString()
|
||||
.removePrefix("https://$domain")
|
||||
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
title = "Chapitre $number",
|
||||
number = number.toFloat(),
|
||||
volume = 0,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseChaptersFromJsonApi(
|
||||
mangaUrl: String,
|
||||
mangaTitle: String
|
||||
): List<MangaChapter> {
|
||||
val chapterInfoUrl =
|
||||
"$baseUrl/s2/scans/get_nb_chap_et_img.php?oeuvre=${mangaTitle.urlEncoded()}"
|
||||
|
||||
return try {
|
||||
val chapterInfo = webClient.httpGet(chapterInfoUrl).parseJson()
|
||||
chapterInfo.keys().asSequence()
|
||||
.mapNotNull { it.toIntOrNull() }
|
||||
.sorted()
|
||||
.map { chapterNumber ->
|
||||
val dataUrl = "$mangaUrl#${mangaTitle.urlEncoded()}#$chapterNumber"
|
||||
MangaChapter(
|
||||
id = generateUid(dataUrl),
|
||||
title = "Chapitre $chapterNumber",
|
||||
number = chapterNumber.toFloat(),
|
||||
volume = 0,
|
||||
url = dataUrl,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
}.toList()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val encodedTitle = extractEncodedTitle(chapter.url) ?: return emptyList()
|
||||
val chapterInfoUrl = "$baseUrl/s2/scans/get_nb_chap_et_img.php?oeuvre=$encodedTitle"
|
||||
val decodedTitle = encodedTitle.urlDecode()
|
||||
|
||||
return try {
|
||||
val chapterInfo = webClient.httpGet(chapterInfoUrl).parseJson()
|
||||
val chapterKey = chapter.number.toInt().toString()
|
||||
val pageCount = chapterInfo.optInt(chapterKey)
|
||||
|
||||
if (pageCount == 0) return emptyList()
|
||||
|
||||
(1..pageCount).map { pageIndex ->
|
||||
MangaPage(
|
||||
id = generateUid("${chapter.id}_$pageIndex"),
|
||||
url = "$cdnUrl$decodedTitle/$chapterKey/$pageIndex.jpg",
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeTitle(title: String) = title.replace("’", "'")
|
||||
|
||||
private fun extractEncodedTitle(url: String): String? {
|
||||
return if ('#' in url) {
|
||||
url.split('#').getOrNull(1)
|
||||
} else {
|
||||
"$baseUrl$url".toHttpUrl().queryParameter("title")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user