Compare commits
8 Commits
source/kom
...
source/ar
| Author | SHA1 | Date | |
|---|---|---|---|
| 9952f8f00e | |||
| de0c12c078 | |||
| b3a9c5fcda | |||
| c73d34b04f | |||
| 583b24854f | |||
| 1c79b29845 | |||
| c348d68983 | |||
| 6d7506cc1b |
90
CONTRIBUTING.md
Normal file
90
CONTRIBUTING.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
The following is guide for creating a Kotatsu parsers. Thanks for taking the time to contribute!
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you start, please note that the ability to use following technologies is **required**.
|
||||||
|
|
||||||
|
- Basic [Android development](https://developer.android.com/)
|
||||||
|
- [Kotlin](https://kotlinlang.org/)
|
||||||
|
- Web scraping ([JSoup](https://jsoup.org/)) or JSON API
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- [Android Studio](https://developer.android.com/studio)
|
||||||
|
- [IntelliJ IDEA](https://www.jetbrains.com/idea/) (Community edition is enough)
|
||||||
|
- Android device (or emulator)
|
||||||
|
|
||||||
|
Kotatsu parsers is not a part of Android application, but you can easily develop and test it directly inside an Android
|
||||||
|
application project and relocate it to the library project when done.
|
||||||
|
|
||||||
|
### Before you start
|
||||||
|
|
||||||
|
First, take a look at `kotatsu-parsers` project structure. Each parser is a single class that
|
||||||
|
extends `MangaParser` class and have a `MangaSourceParser` annotation.
|
||||||
|
Also pay attention on extensions in `util` package. For example, extensions from `Jsoup` file
|
||||||
|
should be used instead of existing JSoup functions because they have better nullability support
|
||||||
|
and improved error messages.
|
||||||
|
|
||||||
|
## Writing your parser
|
||||||
|
|
||||||
|
So, you want to create a parser, that will provide access to manga from a website.
|
||||||
|
First, you should explore a website for API availability.
|
||||||
|
If it does not contain any documentation about
|
||||||
|
API, [explore network requests](https://firefox-source-docs.mozilla.org/devtools-user/):
|
||||||
|
some websites use ajax.
|
||||||
|
|
||||||
|
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/DesuMeParser.kt)
|
||||||
|
of Json API usage.
|
||||||
|
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/AnibelParser.kt)
|
||||||
|
of GraphQL API usage
|
||||||
|
- [Example](https://github.com/KotatsuApp/kotatsu-parsers/blob/master/src/main/kotlin/org/koitharu/kotatsu/parsers/site/MangaTownParser.kt)
|
||||||
|
of pure HTML parsing.
|
||||||
|
|
||||||
|
If website is based on some engine it is rationally to use common base class for this one (for example, Madara wordress
|
||||||
|
theme
|
||||||
|
and the `MadaraParser` class)
|
||||||
|
|
||||||
|
### Parser class skeleton
|
||||||
|
|
||||||
|
Parser class must have exactly one primary constructor parameter of type `MangaLoaderContext` and have an
|
||||||
|
`MangaSourceParser` annotation that provides internal name, title and language of a manga source.
|
||||||
|
|
||||||
|
All functions in `MangaParser` class are documented. Pay attention to some peculiarities:
|
||||||
|
|
||||||
|
- Never hardcode domain. Specify default domain in `configKeyDomain` field and obtain an actual one using `getDomain()`.
|
||||||
|
- All ids must be unique and domain-independent. Use `generateUid` functions with relative url or some internal id which
|
||||||
|
is unique across the manga source.
|
||||||
|
- `sortOrders` set should not be empty. If your source is not support sorting, specify one most relevance value.
|
||||||
|
- If you cannot obtain direct links to pages images inside `getPages` method, it is ok to use an intermediate url
|
||||||
|
as `Page.url` and fetch a direct link at `getPageUrl` function.
|
||||||
|
- `getFaviconUrl` function is deprecated, but Kotatsu application before v4 still use it, so it is recommended to
|
||||||
|
override it too.
|
||||||
|
- You can use _asserts_ to check some optional fields. For example. `Manga.author` field is not required, but if your
|
||||||
|
source provide such information, add `assert(it != null)`. This will not have any effect on production but help to
|
||||||
|
find issues during unit testing.
|
||||||
|
- If your source website (or it's api) uses pages for pagination instead of offset you should extend `PagedMangaParser`
|
||||||
|
instead of `MangaParser`.
|
||||||
|
|
||||||
|
## Development process
|
||||||
|
|
||||||
|
During the development it is recommended (but not necessary) to write it directly
|
||||||
|
in the Kotatsu android application project. You can use `core.parser.DummyParser` class as a sandbox. `Dummy` manga
|
||||||
|
source is available in debug Kotatsu build.
|
||||||
|
|
||||||
|
Once parser is ready you can relocate your code into `kotatsu-parsers` library project in a `site` package and create a
|
||||||
|
Pull Request.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
It is recommended to run unit tests before submitting a PR.
|
||||||
|
|
||||||
|
- Temporary modify the `MangaSources` annotation class: specify your parser(s) name(s) and change mode
|
||||||
|
to `EnumSource.Mode.INCLUDE`
|
||||||
|
- Run the `MangaParserTest` (`gradlew :test --tests "org.koitharu.kotatsu.parsers.MangaParserTest"`)
|
||||||
|
- Optionally, you can run the `generateTestsReport` gradle task to get a pretty readable html report from test results.
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
If you need a help or have some questions, ask a community in our [Discord server](https://discord.gg/NNJ5RgVBC5).
|
||||||
13
README.md
13
README.md
@@ -4,7 +4,7 @@ Library that provides manga sources.
|
|||||||
|
|
||||||
[](https://jitpack.io/#KotatsuApp/kotatsu-parsers)   [](https://discord.gg/NNJ5RgVBC5)
|
[](https://jitpack.io/#KotatsuApp/kotatsu-parsers)   [](https://discord.gg/NNJ5RgVBC5)
|
||||||
|
|
||||||
### Usage
|
## Usage
|
||||||
|
|
||||||
1. Add it in your root build.gradle at the end of repositories:
|
1. Add it in your root build.gradle at the end of repositories:
|
||||||
|
|
||||||
@@ -48,4 +48,13 @@ Library that provides manga sources.
|
|||||||
and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/main/kotlin/org/koitharu/kotatsu_dl/env/MangaLoaderContextImpl.kt)
|
and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/main/kotlin/org/koitharu/kotatsu_dl/env/MangaLoaderContextImpl.kt)
|
||||||
implementation examples.
|
implementation examples.
|
||||||
|
|
||||||
Note that the `MangaSource.LOCAL` and `MangaSource.DUMMY` parsers cannot be instantiated.
|
Note that the `MangaSource.LOCAL` and `MangaSource.DUMMY` parsers cannot be instantiated.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
|
||||||
|
|
||||||
|
## DMCA disclaimer
|
||||||
|
|
||||||
|
The developers of this application does not have any affiliation with the content available in the app. It is collecting
|
||||||
|
from the sources freely available through any web browser.
|
||||||
@@ -69,6 +69,7 @@ class ParserProcessor(
|
|||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
fun MangaSource.newParser(context: MangaLoaderContext): MangaParser = when (this) {
|
fun MangaSource.newParser(context: MangaLoaderContext): MangaParser = when (this) {
|
||||||
|
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
@@ -121,6 +122,7 @@ class ParserProcessor(
|
|||||||
logger.error("Only non-abstract can be annotated with @MangaSourceParser", classDeclaration)
|
logger.error("Only non-abstract can be annotated with @MangaSourceParser", classDeclaration)
|
||||||
}
|
}
|
||||||
val annotation = classDeclaration.annotations.single { it.shortName.asString() == "MangaSourceParser" }
|
val annotation = classDeclaration.annotations.single { it.shortName.asString() == "MangaSourceParser" }
|
||||||
|
val deprecation = classDeclaration.annotations.singleOrNull { it.shortName.asString() == "Deprecated" }
|
||||||
val name = annotation.arguments.single { it.name?.asString() == "name" }.value as String
|
val name = annotation.arguments.single { it.name?.asString() == "name" }.value as String
|
||||||
val title = annotation.arguments.single { it.name?.asString() == "title" }.value as String
|
val title = annotation.arguments.single { it.name?.asString() == "title" }.value as String
|
||||||
val locale = annotation.arguments.single { it.name?.asString() == "locale" }.value as String
|
val locale = annotation.arguments.single { it.name?.asString() == "locale" }.value as String
|
||||||
@@ -142,11 +144,15 @@ class ParserProcessor(
|
|||||||
classDeclaration,
|
classDeclaration,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val className = classDeclaration.qualifiedName?.asString()
|
val className = classDeclaration.qualifiedName?.asString()
|
||||||
factoryWriter?.write("\tMangaSource.$name -> $className(context)\n")
|
factoryWriter?.write("\tMangaSource.$name -> $className(context)\n")
|
||||||
|
val deprecationString = if (deprecation != null) {
|
||||||
|
val reason = deprecation.arguments
|
||||||
|
.find { it.name?.asString() == "message" }?.value?.toString() ?: "Unknown reason"
|
||||||
|
"@Deprecated(\"$reason\") "
|
||||||
|
} else ""
|
||||||
val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty()
|
val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty()
|
||||||
sourcesWriter?.write("\t$name(\"$title\", $localeString$localeComment),\n")
|
sourcesWriter?.write("\t$deprecationString$name(\"$title\", $localeString$localeComment),\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.*
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@MangaSourceParser("CLONEMANGA", "CloneManga", "en")
|
||||||
|
internal class CloneMangaParser(override val context: MangaLoaderContext) : PagedMangaParser(
|
||||||
|
MangaSource.CLONEMANGA,
|
||||||
|
pageSize = 1,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder> = Collections.singleton(
|
||||||
|
SortOrder.POPULARITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("manga.clone-army.org", null)
|
||||||
|
|
||||||
|
override fun getFaviconUrl(): String {
|
||||||
|
return "https://pbs.twimg.com/profile_images/458758466346029056/Ys93EANp_400x400.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getListPage(
|
||||||
|
page: Int,
|
||||||
|
query: String?,
|
||||||
|
tags: Set<MangaTag>?,
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
): List<Manga> {
|
||||||
|
if (query != null || page > 1) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val link = "https://${getDomain()}/viewer_landing.php"
|
||||||
|
val doc = context.httpGet(link).parseHtml()
|
||||||
|
val mangas = doc.getElementsByClass("comicPreviewContainer")
|
||||||
|
return mangas.mapNotNull { item ->
|
||||||
|
val attr = item.getElementsByClass("comicPreview").attr("style")
|
||||||
|
val href = item.selectFirst("a")?.attrAsAbsoluteUrl("href") ?: return@mapNotNull null
|
||||||
|
val cover = attr.substring(attr.indexOf("site/themes"), attr.indexOf(")"))
|
||||||
|
Manga(
|
||||||
|
id = generateUid(href),
|
||||||
|
title = item.selectFirst("h3")?.text() ?: return@mapNotNull null,
|
||||||
|
coverUrl = "https://${getDomain()}/$cover",
|
||||||
|
altTitle = null,
|
||||||
|
author = "Dan Kim",
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
url = href,
|
||||||
|
isNsfw = false,
|
||||||
|
tags = emptySet(),
|
||||||
|
state = null,
|
||||||
|
publicUrl = href.toAbsoluteUrl(getDomain()),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val doc = context.httpGet(manga.publicUrl).parseHtml()
|
||||||
|
val series = doc.location()
|
||||||
|
val numChapters = Regex(
|
||||||
|
pattern = "&page=(.*)&lang=",
|
||||||
|
).findAll(
|
||||||
|
input = doc.getElementsByTag("script")[3].toString(),
|
||||||
|
)
|
||||||
|
.elementAt(3).destructured.component1()
|
||||||
|
.toInt()
|
||||||
|
val chapters = ArrayList<MangaChapter>()
|
||||||
|
for (i in 0..numChapters) {
|
||||||
|
val chapter = MangaChapter(
|
||||||
|
id = generateUid("$series&page=$i"),
|
||||||
|
name = "Chapter ${i + 1}",
|
||||||
|
number = i + 1,
|
||||||
|
url = "$series&page=$i",
|
||||||
|
scanlator = null,
|
||||||
|
branch = null,
|
||||||
|
uploadDate = 0L,
|
||||||
|
source = MangaSource.DUMMY,
|
||||||
|
)
|
||||||
|
chapters.add(chapter)
|
||||||
|
}
|
||||||
|
return manga.copy(chapters = chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val doc = context.httpGet(chapter.url.toAbsoluteUrl(getDomain())).parseHtml()
|
||||||
|
val imgUrl = doc.getElementsByClass("subsectionContainer")[0]
|
||||||
|
.selectFirst("img")
|
||||||
|
?.attrAsAbsoluteUrlOrNull("src") ?: doc.parseFailed("Something broken")
|
||||||
|
return listOf(
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(imgUrl),
|
||||||
|
url = imgUrl,
|
||||||
|
referer = imgUrl,
|
||||||
|
preview = null,
|
||||||
|
source = MangaSource.DUMMY,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.parsers.util.*
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@Deprecated("Permanently shutdown")
|
||||||
@MangaSourceParser("MANGAOWL", "MangaOwl", "en")
|
@MangaSourceParser("MANGAOWL", "MangaOwl", "en")
|
||||||
internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.MANGAOWL) {
|
internal class MangaOwlParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.MANGAOWL) {
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException
|
|||||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
import org.koitharu.kotatsu.parsers.util.*
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
import org.koitharu.kotatsu.parsers.util.json.*
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@@ -143,6 +140,9 @@ internal class RemangaParser(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
chapters = chapters.mapChapters { i, jo ->
|
chapters = chapters.mapChapters { i, jo ->
|
||||||
|
if (jo.getBooleanOrDefault("is_paid", false)) {
|
||||||
|
return@mapChapters null
|
||||||
|
}
|
||||||
val id = jo.getLong("id")
|
val id = jo.getLong("id")
|
||||||
val name = jo.getString("name").toTitleCase(Locale.ROOT)
|
val name = jo.getString("name").toTitleCase(Locale.ROOT)
|
||||||
val publishers = jo.optJSONArray("publishers")
|
val publishers = jo.optJSONArray("publishers")
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class UnionMangasParser(override val context: MangaLoaderContext) : PagedMangaPa
|
|||||||
url = href,
|
url = href,
|
||||||
scanlator = div.selectLast("a")?.text()?.takeUnless { it == title },
|
scanlator = div.selectLast("a")?.text()?.takeUnless { it == title },
|
||||||
uploadDate = dateFormat.tryParse(
|
uploadDate = dateFormat.tryParse(
|
||||||
a.nextElementSibling()?.text()?.removeSurrounding('(', ')'),
|
a.nextElementSibling()?.text()?.removeSurrounding("(", ")"),
|
||||||
),
|
),
|
||||||
branch = null,
|
branch = null,
|
||||||
source = source,
|
source = source,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import java.util.*
|
|||||||
internal class IsekaiScanEuParser(context: MangaLoaderContext) :
|
internal class IsekaiScanEuParser(context: MangaLoaderContext) :
|
||||||
MadaraParser(context, MangaSource.ISEKAISCAN_EU, "isekaiscan.eu") {
|
MadaraParser(context, MangaSource.ISEKAISCAN_EU, "isekaiscan.eu") {
|
||||||
|
|
||||||
|
override val datePattern = "MM/dd/yyyy"
|
||||||
|
|
||||||
override suspend fun getChapters(manga: Manga, doc: Document): List<MangaChapter> {
|
override suspend fun getChapters(manga: Manga, doc: Document): List<MangaChapter> {
|
||||||
val mangaId = doc.body().requireElementById("manga-chapters-holder").attr("data-id")
|
val mangaId = doc.body().requireElementById("manga-chapters-holder").attr("data-id")
|
||||||
val ul = context.httpPost(
|
val ul = context.httpPost(
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ internal abstract class MadaraParser(
|
|||||||
protected open suspend fun getChapters(manga: Manga, doc: Document): List<MangaChapter> {
|
protected open suspend fun getChapters(manga: Manga, doc: Document): List<MangaChapter> {
|
||||||
val root2 = doc.body().selectFirstOrThrow("div.content-area")
|
val root2 = doc.body().selectFirstOrThrow("div.content-area")
|
||||||
.selectFirstOrThrow("div.c-page")
|
.selectFirstOrThrow("div.c-page")
|
||||||
val dateFormat = SimpleDateFormat(datePattern, Locale.US)
|
val dateFormat = SimpleDateFormat(datePattern, sourceLocale ?: Locale.US)
|
||||||
return root2.select("li").asReversed().mapChapters { i, li ->
|
return root2.select("li").asReversed().mapChapters { i, li ->
|
||||||
val a = li.selectFirst("a")
|
val a = li.selectFirst("a")
|
||||||
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
|
||||||
@@ -305,10 +305,13 @@ internal abstract class MadaraParser(
|
|||||||
@MangaSourceParser("MANGAREAD", "MangaRead", "en")
|
@MangaSourceParser("MANGAREAD", "MangaRead", "en")
|
||||||
class MangaRead(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAREAD, "www.mangaread.org") {
|
class MangaRead(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAREAD, "www.mangaread.org") {
|
||||||
override val tagPrefix = "genres/"
|
override val tagPrefix = "genres/"
|
||||||
|
override val datePattern = "dd.MM.yyyy"
|
||||||
}
|
}
|
||||||
|
|
||||||
@MangaSourceParser("MANGAWEEBS", "MangaWeebs", "en")
|
@MangaSourceParser("MANGAWEEBS", "MangaWeebs", "en")
|
||||||
class MangaWeebs(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAWEEBS, "mangaweebs.in")
|
class MangaWeebs(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAWEEBS, "mangaweebs.in") {
|
||||||
|
override val datePattern = "dd MMMM HH:mm"
|
||||||
|
}
|
||||||
|
|
||||||
@MangaSourceParser("KINGMANGA", "KingManga", "en")
|
@MangaSourceParser("KINGMANGA", "KingManga", "en")
|
||||||
class KingManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.KINGMANGA, "king-manga.com") {
|
class KingManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.KINGMANGA, "king-manga.com") {
|
||||||
@@ -318,6 +321,7 @@ internal abstract class MadaraParser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Not working, unknown reason, maybe shutdown or domain expired")
|
||||||
@MangaSourceParser("MANGAHATACHI", "MangahaTachi", "ja")
|
@MangaSourceParser("MANGAHATACHI", "MangahaTachi", "ja")
|
||||||
class MangahaTachi(context: MangaLoaderContext) :
|
class MangahaTachi(context: MangaLoaderContext) :
|
||||||
MadaraParser(context, MangaSource.MANGAHATACHI, "mangahatachi.com")
|
MadaraParser(context, MangaSource.MANGAHATACHI, "mangahatachi.com")
|
||||||
@@ -326,7 +330,7 @@ internal abstract class MadaraParser(
|
|||||||
class PianManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.PIANMANGA, "pianmanga.com")
|
class PianManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.PIANMANGA, "pianmanga.com")
|
||||||
|
|
||||||
@MangaSourceParser("MANGAROSIE", "MangaRosie", "en")
|
@MangaSourceParser("MANGAROSIE", "MangaRosie", "en")
|
||||||
class MangaRosie(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAROSIE, "mangarosie.me")
|
class MangaRosie(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAROSIE, "mangarosie.love")
|
||||||
|
|
||||||
@MangaSourceParser("READMANWHA", "ReadManwha", "en")
|
@MangaSourceParser("READMANWHA", "ReadManwha", "en")
|
||||||
class ReadManwha(context: MangaLoaderContext) : MadaraParser(context, MangaSource.READMANWHA, "readmanwha.net")
|
class ReadManwha(context: MangaLoaderContext) : MadaraParser(context, MangaSource.READMANWHA, "readmanwha.net")
|
||||||
@@ -335,7 +339,9 @@ internal abstract class MadaraParser(
|
|||||||
class MangaTx(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGATX, "mangatx.com")
|
class MangaTx(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGATX, "mangatx.com")
|
||||||
|
|
||||||
@MangaSourceParser("MANGAEFFECT", "MangaEffect", "en")
|
@MangaSourceParser("MANGAEFFECT", "MangaEffect", "en")
|
||||||
class MangaEffect(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAEFFECT, "mangaeffect.com")
|
class MangaEffect(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGAEFFECT, "mangaeffect.com") {
|
||||||
|
override val datePattern = "dd.MM.yyyy"
|
||||||
|
}
|
||||||
|
|
||||||
@MangaSourceParser("AQUAMANGA", "AquaManga", "en")
|
@MangaSourceParser("AQUAMANGA", "AquaManga", "en")
|
||||||
class AquaManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.AQUAMANGA, "aquamanga.com") {
|
class AquaManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.AQUAMANGA, "aquamanga.com") {
|
||||||
@@ -357,7 +363,9 @@ internal abstract class MadaraParser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MangaSourceParser("HARIMANGA", "HariManga", "en")
|
@MangaSourceParser("HARIMANGA", "HariManga", "en")
|
||||||
class HariManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.HARIMANGA, "harimanga.com")
|
class HariManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.HARIMANGA, "harimanga.com") {
|
||||||
|
override val datePattern = "MM/dd/yyyy"
|
||||||
|
}
|
||||||
|
|
||||||
@MangaSourceParser("KISSMANGA", "KissManga", "en")
|
@MangaSourceParser("KISSMANGA", "KissManga", "en")
|
||||||
class KissManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.KISSMANGA, "kissmanga.in")
|
class KissManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.KISSMANGA, "kissmanga.in")
|
||||||
@@ -392,7 +400,9 @@ internal abstract class MadaraParser(
|
|||||||
class ManhwaClan(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANHWACLAN, "manhwaclan.com")
|
class ManhwaClan(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANHWACLAN, "manhwaclan.com")
|
||||||
|
|
||||||
@MangaSourceParser("MANGA_3S", "Manga3s", "en")
|
@MangaSourceParser("MANGA_3S", "Manga3s", "en")
|
||||||
class Manga3s(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGA_3S, "manga3s.com")
|
class Manga3s(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGA_3S, "manga3s.com") {
|
||||||
|
override val tagPrefix = "manhwa-genre/"
|
||||||
|
}
|
||||||
|
|
||||||
@MangaSourceParser("MANHWAKOOL", "Manhwa Kool", "en")
|
@MangaSourceParser("MANHWAKOOL", "Manhwa Kool", "en")
|
||||||
class ManhwaKool(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANHWAKOOL, "manhwakool.com") {
|
class ManhwaKool(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANHWAKOOL, "manhwakool.com") {
|
||||||
@@ -404,6 +414,7 @@ internal abstract class MadaraParser(
|
|||||||
@MangaSourceParser("TOPMANHUA", "Top Manhua", "en")
|
@MangaSourceParser("TOPMANHUA", "Top Manhua", "en")
|
||||||
class TopManhua(context: MangaLoaderContext) : MadaraParser(context, MangaSource.TOPMANHUA, "www.topmanhua.com") {
|
class TopManhua(context: MangaLoaderContext) : MadaraParser(context, MangaSource.TOPMANHUA, "www.topmanhua.com") {
|
||||||
override val tagPrefix = "manhua-genre/"
|
override val tagPrefix = "manhua-genre/"
|
||||||
|
override val datePattern = "MM/dd/yyyy"
|
||||||
}
|
}
|
||||||
|
|
||||||
@MangaSourceParser("X2MANGA", "X2Manga", "en")
|
@MangaSourceParser("X2MANGA", "X2Manga", "en")
|
||||||
@@ -498,6 +509,8 @@ internal abstract class MadaraParser(
|
|||||||
@MangaSourceParser("TREE_MANGA", "Tree Manga", "en")
|
@MangaSourceParser("TREE_MANGA", "Tree Manga", "en")
|
||||||
class TreeManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.TREE_MANGA, "treemanga.com") {
|
class TreeManga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.TREE_MANGA, "treemanga.com") {
|
||||||
|
|
||||||
|
override val datePattern = "MM/dd/yyyy"
|
||||||
|
|
||||||
override fun getFaviconUrl(): String {
|
override fun getFaviconUrl(): String {
|
||||||
return "https://${getDomain()}/wp-content/uploads/2017/10/lgoo-treemanga-2-1.jpg"
|
return "https://${getDomain()}/wp-content/uploads/2017/10/lgoo-treemanga-2-1.jpg"
|
||||||
}
|
}
|
||||||
@@ -532,7 +545,9 @@ internal abstract class MadaraParser(
|
|||||||
class Manga365(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGA_365, "365manga.com")
|
class Manga365(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGA_365, "365manga.com")
|
||||||
|
|
||||||
@MangaSourceParser("MANGACLASH", "Mangaclash", "en")
|
@MangaSourceParser("MANGACLASH", "Mangaclash", "en")
|
||||||
class Mangaclash(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGACLASH, "mangaclash.com")
|
class Mangaclash(context: MangaLoaderContext) : MadaraParser(context, MangaSource.MANGACLASH, "mangaclash.com") {
|
||||||
|
override val datePattern = "MM/dd/yyyy"
|
||||||
|
}
|
||||||
|
|
||||||
@MangaSourceParser("ZINMANGA", "ZINMANGA", "en")
|
@MangaSourceParser("ZINMANGA", "ZINMANGA", "en")
|
||||||
class Zinmanga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.ZINMANGA, "zinmanga.com")
|
class Zinmanga(context: MangaLoaderContext) : MadaraParser(context, MangaSource.ZINMANGA, "zinmanga.com")
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.madara
|
||||||
|
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
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.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrlOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
|
||||||
|
|
||||||
|
@MangaSourceParser("MANGATREND", "MangaTrend", "ar")
|
||||||
|
internal class MangaTrendParser(context: MangaLoaderContext) :
|
||||||
|
Madara6Parser(context, MangaSource.MANGATREND, "mangaatrend.com") {
|
||||||
|
|
||||||
|
override val tagPrefix = "manga-genre/"
|
||||||
|
|
||||||
|
override fun getFaviconUrl(): String {
|
||||||
|
return "https://${getDomain()}/wp-content/uploads/2022/08/23UOmUDN_400x400-150x150.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseDetails(manga: Manga, body: Element, chapters: List<MangaChapter>): Manga {
|
||||||
|
val root = body.selectFirstOrThrow(".site-content")
|
||||||
|
val postContent = root.selectFirstOrThrow(".post-content")
|
||||||
|
val tags = postContent.getElementsContainingOwnText("التصنيفات")
|
||||||
|
.firstOrNull()?.tableValue()
|
||||||
|
?.getElementsByAttributeValueContaining("href", tagPrefix)
|
||||||
|
?.mapToSet { a -> a.asMangaTag() } ?: manga.tags
|
||||||
|
return manga.copy(
|
||||||
|
largeCoverUrl = root.selectFirst("summary_image")
|
||||||
|
?.selectFirst("img[src]")
|
||||||
|
?.attrAsAbsoluteUrlOrNull("src"),
|
||||||
|
description = (root.selectFirst(".detail-content")
|
||||||
|
?: root.selectFirstOrThrow(".manga-excerpt")).html(),
|
||||||
|
author = postContent.getElementsContainingOwnText("المؤلف")
|
||||||
|
.firstOrNull()?.tableValue()?.text()?.trim(),
|
||||||
|
state = postContent.getElementsContainingOwnText("الحالة")
|
||||||
|
.firstOrNull()?.tableValue()?.text()?.asMangaState(),
|
||||||
|
tags = tags,
|
||||||
|
isNsfw = body.hasClass("adult-content"),
|
||||||
|
chapters = chapters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun String.asMangaState() = when (trim()) {
|
||||||
|
"OnGoing" -> MangaState.ONGOING
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ internal class MangaParserTest {
|
|||||||
val page2 = parser.getList(page1.size, sortOrder = null, tags = null)
|
val page2 = parser.getList(page1.size, sortOrder = null, tags = null)
|
||||||
val intersection = page1.intersect(page2.toSet())
|
val intersection = page1.intersect(page2.toSet())
|
||||||
assert(intersection.isEmpty()) {
|
assert(intersection.isEmpty()) {
|
||||||
"Pages are intersected: " + intersection.joinToString { it.publicUrl }
|
"Pages are intersected by " + intersection.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user