10 Commits

Author SHA1 Message Date
a8f9423307 Migrate chapter number to float 2024-01-22 13:45:36 +02:00
7c871edbca Merge pull request #503 from mangadventure/master
Add MangAdventure sources (Arc-Relight, Assorted Scans)
2024-01-22 13:14:35 +02:00
ObserverOfTime
f7d9579cee Relax pagination test 2024-01-20 19:27:16 +02:00
ObserverOfTime
b7e6ca8a26 Add MangAdventure sources
- Arc-Relight
- Assorted Scans
2024-01-20 19:27:16 +02:00
8852d1e22e [Remanga] Attempt to fix No value for page id 2024-01-19 23:31:21 +03:00
fcaa0ea442 Add volume field to chapter 2024-01-17 15:03:33 +02:00
8e7d7e0bde Add some sources 2024-01-15 01:58:21 +03:00
scaledzdn
50c6c9f842 src: id/shinigami.kt: Change url source (#481)
* Previous url got banned on Indonesia
2024-01-13 13:40:06 +03:00
a2979753a9 [Grouple] Fix pages parsing 2024-01-12 18:10:58 +02:00
789e39b6cb Fixes batch 2024-01-11 17:36:07 +02:00
24 changed files with 507 additions and 233 deletions

View File

@@ -10,9 +10,13 @@ class MangaChapter(
*/
@JvmField val name: String,
/**
* Chapter number starting from 1
* Chapter number starting from 1, 0 if unknown
*/
@JvmField val number: Int,
@JvmField val number: Float,
/**
* Volume number starting from 1, 0 if unknown
*/
@JvmField val volume: Int,
/**
* Relative url to chapter (**without** a domain) or any other uri.
* Used principally in parsers
@@ -32,11 +36,29 @@ class MangaChapter(
*/
@JvmField val branch: String?,
@JvmField val source: MangaSource,
) : Comparable<MangaChapter> {
) {
override fun compareTo(other: MangaChapter): Int {
return number.compareTo(other.number)
}
@Deprecated(message = "Consider using constructor with volume value")
constructor(
id: Long,
name: String,
number: Int,
url: String,
scanlator: String?,
uploadDate: Long,
branch: String?,
source: MangaSource,
) : this(
id = id,
name = name,
number = number.toFloat(),
volume = 0,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = source,
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -47,6 +69,7 @@ class MangaChapter(
if (id != other.id) return false
if (name != other.name) return false
if (number != other.number) return false
if (volume != other.volume) return false
if (url != other.url) return false
if (scanlator != other.scanlator) return false
if (uploadDate != other.uploadDate) return false
@@ -59,7 +82,8 @@ class MangaChapter(
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + number
result = 31 * result + number.hashCode()
result = 31 * result + volume
result = 31 * result + url.hashCode()
result = 31 * result + (scanlator?.hashCode() ?: 0)
result = 31 * result + uploadDate.hashCode()
@@ -71,4 +95,16 @@ class MangaChapter(
override fun toString(): String {
return "MangaChapter($id - #$number [$url] - $source)"
}
internal fun copy(volume: Int, number: Float) = MangaChapter(
id = id,
name = name,
number = number,
volume = volume,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = source,
)
}

View File

@@ -417,6 +417,4 @@ internal class ExHentaiParser(
}
return joiner.complete().takeUnless { it.isEmpty() }
}
private fun String.isNumeric() = all { c -> c.isDigit() }
}

View File

@@ -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")
}
}

View File

@@ -10,7 +10,7 @@ import java.util.*
@MangaSourceParser("SHINIGAMI", "Shinigami", "id")
internal class Shinigami(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.SHINIGAMI, "shinigami.moe", 10) {
MadaraParser(context, MangaSource.SHINIGAMI, "shinigamitoon.com", 10) {
private val userAgentKey = ConfigKey.UserAgent(UserAgents.CHROME_MOBILE)
override val tagPrefix = "genre/"

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.parsers.site.madara.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("BORUTOEXPLORER", "BorutoExplorer", "pt")
internal class BorutoExplorer(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.BORUTOEXPLORER, "leitor.borutoexplorer.com.br", 10) {
override val datePattern: String = "dd 'de' MMMMM 'de' yyyy"
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.parsers.site.madara.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.madara.MadaraParser
@MangaSourceParser("NEROXUS", "Neroxus", "pt")
internal class Neroxus(context: MangaLoaderContext) :
MadaraParser(context, MangaSource.NEROXUS, "neroxus.com.br", 10) {
override val datePattern = "MMM d, yyyy"
}

View File

@@ -0,0 +1,213 @@
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.*
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")?.toJSONList()?.mapChapters { _, it ->
MangaChapter(
id = generateUid(it.getLong("id")),
name = it.getString("full_title"),
number = it.getFloatOrDefault("number", 0f),
volume = it.getIntOrDefault("volume", 0),
url = it.getString("url"),
scanlator = it.getJSONArray("groups").joinToString(),
uploadDate = it.getString("published").toLong(),
branch = null,
source = source,
)
}.orEmpty(),
)
}
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(
id = generateUid(it.getLong("id")),
url = it.getString("image"),
preview = null,
source = 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 val Manga.slug: String
get() = url.substring(8, url.length - 1)
protected suspend 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"
}
}

View File

@@ -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())
}
}

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.parsers.site.mangadventure.en
import androidx.collection.ArraySet
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> {
val tags = super.getAvailableTags()
return tags.filterNotTo(ArraySet(tags.size)) { it.key in emptyTags }
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.parsers.site.mangareader.pt
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
@MangaSourceParser("FRANXXMANGAS", "FranxxMangas", "pt")
internal class FranxxMangas(context: MangaLoaderContext) :
MangaReaderParser(context, MangaSource.FRANXXMANGAS, "franxxmangas.net", pageSize = 10, searchPageSize = 10)

View File

@@ -271,7 +271,8 @@ internal class RemangaParser(
}
private fun parsePage(jo: JSONObject) = MangaPage(
id = generateUid(jo.getLong("id")),
// id = generateUid(jo.getLong("id")), 19.01.2024 page id is gone
id = generateUid(jo.getString("link")),
url = jo.getString("link"),
preview = null,
source = source,

View File

@@ -155,28 +155,13 @@ internal abstract class GroupleParser(
val scripts = doc.select("script")
for (script in scripts) {
val data = script.html()
val pos = data.indexOf("rm_h.readerInit( 0,")
if (pos == -1) {
continue
var pos = data.indexOf("rm_h.readerDoInit(")
if (pos != -1) {
parsePagesNew(data, pos)?.let { return it }
}
val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')')
if (json.isEmpty()) {
continue
}
val ja = JSONArray("[$json]")
val pages = ja.getJSONArray(1)
val servers = ja.getJSONArray(3).mapJSON { it.getString("path") }
val serversStr = servers.joinToString("|")
return (0 until pages.length()).map { i ->
val page = pages.getJSONArray(i)
val primaryServer = page.getString(0)
val url = page.getString(2)
MangaPage(
id = generateUid(url),
url = "$primaryServer|$serversStr|$url",
preview = null,
source = source,
)
pos = data.indexOf("rm_h.readerInit( 0,")
if (pos != -1) {
parsePagesOld(data, pos)?.let { return it }
}
}
doc.parseFailed("Pages list not found at ${chapter.url}")
@@ -373,4 +358,48 @@ internal abstract class GroupleParser(
source = source,
)
}
private fun parsePagesNew(data: String, pos: Int): List<MangaPage>? {
val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')')
if (json.isEmpty()) {
return null
}
val ja = JSONArray("[$json]")
val pages = ja.getJSONArray(0)
val servers = ja.getJSONArray(2).mapJSON { it.getString("path") }
val serversStr = servers.joinToString("|")
return (0 until pages.length()).map { i ->
val page = pages.getJSONArray(i)
val primaryServer = page.getString(0)
val url = page.getString(2)
MangaPage(
id = generateUid(url),
url = "$primaryServer|$serversStr|$url",
preview = null,
source = source,
)
}
}
private fun parsePagesOld(data: String, pos: Int): List<MangaPage>? {
val json = data.substring(pos).substringAfter('(').substringBefore('\n').substringBeforeLast(')')
if (json.isEmpty()) {
return null
}
val ja = JSONArray("[$json]")
val pages = ja.getJSONArray(1)
val servers = ja.getJSONArray(3).mapJSON { it.getString("path") }
val serversStr = servers.joinToString("|")
return (0 until pages.length()).map { i ->
val page = pages.getJSONArray(i)
val primaryServer = page.getString(0)
val url = page.getString(2)
MangaPage(
id = generateUid(url),
url = "$primaryServer|$serversStr|$url",
preview = null,
source = source,
)
}
}
}

View File

@@ -11,9 +11,8 @@ internal class MintMangaParser(
) : GroupleParser(context, MangaSource.MINTMANGA, 2) {
override val configKeyDomain = ConfigKey.Domain(
"23.mintmanga.live",
"24.mintmanga.one",
"mintmanga.live",
"mintmanga.com",
"m.mintmanga.live",
)
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.parsers.site.ru.multichan
import okhttp3.HttpUrl
import org.jsoup.internal.StringUtil
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
@@ -22,21 +23,17 @@ internal abstract class ChanParser(
SortOrder.RATING,
)
override val isTagsExclusionSupported: Boolean = true
override val authUrl: String
get() = "https://${domain}"
override val isAuthorized: Boolean
get() = context.cookieJar.getCookies(domain).any { it.name == "dle_user_id" }
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
val domain = domain
val doc = webClient.httpGet(buildUrl(offset, query, tags, sortOrder)).parseHtml()
val doc = webClient.httpGet(buildUrl(offset, filter)).parseHtml()
val root = doc.body().selectFirst("div.main_fon")?.getElementById("content")
?: doc.parseFailed("Cannot find root")
return root.select("div.content_row").mapNotNull { row ->
@@ -180,48 +177,56 @@ internal abstract class ChanParser(
protected open fun buildUrl(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
filter: MangaListFilter?,
): HttpUrl {
val builder = urlBuilder()
builder.addQueryParameter("offset", offset.toString())
when {
!query.isNullOrEmpty() -> {
when (filter) {
is MangaListFilter.Search -> {
builder.addQueryParameter("do", "search")
builder.addQueryParameter("subaction", "search")
builder.addQueryParameter("search_start", ((offset / 40) + 1).toString())
builder.addQueryParameter("full_search", "0")
builder.addQueryParameter("result_from", (offset + 1).toString())
builder.addQueryParameter("result_num", "40")
builder.addQueryParameter("story", query)
builder.addQueryParameter("story", filter.query)
builder.addQueryParameter("need_sort_date", "false")
}
!tags.isNullOrEmpty() -> {
builder.addPathSegment("tags")
builder.addPathSegment(tags.joinToString("+") { it.key })
builder.addQueryParameter(
"n",
when (sortOrder) {
SortOrder.RATING,
SortOrder.POPULARITY,
-> "favdesc"
is MangaListFilter.Advanced -> {
if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty()) {
builder.addPathSegment("tags")
val joiner = StringUtil.StringJoiner("+")
filter.tags.forEach { joiner.add(it.key) }
filter.tagsExclude.forEach { joiner.add("-"); joiner.append(it.key) }
builder.addPathSegment(joiner.complete())
builder.addQueryParameter(
"n",
when (filter.sortOrder) {
SortOrder.RATING,
SortOrder.POPULARITY,
-> "favdesc"
SortOrder.ALPHABETICAL -> "abcasc"
else -> "" // SortOrder.NEWEST
},
)
SortOrder.ALPHABETICAL -> "abcasc"
else -> "" // SortOrder.NEWEST
},
)
} else {
when (filter.sortOrder) {
SortOrder.POPULARITY -> builder.addPathSegment("mostviews")
SortOrder.ALPHABETICAL -> builder.addPathSegment("catalog")
SortOrder.RATING -> builder.addPathSegment("mostfavorites")
else -> { // SortOrder.NEWEST
builder.addPathSegment("manga")
builder.addPathSegment("new")
}
}
}
}
else -> when (sortOrder) {
SortOrder.POPULARITY -> builder.addPathSegment("mostviews")
SortOrder.ALPHABETICAL -> builder.addPathSegment("catalog")
SortOrder.RATING -> builder.addPathSegment("mostfavorites")
else -> { // SortOrder.NEWEST
builder.addPathSegment("manga")
builder.addPathSegment("new")
}
null -> {
builder.addPathSegment("manga")
builder.addPathSegment("new")
}
}
return builder.build()

View File

@@ -58,10 +58,10 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context,
)
}
override fun buildUrl(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): HttpUrl {
if (query.isNullOrEmpty() && tags.isNullOrEmpty()) {
override fun buildUrl(offset: Int, filter: MangaListFilter?): HttpUrl = when {
filter is MangaListFilter.Advanced && filter.tags.isEmpty() && filter.tagsExclude.isEmpty() -> {
val builder = urlBuilder().addQueryParameter("offset", offset.toString())
when (sortOrder) {
when (filter.sortOrder) {
SortOrder.POPULARITY -> {
builder.addPathSegment("mostviews")
builder.addQueryParameter("sort", "manga")
@@ -77,8 +77,18 @@ internal class HenChanParser(context: MangaLoaderContext) : ChanParser(context,
builder.addPathSegment("newest")
}
}
return builder.build()
builder.build()
}
filter == null -> {
val builder = urlBuilder().addQueryParameter("offset", offset.toString())
builder.addPathSegment("manga")
builder.addPathSegment("newest")
builder.build()
}
else -> {
super.buildUrl(offset, filter)
}
return super.buildUrl(offset, query, tags, sortOrder)
}
}

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.parsers.site.ru.rulib
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.Response
@@ -108,31 +107,24 @@ internal open class MangaLibParser(
val id = json.optJSONObject("user")?.getLong("id")?.toString() ?: "not"
val total = list.length()
chapters = ChaptersListBuilder(total)
val counters = ArrayMap<Int, Int>(branches.size)
for (i in (0 until total).reversed()) {
val item = list.getJSONObject(i)
val chapterId = item.getLong("chapter_id")
val scanlator = item.getStringOrNull("username")
val volume = item.getInt("chapter_volume")
val number = item.getString("chapter_number")
val url = buildString {
append(manga.url)
append("/v")
append(volume)
append("/c")
append(number)
if (isAuthorized) {
append(manga.url)
append("/v")
append(item.getInt("chapter_volume"))
append("/c")
append(item.getString("chapter_number"))
append("?ui=")
append(id)
} else {
append(manga.url)
append("/v")
append(item.getInt("chapter_volume"))
append("/c")
append(item.getString("chapter_number"))
}
}
val nameChapter = item.getStringOrNull("chapter_name")
val volume = item.getInt("chapter_volume")
val number = item.getString("chapter_number")
val fullNameChapter = "Том $volume. Глава $number"
val branchId = item.getIntOrDefault("branch_id", 0)
chapters.add(
@@ -140,7 +132,8 @@ internal open class MangaLibParser(
id = generateUid(chapterId),
url = url,
source = source,
number = counters.incrementAndGet(branchId),
number = number.toFloatOrNull() ?: 0f,
volume = volume,
uploadDate = dateFormat.tryParse(
item.getString("chapter_created_at").substringBefore(" "),
),
@@ -347,13 +340,6 @@ internal open class MangaLibParser(
return isSuccessful && mimeType?.startsWith("image/") == true && headersContentLength() >= 1024L
}
private fun MutableMap<Int, Int>.incrementAndGet(key: Int): Int {
var v = getOrDefault(key, 0)
v++
put(key, v)
return v
}
@MangaSourceParser("MANGALIB", "MangaLib", "ru")
class Impl(context: MangaLoaderContext) : MangaLibParser(context, MangaSource.MANGALIB)

View File

@@ -11,9 +11,10 @@ import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed
import java.text.SimpleDateFormat
import java.util.*
@@ -51,21 +52,24 @@ class HoneyMangaParser(context: MangaLoaderContext) : PagedMangaParser(context,
body.put("sortOrder", "ASC")
val chapterRequest = webClient.httpPost(chapterApi, body).parseJson()
return manga.copy(
chapters = chapterRequest.getJSONArray("data").mapJSONIndexed { i, jo ->
chapters = chapterRequest.getJSONArray("data").mapJSON { jo ->
val number = jo.getFloatOrDefault("chapterNum", 0f)
val volume = jo.getIntOrDefault("volume", 0)
MangaChapter(
id = generateUid(jo.getString("id")),
name = buildString {
append("Том ")
append(jo.optString("volume", "0"))
append(volume)
append(". ")
append("Розділ ")
append(jo.optString("chapterNum", "0"))
append(number)
if (jo.optString("title") != "Title") {
append(" - ")
append(jo.optString("title"))
}
},
number = i + 1,
number = number,
volume = volume,
url = jo.optString("chapterResourcesId"),
scanlator = null,
uploadDate = dateFormat.tryParse(jo.getString("lastUpdated")),

View File

@@ -178,9 +178,11 @@ internal abstract class WpComicsParser(
val tagItems = doc.select("div.genre-item")
val result = ArrayMap<String, MangaTag>(tagItems.size)
for (item in tagItems) {
val title = item.text().trim()
val title = item.text()
val key = item.select("span[data-id]").attr("data-id")
result[title] = MangaTag(title = title, key = key, source = source)
if (key.isNotEmpty() && title.isNotEmpty()) {
result[title] = MangaTag(title = title, key = key, source = source)
}
}
tagCache = result
result
@@ -216,7 +218,7 @@ internal abstract class WpComicsParser(
protected open val selectDate = "div.col-xs-4"
protected open val selectChapter = "div#nt_listchapter li:not(.heading)"
protected open val selectChapter = "div#nt_listchapter li .chapter"
protected open suspend fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li ->

View File

@@ -1,13 +1,15 @@
package org.koitharu.kotatsu.parsers.site.wpcomics.en
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
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.wpcomics.WpComicsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet
import java.util.*
@MangaSourceParser("XOXOCOMICS", "XoxoComics", "en", ContentType.COMICS)
internal class XoxoComics(context: MangaLoaderContext) :
@@ -146,6 +148,29 @@ internal class XoxoComics(context: MangaLoaderContext) :
)
}
override suspend fun getChapters(doc: Document): List<MangaChapter> {
val pages = doc.select("ul.pagination > li:not(.active)")
return if (pages.size <= 1) {
super.getChapters(doc)
} else {
val list = coroutineScope {
pages.mapNotNull { page ->
val a = page.selectFirst("a") ?: return@mapNotNull null
if (a.text().isNumeric()) {
val href = a.attrAsAbsoluteUrl("href")
async {
super.getChapters(webClient.httpGet(href).parseHtml()).asReversed()
}
} else {
null // TODO support pagination with overflow
}
}.awaitAll().flattenTo(ArrayList())
}
list.addAll(super.getChapters(doc).asReversed())
list.reverse()
list.mapIndexed { i, x -> x.copy(volume = x.volume, number = (i + 1).toFloat()) }
}
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain) + "/all"

View File

@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
internal class NetTruyen(context: MangaLoaderContext) :
WpComicsParser(context, MangaSource.NETTRUYEN, "www.nettruyenlive.com", 36) {
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain(
"www.nettruyenaz.com",
"www.nettruyenlive.com",
"www.nettruyenio.com",
"www.nettruyento.com",

View File

@@ -7,4 +7,4 @@ import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
@MangaSourceParser("NHATTRUYENMIN", "NhattruyenPlus", "vi")
internal class Nhattruyenmin(context: MangaLoaderContext) :
WpComicsParser(context, MangaSource.NHATTRUYENMIN, "nhattruyenplus.com")
WpComicsParser(context, MangaSource.NHATTRUYENMIN, "nhattruyenmax.com")

View File

@@ -34,6 +34,13 @@ internal class ChaptersListBuilder(initialSize: Int) {
return chapter != null && ids.add(chapter.id) && list.add(chapter)
}
fun addAll(chapters: Iterable<MangaChapter?>) {
if (chapters is Collection<*>) {
list.ensureCapacity(list.size + chapters.size)
}
chapters.forEach { add(it) }
}
operator fun plusAssign(chapter: MangaChapter?) {
add(chapter)
}

View File

@@ -240,3 +240,5 @@ inline fun <T> Appendable.appendAll(
append(transform(item))
}
}
fun String.isNumeric() = all { c -> c.isDigit() }

View File

@@ -40,7 +40,7 @@ internal class MangaParserTest {
val page1 = parser.getList(0, filter = null)
val page2 = parser.getList(page1.size, filter = null)
if (parser is PagedMangaParser) {
assert(parser.pageSize == page1.size) {
assert(parser.pageSize >= page1.size) {
"Page size is ${page1.size} but ${parser.pageSize} expected"
}
}