Merge pull request #503 from mangadventure/master
Add MangAdventure sources (Arc-Relight, Assorted Scans)master
commit
7c871edbca
@ -1,125 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.foolslide.en
|
|
||||||
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
|
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
|
||||||
|
|
||||||
@MangaSourceParser("ASSORTEDSCANS", "AssortedScans", "en")
|
|
||||||
internal class AssortedScans(context: MangaLoaderContext) :
|
|
||||||
FoolSlideParser(context, MangaSource.ASSORTEDSCANS, "assortedscans.com", 56) {
|
|
||||||
|
|
||||||
override val listUrl = "reader/"
|
|
||||||
override val pagination = false
|
|
||||||
override val selectInfo = "div.#series-info"
|
|
||||||
|
|
||||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
|
||||||
if (page > 1) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = buildString {
|
|
||||||
append("https://")
|
|
||||||
append(domain)
|
|
||||||
append('/')
|
|
||||||
when (filter) {
|
|
||||||
is MangaListFilter.Search -> {
|
|
||||||
append(searchUrl)
|
|
||||||
append("?q=")
|
|
||||||
append(filter.query.urlEncoded())
|
|
||||||
}
|
|
||||||
|
|
||||||
is MangaListFilter.Advanced -> {
|
|
||||||
append(listUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
null -> append(listUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val doc = webClient.httpGet(url).parseHtml()
|
|
||||||
return doc.select("section.series, tr.result").map { div ->
|
|
||||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
|
||||||
Manga(
|
|
||||||
id = generateUid(href),
|
|
||||||
url = href,
|
|
||||||
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
|
||||||
coverUrl = div.selectFirst("img")?.src().orEmpty(),// in search no img
|
|
||||||
title = div.selectFirstOrThrow("a").text().orEmpty(),
|
|
||||||
altTitle = null,
|
|
||||||
rating = RATING_UNKNOWN,
|
|
||||||
tags = emptySet(),
|
|
||||||
author = null,
|
|
||||||
state = null,
|
|
||||||
source = source,
|
|
||||||
isNsfw = isNsfwSource,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
|
||||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
|
||||||
val testAdultPage = webClient.httpGet(fullUrl).parseHtml()
|
|
||||||
val doc = if (testAdultPage.selectFirst("div.info form") != null) {
|
|
||||||
webClient.httpPost(fullUrl, "adult=true").parseHtml()
|
|
||||||
} else {
|
|
||||||
testAdultPage
|
|
||||||
}
|
|
||||||
val chapters = getChapters(doc)
|
|
||||||
val desc = doc.getElementById("series-desc")?.selectFirst("div")?.html()
|
|
||||||
val alt = doc.getElementById("series-aliases")?.selectFirst("div.alias")?.text()
|
|
||||||
val author = doc.getElementById("series-authors")?.selectFirst("div.author")?.text()
|
|
||||||
val state = doc.getElementById("series-status")?.selectFirst("span")?.text()
|
|
||||||
manga.copy(
|
|
||||||
coverUrl = doc.selectFirst(".cover")?.src() ?: manga.coverUrl,
|
|
||||||
description = desc,
|
|
||||||
altTitle = alt,
|
|
||||||
author = author,
|
|
||||||
state = when (state) {
|
|
||||||
"Ongoing" -> MangaState.ONGOING
|
|
||||||
"Completed" -> MangaState.FINISHED
|
|
||||||
"Canceled" -> MangaState.ABANDONED
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
chapters = chapters,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChapters(doc: Document): List<MangaChapter> {
|
|
||||||
return doc.body().select("div.chapter").mapChapters(reversed = true) { i, div ->
|
|
||||||
val a = div.selectFirstOrThrow("a")
|
|
||||||
val href = a.attrAsRelativeUrl("href")
|
|
||||||
MangaChapter(
|
|
||||||
id = generateUid(href),
|
|
||||||
name = a.text(),
|
|
||||||
number = i + 1,
|
|
||||||
url = href,
|
|
||||||
uploadDate = 0,
|
|
||||||
source = source,
|
|
||||||
scanlator = null,
|
|
||||||
branch = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
||||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
|
||||||
return doc.body().select(".page-list .dropdown-list li a").map { a ->
|
|
||||||
val url = a.attr("href").toRelativeUrl(domain)
|
|
||||||
MangaPage(
|
|
||||||
id = generateUid(url),
|
|
||||||
url = url,
|
|
||||||
preview = null,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String {
|
|
||||||
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
|
|
||||||
val root = doc.body()
|
|
||||||
return root.requireElementById("page-image").attr("src") ?: doc.parseFailed("Page image not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.mangadventure
|
||||||
|
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.*
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
internal abstract class MangAdventureParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaSource,
|
||||||
|
domain: String,
|
||||||
|
pageSize: Int = 25
|
||||||
|
) : PagedMangaParser(context, source, pageSize) {
|
||||||
|
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||||
|
|
||||||
|
override val headers: Headers =
|
||||||
|
Headers.Builder().add("User-Agent", UserAgents.KOTATSU).build()
|
||||||
|
|
||||||
|
override val availableStates: Set<MangaState> = EnumSet.of(
|
||||||
|
MangaState.ONGOING,
|
||||||
|
MangaState.FINISHED,
|
||||||
|
MangaState.ABANDONED,
|
||||||
|
MangaState.PAUSED
|
||||||
|
)
|
||||||
|
|
||||||
|
override val availableContentRating: Set<ContentRating> =
|
||||||
|
EnumSet.of(ContentRating.SAFE)
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.ALPHABETICAL,
|
||||||
|
SortOrder.ALPHABETICAL_DESC,
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
SortOrder.POPULARITY
|
||||||
|
)
|
||||||
|
|
||||||
|
override val defaultSortOrder = SortOrder.ALPHABETICAL
|
||||||
|
|
||||||
|
override val isTagsExclusionSupported = true
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
|
val url = apiUrl.addEncodedPathSegment("series")
|
||||||
|
.addEncodedQueryParameter("limit", pageSize.toString())
|
||||||
|
.addEncodedQueryParameter("page", page.toString())
|
||||||
|
when (filter) {
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
url.addQueryParameter("title", filter.query)
|
||||||
|
}
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
url.addQueryParameter(
|
||||||
|
"categories",
|
||||||
|
buildString {
|
||||||
|
if (filter.tags.isNotEmpty() && filter.tagsExclude.isNotEmpty()) {
|
||||||
|
filter.tags.joinTo(this, ",", postfix = ",") { it.key }
|
||||||
|
filter.tagsExclude.joinTo(this, ",") { "-" + it.key }
|
||||||
|
} else if (filter.tags.isNotEmpty()) {
|
||||||
|
filter.tags.joinTo(this, ",") { it.key }
|
||||||
|
} else if (filter.tagsExclude.isNotEmpty()) {
|
||||||
|
filter.tagsExclude.joinTo(this, ",") { "-" + it.key }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
when (filter.states.oneOrThrowIfMany()) {
|
||||||
|
null -> url.addEncodedQueryParameter("status", "any")
|
||||||
|
MangaState.ONGOING -> url.addEncodedQueryParameter("status", "ongoing")
|
||||||
|
MangaState.FINISHED -> url.addEncodedQueryParameter("status", "completed")
|
||||||
|
MangaState.ABANDONED -> url.addEncodedQueryParameter("status", "canceled")
|
||||||
|
MangaState.PAUSED -> url.addEncodedQueryParameter("status", "hiatus")
|
||||||
|
else -> throw IllegalArgumentException(ERROR_UNSUPPORTED_STATE)
|
||||||
|
}
|
||||||
|
when (filter.sortOrder) {
|
||||||
|
SortOrder.ALPHABETICAL -> url.addEncodedQueryParameter("sort", "title")
|
||||||
|
SortOrder.ALPHABETICAL_DESC -> url.addEncodedQueryParameter("sort", "-title")
|
||||||
|
SortOrder.UPDATED -> url.addEncodedQueryParameter("sort", "-latest_upload")
|
||||||
|
SortOrder.POPULARITY -> url.addEncodedQueryParameter("sort", "-views")
|
||||||
|
else -> throw IllegalArgumentException(ERROR_UNSUPPORTED_SORT_ORDER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
return runCatchingCancellable { getManga(url.get()) }.getOrElse {
|
||||||
|
if (it is NotFoundException) emptyList() else throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val url = apiUrl.addEncodedPathSegment("series").addPathSegment(manga.slug)
|
||||||
|
val details = requireNotNull(url.get())
|
||||||
|
val chapters = url.addEncodedPathSegment("chapters")
|
||||||
|
.addEncodedQueryParameter("date_format", "timestamp").get()
|
||||||
|
return manga.copy(
|
||||||
|
description = details.getStringOrNull("description"),
|
||||||
|
altTitle = details.getJSONArray("aliases").joinToString(),
|
||||||
|
author = buildString {
|
||||||
|
val authors = details.getJSONArray("authors")
|
||||||
|
val artists = details.getJSONArray("artists")
|
||||||
|
if (authors.length() > 0 && artists.length() > 0) {
|
||||||
|
authors.joinTo(this, postfix = ", ")
|
||||||
|
artists.joinTo(this)
|
||||||
|
} else if (authors.length() > 0) {
|
||||||
|
authors.joinTo(this)
|
||||||
|
} else if (artists.length() > 0) {
|
||||||
|
artists.joinTo(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags = details.getJSONArray("categories").mapTo(HashSet()) {
|
||||||
|
val name = it as String
|
||||||
|
MangaTag(name, name, source)
|
||||||
|
},
|
||||||
|
state = when (details.getString("status")) {
|
||||||
|
"ongoing" -> MangaState.ONGOING
|
||||||
|
"completed" -> MangaState.FINISHED
|
||||||
|
"canceled" -> MangaState.ABANDONED
|
||||||
|
"hiatus" -> MangaState.PAUSED
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
chapters = chapters?.optJSONArray("results")?.mapJSON {
|
||||||
|
val number = it.getFloat("number")
|
||||||
|
MangaChapter(
|
||||||
|
id = it.getLong("id"),
|
||||||
|
name = it.getString("full_title"),
|
||||||
|
number = number.toInt(),
|
||||||
|
volume = it.getIntOrDefault("volume", 0),
|
||||||
|
url = it.getString("url"),
|
||||||
|
scanlator = it.getJSONArray("groups").joinToString(),
|
||||||
|
uploadDate = it.getString("published").toLong(),
|
||||||
|
branch = null,
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val url = apiUrl.addEncodedPathSegment("chapters")
|
||||||
|
.addEncodedPathSegment(chapter.id.toString())
|
||||||
|
.addEncodedPathSegment("pages")
|
||||||
|
.addEncodedQueryParameter("track", "true")
|
||||||
|
return url.get()?.optJSONArray("results")?.mapJSON {
|
||||||
|
MangaPage(it.getLong("id"), it.getString("image"), null, source)
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPageUrl(page: MangaPage) = page.url
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
|
val url = apiUrl.addEncodedPathSegment("categories")
|
||||||
|
return url.get()?.optJSONArray("results")?.mapJSONToSet {
|
||||||
|
val name = it.getString("name")
|
||||||
|
MangaTag(name, name, source)
|
||||||
|
} ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// webp favicons are not supported
|
||||||
|
override suspend fun getFavicons() =
|
||||||
|
Favicons(listOf(Favicon("https://$domain/media/logo.png", 512, "")), domain)
|
||||||
|
|
||||||
|
/* Get a list of manga from the given [JSONObject]. */
|
||||||
|
protected fun getManga(json: JSONObject?): List<Manga> {
|
||||||
|
return json?.optJSONArray("results")?.mapJSONNotNull {
|
||||||
|
// exclude licensed series
|
||||||
|
if (it.opt("chapters") == JSONObject.NULL)
|
||||||
|
return@mapJSONNotNull null
|
||||||
|
val path = it.getString("url")
|
||||||
|
val publicUrl = urlBuilder().addEncodedPathSegments(path).toString()
|
||||||
|
Manga(
|
||||||
|
id = generateUid(it.getString("slug")),
|
||||||
|
title = it.getString("title"),
|
||||||
|
altTitle = null,
|
||||||
|
url = path,
|
||||||
|
publicUrl = publicUrl,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
isNsfw = false,
|
||||||
|
coverUrl = it.getString("cover"),
|
||||||
|
tags = emptySet(),
|
||||||
|
state = null,
|
||||||
|
author = null,
|
||||||
|
source = source
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val apiUrl: HttpUrl.Builder
|
||||||
|
get() = urlBuilder().addEncodedPathSegments("api/v2")
|
||||||
|
|
||||||
|
// /reader/{slug}/
|
||||||
|
private inline val Manga.slug: String
|
||||||
|
get() = url.substring(8, url.length - 1)
|
||||||
|
|
||||||
|
protected suspend inline fun HttpUrl.Builder.get() =
|
||||||
|
webClient.httpGet(build()).body?.string()?.let(::JSONObject)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val ERROR_UNSUPPORTED_STATE =
|
||||||
|
"The selected state is not supported by this source"
|
||||||
|
|
||||||
|
private const val ERROR_UNSUPPORTED_SORT_ORDER =
|
||||||
|
"The selected sort order is not supported by this source"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.mangadventure.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.mangadventure.MangAdventureParser
|
||||||
|
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||||
|
|
||||||
|
@MangaSourceParser("ARCRELIGHT", "Arc-Relight", "en")
|
||||||
|
internal class ArcRelight(context: MangaLoaderContext) :
|
||||||
|
MangAdventureParser(context, MangaSource.ARCRELIGHT, "arc-relight.com", pageSize = 10) {
|
||||||
|
private val franchises = setOf("Jubilee", "Steins;Gate", "Robotics;Notes", "Chaos;Head")
|
||||||
|
|
||||||
|
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||||
|
val tags = franchises.filter { franchise ->
|
||||||
|
seed.tags.find { it.key == franchise } != null
|
||||||
|
}
|
||||||
|
if (tags.isEmpty()) return emptyList()
|
||||||
|
val url = apiUrl.addEncodedPathSegment("series")
|
||||||
|
.addQueryParameter("categories", tags.joinToString(","))
|
||||||
|
return getManga(url.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.mangadventure.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.site.mangadventure.MangAdventureParser
|
||||||
|
|
||||||
|
@MangaSourceParser("ASSORTEDSCANS", "Assorted Scans", "en")
|
||||||
|
internal class AssortedScans(context: MangaLoaderContext) :
|
||||||
|
MangAdventureParser(context, MangaSource.ASSORTEDSCANS, "assortedscans.com") {
|
||||||
|
// tags that don't have any series and make the tests fail
|
||||||
|
private val emptyTags = setOf(
|
||||||
|
"Doujinshi", "Harem", "Hentai", "Mecha",
|
||||||
|
"Shoujo Ai", "Shounen Ai", "Smut", "Yaoi"
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> =
|
||||||
|
super.getAvailableTags().filterTo(HashSet()) { it.key !in emptyTags }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue