Compare commits
160 Commits
feature/fi
...
4a8c7fa36a
| Author | SHA1 | Date | |
|---|---|---|---|
|
4a8c7fa36a
|
|||
| 1251a6cd75 | |||
|
|
0ce8eae969 | ||
|
|
8b9c76fca5 | ||
|
52e4e6b802
|
|||
|
57c9d26916
|
|||
|
19d7003a9e
|
|||
|
b11e50ab22
|
|||
|
a03ab0e0c0
|
|||
|
05d4db00b3
|
|||
|
f4aaa14fc5
|
|||
|
a8f9423307
|
|||
| 7c871edbca | |||
|
|
f7d9579cee | ||
|
|
b7e6ca8a26 | ||
|
8852d1e22e
|
|||
|
fcaa0ea442
|
|||
|
8e7d7e0bde
|
|||
|
|
50c6c9f842 | ||
|
a2979753a9
|
|||
|
789e39b6cb
|
|||
|
2f5441ef20
|
|||
|
|
6ad08f13d4 | ||
|
|
656f621711 | ||
|
|
6dc0e12fb0 | ||
|
0e8579017b
|
|||
|
|
e03d0efe71 | ||
|
0b497c4e0b
|
|||
|
|
704f589b6d | ||
|
3feb84ac9e
|
|||
|
e375654009
|
|||
|
ce7ac5d8f4
|
|||
|
|
a172fa5a5b | ||
|
7c89f53988
|
|||
|
4e2d739840
|
|||
|
|
5cdbda700b | ||
|
b274b51699
|
|||
| efb5d34279 | |||
|
|
9ecb75b51c | ||
|
69332a85da
|
|||
|
ea095084cc
|
|||
|
1f176ebab2
|
|||
|
|
327c05d03e | ||
|
|
2d7c120d19 | ||
|
|
a2e9f19814 | ||
|
|
c602355e65 | ||
|
|
a390e0de49 | ||
| a228d71d57 | |||
|
|
b2f0eaccc4 | ||
|
|
07aaec7d3c | ||
|
|
392e9af944 | ||
|
904e0719eb
|
|||
| 52ac3d2e6c | |||
|
|
859b07e454 | ||
|
|
d4b252fd71 | ||
|
4a0e7221b0
|
|||
| 43bdbe5a01 | |||
| 3d10456a87 | |||
|
d5a6c95ffb
|
|||
|
|
edc07b5dbd | ||
|
|
a5219ceb6c | ||
|
|
039075086e | ||
|
|
495c9fad33 | ||
|
|
b61c5e8f12 | ||
|
|
a40a8d329a | ||
|
|
3f3ab5a7be | ||
|
|
d8c32047d0 | ||
|
|
f5f2854f36 | ||
|
|
86a2b95141 | ||
|
4db7ccc853
|
|||
|
b1da5c2924
|
|||
|
|
0fe6fed838 | ||
|
|
a3f8c0a236 | ||
|
|
c88d7de138 | ||
|
|
eeafb3c6fd | ||
|
|
0208f5be97 | ||
| 42ecf8a958 | |||
| d780623b98 | |||
|
|
1b1a167580 | ||
|
|
49d0c80070 | ||
|
|
a35c2f3394 | ||
|
|
3034a87929 | ||
|
|
33b96043c9 | ||
|
|
4d02dc0d12 | ||
|
|
79fd2a58c1 | ||
|
|
ab82d0eba0 | ||
| 75cc0716fd | |||
|
87f99addbb
|
|||
|
|
3e1b15a23c | ||
|
|
5758e9f68f | ||
|
|
0964c02a44 | ||
|
|
bf06ab7492 | ||
|
|
e992566899 | ||
|
|
97338f31c5 | ||
|
|
0ff4f976e1 | ||
|
|
496b3388ee | ||
|
|
d6b578acb0 | ||
|
b3be87f541
|
|||
|
|
786cd12dba | ||
|
|
a1a17c02d7 | ||
|
|
b8caf8e572 | ||
|
44c2c074a8
|
|||
|
3ed960254d
|
|||
| f1e8fbec5d | |||
| c5ed9cf54b | |||
|
|
65c567dbcb | ||
|
|
b312823afd | ||
|
|
56eb059bcd | ||
|
|
6ad78f1cba | ||
|
|
26b758ae50 | ||
|
|
019b8cc087 | ||
|
|
c95845083a | ||
| 9c7f314f65 | |||
|
|
743a92a3e8 | ||
|
|
37e9b33c24 | ||
|
|
7c2ac033d7 | ||
|
0efd5437f9
|
|||
|
|
db770351f3 | ||
|
|
14b2457627 | ||
|
|
f62ea43de8 | ||
|
|
ab2aad6bd0 | ||
|
|
1293233ad9 | ||
|
|
5fff2adbfb | ||
|
|
7d67b718a5 | ||
|
|
b8ca21d588 | ||
|
|
44d91e43ad | ||
| 3bb10ee948 | |||
|
|
81caa59a36 | ||
|
46e863ef79
|
|||
|
cecba7623e
|
|||
| 7dee865c8a | |||
|
|
5ac3cdb03c | ||
|
c3613f3ba4
|
|||
|
e7ac1c1feb
|
|||
|
24573d12f5
|
|||
|
9d2f633e14
|
|||
| 570b0c19c5 | |||
|
|
a11e42a4d6 | ||
|
|
410c73a74f | ||
|
|
3ea088371b | ||
|
|
09a0bb364a | ||
|
|
d91947ae15 | ||
|
|
0a8a9e5935 | ||
|
|
59226b298f | ||
|
|
dc41bf3e83 | ||
|
|
a790a18f7f | ||
|
|
1fcc1096f5 | ||
|
|
86aaca271f | ||
|
|
b8cc2d6953 | ||
|
|
31459ea6e7 | ||
|
|
7ab84688c2 | ||
|
|
d18f287804 | ||
|
|
1c3b41acba | ||
|
|
ecfc7e40c9 | ||
|
41eea1c420
|
|||
|
|
594524afc3 | ||
|
|
70a5337604 | ||
|
|
924fde8790 | ||
|
dea70974bf
|
|||
|
43fd7b8d47
|
9
.github/workflows/test-parsers.yml
vendored
9
.github/workflows/test-parsers.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/site/**'
|
||||
- 'src/main/kotlin/org/koitharu/kotatsu/parsers/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,9 +19,4 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- run: ./gradlew :test --tests "org.koitharu.kotatsu.parsers.MangaParserTest" || true
|
||||
- run: ./gradlew generateTestsReport
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Report
|
||||
path: build/test-results-html/TEST-org.koitharu.kotatsu.parsers.MangaParserTest.htm
|
||||
- run: ./gradlew assemble
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -29,6 +29,7 @@
|
||||
.idea/compiler.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/modules.xml
|
||||
.idea/ktlint-plugin.xml
|
||||
.idea/*.iml
|
||||
.idea/modules
|
||||
*.iml
|
||||
@@ -76,4 +77,4 @@ build/
|
||||
.idea/codeStyles/
|
||||
|
||||
src/test/resources/cookies.txt
|
||||
local.properties
|
||||
local.properties
|
||||
|
||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
<option name="version" value="1.9.22" />
|
||||
</component>
|
||||
</project>
|
||||
10
README.md
10
README.md
@@ -37,6 +37,12 @@ This library provides manga sources.
|
||||
|
||||
Versions are available on [JitPack](https://jitpack.io/#KotatsuApp/kotatsu-parsers)
|
||||
|
||||
When used in Android
|
||||
projects, [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) with
|
||||
the [NIO specification](https://developer.android.com/studio/write/java11-nio-support-table) should be enabled to
|
||||
support Java 8+ features.
|
||||
|
||||
|
||||
3. Usage in code
|
||||
|
||||
```kotlin
|
||||
@@ -45,8 +51,8 @@ This library provides manga sources.
|
||||
|
||||
`mangaLoaderContext` is an implementation of the `MangaLoaderContext` class.
|
||||
See examples
|
||||
of [Android](https://github.com/KotatsuApp/Kotatsu/blob/devel/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt)
|
||||
and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/main/kotlin/org/koitharu/kotatsu_dl/env/MangaLoaderContextImpl.kt)
|
||||
of [Android](https://github.com/KotatsuApp/Kotatsu/blob/devel/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt)
|
||||
and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/jvmMain/kotlin/org/koitharu/kotatsu_dl/logic/MangaLoaderContextImpl.kt)
|
||||
implementation.
|
||||
|
||||
Note that the `MangaSource.LOCAL` and `MangaSource.DUMMY` parsers cannot be instantiated.
|
||||
|
||||
18
build.gradle
18
build.gradle
@@ -2,8 +2,8 @@ import tasks.ReportGenerateTask
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.9.20'
|
||||
id 'com.google.devtools.ksp' version '1.9.20-1.0.14'
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
|
||||
id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ compileTestKotlin {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(8)
|
||||
sourceSets {
|
||||
main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin'
|
||||
}
|
||||
@@ -55,16 +55,16 @@ afterEvaluate {
|
||||
dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.okio:okio:3.6.0'
|
||||
api 'org.jsoup:jsoup:1.16.2'
|
||||
implementation 'com.squareup.okio:okio:3.7.0'
|
||||
api 'org.jsoup:jsoup:1.17.2'
|
||||
implementation 'org.json:json:20231013'
|
||||
implementation 'androidx.collection:collection-ktx:1.3.0'
|
||||
implementation 'androidx.collection:collection:1.4.0'
|
||||
|
||||
ksp project(':kotatsu-parsers-ksp')
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.1'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
testImplementation 'io.webfolder:quickjs:1.1.0'
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.8.22'
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.9.22'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation gradleApi()
|
||||
implementation 'org.simpleframework:simple-xml:2.7.1'
|
||||
implementation 'com.soywiz.korlibs.korte:korte-jvm:4.0.9'
|
||||
implementation 'com.soywiz.korlibs.korte:korte-jvm:4.0.10'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ plugins {
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14'
|
||||
implementation 'com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.17'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
object ErrorMessages {
|
||||
|
||||
const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED = "Multiple states are not supported by this source"
|
||||
const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED = "Multiple genres are not supported by this source"
|
||||
const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED =
|
||||
"Multiple Content Rating are not supported by this source"
|
||||
const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED =
|
||||
"Filtering by both genres and locale is not supported by this source"
|
||||
const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED =
|
||||
"Filtering by both genres and states is not supported by this source"
|
||||
const val SEARCH_NOT_SUPPORTED = "Search is not supported by this source"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
@@ -33,16 +34,30 @@ abstract class MangaParser @InternalParsersApi constructor(
|
||||
open val availableStates: Set<MangaState>
|
||||
get() = emptySet()
|
||||
|
||||
|
||||
open val availableContentRating: Set<ContentRating>
|
||||
get() = emptySet()
|
||||
|
||||
/**
|
||||
* Whether parser supports filtering by more than one tag
|
||||
*/
|
||||
open val isMultipleTagsSupported: Boolean = true
|
||||
|
||||
/**
|
||||
* Whether parser supports tagsExclude field in filter
|
||||
*/
|
||||
open val isTagsExclusionSupported: Boolean = false
|
||||
|
||||
/**
|
||||
* Whether parser supports searching by string query using [MangaListFilter.Search]
|
||||
*/
|
||||
open val isSearchSupported: Boolean = true
|
||||
|
||||
@Deprecated(
|
||||
message = "Use availableSortOrders instead",
|
||||
replaceWith = ReplaceWith("availableSortOrders"),
|
||||
)
|
||||
val sortOrders: Set<SortOrder>
|
||||
open val sortOrders: Set<SortOrder>
|
||||
get() = availableSortOrders
|
||||
|
||||
val config by lazy { context.getConfig(source) }
|
||||
@@ -67,7 +82,8 @@ abstract class MangaParser @InternalParsersApi constructor(
|
||||
/**
|
||||
* Used as fallback if value of `sortOrder` passed to [getList] is null
|
||||
*/
|
||||
protected open val defaultSortOrder: SortOrder
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
|
||||
open val defaultSortOrder: SortOrder
|
||||
get() {
|
||||
val supported = availableSortOrders
|
||||
return SortOrder.entries.first { it in supported }
|
||||
@@ -91,12 +107,13 @@ abstract class MangaParser @InternalParsersApi constructor(
|
||||
"Use getList with filter instead",
|
||||
replaceWith = ReplaceWith("getList(offset, filter)"),
|
||||
)
|
||||
abstract suspend fun getList(
|
||||
open suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
tagsExclude: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga>
|
||||
): List<Manga> = throw NotImplementedError("Please implement getList(offset, filter) instead")
|
||||
|
||||
/**
|
||||
* Parse list of manga with search by text query
|
||||
@@ -130,18 +147,51 @@ abstract class MangaParser @InternalParsersApi constructor(
|
||||
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
|
||||
),
|
||||
)
|
||||
open suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
||||
open suspend fun getList(
|
||||
offset: Int,
|
||||
tags: Set<MangaTag>?,
|
||||
tagsExclude: Set<MangaTag>?,
|
||||
sortOrder: SortOrder?,
|
||||
): List<Manga> {
|
||||
return getList(
|
||||
offset,
|
||||
MangaListFilter.Advanced(sortOrder ?: defaultSortOrder, tags.orEmpty(), null, emptySet()),
|
||||
MangaListFilter.Advanced(
|
||||
sortOrder = sortOrder ?: defaultSortOrder,
|
||||
tags = tags.orEmpty(),
|
||||
tagsExclude = tagsExclude.orEmpty(),
|
||||
locale = null,
|
||||
states = emptySet(),
|
||||
contentRating = emptySet(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
open suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
return when (filter) {
|
||||
is MangaListFilter.Advanced -> getList(offset, null, filter.tags, filter.sortOrder)
|
||||
is MangaListFilter.Search -> getList(offset, filter.query, null, defaultSortOrder)
|
||||
null -> getList(offset, null, null, defaultSortOrder)
|
||||
is MangaListFilter.Advanced -> getList(
|
||||
offset = offset,
|
||||
query = null,
|
||||
tags = filter.tags,
|
||||
tagsExclude = filter.tagsExclude,
|
||||
sortOrder = filter.sortOrder,
|
||||
)
|
||||
|
||||
is MangaListFilter.Search -> getList(
|
||||
offset = offset,
|
||||
query = filter.query,
|
||||
tags = null,
|
||||
tagsExclude = null,
|
||||
sortOrder = defaultSortOrder,
|
||||
)
|
||||
|
||||
null -> getList(
|
||||
offset = offset,
|
||||
query = null,
|
||||
tags = null,
|
||||
tagsExclude = null,
|
||||
sortOrder = defaultSortOrder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.Paginator
|
||||
|
||||
@InternalParsersApi
|
||||
@@ -21,12 +18,16 @@ abstract class PagedMangaParser(
|
||||
@JvmField
|
||||
protected val searchPaginator = Paginator(searchPageSize)
|
||||
|
||||
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||
return getList(searchPaginator, offset, query, null, defaultSortOrder)
|
||||
}
|
||||
|
||||
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
||||
return getList(paginator, offset, null, tags, sortOrder ?: defaultSortOrder)
|
||||
final override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
return getList(
|
||||
paginator = if (filter is MangaListFilter.Search) {
|
||||
searchPaginator
|
||||
} else {
|
||||
paginator
|
||||
},
|
||||
offset = offset,
|
||||
filter = filter,
|
||||
)
|
||||
}
|
||||
|
||||
@InternalParsersApi
|
||||
@@ -35,20 +36,53 @@ abstract class PagedMangaParser(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
tagsExclude: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
|
||||
|
||||
abstract suspend fun getListPage(page: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga>
|
||||
open suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
tagsExclude: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead")
|
||||
|
||||
open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
return when (filter) {
|
||||
is MangaListFilter.Advanced -> getListPage(
|
||||
page = page,
|
||||
query = null,
|
||||
tags = filter.tags,
|
||||
tagsExclude = filter.tagsExclude,
|
||||
sortOrder = filter.sortOrder,
|
||||
)
|
||||
|
||||
is MangaListFilter.Search -> getListPage(
|
||||
page = page,
|
||||
query = filter.query,
|
||||
tags = null,
|
||||
tagsExclude = null,
|
||||
sortOrder = defaultSortOrder,
|
||||
)
|
||||
|
||||
null -> getListPage(
|
||||
page = page,
|
||||
query = null,
|
||||
tags = null,
|
||||
tagsExclude = null,
|
||||
sortOrder = defaultSortOrder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getList(
|
||||
paginator: Paginator,
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
filter: MangaListFilter?,
|
||||
): List<Manga> {
|
||||
val page = paginator.getPage(offset)
|
||||
val list = getListPage(page, query, tags, sortOrder)
|
||||
val list = getListPage(page, filter)
|
||||
paginator.onListReceived(offset, page, list.size)
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
enum class ContentRating {
|
||||
SAFE,
|
||||
SUGGESTIVE,
|
||||
ADULT
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import java.util.*
|
||||
|
||||
sealed interface MangaListFilter {
|
||||
@@ -8,6 +9,16 @@ sealed interface MangaListFilter {
|
||||
|
||||
val sortOrder: SortOrder?
|
||||
|
||||
fun isValid(parser: MangaParser): Boolean = when (this) {
|
||||
is Advanced -> (sortOrder in parser.availableSortOrders) &&
|
||||
(tags.size <= 1 || parser.isMultipleTagsSupported) &&
|
||||
(tagsExclude.isEmpty() || parser.isTagsExclusionSupported) &&
|
||||
(contentRating.isEmpty() || parser.availableContentRating.containsAll(contentRating)) &&
|
||||
(states.isEmpty() || parser.availableStates.containsAll(states))
|
||||
|
||||
is Search -> parser.isSearchSupported
|
||||
}
|
||||
|
||||
data class Search(
|
||||
@JvmField val query: String,
|
||||
) : MangaListFilter {
|
||||
@@ -20,10 +31,63 @@ sealed interface MangaListFilter {
|
||||
data class Advanced(
|
||||
override val sortOrder: SortOrder,
|
||||
@JvmField val tags: Set<MangaTag>,
|
||||
@JvmField val tagsExclude: Set<MangaTag>,
|
||||
@JvmField val locale: Locale?,
|
||||
@JvmField val states: Set<MangaState>,
|
||||
@JvmField val contentRating: Set<ContentRating>,
|
||||
) : MangaListFilter {
|
||||
|
||||
override fun isEmpty(): Boolean = tags.isEmpty() && locale == null && states.isEmpty()
|
||||
override fun isEmpty(): Boolean =
|
||||
tags.isEmpty() && tagsExclude.isEmpty() && locale == null && states.isEmpty() && contentRating.isEmpty()
|
||||
|
||||
fun newBuilder() = Builder(sortOrder)
|
||||
.tags(tags)
|
||||
.tagsExclude(tagsExclude)
|
||||
.locale(locale)
|
||||
.states(states)
|
||||
.contentRatings(contentRating)
|
||||
|
||||
class Builder(sortOrder: SortOrder) {
|
||||
|
||||
private var _sortOrder: SortOrder = sortOrder
|
||||
private var _tags: Set<MangaTag>? = null
|
||||
private var _tagsExclude: Set<MangaTag>? = null
|
||||
private var _locale: Locale? = null
|
||||
private var _states: Set<MangaState>? = null
|
||||
private var _contentRating: Set<ContentRating>? = null
|
||||
|
||||
fun sortOrder(order: SortOrder) = apply {
|
||||
_sortOrder = order
|
||||
}
|
||||
|
||||
fun tags(tags: Set<MangaTag>?) = apply {
|
||||
_tags = tags
|
||||
}
|
||||
|
||||
fun tagsExclude(tags: Set<MangaTag>?) = apply {
|
||||
_tagsExclude = tags
|
||||
}
|
||||
|
||||
fun locale(locale: Locale?) = apply {
|
||||
_locale = locale
|
||||
}
|
||||
|
||||
fun states(states: Set<MangaState>?) = apply {
|
||||
_states = states
|
||||
}
|
||||
|
||||
fun contentRatings(rating: Set<ContentRating>?) = apply {
|
||||
_contentRating = rating
|
||||
}
|
||||
|
||||
fun build() = Advanced(
|
||||
sortOrder = _sortOrder,
|
||||
tags = _tags.orEmpty(),
|
||||
tagsExclude = _tagsExclude.orEmpty(),
|
||||
locale = _locale,
|
||||
states = _states.orEmpty(),
|
||||
contentRating = _contentRating.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
enum class MangaState {
|
||||
ONGOING, FINISHED, ABANDONED, PAUSED
|
||||
ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ enum class SortOrder {
|
||||
POPULARITY,
|
||||
RATING,
|
||||
NEWEST,
|
||||
ALPHABETICAL
|
||||
}
|
||||
ALPHABETICAL,
|
||||
ALPHABETICAL_DESC
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import org.koitharu.kotatsu.parsers.util.json.isNullOrEmpty
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
@@ -33,6 +34,12 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
|
||||
|
||||
override val isTagsExclusionSupported: Boolean = true
|
||||
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain(
|
||||
"bato.to",
|
||||
"batocomic.com",
|
||||
@@ -61,37 +68,85 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
"zbato.org",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
if (!query.isNullOrEmpty()) {
|
||||
return search(page, query)
|
||||
}
|
||||
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/browse?sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED,
|
||||
-> append("update.za")
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
SortOrder.POPULARITY -> append("views_a.za")
|
||||
SortOrder.NEWEST -> append("create.za")
|
||||
SortOrder.ALPHABETICAL -> append("title.az")
|
||||
SortOrder.RATING -> Unit
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
return search(page, filter.query)
|
||||
}
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&genres=")
|
||||
appendAll(tags, ",") { it.key }
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
|
||||
append("/browse?sort=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> append("update.za")
|
||||
SortOrder.POPULARITY -> append("views_a.za")
|
||||
SortOrder.NEWEST -> append("create.za")
|
||||
SortOrder.ALPHABETICAL -> append("title.az")
|
||||
else -> append("update.za")
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&release=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "completed"
|
||||
MangaState.ABANDONED -> "cancelled"
|
||||
MangaState.PAUSED -> "hiatus"
|
||||
MangaState.UPCOMING -> "pending"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
filter.locale?.let {
|
||||
append("&langs=")
|
||||
append(it.language)
|
||||
}
|
||||
|
||||
append("&genres=")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
appendAll(filter.tags, ",") { it.key }
|
||||
}
|
||||
|
||||
append("|")
|
||||
if (filter.tagsExclude.isNotEmpty()) {
|
||||
appendAll(filter.tagsExclude, ",") { it.key }
|
||||
}
|
||||
|
||||
if (filter.contentRating.isNotEmpty()) {
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
ContentRating.SAFE -> append(",gore,bloody,violence,ecchi,adult,mature,smut,hentai")
|
||||
else -> append("")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
return parseList(url, page)
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/browse?sort=update.za")
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
return parseList(url, page)
|
||||
}
|
||||
append("&page=")
|
||||
append(page)
|
||||
}
|
||||
return parseList(url, page)
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
@@ -109,9 +164,11 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
?.selectFirst(".limit-html")
|
||||
?.html(),
|
||||
tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(),
|
||||
state = when (attrs["Release status:"]?.text()) {
|
||||
state = when (attrs["Original work:"]?.text()) {
|
||||
"Ongoing" -> MangaState.ONGOING
|
||||
"Completed" -> MangaState.FINISHED
|
||||
"Cancelled" -> MangaState.ABANDONED
|
||||
"Hiatus" -> MangaState.PAUSED
|
||||
else -> manga.state
|
||||
},
|
||||
author = attrs["Authors:"]?.text()?.trim() ?: manga.author,
|
||||
@@ -129,7 +186,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
val scripts = webClient.httpGet(fullUrl).parseHtml().select("script")
|
||||
for (script in scripts) {
|
||||
val scriptSrc = script.html()
|
||||
val p = scriptSrc.indexOf("const imgHttpLis =")
|
||||
val p = scriptSrc.indexOf("const imgHttps =")
|
||||
if (p == -1) continue
|
||||
val start = scriptSrc.indexOf('[', p)
|
||||
val end = scriptSrc.indexOf(';', start)
|
||||
@@ -149,7 +206,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
val url = images.getString(i)
|
||||
result += MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url + "?" + args.getString(i),
|
||||
url = if (args.length() == 0) url else url + "?" + args.getString(i),
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
@@ -170,7 +227,7 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
jo.keys().forEach { key ->
|
||||
val item = jo.getJSONObject(key)
|
||||
result += MangaTag(
|
||||
title = item.getString("text").toTitleCase(),
|
||||
title = item.getString("text").toTitleCase(Locale.ENGLISH),
|
||||
key = item.getString("file"),
|
||||
source = source,
|
||||
)
|
||||
@@ -180,6 +237,22 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
throw ParseException("Cannot find gernes list", scripts[0].baseUri())
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.CHINESE, Locale.ENGLISH, Locale.US, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN, Locale.JAPANESE,
|
||||
Locale("af"), Locale("ar"), Locale("az"), Locale("eu"), Locale("be"),
|
||||
Locale("bn"), Locale("bs"), Locale("bg"), Locale("my"), Locale("km"),
|
||||
Locale("ceb"), Locale("zh_hk"), Locale("zh_tw"), Locale("hr"), Locale("cs"),
|
||||
Locale("da"), Locale("nl"), Locale("eo"), Locale("et"), Locale("fil"),
|
||||
Locale("fi"), Locale("ka"), Locale("el"), Locale("ht"), Locale("he"),
|
||||
Locale("hi"), Locale("hu"), Locale("id"), Locale("kk"), Locale("ko"),
|
||||
Locale("lv"), Locale("ms"), Locale("ml"), Locale("mo"), Locale("mn"),
|
||||
Locale("ne"), Locale("no"), Locale("fa"), Locale("pl"), Locale("pt"),
|
||||
Locale("pt_br"), Locale("pt_pt"), Locale("ro"), Locale("ru"), Locale("sr"),
|
||||
Locale("si"), Locale("sk"), Locale("es"), Locale("es_419"), Locale("ta"),
|
||||
Locale("te"), Locale("th"), Locale("ti"), Locale("tr"), Locale("uk"),
|
||||
Locale("vi"), Locale("zu"),
|
||||
)
|
||||
|
||||
private suspend fun search(page: Int, query: String): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
|
||||
@@ -2,10 +2,11 @@ package org.koitharu.kotatsu.parsers.site.all
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.*
|
||||
@@ -14,16 +15,15 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* https://api.comick.fun/docs/static/index.html
|
||||
* cc
|
||||
*/
|
||||
|
||||
private const val PAGE_SIZE = 20
|
||||
private const val CHAPTERS_LIMIT = 99999
|
||||
|
||||
@MangaSourceParser("COMICK_FUN", "ComicK")
|
||||
internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.COMICK_FUN) {
|
||||
internal class ComickFunParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.COMICK_FUN, 20) {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("comick.app")
|
||||
override val configKeyDomain = ConfigKey.Domain("comick.cc")
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.POPULARITY,
|
||||
@@ -31,47 +31,58 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
SortOrder.RATING,
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var cachedTags: SparseArrayCompat<MangaTag>? = null
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
private val tagsArray = SuspendLazy(::loadTags)
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val domain = domain
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domain)
|
||||
append("/v1.0/search?tachiyomi=true")
|
||||
if (!query.isNullOrEmpty()) {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
val url = urlBuilder()
|
||||
.host("api.$domain")
|
||||
.addPathSegment("v1.0")
|
||||
.addPathSegment("search")
|
||||
.addQueryParameter("type", "comic")
|
||||
.addQueryParameter("tachiyomi", "true")
|
||||
.addQueryParameter("limit", pageSize.toString())
|
||||
.addQueryParameter("page", page.toString())
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
url.addQueryParameter("q", filter.query)
|
||||
}
|
||||
|
||||
null -> {
|
||||
url.addQueryParameter("sort", "view")
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
filter.tags.forEach { tag ->
|
||||
url.addQueryParameter("genres", tag.key)
|
||||
}
|
||||
append("&q=")
|
||||
append(query.urlEncoded())
|
||||
} else {
|
||||
append("&limit=")
|
||||
append(PAGE_SIZE)
|
||||
append("&page=")
|
||||
append((offset / PAGE_SIZE) + 1)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&genres=")
|
||||
appendAll(tags, "&genres=", MangaTag::key)
|
||||
}
|
||||
append("&sort=") // view, uploaded, rating, follow, user_follow_count
|
||||
append(
|
||||
when (sortOrder) {
|
||||
url.addQueryParameter(
|
||||
"sort",
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> "view"
|
||||
SortOrder.RATING -> "rating"
|
||||
else -> "uploaded"
|
||||
},
|
||||
)
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
url.addQueryParameter(
|
||||
"status",
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "1"
|
||||
MangaState.FINISHED -> "2"
|
||||
MangaState.ABANDONED -> "3"
|
||||
MangaState.PAUSED -> "4"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val ja = webClient.httpGet(url).parseJsonArray()
|
||||
val tagsMap = cachedTags ?: loadTags()
|
||||
val ja = webClient.httpGet(url.build()).parseJsonArray()
|
||||
val tagsMap = tagsArray.get()
|
||||
return ja.mapJSON { jo ->
|
||||
val slug = jo.getString("slug")
|
||||
Manga(
|
||||
@@ -86,13 +97,13 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
largeCoverUrl = null,
|
||||
description = jo.getStringOrNull("desc"),
|
||||
tags = jo.selectGenres(tagsMap),
|
||||
state = runCatching {
|
||||
if (jo.getBoolean("translation_completed")) {
|
||||
MangaState.FINISHED
|
||||
} else {
|
||||
MangaState.ONGOING
|
||||
}
|
||||
}.getOrNull(),
|
||||
state = when (jo.getIntOrDefault("status", 0)) {
|
||||
1 -> MangaState.ONGOING
|
||||
2 -> MangaState.FINISHED
|
||||
3 -> MangaState.ABANDONED
|
||||
4 -> MangaState.PAUSED
|
||||
else -> null
|
||||
},
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
@@ -104,11 +115,12 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
val url = "https://api.$domain/comic/${manga.url}?tachiyomi=true"
|
||||
val jo = webClient.httpGet(url).parseJson()
|
||||
val comic = jo.getJSONObject("comic")
|
||||
var alt = ""
|
||||
comic.getJSONArray("md_titles").mapJSON { alt += it.getString("title") + " - " }
|
||||
return manga.copy(
|
||||
title = comic.getString("title"),
|
||||
altTitle = null, // TODO
|
||||
altTitle = alt.ifEmpty { comic.getStringOrNull("title") },
|
||||
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
|
||||
description = comic.getStringOrNull("parsed") ?: comic.getString("desc"),
|
||||
description = comic.getStringOrNull("parsed") ?: comic.getStringOrNull("desc"),
|
||||
tags = manga.tags + comic.getJSONArray("md_comic_md_genres").mapJSONToSet {
|
||||
val g = it.getJSONObject("md_genres")
|
||||
MangaTag(
|
||||
@@ -138,7 +150,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val sparseArray = cachedTags ?: loadTags()
|
||||
val sparseArray = tagsArray.get()
|
||||
val set = ArraySet<MangaTag>(sparseArray.size())
|
||||
for (i in 0 until sparseArray.size()) {
|
||||
set.add(sparseArray.valueAt(i))
|
||||
@@ -153,13 +165,12 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
tags.append(
|
||||
jo.getInt("id"),
|
||||
MangaTag(
|
||||
title = jo.getString("name"),
|
||||
title = jo.getString("name").toTitleCase(Locale.ENGLISH),
|
||||
key = jo.getString("slug"),
|
||||
source = source,
|
||||
),
|
||||
)
|
||||
}
|
||||
cachedTags = tags
|
||||
return tags
|
||||
}
|
||||
|
||||
@@ -168,9 +179,39 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
url = "https://api.${domain}/comic/$hid/chapters?limit=$CHAPTERS_LIMIT",
|
||||
).parseJson().getJSONArray("chapters")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||
val list = ja.toJSONList().reversed()
|
||||
val counters = HashMap<String?, Int>()
|
||||
return ja.toJSONList().reversed().mapChapters { _, jo ->
|
||||
val vol = jo.getStringOrNull("vol")
|
||||
val chap = jo.getStringOrNull("chap")
|
||||
val locale = Locale.forLanguageTag(jo.getString("lang"))
|
||||
val group = jo.optJSONArray("group_name")?.joinToString(", ")
|
||||
val branch = buildString {
|
||||
append(locale.getDisplayName(locale).toTitleCase(locale))
|
||||
if (!group.isNullOrEmpty()) {
|
||||
append(" (")
|
||||
append(group)
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
MangaChapter(
|
||||
id = generateUid(jo.getLong("id")),
|
||||
name = buildString {
|
||||
vol?.let { append("Vol ").append(it).append(' ') }
|
||||
chap?.let { append("Chap ").append(it) }
|
||||
jo.getStringOrNull("title")?.let { append(": ").append(it) }
|
||||
},
|
||||
number = counters.incrementAndGet(branch),
|
||||
url = jo.getString("hid"),
|
||||
scanlator = jo.optJSONArray("group_name")?.asIterable<String>()?.joinToString()
|
||||
?.takeUnless { it.isBlank() },
|
||||
uploadDate = dateFormat.tryParse(jo.getString("created_at").substringBefore('T')),
|
||||
branch = branch,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
val chaptersBuilder = ChaptersListBuilder(list.size)
|
||||
|
||||
/*val chaptersBuilder = ChaptersListBuilder(list.size)
|
||||
val branchedChapters = HashMap<String?, HashMap<Pair<String?, String?>, MangaChapter>>()
|
||||
for (jo in list) {
|
||||
val vol = jo.getStringOrNull("vol")
|
||||
@@ -201,7 +242,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
branchedChapters.getOrPut(branch, ::HashMap)[volChap] = chapter
|
||||
}
|
||||
}
|
||||
return chaptersBuilder.toList()
|
||||
return chaptersBuilder.toList()*/
|
||||
}
|
||||
|
||||
private fun JSONObject.selectGenres(tags: SparseArrayCompat<MangaTag>): Set<MangaTag> {
|
||||
@@ -209,9 +250,13 @@ internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
val res = ArraySet<MangaTag>(array.length())
|
||||
for (i in 0 until array.length()) {
|
||||
val id = array.getInt(i)
|
||||
val tag = tags.get(id) ?: continue
|
||||
val tag = tags[id] ?: continue
|
||||
res.add(tag)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private fun JSONArray.joinToString(separator: String): String {
|
||||
return (0 until length()).joinToString(separator) { i -> getString(i) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.koitharu.kotatsu.parsers.site.all
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.collection.set
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.jsoup.internal.StringUtil
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
@@ -23,9 +26,8 @@ internal class ExHentaiParser(
|
||||
context: MangaLoaderContext,
|
||||
) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = Collections.singleton(
|
||||
SortOrder.NEWEST,
|
||||
)
|
||||
override val availableSortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST)
|
||||
override val isTagsExclusionSupported: Boolean = true
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain(
|
||||
@@ -40,7 +42,8 @@ internal class ExHentaiParser(
|
||||
private val authCookies = arrayOf("ipb_member_id", "ipb_pass_hash")
|
||||
private var updateDm = false
|
||||
private val nextPages = SparseArrayCompat<Long>()
|
||||
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(true)
|
||||
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
|
||||
private val tagsMap = SuspendLazy(::fetchTags)
|
||||
|
||||
override val isAuthorized: Boolean
|
||||
get() {
|
||||
@@ -65,38 +68,54 @@ internal class ExHentaiParser(
|
||||
paginator.firstPage = 0
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
var search = query?.urlEncoded().orEmpty()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val next = nextPages.get(page, 0L)
|
||||
|
||||
if (page > 0 && next == 0L) {
|
||||
assert(false) { "Page timestamp not found" }
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
var search = ""
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/?next=")
|
||||
append(next)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
var fCats = 0
|
||||
for (tag in tags) {
|
||||
tag.key.toIntOrNull()?.let { fCats = fCats or it } ?: run {
|
||||
search += tag.key + " "
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
search += filter.query.urlEncoded()
|
||||
append("&f_search=")
|
||||
append(search.trim().replace(' ', '+'))
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
filter.toSearchQuery()?.let { sq ->
|
||||
append("&f_search=")
|
||||
append(sq.urlEncoded())
|
||||
}
|
||||
|
||||
val catsOn = filter.tags.mapNotNullToSet { it.key.toIntOrNull() }
|
||||
val catsOff = filter.tagsExclude.mapNotNullToSet { it.key.toIntOrNull() }
|
||||
if (catsOff.size >= 10) {
|
||||
return emptyList()
|
||||
}
|
||||
var fCats = catsOn.fold(0, Int::or)
|
||||
if (fCats != 0) {
|
||||
fCats = 1023 - fCats
|
||||
}
|
||||
fCats = catsOff.fold(fCats, Int::or)
|
||||
|
||||
if (fCats != 0) {
|
||||
append("&f_cats=")
|
||||
append(fCats)
|
||||
}
|
||||
}
|
||||
if (fCats != 0) {
|
||||
append("&f_cats=")
|
||||
append(1023 - fCats)
|
||||
}
|
||||
}
|
||||
if (search.isNotEmpty()) {
|
||||
append("&f_search=")
|
||||
append(search.trim().replace(' ', '+'))
|
||||
|
||||
null -> {}
|
||||
}
|
||||
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
|
||||
if (updateDm) {
|
||||
@@ -107,6 +126,7 @@ internal class ExHentaiParser(
|
||||
append("&f_sh=on")
|
||||
}
|
||||
}
|
||||
|
||||
val body = webClient.httpGet(url).parseHtml().body()
|
||||
val root = body.selectFirst("table.itg")
|
||||
?.selectFirst("tbody")
|
||||
@@ -114,17 +134,17 @@ internal class ExHentaiParser(
|
||||
body.parseFailed("Cannot find root")
|
||||
} else {
|
||||
updateDm = true
|
||||
return getListPage(page, query, tags, sortOrder)
|
||||
return getListPage(page, filter)
|
||||
}
|
||||
updateDm = false
|
||||
nextPages[page + 1] = getNextTimestamp(body)
|
||||
return root.children().mapNotNull { tr ->
|
||||
if (tr.childrenSize() != 2) return@mapNotNull null
|
||||
val (td1, td2) = tr.children()
|
||||
val glink = td2.selectFirstOrThrow("div.glink")
|
||||
val a = glink.parents().select("a").first() ?: glink.parseFailed("link not found")
|
||||
val gLink = td2.selectFirstOrThrow("div.glink")
|
||||
val a = gLink.parents().select("a").first() ?: gLink.parseFailed("link not found")
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
val tagsDiv = glink.nextElementSibling() ?: glink.parseFailed("tags div not found")
|
||||
val tagsDiv = gLink.nextElementSibling() ?: gLink.parseFailed("tags div not found")
|
||||
val mainTag = td2.selectFirst("div.cn")?.let { div ->
|
||||
MangaTag(
|
||||
title = div.text().toTitleCase(),
|
||||
@@ -134,7 +154,7 @@ internal class ExHentaiParser(
|
||||
}
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = glink.text().cleanupTitle(),
|
||||
title = gLink.text().cleanupTitle(),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = a.absUrl("href"),
|
||||
@@ -155,8 +175,17 @@ internal class ExHentaiParser(
|
||||
val root = doc.body().selectFirstOrThrow("div.gm")
|
||||
val cover = root.getElementById("gd1")?.children()?.first()
|
||||
val title = root.getElementById("gd2")
|
||||
val taglist = root.getElementById("taglist")
|
||||
val tagList = root.getElementById("taglist")
|
||||
val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
|
||||
val lang = root.getElementById("gd3")
|
||||
?.selectFirst("tr:contains(Language)")
|
||||
?.selectFirst(".gdt2")?.ownTextOrNull()
|
||||
|
||||
val tagMap = tagsMap.get()
|
||||
val tags = ArraySet<MangaTag>()
|
||||
tagList?.selectFirst("tr:contains(female:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
|
||||
tagList?.selectFirst("tr:contains(male:)")?.select("a")?.mapNotNullTo(tags) { tagMap[it.text()] }
|
||||
|
||||
return manga.copy(
|
||||
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title,
|
||||
altTitle = title?.getElementById("gj")?.text()?.cleanupTitle() ?: manga.altTitle,
|
||||
@@ -166,10 +195,11 @@ internal class ExHentaiParser(
|
||||
?.toFloatOrNull()
|
||||
?.div(5f) ?: manga.rating,
|
||||
largeCoverUrl = cover?.styleValueOrNull("background")?.cssUrl(),
|
||||
description = taglist?.select("tr")?.joinToString("<br>") { tr ->
|
||||
tags = tags,
|
||||
description = tagList?.select("tr")?.joinToString("<br>") { tr ->
|
||||
val (tc, td) = tr.children()
|
||||
val subtags = td.select("a").joinToString { it.html() }
|
||||
"<b>${tc.html()}</b> $subtags"
|
||||
val subTags = td.select("a").joinToString { it.html() }
|
||||
"<b>${tc.html()}</b> $subTags"
|
||||
},
|
||||
chapters = tabs?.select("a")?.findLast { a ->
|
||||
a.text().toIntOrNull() != null
|
||||
@@ -186,7 +216,7 @@ internal class ExHentaiParser(
|
||||
uploadDate = 0L,
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
branch = lang,
|
||||
)
|
||||
}
|
||||
chapters.toList()
|
||||
@@ -213,18 +243,72 @@ internal class ExHentaiParser(
|
||||
return doc.body().requireElementById("img").attrAsAbsoluteUrl("src")
|
||||
}
|
||||
|
||||
private val tags =
|
||||
"ahegao,anal,angel,apron,bandages,bbw,bdsm,beauty mark,big areolae,big ass,big breasts,big clit,big lips," +
|
||||
"big nipples,bikini,blackmail,bloomers,blowjob,bodysuit,bondage,breast expansion,bukkake,bunny girl,business suit," +
|
||||
"catgirl,centaur,cheating,chinese dress,christmas,collar,corset,cosplaying,cowgirl,crossdressing,cunnilingus," +
|
||||
"dark skin,daughter,deepthroat,defloration,demon girl,double penetration,dougi,dragon,drunk,elf,exhibitionism,farting," +
|
||||
"females only,femdom,filming,fingering,fishnets,footjob,fox girl,furry,futanari,garter belt,ghost,giantess," +
|
||||
"glasses,gloves,goblin,gothic lolita,growth,guro,gyaru,hair buns,hairy,hairy armpits,handjob,harem,hidden sex," +
|
||||
"horns,huge breasts,humiliation,impregnation,incest,inverted nipples,kemonomimi,kimono,kissing,lactation," +
|
||||
"latex,leg lock,leotard,lingerie,lizard girl,maid,masked face,masturbation,midget,miko,milf,mind break," +
|
||||
"mind control,monster girl,mother,muscle,nakadashi,netorare,nose hook,nun,nurse,oil,paizuri,panda girl," +
|
||||
"pantyhose,piercing,pixie cut,policewoman,ponytail,pregnant,rape,rimjob,robot,scat,lolicon,schoolgirl uniform," +
|
||||
"sex toys,shemale,sister,small breasts,smell,sole dickgirl,sole female,squirting,stockings,sundress,sweating," +
|
||||
"swimsuit,swinging,tail,tall girl,teacher,tentacles,thigh high boots,tomboy,transformation,twins,twintails," +
|
||||
"unusual pupils,urination,vore,vtuber,widow,wings,witch,wolf girl,x-ray,yuri,zombie,sole male,males only,yaoi," +
|
||||
"tomgirl,tall man,oni,shotacon,prostate massage,policeman,males only,huge penis,fox boy,feminization,dog boy,dickgirl on male,big penis"
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
return tagsMap.get().values.toSet()
|
||||
}
|
||||
|
||||
private suspend fun fetchTags(): Map<String, MangaTag> {
|
||||
val tagMap = ArrayMap<String, MangaTag>()
|
||||
val tagElements = tags.split(",")
|
||||
for (el in tagElements) {
|
||||
if (el.isEmpty()) continue
|
||||
tagMap[el] = MangaTag(
|
||||
title = el.toTitleCase(Locale.ENGLISH),
|
||||
key = el,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet("https://${domain}").parseHtml()
|
||||
val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table")
|
||||
return root.select("div.cs").mapNotNullToSet { div ->
|
||||
val id = div.id().substringAfterLast('_').toIntOrNull()
|
||||
?: return@mapNotNullToSet null
|
||||
MangaTag(
|
||||
title = div.text().toTitleCase(),
|
||||
root.select("div.cs").mapNotNullToSet { div ->
|
||||
val id = div.id().substringAfterLast('_').toIntOrNull() ?: return@mapNotNullToSet null
|
||||
val name = div.text().toTitleCase(Locale.ENGLISH)
|
||||
tagMap[name] = MangaTag(
|
||||
title = "Kind: $name",
|
||||
key = id.toString(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
return tagMap
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.JAPANESE,
|
||||
Locale.ENGLISH,
|
||||
Locale.CHINESE,
|
||||
Locale("nl"),
|
||||
Locale.FRENCH,
|
||||
Locale.GERMAN,
|
||||
Locale("hu"),
|
||||
Locale.ITALIAN,
|
||||
Locale("kr"),
|
||||
Locale("pl"),
|
||||
Locale("pt"),
|
||||
Locale("ru"),
|
||||
Locale("es"),
|
||||
Locale("th"),
|
||||
Locale("vi"),
|
||||
)
|
||||
|
||||
private fun Locale.toLanguagePath() = when (language) {
|
||||
else -> getDisplayLanguage(Locale.ENGLISH).lowercase()
|
||||
}
|
||||
|
||||
override suspend fun getUsername(): String {
|
||||
@@ -307,4 +391,30 @@ internal class ExHentaiParser(
|
||||
?.queryParameter("next")
|
||||
?.toLongOrNull() ?: 1
|
||||
}
|
||||
|
||||
private fun MangaListFilter.Advanced.toSearchQuery(): String? {
|
||||
val joiner = StringUtil.StringJoiner(" ")
|
||||
for (tag in tags) {
|
||||
if (tag.key.isNumeric()) {
|
||||
continue
|
||||
}
|
||||
joiner.add("tag:\"")
|
||||
joiner.append(tag.key)
|
||||
joiner.append("\"$")
|
||||
}
|
||||
for (tag in tagsExclude) {
|
||||
if (tag.key.isNumeric()) {
|
||||
continue
|
||||
}
|
||||
joiner.add("-tag:\"")
|
||||
joiner.append(tag.key)
|
||||
joiner.append("\"$")
|
||||
}
|
||||
locale?.let { lc ->
|
||||
joiner.add("language:\"")
|
||||
joiner.append(lc.toLanguagePath())
|
||||
joiner.append("\"$")
|
||||
}
|
||||
return joiner.complete().takeUnless { it.isEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,706 @@
|
||||
package org.koitharu.kotatsu.parsers.site.all
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.Headers
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.Jsoup
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
@MangaSourceParser("HITOMILA", "Hitomi.La", type = ContentType.HENTAI)
|
||||
class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.HITOMILA) {
|
||||
override val configKeyDomain = ConfigKey.Domain("hitomi.la")
|
||||
|
||||
private val ltnBaseUrl get() = "https://${getDomain("ltn")}"
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.POPULARITY,
|
||||
)
|
||||
|
||||
private val localeMap: Map<Locale, String> = mapOf(
|
||||
Locale("id") to "indonesian",
|
||||
Locale("jv") to "javanese",
|
||||
Locale("ca") to "catalan",
|
||||
Locale("ceb") to "cebuano",
|
||||
Locale("cs") to "czech",
|
||||
Locale("da") to "danish",
|
||||
Locale("de") to "german",
|
||||
Locale("et") to "estonian",
|
||||
Locale.ENGLISH to "english",
|
||||
Locale("es") to "spanish",
|
||||
Locale("eo") to "esperanto",
|
||||
Locale("fr") to "french",
|
||||
Locale("it") to "italian",
|
||||
Locale("hi") to "hindi",
|
||||
Locale("hu") to "hungarian",
|
||||
Locale("pl") to "polish",
|
||||
Locale("pt") to "portuguese",
|
||||
Locale("vi") to "vietnamese",
|
||||
Locale("tr") to "turkish",
|
||||
Locale("ru") to "russian",
|
||||
Locale("uk") to "ukrainian",
|
||||
Locale("ar") to "arabic",
|
||||
Locale.KOREAN to "korean",
|
||||
Locale.CHINESE to "chinese",
|
||||
Locale.JAPANESE to "japanese",
|
||||
)
|
||||
|
||||
private fun Locale?.getSiteLang(): String = when (this) {
|
||||
null -> "all"
|
||||
else -> localeMap[this] ?: "all"
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = localeMap.keys
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = coroutineScope {
|
||||
('a'..'z').map { alphabet ->
|
||||
async {
|
||||
val doc = webClient.httpGet("https://$domain/alltags-$alphabet.html").parseHtml()
|
||||
|
||||
doc.select(".posts > li").mapNotNull { element ->
|
||||
val num =
|
||||
element.ownText().let {
|
||||
Regex("""\((\d+)\)""").find(it)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||
}
|
||||
|
||||
if (num > 100) {
|
||||
val url = element.selectFirst("a")
|
||||
val href =
|
||||
url?.attrAsRelativeUrl("href")
|
||||
?: return@mapNotNull null
|
||||
|
||||
MangaTag(
|
||||
title = url.ownText().toTagTitle(),
|
||||
key = href.tagUrlToTag(),
|
||||
source = source,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll().flatten().toSet()
|
||||
}
|
||||
|
||||
private var cachedSearchIds: List<Int> = emptyList()
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
filter: MangaListFilter?,
|
||||
): List<Manga> = when (filter) {
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isEmpty()) {
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> {
|
||||
getGalleryIDsFromNozomi(
|
||||
"popular",
|
||||
"today",
|
||||
filter.locale.getSiteLang(),
|
||||
offset.nextOffsetRange(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
getGalleryIDsFromNozomi(null, "index", filter.locale.getSiteLang(), offset.nextOffsetRange())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (offset == 0) {
|
||||
cachedSearchIds =
|
||||
hitomiSearch(
|
||||
filter.tags.joinToString(" ") { it.key },
|
||||
filter.sortOrder == SortOrder.POPULARITY,
|
||||
filter.locale.getSiteLang(),
|
||||
).toList()
|
||||
}
|
||||
cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
|
||||
}
|
||||
}
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
if (offset == 0) {
|
||||
cachedSearchIds = hitomiSearch(filter.query, filter.sortOrder == SortOrder.POPULARITY).toList()
|
||||
}
|
||||
cachedSearchIds.subList(offset, min(offset + 25, cachedSearchIds.size))
|
||||
}
|
||||
|
||||
else -> getGalleryIDsFromNozomi(null, "popular", "all", offset.nextOffsetRange())
|
||||
}.toMangaList()
|
||||
|
||||
private fun Int.nextOffsetRange(): LongRange {
|
||||
val bytes = this * 4L
|
||||
return bytes.until(bytes + 100L)
|
||||
}
|
||||
|
||||
private suspend fun hitomiSearch(
|
||||
query: String,
|
||||
sortByPopularity: Boolean = false,
|
||||
language: String = "all",
|
||||
): Set<Int> =
|
||||
coroutineScope {
|
||||
val terms = query
|
||||
.trim()
|
||||
.replace(Regex("""^\?"""), "")
|
||||
.lowercase()
|
||||
.split(Regex("\\s+"))
|
||||
.map {
|
||||
it.replace('_', ' ')
|
||||
}
|
||||
|
||||
val positiveTerms = LinkedList<String>()
|
||||
val negativeTerms = LinkedList<String>()
|
||||
|
||||
for (term in terms) {
|
||||
if (term.startsWith("-")) {
|
||||
negativeTerms.push(term.removePrefix("-"))
|
||||
} else if (term.isNotBlank()) {
|
||||
positiveTerms.push(term)
|
||||
}
|
||||
}
|
||||
|
||||
val positiveResults = positiveTerms.map {
|
||||
async {
|
||||
runCatchingCancellable {
|
||||
getGalleryIDsForQuery(it, language)
|
||||
}.getOrDefault(emptySet())
|
||||
}
|
||||
}
|
||||
|
||||
val negativeResults = negativeTerms.map {
|
||||
async {
|
||||
runCatchingCancellable {
|
||||
getGalleryIDsForQuery(it, language)
|
||||
}.getOrDefault(emptySet())
|
||||
}
|
||||
}
|
||||
|
||||
val results = when {
|
||||
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", language)
|
||||
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", language)
|
||||
else -> emptySet()
|
||||
}.toMutableSet()
|
||||
|
||||
fun filterPositive(newResults: Set<Int>) {
|
||||
when {
|
||||
results.isEmpty() -> results.addAll(newResults)
|
||||
else -> results.retainAll(newResults)
|
||||
}
|
||||
}
|
||||
|
||||
fun filterNegative(newResults: Set<Int>) {
|
||||
results.removeAll(newResults)
|
||||
}
|
||||
|
||||
// positive results
|
||||
positiveResults.forEach {
|
||||
filterPositive(it.await())
|
||||
}
|
||||
|
||||
// negative results
|
||||
negativeResults.forEach {
|
||||
filterNegative(it.await())
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
// search.js
|
||||
private suspend fun getGalleryIDsForQuery(
|
||||
query: String,
|
||||
language: String = "all",
|
||||
): Set<Int> {
|
||||
query.replace("_", " ").let {
|
||||
if (it.indexOf(':') > -1) {
|
||||
val sides = it.split(":")
|
||||
val ns = sides[0]
|
||||
var tag = sides[1]
|
||||
|
||||
var area: String? = ns
|
||||
var lang = language
|
||||
when (ns) {
|
||||
"female", "male" -> {
|
||||
area = "tag"
|
||||
tag = it
|
||||
}
|
||||
|
||||
"language" -> {
|
||||
area = null
|
||||
lang = tag
|
||||
tag = "index"
|
||||
}
|
||||
}
|
||||
|
||||
return getGalleryIDsFromNozomi(area, tag, lang)
|
||||
}
|
||||
|
||||
val key = hashTerm(it)
|
||||
val node = getGalleryNodeAtAddress(0)
|
||||
val data = bSearch(key, node) ?: return emptySet()
|
||||
|
||||
return getGalleryIDsFromData(data)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGalleryIDsFromData(data: Pair<Long, Int>): Set<Int> {
|
||||
val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.data"
|
||||
val (offset, length) = data
|
||||
require(length in 1..100000000) {
|
||||
"Length $length is too long"
|
||||
}
|
||||
|
||||
val inbuf = getRangedResponse(url, offset.until(offset + length))
|
||||
|
||||
val galleryIDs = mutableSetOf<Int>()
|
||||
|
||||
val buffer =
|
||||
ByteBuffer
|
||||
.wrap(inbuf)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
val numberOfGalleryIDs = buffer.int
|
||||
|
||||
val expectedLength = numberOfGalleryIDs * 4 + 4
|
||||
|
||||
require(numberOfGalleryIDs in 1..10000000) {
|
||||
"number_of_galleryids $numberOfGalleryIDs is too long"
|
||||
}
|
||||
require(inbuf.size == expectedLength) {
|
||||
"inbuf.byteLength ${inbuf.size} != expected_length $expectedLength"
|
||||
}
|
||||
|
||||
for (i in 0.until(numberOfGalleryIDs))
|
||||
galleryIDs.add(buffer.int)
|
||||
|
||||
return galleryIDs
|
||||
}
|
||||
|
||||
private suspend fun bSearch(
|
||||
key: UByteArray,
|
||||
node: Node,
|
||||
): Pair<Long, Int>? {
|
||||
fun compareArrayBuffers(
|
||||
dv1: UByteArray,
|
||||
dv2: UByteArray,
|
||||
): Int {
|
||||
val top = min(dv1.size, dv2.size)
|
||||
|
||||
for (i in 0.until(top)) {
|
||||
if (dv1[i] < dv2[i]) {
|
||||
return -1
|
||||
} else if (dv1[i] > dv2[i]) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
fun locateKey(
|
||||
key: UByteArray,
|
||||
node: Node,
|
||||
): Pair<Boolean, Int> {
|
||||
for (i in node.keys.indices) {
|
||||
val cmpResult = compareArrayBuffers(key, node.keys[i])
|
||||
|
||||
if (cmpResult <= 0) {
|
||||
return Pair(cmpResult == 0, i)
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(false, node.keys.size)
|
||||
}
|
||||
|
||||
fun isLeaf(node: Node): Boolean {
|
||||
for (subnode in node.subNodeAddresses)
|
||||
if (subnode != 0L) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (node.keys.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val (there, where) = locateKey(key, node)
|
||||
if (there) {
|
||||
return node.datas[where]
|
||||
} else if (isLeaf(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val nextNode = getGalleryNodeAtAddress(node.subNodeAddresses[where])
|
||||
return bSearch(key, nextNode)
|
||||
}
|
||||
|
||||
private suspend fun getGalleryIDsFromNozomi(
|
||||
area: String?,
|
||||
tag: String,
|
||||
language: String,
|
||||
range: LongRange? = null,
|
||||
): Set<Int> {
|
||||
val nozomiAddress = when (area) {
|
||||
null -> "$ltnBaseUrl/$tag-$language.nozomi"
|
||||
else -> "$ltnBaseUrl/$area/$tag-$language.nozomi"
|
||||
}
|
||||
|
||||
val bytes = getRangedResponse(nozomiAddress, range)
|
||||
val nozomi = mutableSetOf<Int>()
|
||||
|
||||
val arrayBuffer = ByteBuffer
|
||||
.wrap(bytes)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
while (arrayBuffer.hasRemaining())
|
||||
nozomi.add(arrayBuffer.int)
|
||||
|
||||
return nozomi
|
||||
}
|
||||
|
||||
private val galleriesIndexVersion = SuspendLazy {
|
||||
webClient.httpGet("$ltnBaseUrl/galleriesindex/version?_=${System.currentTimeMillis()}").parseRaw()
|
||||
}
|
||||
|
||||
private data class Node(
|
||||
val keys: List<UByteArray>,
|
||||
val datas: List<Pair<Long, Int>>,
|
||||
val subNodeAddresses: List<Long>,
|
||||
)
|
||||
|
||||
private fun decodeNode(data: ByteArray): Node {
|
||||
val buffer = ByteBuffer
|
||||
.wrap(data)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
val uData = data.toUByteArray()
|
||||
|
||||
val numberOfKeys = buffer.int
|
||||
val keys = ArrayList<UByteArray>()
|
||||
|
||||
for (i in 0.until(numberOfKeys)) {
|
||||
val keySize = buffer.int
|
||||
|
||||
if (keySize == 0 || keySize > 32) {
|
||||
throw Exception("fatal: !keySize || keySize > 32")
|
||||
}
|
||||
|
||||
keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize)))
|
||||
buffer.position(buffer.position() + keySize)
|
||||
}
|
||||
|
||||
val numberOfDatas = buffer.int
|
||||
val datas = ArrayList<Pair<Long, Int>>()
|
||||
|
||||
for (i in 0.until(numberOfDatas)) {
|
||||
val offset = buffer.long
|
||||
val length = buffer.int
|
||||
|
||||
datas.add(Pair(offset, length))
|
||||
}
|
||||
|
||||
val numberOfSubNodeAddresses = 16 + 1
|
||||
val subNodeAddresses = ArrayList<Long>()
|
||||
|
||||
for (i in 0.until(numberOfSubNodeAddresses)) {
|
||||
val subNodeAddress = buffer.long
|
||||
subNodeAddresses.add(subNodeAddress)
|
||||
}
|
||||
|
||||
return Node(keys, datas, subNodeAddresses)
|
||||
}
|
||||
|
||||
private suspend fun getGalleryNodeAtAddress(address: Long): Node {
|
||||
val url = "$ltnBaseUrl/galleriesindex/galleries.${galleriesIndexVersion.get()}.index"
|
||||
|
||||
val nodedata = getRangedResponse(url, address.until(address + 464))
|
||||
|
||||
return decodeNode(nodedata)
|
||||
}
|
||||
|
||||
private suspend fun getRangedResponse(
|
||||
url: String,
|
||||
range: LongRange? = null,
|
||||
): ByteArray {
|
||||
val rangeHeaders = when (range) {
|
||||
null -> Headers.headersOf()
|
||||
else -> Headers.headersOf("Range", "bytes=${range.first}-${range.last}")
|
||||
}
|
||||
|
||||
return webClient.httpGet(url, rangeHeaders).parseBytes()
|
||||
}
|
||||
|
||||
private fun hashTerm(term: String): UByteArray {
|
||||
return sha256(term.toByteArray()).copyOfRange(0, 4).toUByteArray()
|
||||
}
|
||||
|
||||
private fun sha256(data: ByteArray): ByteArray {
|
||||
return MessageDigest.getInstance("SHA-256").digest(data)
|
||||
}
|
||||
|
||||
private suspend fun Collection<Int>.toMangaList(): List<Manga> = coroutineScope {
|
||||
map { id ->
|
||||
async {
|
||||
runCatchingCancellable {
|
||||
val doc = webClient.httpGet("$ltnBaseUrl/galleryblock/$id.html").let {
|
||||
val baseUri = it.request.url.toString()
|
||||
val html = it.parseRaw()
|
||||
Jsoup.parse(rewriteTnPaths(html), baseUri)
|
||||
}
|
||||
|
||||
Manga(
|
||||
id = generateUid(id.toString()),
|
||||
title = doc.selectFirstOrThrow("h1").text(),
|
||||
url = id.toString(),
|
||||
coverUrl =
|
||||
"https:" +
|
||||
doc.selectFirstOrThrow("picture > source")
|
||||
.attr("data-srcset")
|
||||
.substringBefore(" "),
|
||||
publicUrl =
|
||||
doc.selectFirstOrThrow("h1 > a")
|
||||
.attrAsRelativeUrl("href")
|
||||
.toAbsoluteUrl(domain),
|
||||
author = null,
|
||||
tags = emptySet(),
|
||||
isNsfw = true,
|
||||
rating = RATING_UNKNOWN,
|
||||
altTitle = null,
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val json = webClient.httpGet("$ltnBaseUrl/galleries/${manga.url}.js")
|
||||
.parseRaw()
|
||||
.substringAfter("var galleryinfo = ")
|
||||
.let(::JSONObject)
|
||||
|
||||
return manga.copy(
|
||||
title = json.getString("title"),
|
||||
largeCoverUrl =
|
||||
json.getJSONArray("files").getJSONObject(0).let {
|
||||
val hash = it.getString("hash")
|
||||
val commonId = commonImageId()
|
||||
val imageId = imageIdFromHash(hash)
|
||||
val subDomain = 'a' + subdomainOffset(imageId)
|
||||
|
||||
"https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
|
||||
},
|
||||
author =
|
||||
json.optJSONArray("artists")
|
||||
?.mapJSON { it.getString("artist").toCamelCase() }
|
||||
?.joinToString(),
|
||||
publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain),
|
||||
tags =
|
||||
buildSet {
|
||||
json.optJSONArray("characters")
|
||||
?.mapToTags("character")
|
||||
?.let(::addAll)
|
||||
json.optJSONArray("tags")
|
||||
?.mapToTags("tag")
|
||||
?.let(::addAll)
|
||||
json.optJSONArray("artists")
|
||||
?.mapToTags("artist")
|
||||
?.let(::addAll)
|
||||
json.optJSONArray("parodys")
|
||||
?.mapToTags("parody")
|
||||
?.let(::addAll)
|
||||
json.optJSONArray("groups")
|
||||
?.mapToTags("group")
|
||||
?.let(::addAll)
|
||||
},
|
||||
chapters = listOf(
|
||||
MangaChapter(
|
||||
id = generateUid(manga.url),
|
||||
url = manga.url,
|
||||
name = json.getString("title"),
|
||||
scanlator = json.getString("type").toTitleCase(),
|
||||
number = 1,
|
||||
branch = json.getString("language_localname"),
|
||||
source = source,
|
||||
uploadDate = dateFormat.tryParse(json.getString("date").substringBeforeLast("-")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
|
||||
private fun JSONArray.mapToTags(key: String): Set<MangaTag> {
|
||||
val tags = ArraySet<MangaTag>(length())
|
||||
mapJSON {
|
||||
MangaTag(
|
||||
title =
|
||||
it.getString(key).toCamelCase().let { title ->
|
||||
if (it.getStringOrNull("female")?.toIntOrNull() == 1) {
|
||||
"$title ♀"
|
||||
} else if (it.getStringOrNull("male")?.toIntOrNull() == 1) {
|
||||
"$title ♂"
|
||||
} else {
|
||||
title
|
||||
}
|
||||
},
|
||||
key = it.getString("url").tagUrlToTag(),
|
||||
source = source,
|
||||
).let(tags::add)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
private fun String.tagUrlToTag(): String {
|
||||
val urlContent = this.split("/")
|
||||
val ns = urlContent[1]
|
||||
val tag =
|
||||
urlContent[2]
|
||||
.substringBeforeLast("-")
|
||||
.urlDecode()
|
||||
.replace(" ", "_")
|
||||
|
||||
return if (tag.split(":")[0] in listOf("female", "male")) {
|
||||
tag
|
||||
} else {
|
||||
"$ns:$tag"
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||
val json = webClient.httpGet("$ltnBaseUrl/galleries/${seed.url}.js")
|
||||
.parseRaw()
|
||||
.substringAfter("var galleryinfo = ")
|
||||
.let(::JSONObject)
|
||||
|
||||
// any better way to get List<Int> from this json?
|
||||
return json.getJSONArray("related").let {
|
||||
0.until(it.length()).map { i -> it.getInt(i) }
|
||||
}.toMangaList()
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val json = webClient.httpGet("$ltnBaseUrl/galleries/${chapter.url}.js")
|
||||
.parseRaw()
|
||||
.substringAfter("var galleryinfo = ")
|
||||
.let(::JSONObject)
|
||||
|
||||
return json.getJSONArray("files").mapJSON { image ->
|
||||
val hash = image.getString("hash")
|
||||
val commonId = commonImageId()
|
||||
val imageId = imageIdFromHash(hash)
|
||||
val subDomain = 'a' + subdomainOffset(imageId)
|
||||
|
||||
MangaPage(
|
||||
id = generateUid(hash),
|
||||
url = "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp",
|
||||
preview = "https://${getDomain("${subDomain}tn")}/webpsmalltn/${thumbPathFromHash(hash)}/$hash.webp",
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// / --->
|
||||
|
||||
private var scriptLastRetrieval: Long? = null
|
||||
private val mutex = Mutex()
|
||||
private var subdomainOffsetDefault = 0
|
||||
private val subdomainOffsetMap = mutableMapOf<Int, Int>()
|
||||
private var commonImageId = ""
|
||||
|
||||
private suspend fun refreshScript() = mutex.withLock {
|
||||
if (scriptLastRetrieval == null || (scriptLastRetrieval!! + 60000) < System.currentTimeMillis()) {
|
||||
val ggScript = webClient.httpGet("$ltnBaseUrl/gg.js?_=${System.currentTimeMillis()}").parseRaw()
|
||||
|
||||
subdomainOffsetDefault = Regex("var o = (\\d)").find(ggScript)!!.groupValues[1].toInt()
|
||||
val o = Regex("o = (\\d); break;").find(ggScript)!!.groupValues[1].toInt()
|
||||
|
||||
subdomainOffsetMap.clear()
|
||||
Regex("case (\\d+):").findAll(ggScript).forEach {
|
||||
val case = it.groupValues[1].toInt()
|
||||
subdomainOffsetMap[case] = o
|
||||
}
|
||||
|
||||
commonImageId = Regex("b: '(.+)'").find(ggScript)!!.groupValues[1]
|
||||
|
||||
scriptLastRetrieval = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
// m <-- gg.js
|
||||
private suspend fun subdomainOffset(imageId: Int): Int {
|
||||
refreshScript()
|
||||
return subdomainOffsetMap[imageId] ?: subdomainOffsetDefault
|
||||
}
|
||||
|
||||
// b <-- gg.js
|
||||
private suspend fun commonImageId(): String {
|
||||
refreshScript()
|
||||
return commonImageId
|
||||
}
|
||||
|
||||
// s <-- gg.js
|
||||
private fun imageIdFromHash(hash: String): Int {
|
||||
val match = Regex("(..)(.)$").find(hash)
|
||||
return match!!.groupValues.let { it[2] + it[1] }.toInt(16)
|
||||
}
|
||||
|
||||
// real_full_path_from_hash <-- common.js
|
||||
private fun thumbPathFromHash(hash: String): String {
|
||||
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
|
||||
}
|
||||
|
||||
private suspend fun subdomainFromURL(url: String, base: String? = null): String {
|
||||
var retval = "b"
|
||||
|
||||
if (!base.isNullOrBlank())
|
||||
retval = base
|
||||
|
||||
val regex = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""")
|
||||
val hashMatch = regex.find(url) ?: return "a"
|
||||
val imageId = hashMatch.groupValues.let { it[2] + it[1] }.toIntOrNull(16)
|
||||
|
||||
if (imageId != null) {
|
||||
retval = ('a' + subdomainOffset(imageId)).toString() + retval
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
// rewrite_tn_paths <-- common.js
|
||||
private suspend fun rewriteTnPaths(html: String): String {
|
||||
val tnRegex = Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""")
|
||||
val url = tnRegex.find(html)?.value ?: return html
|
||||
val newSubdomain = subdomainFromURL(url, "tn")
|
||||
val newUrl = url.replace(Regex("""//..?\.hitomi\.la/"""), "//${getDomain(newSubdomain)}/")
|
||||
|
||||
return html.replace(tnRegex, newUrl)
|
||||
}
|
||||
|
||||
private fun String.toTagTitle(): String {
|
||||
return toCamelCase()
|
||||
.replace("♂", "(male)")
|
||||
.replace("♀", "(female)")
|
||||
}
|
||||
}
|
||||
@@ -22,45 +22,45 @@ internal class ImHentai(context: MangaLoaderContext) :
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("imhentai.xxx")
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("/search/?key=")
|
||||
append(query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page)
|
||||
} else if (!tags.isNullOrEmpty()) {
|
||||
append("/tag/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/")
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED -> append("")
|
||||
SortOrder.POPULARITY -> append("popular/")
|
||||
else -> append("")
|
||||
append("/search/?page=")
|
||||
append(page.toString())
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&key=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
append("?page=")
|
||||
append(page)
|
||||
} else {
|
||||
append("/search/?page=")
|
||||
append(page)
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED -> append("<=1&pp=0")
|
||||
SortOrder.POPULARITY -> append("<=0&pp=1")
|
||||
SortOrder.RATING -> append("<=0&pp=0")
|
||||
else -> append("<=1&pp=0")
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("&key=")
|
||||
append(filter.tags.joinToString(separator = ",") { it.key })
|
||||
}
|
||||
|
||||
var lang = "&en=1&jp=1&es=1&fr=1&kr=1&de=1&ru=1"
|
||||
filter.locale?.let {
|
||||
lang = "&en=0&jp=0&es=0&fr=0&kr=0&de=0&ru=0"
|
||||
lang = lang.replace("${it.language}=0", "${it.language}=1")
|
||||
}
|
||||
append(lang)
|
||||
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> append("<=1&pp=0")
|
||||
SortOrder.POPULARITY -> append("<=0&pp=1")
|
||||
SortOrder.RATING -> append("<=0&pp=0")
|
||||
else -> append("<=1&pp=0")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("<=1&pp=0")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.galleries div.thumb").map { div ->
|
||||
@@ -103,11 +103,15 @@ internal class ImHentai(context: MangaLoaderContext) :
|
||||
val href = it.attr("href").substringAfterLast("tag/").substringBeforeLast('/')
|
||||
MangaTag(
|
||||
key = href,
|
||||
title = it.selectFirstOrThrow("h3.list_tag").text(),
|
||||
title = it.selectFirstOrThrow("h3.list_tag").text().toTitleCase(sourceLocale),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH, Locale.JAPANESE, Locale("es"), Locale.FRENCH, Locale("kr"), Locale.GERMAN, Locale("ru"),
|
||||
)
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
@@ -132,83 +132,117 @@ internal abstract class LineWebtoonsParser(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val genre = tags.oneOrThrowIfMany()?.key ?: "ALL"
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val manga =
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${filter.query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
|
||||
.getJSONObject("challengeSearch")
|
||||
.getJSONArray("titleList")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
val sortOrderStr = when (sortOrder) {
|
||||
SortOrder.UPDATED -> "UPDATE"
|
||||
SortOrder.POPULARITY -> "READ_COUNT"
|
||||
SortOrder.RATING -> "LIKEIT"
|
||||
else -> throw IllegalArgumentException("Unsupported sort order: $sortOrder")
|
||||
}
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = null,
|
||||
tags = emptySet(),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = null,
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val manga = if (query != null) {
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
throw IllegalArgumentException("This source does not support search with tags")
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val genre = filter.tags.oneOrThrowIfMany()?.key ?: "ALL"
|
||||
|
||||
val sortOrderStr = when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> "UPDATE"
|
||||
SortOrder.POPULARITY -> "READ_COUNT"
|
||||
SortOrder.RATING -> "LIKEIT"
|
||||
else -> throw IllegalArgumentException("Unsupported sort order: ${filter.sortOrder}")
|
||||
}
|
||||
|
||||
val result =
|
||||
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
|
||||
|
||||
val genres = result.getJSONObject("genreList")
|
||||
.getJSONArray("challengeGenres")
|
||||
.mapJSON { jo -> parseTag(jo) }
|
||||
.associateBy { tag -> tag.key }
|
||||
|
||||
result
|
||||
.getJSONObject("titleList")
|
||||
.getJSONArray("titles")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||
tags = setOfNotNull(genres[jo.getString("representGenre")]),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = jo.getString("synopsis"),
|
||||
// I don't think the API provides this info
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
|
||||
val result =
|
||||
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=ALL&sortOrder=UPDATE&startIndex=${offset + 1}&pageSize=20")
|
||||
|
||||
val genres = result.getJSONObject("genreList")
|
||||
.getJSONArray("challengeGenres")
|
||||
.mapJSON { jo -> parseTag(jo) }
|
||||
.associateBy { tag -> tag.key }
|
||||
|
||||
result
|
||||
.getJSONObject("titleList")
|
||||
.getJSONArray("titles")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||
tags = setOfNotNull(genres[jo.getString("representGenre")]),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = jo.getString("synopsis"),
|
||||
// I don't think the API provides this info
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeRequest("/lineWebtoon/webtoon/searchChallenge?query=${query.urlEncoded()}&startIndex=${offset + 1}&pageSize=20")
|
||||
.getJSONObject("challengeSearch")
|
||||
.getJSONArray("titleList")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = null,
|
||||
tags = emptySet(),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = null,
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val result =
|
||||
makeRequest("/lineWebtoon/webtoon/challengeGenreTitleList.json?genre=$genre&sortOrder=$sortOrderStr&startIndex=${offset + 1}&pageSize=20")
|
||||
|
||||
val genres = result.getJSONObject("genreList")
|
||||
.getJSONArray("challengeGenres")
|
||||
.mapJSON { jo -> parseTag(jo) }
|
||||
.associateBy { tag -> tag.key }
|
||||
|
||||
result
|
||||
.getJSONObject("titleList")
|
||||
.getJSONArray("titles")
|
||||
.mapJSON { jo ->
|
||||
val titleNo = jo.getLong("titleNo")
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleNo),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = titleNo.toString(),
|
||||
publicUrl = "https://$domain/$languageCode/canvas/a/list?title_no=$titleNo",
|
||||
rating = jo.getFloatOrDefault("starScoreAverage", -10f) / 10f,
|
||||
isNsfw = jo.getBooleanOrDefault("ageGradeNotice", isNsfwSource),
|
||||
coverUrl = jo.getString("thumbnail").toAbsoluteUrl(staticDomain),
|
||||
largeCoverUrl = jo.getStringOrNull("thumbnailVertical")?.toAbsoluteUrl(staticDomain),
|
||||
tags = setOfNotNull(genres[jo.getString("representGenre")]),
|
||||
author = jo.getStringOrNull("writingAuthorName"),
|
||||
description = jo.getString("synopsis"),
|
||||
// I don't think the API provides this info
|
||||
state = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
@@ -309,10 +343,10 @@ internal abstract class LineWebtoonsParser(
|
||||
}
|
||||
|
||||
fun makeEncryptUrl(urlBuilder: HttpUrl.Builder) {
|
||||
val msgpad = Calendar.getInstance().timeInMillis.toString()
|
||||
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgpad))
|
||||
val msgPad = Calendar.getInstance().timeInMillis.toString()
|
||||
val digest = getMessageDigest(getMessage(urlBuilder.build().toString(), msgPad))
|
||||
urlBuilder
|
||||
.addQueryParameter("msgpad", msgpad)
|
||||
.addQueryParameter("msgpad", msgPad)
|
||||
.addQueryParameter("md", digest)
|
||||
// .addEncodedQueryParameter("md", digest.urlEncoded())
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ private const val CHAPTERS_FIRST_PAGE_SIZE = 120
|
||||
private const val CHAPTERS_MAX_PAGE_SIZE = 500
|
||||
private const val CHAPTERS_PARALLELISM = 3
|
||||
private const val CHAPTERS_MAX_COUNT = 10_000 // strange api behavior, looks like a bug
|
||||
private const val CONTENT_RATING =
|
||||
"contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic"
|
||||
private const val LOCALE_FALLBACK = "en"
|
||||
|
||||
@MangaSourceParser("MANGADEX", "MangaDex")
|
||||
@@ -30,19 +28,17 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("mangadex.org")
|
||||
|
||||
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.POPULARITY,
|
||||
)
|
||||
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java)
|
||||
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
|
||||
|
||||
override val isTagsExclusionSupported: Boolean = true
|
||||
|
||||
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val domain = domain
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
@@ -51,30 +47,65 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
|
||||
append(PAGE_SIZE)
|
||||
append("&offset=")
|
||||
append(offset)
|
||||
append("&includes[]=cover_art&includes[]=author&includes[]=artist&")
|
||||
tags?.forEach { tag ->
|
||||
append("includedTags[]=")
|
||||
append(tag.key)
|
||||
append('&')
|
||||
}
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("title=")
|
||||
append(query.urlEncoded())
|
||||
append('&')
|
||||
}
|
||||
append(CONTENT_RATING)
|
||||
append("&order")
|
||||
append(
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED,
|
||||
-> "[latestUploadedChapter]=desc"
|
||||
append("&includes[]=cover_art&includes[]=author&includes[]=artist")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&title=")
|
||||
append(filter.query)
|
||||
}
|
||||
|
||||
SortOrder.ALPHABETICAL -> "[title]=asc"
|
||||
SortOrder.NEWEST -> "[createdAt]=desc"
|
||||
SortOrder.POPULARITY -> "[followedCount]=desc"
|
||||
else -> "[followedCount]=desc"
|
||||
},
|
||||
)
|
||||
is MangaListFilter.Advanced -> {
|
||||
filter.tags.forEach {
|
||||
append("&includedTags[]=")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
filter.tagsExclude.forEach {
|
||||
append("&excludedTags[]=")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
if (filter.contentRating.isNotEmpty()) {
|
||||
filter.contentRating.forEach {
|
||||
when (it) {
|
||||
ContentRating.SAFE -> append("&contentRating[]=safe")
|
||||
ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica")
|
||||
ContentRating.ADULT -> append("&contentRating[]=pornographic")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
append("&order")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> "[latestUploadedChapter]=desc"
|
||||
SortOrder.RATING -> "[rating]=desc"
|
||||
SortOrder.ALPHABETICAL -> "[title]=asc"
|
||||
SortOrder.ALPHABETICAL_DESC -> "[title]=desc"
|
||||
SortOrder.NEWEST -> "[createdAt]=desc"
|
||||
SortOrder.POPULARITY -> "[followedCount]=desc"
|
||||
},
|
||||
)
|
||||
filter.states.forEach {
|
||||
append("&status[]=")
|
||||
when (it) {
|
||||
MangaState.ONGOING -> append("ongoing")
|
||||
MangaState.FINISHED -> append("completed")
|
||||
MangaState.ABANDONED -> append("cancelled")
|
||||
MangaState.PAUSED -> append("hiatus")
|
||||
else -> append("")
|
||||
}
|
||||
}
|
||||
filter.locale?.let {
|
||||
append("&availableTranslatedLanguage[]=")
|
||||
append(it.language)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("&order[latestUploadedChapter]=desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
val json = webClient.httpGet(url).parseJson().getJSONArray("data")
|
||||
return json.mapJSON { jo ->
|
||||
@@ -96,7 +127,10 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
|
||||
url = id,
|
||||
publicUrl = "https://$domain/title/$id",
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = attrs.getStringOrNull("contentRating") == "erotica",
|
||||
isNsfw = when (attrs.getStringOrNull("contentRating")) {
|
||||
"erotica", "pornographic" -> true
|
||||
else -> false
|
||||
},
|
||||
coverUrl = cover?.plus(".256.jpg").orEmpty(),
|
||||
largeCoverUrl = cover,
|
||||
description = attrs.optJSONObject("description")?.selectByLocale(),
|
||||
@@ -110,9 +144,11 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
state = when (jo.getStringOrNull("status")) {
|
||||
state = when (attrs.getStringOrNull("status")) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"completed" -> MangaState.FINISHED
|
||||
"hiatus" -> MangaState.PAUSED
|
||||
"cancelled" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
author = (relations["author"] ?: relations["artist"])
|
||||
@@ -164,13 +200,24 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
|
||||
.getJSONArray("data")
|
||||
return tags.mapJSONToSet { jo ->
|
||||
MangaTag(
|
||||
title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue().toTitleCase(),
|
||||
title = jo.getJSONObject("attributes").getJSONObject("name")
|
||||
.firstStringValue()
|
||||
.toTitleCase(Locale.ENGLISH),
|
||||
key = jo.getString("id"),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> {
|
||||
val head = webClient.httpGet("https://$domain/").parseHtml().head()
|
||||
return head.getElementsByAttributeValue("property", "og:locale:alternate")
|
||||
.mapNotNullToSet { meta ->
|
||||
val raw = meta.attrOrNull("content") ?: return@mapNotNullToSet null
|
||||
Locale(raw.substringBefore('_'), raw.substringAfter('_', ""))
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.firstStringValue() = values().next() as String
|
||||
|
||||
private fun JSONObject.selectByLocale(): String? {
|
||||
@@ -220,8 +267,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
|
||||
append(limitedLimit)
|
||||
append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=")
|
||||
append(offset)
|
||||
append('&')
|
||||
append(CONTENT_RATING)
|
||||
append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
|
||||
}
|
||||
val json = webClient.httpGet(url).parseJson()
|
||||
if (json.getString("result") == "ok") {
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.koitharu.kotatsu.parsers.site.all
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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 java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("MANGAPARK", "MangaPark")
|
||||
internal class MangaPark(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.MANGAPARK, pageSize = 36) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
|
||||
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE)
|
||||
|
||||
override val isTagsExclusionSupported: Boolean = true
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("mangapark.net")
|
||||
|
||||
private val tagsMap = SuspendLazy(::parseTags)
|
||||
|
||||
init {
|
||||
context.cookieJar.insertCookies(domain, "nsfw", "2")
|
||||
}
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/search?page=")
|
||||
append(page.toString())
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&word=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("&genres=")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
appendAll(filter.tags, ",") { it.key }
|
||||
}
|
||||
|
||||
append("|")
|
||||
if (filter.tagsExclude.isNotEmpty()) {
|
||||
appendAll(filter.tagsExclude, ",") { it.key }
|
||||
}
|
||||
|
||||
if (filter.contentRating.isNotEmpty()) {
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
ContentRating.SAFE -> append(",gore,bloody,violence,ecchi,adult,mature,smut,hentai")
|
||||
else -> append("")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "completed"
|
||||
MangaState.PAUSED -> "hiatus"
|
||||
MangaState.ABANDONED -> "cancelled"
|
||||
MangaState.UPCOMING -> "pending"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
append("&sortby=")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> "views_d000"
|
||||
SortOrder.UPDATED -> "field_update"
|
||||
SortOrder.NEWEST -> "field_create"
|
||||
SortOrder.ALPHABETICAL -> "field_name"
|
||||
SortOrder.RATING -> "field_score"
|
||||
else -> ""
|
||||
|
||||
},
|
||||
)
|
||||
|
||||
filter.locale?.let {
|
||||
append("&lang=")
|
||||
append(it.language)
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("&sortby=field_update")
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.grid.gap-5 div.flex.border-b").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(),
|
||||
title = div.selectFirst("h3")?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = div.selectFirst("span.text-yellow-500")?.text()?.toFloatOrNull()?.div(10F) ?: RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
source = source,
|
||||
isNsfw = isNsfwSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
return tagsMap.get().values.toSet()
|
||||
}
|
||||
|
||||
private suspend fun parseTags(): Map<String, MangaTag> {
|
||||
val tagElements = webClient.httpGet("https://$domain/search").parseHtml()
|
||||
.select("div.flex-col:contains(Genres) div.whitespace-nowrap")
|
||||
val tagMap = ArrayMap<String, MangaTag>(tagElements.size)
|
||||
for (el in tagElements) {
|
||||
val name = el.selectFirstOrThrow("span.whitespace-nowrap").text().toTitleCase(sourceLocale)
|
||||
if (name.isEmpty()) continue
|
||||
tagMap[name] = MangaTag(
|
||||
title = name,
|
||||
key = el.attr("q:key") ?: continue,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
return tagMap
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale("af"), Locale("sq"), Locale("am"), Locale("ar"), Locale("hy"),
|
||||
Locale("az"), Locale("be"), Locale("bn"), Locale("zh_hk"), Locale("zh_tw"),
|
||||
Locale.CHINESE, Locale("ceb"), Locale("ca"), Locale("km"), Locale("my"),
|
||||
Locale("bg"), Locale("bs"), Locale("hr"), Locale("cs"), Locale("da"),
|
||||
Locale("nl"), Locale.ENGLISH, Locale("et"), Locale("fo"), Locale("fil"),
|
||||
Locale("fi"), Locale("he"), Locale("ha"), Locale("jv"), Locale("lb"),
|
||||
Locale("mn"), Locale("ro"), Locale("si"), Locale("ta"), Locale("uz"),
|
||||
Locale("ur"), Locale("tg"), Locale("sd"), Locale("pt_br"), Locale("mo"),
|
||||
Locale("lt"), Locale.JAPANESE, Locale.ITALIAN, Locale("ht"), Locale("lv"),
|
||||
Locale("mr"), Locale("pt"), Locale("sn"), Locale("sv"), Locale("uk"),
|
||||
Locale("tk"), Locale("sw"), Locale("st"), Locale("pl"), Locale("mi"),
|
||||
Locale("lo"), Locale("ga"), Locale("gu"), Locale("gn"), Locale("id"),
|
||||
Locale("ky"), Locale("mt"), Locale("fa"), Locale("sh"), Locale("es_419"),
|
||||
Locale("tr"), Locale("to"), Locale("vi"), Locale("es"), Locale("sr"),
|
||||
Locale("ps"), Locale("ml"), Locale("ku"), Locale("ig"), Locale("el"),
|
||||
Locale.GERMAN, Locale("is"), Locale.KOREAN, Locale("ms"), Locale("ny"), Locale("sm"),
|
||||
Locale("so"), Locale("ti"), Locale("zu"), Locale("yo"), Locale("th"),
|
||||
Locale("sl"), Locale("ru"), Locale("no"), Locale("mg"), Locale("kk"),
|
||||
Locale("hu"), Locale("ka"), Locale.FRENCH, Locale("hi"), Locale("kn"),
|
||||
Locale("mk"), Locale("ne"), Locale("rm"), Locale("sk"), Locale("te"),
|
||||
)
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val tagMap = tagsMap.get()
|
||||
val selectTag = doc.select("div[q:key=30_2] span.whitespace-nowrap")
|
||||
val tags = selectTag.mapNotNullToSet { tagMap[it.text()] }
|
||||
val nsfw = tags.any { t -> t.key == "hentai" || t.key == "adult" }
|
||||
val dateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
|
||||
manga.copy(
|
||||
altTitle = doc.selectFirst("div[q:key=tz_2]")?.text().orEmpty(),
|
||||
author = doc.selectFirst("div[q:key=tz_4]")?.text().orEmpty(),
|
||||
description = doc.selectFirst("react-island[q:key=0a_9]")?.html().orEmpty(),
|
||||
state = when (doc.selectFirst("span[q:key=Yn_5]")?.text()?.lowercase()) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"completed" -> MangaState.FINISHED
|
||||
"hiatus" -> MangaState.PAUSED
|
||||
"cancelled" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
tags = tags,
|
||||
isNsfw = nsfw,
|
||||
chapters = doc.body().select("div.group.flex div.px-2").mapChapters { i, div ->
|
||||
val a = div.selectFirstOrThrow("a")
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
val dateText = div.selectFirst("span[q:key=Ee_0]")?.text()
|
||||
MangaChapter(
|
||||
id = generateUid(href),
|
||||
name = a.text(),
|
||||
number = i + 1,
|
||||
url = href,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||
val d = date?.lowercase() ?: return 0
|
||||
return when {
|
||||
d.endsWith(" ago") -> parseRelativeDate(date)
|
||||
d.startsWith("just now") -> Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
|
||||
else -> dateFormat.tryParse(date)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRelativeDate(date: String): Long {
|
||||
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
||||
val cal = Calendar.getInstance()
|
||||
return when {
|
||||
WordSet("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
WordSet("minute", "minutes", "mins", "min").anyWordIn(date) -> cal.apply {
|
||||
add(
|
||||
Calendar.MINUTE,
|
||||
-number,
|
||||
)
|
||||
}.timeInMillis
|
||||
|
||||
WordSet("hour", "hours").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
WordSet("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val script = if (doc.selectFirst("script:containsData(comic-)") != null) {
|
||||
doc.selectFirstOrThrow("script:containsData(comic-)").data()
|
||||
.substringAfterLast("\"comic-").split("\",\"")
|
||||
} else {
|
||||
doc.selectFirstOrThrow("script:containsData(manga-)").data()
|
||||
.substringAfterLast("\"manga-").split("\",\"")
|
||||
}
|
||||
return script.mapNotNull { url ->
|
||||
if (!url.startsWith("https://")) {
|
||||
return@mapNotNull null
|
||||
} else {
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package org.koitharu.kotatsu.parsers.site.all
|
||||
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.toJSONList
|
||||
import java.util.*
|
||||
|
||||
internal abstract class MangaPlusParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaSource,
|
||||
private val sourceLang: String,
|
||||
) : MangaParser(context, source), Interceptor {
|
||||
|
||||
private val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
|
||||
override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp")
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
|
||||
|
||||
// no tags or tag search available
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return when (filter) {
|
||||
is MangaListFilter.Advanced -> {
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> getPopularList()
|
||||
SortOrder.UPDATED -> getLatestList()
|
||||
else -> getAllTitleList()
|
||||
}
|
||||
}
|
||||
|
||||
is MangaListFilter.Search -> getAllTitleList(filter.query)
|
||||
else -> getAllTitleList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPopularList(): List<Manga> {
|
||||
val json = apiCall("/title_list/ranking")
|
||||
|
||||
return json.getJSONObject("titleRankingView")
|
||||
.getJSONArray("titles")
|
||||
.toJSONList()
|
||||
.toMangaList()
|
||||
}
|
||||
|
||||
private suspend fun getLatestList(): List<Manga> {
|
||||
val json = apiCall("/title_list/updated")
|
||||
|
||||
return json.getJSONObject("titleUpdatedView")
|
||||
.getJSONArray("latestTitle")
|
||||
.mapJSON { it.getJSONObject("title") }
|
||||
.toMangaList()
|
||||
}
|
||||
|
||||
// since search is local, save network calls on related manga call
|
||||
private val allTitleCache = SuspendLazy {
|
||||
apiCall("/title_list/allV2")
|
||||
.getJSONObject("allTitlesViewV2")
|
||||
.getJSONArray("AllTitlesGroup")
|
||||
.mapJSON { it.getJSONArray("titles").toJSONList() }
|
||||
.flatten()
|
||||
}
|
||||
|
||||
private suspend fun getAllTitleList(query: String? = null): List<Manga> {
|
||||
return allTitleCache.get().toMangaList(query)
|
||||
}
|
||||
|
||||
private fun List<JSONObject>.toMangaList(query: String? = null): List<Manga> {
|
||||
return mapNotNull {
|
||||
val language = it.getStringOrNull("language") ?: "ENGLISH"
|
||||
|
||||
if (language != sourceLang) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val name = it.getString("name")
|
||||
val author = it.getString("author")
|
||||
.split('/')
|
||||
.joinToString(transform = String::trim)
|
||||
|
||||
// filter out any other title or author which doesn't match search input
|
||||
if (query != null && !(name.contains(query, true) || author.contains(query, true))) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val titleId = it.getInt("titleId").toString()
|
||||
|
||||
Manga(
|
||||
id = generateUid(titleId),
|
||||
url = titleId,
|
||||
publicUrl = "/titles/$titleId".toAbsoluteUrl(domain),
|
||||
title = name,
|
||||
coverUrl = it.getString("portraitImageUrl"),
|
||||
altTitle = null,
|
||||
author = author,
|
||||
isNsfw = false,
|
||||
rating = RATING_UNKNOWN,
|
||||
state = null,
|
||||
source = source,
|
||||
tags = emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val json = apiCall("/title_detailV3?title_id=${manga.url}")
|
||||
.getJSONObject("titleDetailView")
|
||||
val title = json.getJSONObject("title")
|
||||
|
||||
val completed = json.getJSONObject("titleLabels")
|
||||
.getString("releaseSchedule").let {
|
||||
it == "DISABLED" || it == "COMPLETED"
|
||||
}
|
||||
|
||||
val hiatus = json.getStringOrNull("nonAppearanceInfo")?.contains("on a hiatus") == true
|
||||
|
||||
return manga.copy(
|
||||
title = title.getString("name"),
|
||||
publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain),
|
||||
coverUrl = title.getString("portraitImageUrl"),
|
||||
author = title.getString("author")
|
||||
.split("/").joinToString(transform = String::trim),
|
||||
description = buildString {
|
||||
json.getString("overview").let(::append)
|
||||
json.getStringOrNull("viewingPeriodDescription")
|
||||
?.takeIf { !completed }
|
||||
?.let { append("<br><br>", it) }
|
||||
},
|
||||
chapters = parseChapters(
|
||||
json.getJSONArray("chapterListGroup"),
|
||||
title.getStringOrNull("language") ?: "ENGLISH",
|
||||
),
|
||||
state = when {
|
||||
completed -> MangaState.FINISHED
|
||||
hiatus -> MangaState.PAUSED
|
||||
else -> MangaState.ONGOING
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseChapters(chapterListGroup: JSONArray, language: String): List<MangaChapter> {
|
||||
val chapterList = chapterListGroup
|
||||
.toJSONList()
|
||||
.flatMap {
|
||||
it.optJSONArray("firstChapterList")?.toJSONList().orEmpty() +
|
||||
it.optJSONArray("lastChapterList")?.toJSONList().orEmpty()
|
||||
}
|
||||
|
||||
return chapterList.mapChapters { _, chapter ->
|
||||
val chapterId = chapter.getInt("chapterId").toString()
|
||||
val subtitle = chapter.getStringOrNull("subTitle") ?: return@mapChapters null
|
||||
|
||||
MangaChapter(
|
||||
id = generateUid(chapterId),
|
||||
url = chapterId,
|
||||
name = subtitle,
|
||||
number = chapter.getString("name")
|
||||
.substringAfter("#")
|
||||
.toIntOrNull() ?: -1,
|
||||
uploadDate = chapter.getInt("startTimeStamp") * 1000L,
|
||||
branch = when (language) {
|
||||
"PORTUGUESE_BR" -> "Portuguese (Brazil)"
|
||||
else -> language.lowercase().toTitleCase()
|
||||
},
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val pages = apiCall("/manga_viewer?chapter_id=${chapter.url}&split=yes&img_quality=super_high")
|
||||
.getJSONObject("mangaViewer")
|
||||
.getJSONArray("pages")
|
||||
|
||||
return pages.mapJSONNotNull {
|
||||
val mangaPage = it.optJSONObject("mangaPage")
|
||||
?: return@mapJSONNotNull null
|
||||
val url = mangaPage.getString("imageUrl")
|
||||
val encryptionKey = mangaPage.getStringOrNull("encryptionKey")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url + if (encryptionKey == null) "" else "#$encryptionKey",
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// image descrambling
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val encryptionKey = request.url.fragment
|
||||
|
||||
if (encryptionKey.isNullOrEmpty()) {
|
||||
return response
|
||||
}
|
||||
|
||||
val contentType = response.headers["Content-Type"] ?: "image/jpeg"
|
||||
|
||||
val image = requireNotNull(response.body).bytes().decodeXorCipher(encryptionKey)
|
||||
val body = image.toResponseBody(contentType.toMediaTypeOrNull())
|
||||
|
||||
return response.newBuilder()
|
||||
.body(body)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ByteArray.decodeXorCipher(key: String): ByteArray {
|
||||
val keyStream = key.chunked(2)
|
||||
.map { it.toInt(16) }
|
||||
|
||||
return mapIndexed { i, byte -> byte.toInt() xor keyStream[i % keyStream.size] }
|
||||
.map(Int::toByte)
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
private suspend fun apiCall(url: String): JSONObject {
|
||||
val newUrl = "$apiUrl$url".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("format", "json")
|
||||
.build()
|
||||
val response = webClient.httpGet(newUrl, extraHeaders).parseJson()
|
||||
|
||||
val success = response.optJSONObject("success")
|
||||
|
||||
return checkNotNull(success) {
|
||||
val error = response.getJSONObject("error")
|
||||
val reason = error.getJSONArray("popups")
|
||||
.toJSONList()
|
||||
.firstOrNull { it.getStringOrNull("language") == null }
|
||||
|
||||
if (reason?.getStringOrNull("subject") == "Not Found" && url.contains("manga_viewer")) {
|
||||
"This chapter has expired"
|
||||
} else {
|
||||
reason?.getStringOrNull("body") ?: "Unknown Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_EN", "MANGA Plus English", "en")
|
||||
class English(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_EN,
|
||||
"ENGLISH",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_ES", "MANGA Plus Spanish", "es")
|
||||
class Spanish(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_ES,
|
||||
"SPANISH",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_FR", "MANGA Plus French", "fr")
|
||||
class French(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_FR,
|
||||
"FRENCH",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_ID", "MANGA Plus Indonesian", "id")
|
||||
class Indonesian(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_ID,
|
||||
"INDONESIAN",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_PTBR", "MANGA Plus Portuguese (Brazil)", "pt")
|
||||
class Portuguese(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_PTBR,
|
||||
"PORTUGUESE_BR",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_RU", "MANGA Plus Russian", "ru")
|
||||
class Russian(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_RU,
|
||||
"RUSSIAN",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_TH", "MANGA Plus Thai", "th")
|
||||
class Thai(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_TH,
|
||||
"THAI",
|
||||
)
|
||||
|
||||
@MangaSourceParser("MANGAPLUSPARSER_VI", "MANGA Plus Vietnamese", "vi")
|
||||
class Vietnamese(context: MangaLoaderContext) : MangaPlusParser(
|
||||
context,
|
||||
MangaSource.MANGAPLUSPARSER_VI,
|
||||
"VIETNAMESE",
|
||||
)
|
||||
}
|
||||
@@ -35,6 +35,13 @@ internal abstract class NineMangaParser(
|
||||
SortOrder.POPULARITY,
|
||||
)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
)
|
||||
|
||||
override val isTagsExclusionSupported: Boolean = true
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val newRequest = if (request.url.host == domain) {
|
||||
@@ -45,37 +52,50 @@ internal abstract class NineMangaParser(
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search/?name_sel=&wd=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page)
|
||||
append(".html")
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("/search/?category_id=")
|
||||
for (tag in tags) {
|
||||
append(tag.key)
|
||||
append(',')
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty() || filter.tagsExclude.isNotEmpty() || filter.states.isNotEmpty()) {
|
||||
append("/search/?category_id=")
|
||||
append(filter.tags.joinToString(separator = ",") { it.key })
|
||||
|
||||
append("&out_category_id=")
|
||||
append(filter.tagsExclude.joinToString(separator = ",") { it.key })
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&completed_series=")
|
||||
when (it) {
|
||||
MangaState.ONGOING -> append("no")
|
||||
MangaState.FINISHED -> append("yes")
|
||||
else -> append("either")
|
||||
}
|
||||
}
|
||||
append("&page=")
|
||||
} else {
|
||||
append("/category/index_")
|
||||
}
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
append(".html")
|
||||
}
|
||||
|
||||
else -> {
|
||||
null -> {
|
||||
append("/category/index_")
|
||||
append(page)
|
||||
append(".html")
|
||||
}
|
||||
}
|
||||
append(page)
|
||||
append(".html")
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
val root = doc.body().selectFirst("ul.direlist") ?: doc.parseFailed("Cannot find root")
|
||||
@@ -171,7 +191,7 @@ internal abstract class NineMangaParser(
|
||||
val cateId = el.attr("cate_id")
|
||||
val a = el.selectFirstOrThrow("a")
|
||||
tagMap[el.text()] = MangaTag(
|
||||
title = a.text().toTitleCase(),
|
||||
title = a.text().toTitleCase(sourceLocale),
|
||||
key = cateId,
|
||||
source = source,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
package org.koitharu.kotatsu.parsers.site.all
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
||||
import org.koitharu.kotatsu.parsers.util.json.toJSONList
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("NINENINENINEHENTAI", "999Hentai", type = ContentType.HENTAI)
|
||||
internal class NineNineNineHentaiParser(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.NINENINENINEHENTAI, size), Interceptor {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("999hentai.net")
|
||||
|
||||
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.of(
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.NEWEST,
|
||||
)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getAvailableLocales() = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.CHINESE,
|
||||
Locale.JAPANESE,
|
||||
Locale("es"),
|
||||
)
|
||||
|
||||
private fun Locale?.getSiteLang(): String {
|
||||
if (this == null) return "all"
|
||||
|
||||
return when {
|
||||
equals(Locale.ENGLISH) -> "en"
|
||||
equals(Locale.CHINESE) -> "cn"
|
||||
equals(Locale.JAPANESE) -> "jp"
|
||||
equals(Locale("es")) -> "es"
|
||||
else -> "all"
|
||||
}
|
||||
}
|
||||
|
||||
// Need for disable encoding (with encoding not working)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val newRequest = if (request.header("Content-Encoding") != null) {
|
||||
request.newBuilder().removeHeader("Content-Encoding").build()
|
||||
} else {
|
||||
request
|
||||
}
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
private val cdnHost = SuspendLazy(::getUpdatedCdnHost)
|
||||
|
||||
private suspend fun getUpdatedCdnHost(): String {
|
||||
val url = "https://$domain/manga-home"
|
||||
val response = webClient.httpGet(url).parseHtml()
|
||||
val cdn = response.selectFirst("img.v-thumbnail")?.attr("data-src")
|
||||
return cdn?.toHttpUrlOrNull()?.host ?: "edge.fast4speed.rsvp"
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val query = """
|
||||
queryTags(
|
||||
search: {format:"tagchapter",sortBy:Popular}
|
||||
page: 1
|
||||
limit: 100
|
||||
) {
|
||||
edges {
|
||||
name
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val tags = apiCall(query)
|
||||
.getJSONObject("queryTags")
|
||||
.getJSONArray("edges")
|
||||
|
||||
return tags.mapJSONToSet {
|
||||
val name = it.getString("name")
|
||||
MangaTag(
|
||||
title = name.toCamelCase(),
|
||||
key = name,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
return when (filter) {
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isEmpty() && filter.sortOrder == SortOrder.POPULARITY) {
|
||||
getPopularList(page, filter.locale)
|
||||
} else {
|
||||
getSearchList(page, null, filter.locale, filter.tags, filter.sortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
getSearchList(page, filter.query, null, null, filter.sortOrder)
|
||||
}
|
||||
|
||||
else -> {
|
||||
getPopularList(page, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPopularList(
|
||||
page: Int,
|
||||
locale: Locale?,
|
||||
): List<Manga> {
|
||||
val query = """
|
||||
queryPopularChapters(
|
||||
size: $size
|
||||
language: "${locale.getSiteLang()}"
|
||||
dateRange: 1
|
||||
page: $page
|
||||
) {
|
||||
edges {
|
||||
_id
|
||||
name
|
||||
firstPics
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
return apiCall(query)
|
||||
.getJSONObject("queryPopularChapters")
|
||||
.getJSONArray("edges")
|
||||
.toMangaList()
|
||||
}
|
||||
|
||||
private suspend fun getSearchList(
|
||||
page: Int,
|
||||
search: String?,
|
||||
locale: Locale?,
|
||||
tags: Set<MangaTag>?,
|
||||
sort: SortOrder?,
|
||||
): List<Manga> {
|
||||
val searchPayload = buildString {
|
||||
if (!search.isNullOrEmpty()) {
|
||||
append("query:\"$search\",")
|
||||
}
|
||||
append("language:\"${locale.getSiteLang()}\"")
|
||||
if (sort == SortOrder.POPULARITY) {
|
||||
append(",sortBy:Popular")
|
||||
}
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
val tag = tags.oneOrThrowIfMany()!!.key
|
||||
append(",tags:[\"$tag\"]")
|
||||
}
|
||||
}
|
||||
val query = """
|
||||
queryChapters(
|
||||
limit: $size
|
||||
search: {$searchPayload}
|
||||
page: $page
|
||||
) {
|
||||
edges {
|
||||
_id
|
||||
name
|
||||
firstPics
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
return apiCall(query)
|
||||
.getJSONObject("queryChapters")
|
||||
.getJSONArray("edges")
|
||||
.toMangaList()
|
||||
}
|
||||
|
||||
private suspend fun JSONArray.toMangaList(): List<Manga> = mapJSON { entry ->
|
||||
val id = entry.getString("_id")
|
||||
val name = entry.getString("name")
|
||||
val cover = runCatching {
|
||||
entry.getJSONArray("firstPics")
|
||||
.getJSONObject(0)
|
||||
.getString("url")
|
||||
}.getOrNull()
|
||||
|
||||
Manga(
|
||||
id = generateUid(id),
|
||||
title = name.replace(shortenTitleRegex, "").trim(),
|
||||
altTitle = name,
|
||||
coverUrl = when {
|
||||
cover?.startsWith("http") == true -> cover
|
||||
cover == null -> ""
|
||||
else -> "https://${cdnHost.get()}/$cover"
|
||||
},
|
||||
author = null,
|
||||
isNsfw = true,
|
||||
url = id,
|
||||
publicUrl = "/hchapter/$id".toAbsoluteUrl(domain),
|
||||
tags = emptySet(),
|
||||
source = source,
|
||||
state = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val query = """
|
||||
queryChapter(
|
||||
chapterId: "${manga.url}"
|
||||
) {
|
||||
_id
|
||||
name
|
||||
uploadDate
|
||||
format
|
||||
description
|
||||
language
|
||||
pages
|
||||
tags
|
||||
pictureUrls {
|
||||
picCdn
|
||||
pics
|
||||
picsS
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val entry = apiCall(query)
|
||||
.getJSONObject("queryChapter")
|
||||
|
||||
val id = entry.getString("_id")
|
||||
val name = entry.getString("name")
|
||||
val cover = entry.getJSONArray("pictureUrls")
|
||||
.getJSONObject(0)
|
||||
.let { pics ->
|
||||
val cdn = pics.getString("picCdn").let {
|
||||
if (it.startsWith("http")) {
|
||||
"$it/"
|
||||
} else {
|
||||
"https://${cdnHost.get()}/$it/"
|
||||
}
|
||||
}
|
||||
val img = pics.getJSONArray("pics").getJSONObject(0).getString("url")
|
||||
val imgS = pics.getJSONArray("picsS").getJSONObject(0).getString("url")
|
||||
Pair(cdn + imgS, cdn + img)
|
||||
}
|
||||
val tags = entry.optJSONArray("tags")?.mapJSON {
|
||||
SiteTag(
|
||||
name = it.getString("tagName"),
|
||||
type = it.getStringOrNull("tagType"),
|
||||
)
|
||||
}
|
||||
return manga.copy(
|
||||
title = name.replace(shortenTitleRegex, "").trim(),
|
||||
altTitle = name,
|
||||
coverUrl = cover.first,
|
||||
largeCoverUrl = cover.second,
|
||||
author = tags?.filter { it.type == "artist" }?.joinToString { it.name.toCamelCase() },
|
||||
isNsfw = true,
|
||||
tags = tags?.mapToSet {
|
||||
MangaTag(
|
||||
title = it.name.toCamelCase(),
|
||||
key = it.name,
|
||||
source = source,
|
||||
)
|
||||
}.orEmpty(),
|
||||
state = null,
|
||||
description = entry.getStringOrNull("description"),
|
||||
chapters = listOf(
|
||||
MangaChapter(
|
||||
id = generateUid(id),
|
||||
name = name,
|
||||
number = 1,
|
||||
url = id,
|
||||
uploadDate = runCatching {
|
||||
dateFormat.parse(entry.getString("uploadDate"))!!.time
|
||||
}.getOrDefault(0L),
|
||||
branch = entry.getStringOrNull("language")?.let {
|
||||
val locale = when (it) {
|
||||
"en" -> Locale.ENGLISH
|
||||
"jp" -> Locale.JAPANESE
|
||||
"cn" -> Locale.CHINESE
|
||||
"es" -> Locale("es")
|
||||
else -> Locale.ROOT
|
||||
}
|
||||
|
||||
return@let locale.getDisplayLanguage(locale)
|
||||
},
|
||||
scanlator = when (entry.getStringOrNull("format")) {
|
||||
"artistcg" -> "ArtistCG"
|
||||
"gamecg" -> "GameCG"
|
||||
"imageset" -> "ImageSet"
|
||||
else -> entry.getStringOrNull("format")?.toCamelCase()
|
||||
},
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private data class SiteTag(
|
||||
val name: String,
|
||||
val type: String?,
|
||||
)
|
||||
|
||||
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||
val query = """
|
||||
queryRecommendations(
|
||||
type: "chapter"
|
||||
_id: "${seed.url}"
|
||||
search: {sortBy:Popular}
|
||||
page: 1
|
||||
size: $size
|
||||
) {
|
||||
chapters {
|
||||
_id
|
||||
name
|
||||
firstPics
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
return apiCall(query)
|
||||
.getJSONObject("queryRecommendations")
|
||||
.getJSONArray("chapters")
|
||||
.toMangaList()
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val query = """
|
||||
queryChapter(
|
||||
chapterId: "${chapter.url}"
|
||||
) {
|
||||
pictureUrls {
|
||||
picCdn
|
||||
pics
|
||||
picsS
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val pages = apiCall(query)
|
||||
.getJSONObject("queryChapter")
|
||||
.getJSONArray("pictureUrls")
|
||||
.getJSONObject(0)
|
||||
|
||||
val cdn = pages.getString("picCdn").let {
|
||||
if (it.startsWith("http")) {
|
||||
"$it/"
|
||||
} else {
|
||||
"https://${cdnHost.get()}/$it/"
|
||||
}
|
||||
}
|
||||
|
||||
val pics = pages.getJSONArray("pics").toJSONList()
|
||||
val picsS = pages.getJSONArray("picsS").toJSONList()
|
||||
|
||||
return pics.zip(picsS).map {
|
||||
val img = it.first.getString("url")
|
||||
val imgS = it.second.getString("url")
|
||||
MangaPage(
|
||||
id = generateUid(img),
|
||||
url = cdn + img,
|
||||
preview = cdn + imgS,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun apiCall(query: String): JSONObject {
|
||||
return webClient.graphQLQuery("https://api.$domain/api", query).getJSONObject("data")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val size = 20
|
||||
private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
// see https://themewagon.com/themes/free-bootstrap-4-html5-gaming-anime-website-template-anime/
|
||||
|
||||
internal abstract class AnimeBootstrapParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaSource,
|
||||
@@ -40,13 +38,7 @@ internal abstract class AnimeBootstrapParser(
|
||||
searchPaginator.firstPage = 1
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
@@ -55,23 +47,31 @@ internal abstract class AnimeBootstrapParser(
|
||||
append(page.toString())
|
||||
append("&type=all")
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("&search=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&search=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&categorie=")
|
||||
append(tag?.key.orEmpty())
|
||||
}
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("view")
|
||||
SortOrder.UPDATED -> append("updated")
|
||||
SortOrder.ALPHABETICAL -> append("default")
|
||||
SortOrder.NEWEST -> append("published")
|
||||
else -> append("updated")
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("&categorie=")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
append("&sort=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("view")
|
||||
SortOrder.UPDATED -> append("updated")
|
||||
SortOrder.ALPHABETICAL -> append("default")
|
||||
SortOrder.NEWEST -> append("published")
|
||||
else -> append("updated")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
null -> append("&sort=updated")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
@@ -99,7 +99,7 @@ internal abstract class AnimeBootstrapParser(
|
||||
val doc = webClient.httpGet("https://$domain$listUrl").parseHtml()
|
||||
return doc.select("div.product__page__filter div:contains(Genre:) option ").mapNotNullToSet { option ->
|
||||
val key = option.attr("value") ?: return@mapNotNullToSet null
|
||||
val name = option.text()
|
||||
val name = option.text().toTitleCase(sourceLocale)
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
@@ -115,11 +115,8 @@ internal abstract class AnimeBootstrapParser(
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val chaptersDeferred = async { getChapters(doc) }
|
||||
|
||||
val desc = doc.selectFirstOrThrow(selectDesc).html()
|
||||
|
||||
val state = if (doc.select(selectState).isNullOrEmpty()) {
|
||||
MangaState.FINISHED
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.parsers.site.animebootstrap.fr
|
||||
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.jsoup.nodes.Document
|
||||
@@ -13,61 +12,56 @@ import java.text.SimpleDateFormat
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
@MangaSourceParser("PAPSCAN", "PapScan", "fr")
|
||||
internal class PapScan(context: MangaLoaderContext) :
|
||||
AnimeBootstrapParser(context, MangaSource.PAPSCAN, "papscan.com") {
|
||||
|
||||
override val sourceLocale: Locale = Locale.ENGLISH
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override val listUrl = "/liste-manga"
|
||||
|
||||
override val selectState = "div.anime__details__widget li:contains(En cours)"
|
||||
override val selectTag = "div.anime__details__widget li:contains(Genre) a"
|
||||
|
||||
override val selectChapter = "ul.chapters li"
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.ALPHABETICAL_DESC,
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/filterList")
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&alpha=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("&alpha=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&cat=")
|
||||
append(tag?.key.orEmpty())
|
||||
}
|
||||
append("&sortBy=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
else -> append("updated")
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("&cat=")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
append("&sortBy=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.ALPHABETICAL_DESC -> append("name&asc=false")
|
||||
SortOrder.ALPHABETICAL -> append("name&asc=true")
|
||||
else -> append("updated")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
null -> append("&sortBy=updated")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return doc.select("div.product__item").map { div ->
|
||||
val href = div.selectFirstOrThrow("h5 a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
@@ -103,17 +97,13 @@ internal class PapScan(context: MangaLoaderContext) :
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val chaptersDeferred = async { getChapters(doc) }
|
||||
|
||||
val desc = doc.selectFirstOrThrow(selectDesc).html()
|
||||
|
||||
val state = if (doc.select(selectState).isNullOrEmpty()) {
|
||||
MangaState.FINISHED
|
||||
} else {
|
||||
MangaState.ONGOING
|
||||
}
|
||||
|
||||
manga.copy(
|
||||
tags = doc.body().select(selectTag).mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
@@ -145,5 +135,4 @@ internal class PapScan(context: MangaLoaderContext) :
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.animebootstrap.AnimeBootstrapParser
|
||||
|
||||
@MangaSourceParser("KOMIKZOID", "Komikzo Id", "id")
|
||||
@MangaSourceParser("KOMIKZOID", "KomikzoId", "id")
|
||||
internal class KomikzoId(context: MangaLoaderContext) :
|
||||
AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.xyz")
|
||||
AnimeBootstrapParser(context, MangaSource.KOMIKZOID, "komikzoid.id")
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.animebootstrap.id
|
||||
|
||||
|
||||
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.animebootstrap.AnimeBootstrapParser
|
||||
|
||||
|
||||
@MangaSourceParser("NEUMANGA", "NeuManga", "id")
|
||||
@MangaSourceParser("NEUMANGA", "NeuManga.xyz", "id")
|
||||
internal class NeuManga(context: MangaLoaderContext) :
|
||||
AnimeBootstrapParser(context, MangaSource.NEUMANGA, "neumanga.xyz")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.parsers.site.animebootstrap.id
|
||||
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
@@ -15,36 +15,81 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("FLIXSCANS", "FlixScans", "ar")
|
||||
@MangaSourceParser("FLIXSCANS", "FlixScans.com", "ar")
|
||||
internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANS, 18) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
|
||||
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.ADULT)
|
||||
override val configKeyDomain = ConfigKey.Domain("flixscans.com")
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val json = if (!query.isNullOrEmpty()) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val json = when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val url = "https://api.$domain/api/v1/search/serie"
|
||||
val body = JSONObject()
|
||||
body.put("title", filter.query.urlEncoded())
|
||||
webClient.httpPost(url, body).parseJson().getJSONArray("data")
|
||||
}
|
||||
val url = "https://api.$domain/api/v1/search/serie"
|
||||
val body = JSONObject()
|
||||
body.put("title", query.urlEncoded())
|
||||
webClient.httpPost(url, body).parseJson().getJSONArray("data")
|
||||
} else if (!tags.isNullOrEmpty()) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domain)
|
||||
append("/api/v1/")
|
||||
if (filter.tags.isNotEmpty() || filter.states.isNotEmpty() || filter.contentRating.isNotEmpty()) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
append("search/advance?=")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
val tagQuery = filter.tags.joinToString(separator = ",") { it.key }
|
||||
append("&genres=")
|
||||
append(tagQuery)
|
||||
}
|
||||
if (filter.states.isNotEmpty()) {
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "completed"
|
||||
MangaState.ABANDONED -> "droped"
|
||||
MangaState.PAUSED -> "onhold"
|
||||
MangaState.UPCOMING -> "soon"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
append("&adult=")
|
||||
append(
|
||||
when (it) {
|
||||
ContentRating.ADULT -> "true"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
append("&serie_type=webtoon")
|
||||
|
||||
} else {
|
||||
append("webtoon/pages/latest/romance?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
|
||||
webClient.httpGet(url).parseJson().getJSONArray("data")
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = "https://api.$domain/api/v1/webtoon/pages/latest/romance?page=$page"
|
||||
webClient.httpGet(url).parseJson().getJSONArray("data")
|
||||
}
|
||||
val tagQuery = tags.joinToString(separator = ",") { it.key }
|
||||
val url = "https://api.$domain/api/v1/search/advance?=&genres=$tagQuery&serie_type=webtoon"
|
||||
webClient.httpGet(url).parseJson().getJSONArray("data")
|
||||
} else {
|
||||
val url = "https://api.$domain/api/v1/webtoon/homepage/latest/home?page=$page"
|
||||
webClient.httpGet(url).parseJson().getJSONArray("data")
|
||||
}
|
||||
return json.mapJSON { j ->
|
||||
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
|
||||
@@ -62,6 +107,8 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
|
||||
state = when (j.getString("status")) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"completed" -> MangaState.FINISHED
|
||||
"onhold" -> MangaState.PAUSED
|
||||
"droped" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
author = null,
|
||||
@@ -77,9 +124,9 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
|
||||
return tagsList.mapNotNullToSet { idTag ->
|
||||
val id = idTag.toInt()
|
||||
val idKey = json.getJSONObject(id).getInt("id")
|
||||
val key = json.get(idKey).toString()
|
||||
val key = json.getInt(idKey).toString()
|
||||
val idName = json.getJSONObject(id).getInt("name")
|
||||
val name = json.get(idName).toString()
|
||||
val name = json.getString(idName)
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.*
|
||||
@MangaSourceParser("MANGASTORM", "MangaStorm", "ar")
|
||||
internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGASTORM, 30) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED)
|
||||
override val configKeyDomain = ConfigKey.Domain("mangastorm.org")
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
@@ -21,35 +21,44 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
val url =
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/categories/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("?page=")
|
||||
append(page)
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/mangas?page=")
|
||||
append(page)
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("&query=")
|
||||
append(query.urlEncoded())
|
||||
append("&query=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
val tag = filter.tags.oneOrThrowIfMany()
|
||||
append("/categories/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("?page=")
|
||||
append(page)
|
||||
} else {
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("/mangas?page=")
|
||||
append(page)
|
||||
} else {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/mangas?page=")
|
||||
append(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.row div.col").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
@@ -74,9 +83,7 @@ internal class MangaStorm(context: MangaLoaderContext) : PagedMangaParser(contex
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
|
||||
val root = doc.selectFirstOrThrow(".card-body .col-lg-9")
|
||||
|
||||
return manga.copy(
|
||||
altTitle = null,
|
||||
state = null,
|
||||
|
||||
@@ -17,46 +17,71 @@ import java.util.*
|
||||
internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.TEAMXNOVEL, 10) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
override val configKeyDomain = ConfigKey.Domain("team11x11.com")
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("teamxnovel.com")
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/series?genre=")
|
||||
append(tag?.key.orEmpty())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page)
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
append("/series?search=")
|
||||
append(filter.query.urlEncoded())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("/series?search=")
|
||||
append(query.urlEncoded())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page)
|
||||
}
|
||||
} else {
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("/series")
|
||||
SortOrder.UPDATED -> append("/")
|
||||
else -> append("/")
|
||||
}
|
||||
if (page > 1) {
|
||||
append("?page=")
|
||||
append(page)
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
val tag = filter.tags.oneOrThrowIfMany()
|
||||
append("/series?genre=")
|
||||
append(tag?.key.orEmpty())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
append("&")
|
||||
} else {
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("/series")
|
||||
SortOrder.UPDATED -> append("/")
|
||||
else -> append("/")
|
||||
}
|
||||
if (page > 1) {
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
append("&")
|
||||
} else {
|
||||
append("?")
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.sortOrder == SortOrder.POPULARITY || filter.tags.isNotEmpty()) {
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "مستمرة"
|
||||
MangaState.FINISHED -> "مكتمل"
|
||||
MangaState.ABANDONED -> "متوقف"
|
||||
else -> "مستمرة"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("/?page=$page")
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.listupd .bs .bsx").ifEmpty {
|
||||
doc.select("div.post-body .box")
|
||||
@@ -74,7 +99,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
|
||||
tags = emptySet(),
|
||||
state = when (div.selectFirst(".status")?.text()) {
|
||||
"مستمرة" -> MangaState.ONGOING
|
||||
"متوقف", "مكتمل" -> MangaState.FINISHED
|
||||
"مكتمل" -> MangaState.FINISHED
|
||||
"متوقف" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
author = null,
|
||||
@@ -88,7 +114,7 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
|
||||
return doc.requireElementById("select_genre").select("option").mapNotNullToSet {
|
||||
MangaTag(
|
||||
key = it.attr("value"),
|
||||
title = it.text(),
|
||||
title = it.text().toTitleCase(sourceLocale),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
@@ -111,7 +137,8 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
|
||||
altTitle = null,
|
||||
state = when (doc.selectFirstOrThrow(".full-list-info:contains(الحالة:) a").text()) {
|
||||
"مستمرة" -> MangaState.ONGOING
|
||||
"متوقف", "مكتمل" -> MangaState.FINISHED
|
||||
"مكتمل" -> MangaState.FINISHED
|
||||
"متوقف" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
tags = doc.select(".review-author-info a").mapNotNullToSet { a ->
|
||||
|
||||
@@ -31,6 +31,7 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
tagsExclude: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
if (!query.isNullOrEmpty()) {
|
||||
|
||||
@@ -15,47 +15,45 @@ internal class BeeToon(context: MangaLoaderContext) :
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("ww7.beetoon.net")
|
||||
override val configKeyDomain = ConfigKey.Domain("manhwafull.net")
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
append("/?s=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("/genre/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/page-")
|
||||
append(page)
|
||||
append("/")
|
||||
}
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
else -> {
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED -> append("/latest-update/")
|
||||
SortOrder.POPULARITY -> append("/popular-manga/")
|
||||
else -> append("/latest-update/")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
val tag = filter.tags.oneOrThrowIfMany()
|
||||
append("/genre/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/page-")
|
||||
append(page)
|
||||
append("/")
|
||||
} else {
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> append("/latest-update/")
|
||||
SortOrder.POPULARITY -> append("/popular-manga/")
|
||||
else -> append("/latest-update/")
|
||||
}
|
||||
append("page-")
|
||||
append(page)
|
||||
append("/")
|
||||
}
|
||||
append("page-")
|
||||
append(page)
|
||||
append("/")
|
||||
}
|
||||
|
||||
null -> append("/latest-update/page-$page/")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
@@ -19,11 +19,25 @@ internal class CloneMangaParser(context: MangaLoaderContext) : MangaParser(conte
|
||||
override val configKeyDomain = ConfigKey.Domain("manga.clone-army.org")
|
||||
|
||||
@InternalParsersApi
|
||||
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
|
||||
if (query != null || offset > 0) {
|
||||
return emptyList()
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val link = when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
"https://$domain/viewer_landing.php"
|
||||
}
|
||||
|
||||
null -> "https://$domain/viewer_landing.php"
|
||||
|
||||
}
|
||||
val link = "https://$domain/viewer_landing.php"
|
||||
|
||||
val doc = webClient.httpGet(link).parseHtml()
|
||||
val mangas = doc.getElementsByClass("comicPreviewContainer")
|
||||
return mangas.mapNotNull { item ->
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
@@ -17,6 +18,8 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("comicextra.me")
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
@@ -25,38 +28,62 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://$domain/")
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append(tag?.key.orEmpty())
|
||||
if (page > 1) {
|
||||
append("/")
|
||||
append(page)
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("comic-search?key=")
|
||||
append(filter.query.urlEncoded())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("comic-search?key=")
|
||||
append(query.urlEncoded())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page)
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty() && filter.states.isEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append(it.key)
|
||||
}
|
||||
} else if (filter.tags.isEmpty() && filter.states.isNotEmpty()) {
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "/ongoing-comic"
|
||||
MangaState.FINISHED -> "/completed-comic"
|
||||
else -> "/ongoing-comic"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
} else if (filter.tags.isNotEmpty() && filter.states.isNotEmpty()) {
|
||||
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED)
|
||||
} else {
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("popular-comic")
|
||||
SortOrder.UPDATED -> append("new-comic")
|
||||
SortOrder.NEWEST -> append("recent-comic")
|
||||
else -> append("new-comic")
|
||||
}
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("/")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("popular-comic/")
|
||||
SortOrder.UPDATED -> append("new-comic/")
|
||||
SortOrder.NEWEST -> append("recent-comic/")
|
||||
else -> append("new-comic/")
|
||||
}
|
||||
if (page > 1) {
|
||||
append(page)
|
||||
|
||||
null -> {
|
||||
append("popular-comic")
|
||||
if (page > 1) {
|
||||
append("/")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.Headers
|
||||
import org.json.JSONArray
|
||||
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.PagedMangaParser
|
||||
@@ -22,85 +27,128 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("/search?q=")
|
||||
append(query.urlEncoded())
|
||||
append("&")
|
||||
append("classes[]".urlEncoded())
|
||||
append("=Serie&page=")
|
||||
append(page.toString())
|
||||
} else if (!tags.isNullOrEmpty()) {
|
||||
append("/tags/")
|
||||
for (tag in tags) {
|
||||
append(tag.key)
|
||||
}
|
||||
append("?view=groupings&page=")
|
||||
append(page.toString())
|
||||
} else {
|
||||
append("/series?view=cover&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
// There are no images on the search page
|
||||
if (!query.isNullOrEmpty()) {
|
||||
return doc.select("dl.chapter-list dd")
|
||||
.map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = div.selectFirstOrThrow("a").text(),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = "",
|
||||
tags = div.select("span.tags a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = a.text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/search?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&")
|
||||
append("classes[]".urlEncoded())
|
||||
append("=Serie&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
} else {
|
||||
return doc.select("li.span2")
|
||||
.map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = div.selectFirstOrThrow("div.caption").text(),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
return parseMangaListQuery(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("/tags/")
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append(it.key)
|
||||
}
|
||||
append("?view=groupings")
|
||||
} else {
|
||||
append("/series?view=cover")
|
||||
|
||||
}
|
||||
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/series?view=cover&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
private fun parseMangaList(doc: Document): List<Manga> {
|
||||
return doc.select("li.span2")
|
||||
.map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = div.selectFirstOrThrow("div.caption").text(),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = div.selectFirstOrThrow("img").attrAsAbsoluteUrl("src"),
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMangaListQuery(doc: Document): List<Manga> {
|
||||
return doc.select("dl.chapter-list dd")
|
||||
.map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = div.selectFirstOrThrow("a").text(),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = "",
|
||||
tags = div.select("span.tags a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = a.text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
return coroutineScope {
|
||||
(1..3).map { page ->
|
||||
async { getTags(page) }
|
||||
}
|
||||
}.awaitAll().flattenTo(ArraySet(360))
|
||||
}
|
||||
|
||||
private suspend fun getTags(page: Int): Set<MangaTag> {
|
||||
val url = "https://$domain/tags?page=$page"
|
||||
val root = webClient.httpGet(url).parseHtml()
|
||||
return root.selectFirstOrThrow(".tag-list ").parseTags()
|
||||
}
|
||||
|
||||
private fun Element.parseTags() = select("a").mapToSet {
|
||||
MangaTag(
|
||||
key = it.attr("href").removeSuffix('/').substringAfterLast('/'),
|
||||
title = it.text(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
@@ -110,19 +158,14 @@ internal class DynastyScans(context: MangaLoaderContext) : PagedMangaParser(cont
|
||||
altTitle = null,
|
||||
state = when (root.select("h2.tag-title small").last()?.text()) {
|
||||
"— Ongoing" -> MangaState.ONGOING
|
||||
"— Completed" -> MangaState.FINISHED
|
||||
"— Completed", "— Completed and Licensed" -> MangaState.FINISHED
|
||||
"— Dropped", "— Licensed and Removed", "— Abandoned" -> MangaState.ABANDONED
|
||||
"— On Hiatus" -> MangaState.PAUSED
|
||||
else -> null
|
||||
},
|
||||
coverUrl = root.selectFirst("img.thumbnail")?.src()
|
||||
.orEmpty(), // It is needed if the manga was found via the search.
|
||||
tags = root.select("div.tag-tags a").mapNotNullToSet { a ->
|
||||
val href = a.attr("href").removeSuffix('/').substringAfterLast('/')
|
||||
MangaTag(
|
||||
key = href,
|
||||
title = a.text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
tags = root.selectFirstOrThrow("div.tag-tags").parseTags(),
|
||||
author = null,
|
||||
description = null,
|
||||
chapters = chapters,
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.mapJSONToSet
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("FAKKU", "Fakku", "en", ContentType.HENTAI)
|
||||
internal class Fakku(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.FAKKU, pageSize = 25) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.NEWEST, SortOrder.UPDATED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("fakku.cc")
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
append("/search?q=")
|
||||
append(query.urlEncoded())
|
||||
append("&")
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("/tags/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("?")
|
||||
}
|
||||
|
||||
else -> {
|
||||
append("?")
|
||||
}
|
||||
}
|
||||
append("page=")
|
||||
append(page)
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
SortOrder.NEWEST -> append("created_at")
|
||||
SortOrder.UPDATED -> append("published_at")
|
||||
else -> append("published_at")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.entries .entry a").map { a ->
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
coverUrl = a.selectFirst("img")?.src().orEmpty(),
|
||||
title = a.selectFirst(".title")?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
source = source,
|
||||
isNsfw = isNsfwSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val root = webClient.httpGet("https://$domain/tags").parseHtml()
|
||||
return root.select("div.entries .entry a").mapToSet {
|
||||
MangaTag(
|
||||
key = it.attr("href").substringAfterLast("/"),
|
||||
title = it.selectFirstOrThrow(".name").text(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val genreDeferred = async {
|
||||
webClient.httpGet(manga.url.toAbsoluteUrl(domain) + ".json").parseJson()
|
||||
}
|
||||
val genre = genreDeferred.await()
|
||||
manga.copy(
|
||||
author = doc.selectFirst("tr.artists a")?.text(),
|
||||
tags = if (genre.toString().contains("tags")) {
|
||||
genre.getJSONArray("tags").mapJSONToSet {
|
||||
MangaTag(
|
||||
key = it.getString("slug"),
|
||||
title = it.getString("name"),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptySet()
|
||||
},
|
||||
chapters = listOf(
|
||||
MangaChapter(
|
||||
id = manga.id,
|
||||
name = manga.title,
|
||||
number = 1,
|
||||
url = manga.url + "/1",
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val totalPages = doc.selectFirstOrThrow(".total").text().toInt()
|
||||
val rawUrl = chapter.url.substringBeforeLast("/")
|
||||
return (1..totalPages).map {
|
||||
val url = "$rawUrl/$it"
|
||||
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.selectFirstOrThrow(".page img").attrAsAbsoluteUrl("src")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.json.toJSONList
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("FLIXSCANSORG", "FlixScans.org", "en")
|
||||
internal class FlixScansOrg(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.FLIXSCANSORG, 18) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
|
||||
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.ADULT)
|
||||
override val configKeyDomain = ConfigKey.Domain("flixscans.org")
|
||||
override val isSearchSupported = false
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val json = when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domain)
|
||||
append("/api/v1/search/advance?=&serie_type=webtoon&page=")
|
||||
append(page.toString())
|
||||
|
||||
append("&genres=")
|
||||
append(filter.tags.joinToString(separator = ",") { it.key })
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "completed"
|
||||
MangaState.ABANDONED -> "droped"
|
||||
MangaState.PAUSED -> "onhold"
|
||||
MangaState.UPCOMING -> "soon"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
append("&adult=")
|
||||
append(
|
||||
when (it) {
|
||||
ContentRating.ADULT -> "true"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
append("&serie_type=webtoon")
|
||||
}
|
||||
webClient.httpGet(url).parseJson().getJSONArray("data")
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = "https://api.$domain/api/v1/search/advance?=&serie_type=webtoon&page=$page"
|
||||
webClient.httpGet(url).parseJson().getJSONArray("data")
|
||||
}
|
||||
}
|
||||
return json.mapJSON { j ->
|
||||
val href = "https://$domain/series/${j.getString("prefix")}-${j.getString("id")}-${j.getString("slug")}"
|
||||
val cover = "https://media.$domain/" + j.getString("thumbnail")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = cover,
|
||||
tags = emptySet(),
|
||||
state = when (j.getString("status")) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"completed" -> MangaState.FINISHED
|
||||
"onhold" -> MangaState.PAUSED
|
||||
"droped" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/search/advance").parseHtml()
|
||||
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
|
||||
val tagsList = json.getJSONArray(3).toString().replace("[", "").replace("]", "").split(",")
|
||||
return tagsList.mapNotNullToSet { idTag ->
|
||||
val id = idTag.toInt()
|
||||
val idKey = json.getJSONObject(id).getInt("id")
|
||||
val key = json.getInt(idKey).toString()
|
||||
val idName = json.getJSONObject(id).getInt("name")
|
||||
val name = json.getString(idName)
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val chaptersDeferred = async { loadChapters(manga.url) }
|
||||
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
|
||||
val descId = json.getJSONObject(6).getInt("story")
|
||||
val desc = json.getString(descId)
|
||||
val tagsId = json.getJSONObject(6).getInt("genres")
|
||||
val tagsList = json.getJSONArray(tagsId).toString().replace("[", "").replace("]", "").split(",")
|
||||
val ratingId = json.getJSONObject(6).getInt("rating")
|
||||
val rating = json.getString(ratingId)
|
||||
val nsfwId = json.getJSONObject(6).getInt("nsfw")
|
||||
val nsfw = json.getBoolean(nsfwId)
|
||||
manga.copy(
|
||||
description = desc,
|
||||
tags = tagsList.mapToSet { idTag ->
|
||||
val id = idTag.toInt()
|
||||
val idKey = json.getJSONObject(id).getInt("id")
|
||||
val key = json.get(idKey).toString()
|
||||
val idName = json.getJSONObject(id).getInt("name")
|
||||
val name = json.get(idName).toString()
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
rating = rating?.toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
|
||||
isNsfw = nsfw,
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale)
|
||||
|
||||
private suspend fun loadChapters(baseUrl: String): List<MangaChapter> {
|
||||
val key = baseUrl.substringAfter("-").substringBefore("-")
|
||||
val seriesKey = baseUrl.substringAfterLast("/").substringBefore("-")
|
||||
val json = JSONArray(
|
||||
webClient.httpGet("https://api.$domain/api/v1/webtoon/chapters/$key-desc").parseRaw(),
|
||||
).toJSONList().reversed()
|
||||
return json.mapChapters { i, j ->
|
||||
val url = "https://$domain/read/webtoon/$seriesKey-${j.getString("id")}-${j.getString("slug")}"
|
||||
val date = j.getString("createdAt").substringBeforeLast("T")
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
name = j.getString("slug").replace('-', ' '),
|
||||
number = i + 1,
|
||||
branch = null,
|
||||
uploadDate = dateFormat.tryParse(date),
|
||||
scanlator = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val json = JSONArray(doc.requireElementById("__NUXT_DATA__").data())
|
||||
val chapterData = json.getJSONObject(6).getInt("chapterData")
|
||||
val pageLocate = json.getJSONObject(chapterData).getInt("webtoon")
|
||||
val jsonPages = json.getJSONArray(pageLocate)
|
||||
val pages = ArrayList<MangaPage>(jsonPages.length())
|
||||
for (i in 0 until jsonPages.length()) {
|
||||
val id = jsonPages.getInt(i)
|
||||
val url = "https://media.$domain/" + json.getString(id)
|
||||
pages.add(
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
),
|
||||
)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.json.JSONArray
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.mapJSON
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("KSKMOE", "Ksk.moe", "en", ContentType.HENTAI)
|
||||
internal class KskMoe(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.KSKMOE, 35) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST, SortOrder.ALPHABETICAL)
|
||||
override val configKeyDomain = ConfigKey.Domain("ksk.moe")
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/tags/")
|
||||
append(tag?.key.orEmpty())
|
||||
} else {
|
||||
append("/browse")
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
}
|
||||
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("?sort=32")
|
||||
SortOrder.UPDATED -> append("")
|
||||
SortOrder.NEWEST -> append("?sort=16")
|
||||
SortOrder.ALPHABETICAL -> append("?sort=1")
|
||||
else -> append("")
|
||||
}
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("?s=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
if (!doc.html().contains("pagination") && page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
return doc.requireElementById("galleries").select("article").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = div.selectLastOrThrow("h3 span").text(),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = div.selectFirstOrThrow("img").src()?.toAbsoluteUrl(domain).orEmpty(),
|
||||
tags = div.select("footer span").mapNotNullToSet { span ->
|
||||
MangaTag(
|
||||
key = span.text().urlEncoded(),
|
||||
title = span.text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
return coroutineScope {
|
||||
(1..2).map { page ->
|
||||
async { getTags(page) }
|
||||
}
|
||||
}.awaitAll().flattenTo(ArraySet(360))
|
||||
}
|
||||
|
||||
private suspend fun getTags(page: Int): Set<MangaTag> {
|
||||
val url = if (page == 1) {
|
||||
"https://$domain/tags"
|
||||
} else {
|
||||
"https://$domain/tags/page/$page"
|
||||
}
|
||||
val root = webClient.httpGet(url).parseHtml().body().getElementById("tags")
|
||||
return root?.parseTags().orEmpty()
|
||||
}
|
||||
|
||||
private fun Element.parseTags() = select("section.tags div a").mapToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").substringAfterLast("/tags/"),
|
||||
title = a.selectFirstOrThrow("span").text(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
private val date = SimpleDateFormat("dd.MM.yyyy hh:mm 'UTC'", Locale.US)
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
|
||||
return manga.copy(
|
||||
tags = doc.requireElementById("metadata").select("main div:contains(Tag) a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").substringAfterLast("/tags/"),
|
||||
title = a.selectFirstOrThrow("span").text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
author = doc.requireElementById("metadata").selectFirstOrThrow("main div:contains(Artist) a span").text(),
|
||||
chapters = listOf(
|
||||
MangaChapter(
|
||||
id = generateUid(manga.id),
|
||||
name = manga.title,
|
||||
number = 1,
|
||||
url = manga.url,
|
||||
scanlator = null,
|
||||
uploadDate = date.tryParse(doc.selectFirstOrThrow("time.updated").text()),
|
||||
branch = null,
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url
|
||||
.replace("/view/", "/read/")
|
||||
.let { "$it/1" }
|
||||
.toAbsoluteUrl(domain)
|
||||
val document = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val id = fullUrl
|
||||
.substringAfter("/read/")
|
||||
.substringBeforeLast("/")
|
||||
|
||||
val cdnUrl = document.selectFirst("meta[itemprop=image]")
|
||||
?.attr("content")
|
||||
?.toHttpUrlOrNull()
|
||||
?.host
|
||||
.let { "https://" + (it ?: domain) }
|
||||
|
||||
val script = document.select("script:containsData(window.metadata)").html()
|
||||
|
||||
val rawJson = script
|
||||
.substringAfter("original:")
|
||||
.substringBefore("resampled:")
|
||||
.substringBeforeLast(",")
|
||||
|
||||
return JSONArray(rawJson).mapJSON {
|
||||
val fileName = it.getString("n")
|
||||
|
||||
val url = "$cdnUrl/original/$id/$fileName"
|
||||
val preview = "$cdnUrl/t/$id/320/$fileName"
|
||||
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = preview,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
@@ -17,7 +19,7 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.POPULARITY, SortOrder.UPDATED, SortOrder.NEWEST)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("www.mangageko.com")
|
||||
override val configKeyDomain = ConfigKey.Domain("www.mgeko.com", "www.mangageko.com")
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
@@ -25,42 +27,49 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = if (!query.isNullOrEmpty()) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
buildString {
|
||||
append("https://$domain/search/?search=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
append("https://$domain/browse-comics/?results=")
|
||||
append(page)
|
||||
append("&filter=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("Updated")
|
||||
SortOrder.NEWEST -> append("New")
|
||||
else -> append("Updated")
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
append("/search/?search=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&genre=")
|
||||
append(tag?.key.orEmpty())
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("/browse-comics/?results=")
|
||||
append(page)
|
||||
|
||||
append("&filter=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("Updated")
|
||||
SortOrder.NEWEST -> append("New")
|
||||
else -> append("Updated")
|
||||
}
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("&genre=")
|
||||
append(it.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/browse-comics/?results=")
|
||||
append(page)
|
||||
append("&filter=Updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return doc.select("li.novel-item").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
@@ -91,10 +100,10 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val dateFormat = SimpleDateFormat("MMM dd, yyyy", sourceLocale)
|
||||
return manga.copy(
|
||||
val chaptersDeferred = async { loadChapters(manga.url) }
|
||||
manga.copy(
|
||||
altTitle = doc.selectFirstOrThrow(".alternative-title").text(),
|
||||
state = when (doc.selectFirstOrThrow(".header-stats span:contains(Status) strong").text()) {
|
||||
"Ongoing" -> MangaState.ONGOING
|
||||
@@ -110,27 +119,34 @@ internal class MangaGeko(context: MangaLoaderContext) : PagedMangaParser(context
|
||||
},
|
||||
author = doc.selectFirstOrThrow(".author").text(),
|
||||
description = doc.selectFirstOrThrow(".description").html(),
|
||||
chapters = doc.requireElementById("chapters").select("ul.chapter-list li")
|
||||
.mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val url = a.attrAsRelativeUrl("href")
|
||||
val name = li.selectFirstOrThrow(".chapter-title").text()
|
||||
val dateText = li.select(".chapter-update").attr("datetime").substringBeforeLast(',')
|
||||
.replace(".", "").replace("Sept", "Sep")
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = name,
|
||||
number = i + 1,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = dateFormat.tryParse(dateText),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
chapters = chaptersDeferred.await(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadChapters(mangaUrl: String): List<MangaChapter> {
|
||||
val urlChapter = mangaUrl + "all-chapters/"
|
||||
val doc = webClient.httpGet(urlChapter.toAbsoluteUrl(domain)).parseHtml()
|
||||
val dateFormat = SimpleDateFormat("MMM dd, yyyy", sourceLocale)
|
||||
return doc.requireElementById("chapters").select("ul.chapter-list li")
|
||||
.mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val url = a.attrAsRelativeUrl("href")
|
||||
val name = li.selectFirstOrThrow(".chapter-title").text()
|
||||
val dateText = li.select(".chapter-update").attr("datetime").substringBeforeLast(',')
|
||||
.replace(".", "").replace("Sept", "Sep")
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = name,
|
||||
number = i + 1,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = dateFormat.tryParse(dateText),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
@@ -22,35 +22,69 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
SortOrder.UPDATED,
|
||||
)
|
||||
|
||||
private val regexTag = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+")
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
)
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val sortKey = when (sortOrder) {
|
||||
SortOrder.ALPHABETICAL -> "?name.az"
|
||||
SortOrder.RATING -> "?rating.za"
|
||||
SortOrder.UPDATED -> "?last_chapter_time.za"
|
||||
else -> ""
|
||||
}
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val page = (offset / 30) + 1
|
||||
val url = when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
if (offset != 0) {
|
||||
return emptyList()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search?name=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
"/search?name=${query.urlEncoded()}".toAbsoluteUrl(domain)
|
||||
}
|
||||
|
||||
tags.isNullOrEmpty() -> "/directory/$page.htm$sortKey".toAbsoluteUrl(domain)
|
||||
tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".toAbsoluteUrl(domain)
|
||||
else -> tags.joinToString(
|
||||
prefix = "/search?page=$page".toAbsoluteUrl(domain),
|
||||
) { tag ->
|
||||
"&genres[${tag.key}]=1"
|
||||
is MangaListFilter.Advanced -> {
|
||||
append("/directory/")
|
||||
append("0-")
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append(it.key)
|
||||
}
|
||||
} else {
|
||||
append("0")
|
||||
}
|
||||
append("-0-")
|
||||
|
||||
if (filter.states.isNotEmpty()) {
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "completed"
|
||||
else -> "0"
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
append("0")
|
||||
}
|
||||
|
||||
append("-0-0/")
|
||||
append(page.toString())
|
||||
append(".htm")
|
||||
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> ""
|
||||
SortOrder.UPDATED -> "?last_chapter_time.za"
|
||||
SortOrder.ALPHABETICAL -> "?name.az"
|
||||
SortOrder.RATING -> "?rating.za"
|
||||
else -> "?last_chapter_time.za"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
null -> append("/directory/$page.htm?last_chapter_time.za")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
@@ -81,7 +115,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x ->
|
||||
MangaTag(
|
||||
title = x.attr("title").toTitleCase(),
|
||||
key = x.attr("href").parseTagKey() ?: return@tags null,
|
||||
key = x.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
|
||||
source = MangaSource.MANGATOWN,
|
||||
)
|
||||
}.orEmpty(),
|
||||
@@ -106,7 +140,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
}?.select("a")?.mapNotNull { a ->
|
||||
MangaTag(
|
||||
title = a.attr("title").toTitleCase(),
|
||||
key = a.attr("href").parseTagKey() ?: return@mapNotNull null,
|
||||
key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-"),
|
||||
source = MangaSource.MANGATOWN,
|
||||
)
|
||||
}.orEmpty(),
|
||||
@@ -165,10 +199,7 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
?.nextElementSibling() ?: doc.parseFailed("Root not found")
|
||||
return root.select("li").mapNotNullToSet { li ->
|
||||
val a = li.selectFirst("a") ?: return@mapNotNullToSet null
|
||||
val key = a.attr("href").parseTagKey()
|
||||
if (key.isNullOrEmpty()) {
|
||||
return@mapNotNullToSet null
|
||||
}
|
||||
val key = a.attr("href").substringAfter("/directory/0-").substringBefore("-0-")
|
||||
MangaTag(
|
||||
source = MangaSource.MANGATOWN,
|
||||
key = key,
|
||||
@@ -211,6 +242,4 @@ internal class MangaTownParser(context: MangaLoaderContext) : MangaParser(contex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.parseTagKey() = split('/').findLast { regexTag matches it }
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.RATING,
|
||||
)
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("mangaowl.to")
|
||||
|
||||
@@ -31,46 +32,56 @@ internal class Mangaowl(context: MangaLoaderContext) :
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val sort = when (sortOrder) {
|
||||
SortOrder.POPULARITY -> "view_count"
|
||||
SortOrder.UPDATED -> "-modified_at"
|
||||
SortOrder.NEWEST -> "created_at"
|
||||
SortOrder.RATING -> "rating"
|
||||
else -> "modified_at"
|
||||
}
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
append("/8-search")
|
||||
append("?q=")
|
||||
append(query.urlEncoded())
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/10-search?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("/8-genres/")
|
||||
for (tag in tags) {
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("/10-comics")
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
|
||||
filter.tags.forEach { tag ->
|
||||
append("&genres=")
|
||||
append(tag.key)
|
||||
}
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "completed"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
append("&ordering=")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> "view_count"
|
||||
SortOrder.UPDATED -> "-modified_at"
|
||||
SortOrder.NEWEST -> "created_at"
|
||||
SortOrder.RATING -> "rating"
|
||||
else -> "modified_at"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
append("/8-comics")
|
||||
append("?page=")
|
||||
null -> {
|
||||
append("/10-comics?ordering=-modified_at&page=")
|
||||
append(page.toString())
|
||||
append("&ordering=")
|
||||
append(sort)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,9 +106,9 @@ internal class Mangaowl(context: MangaLoaderContext) :
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/8-genres").parseHtml()
|
||||
val doc = webClient.httpGet("https://$domain/10-genres").parseHtml()
|
||||
return doc.select("div.genres-container span.genre-item a").mapNotNullToSet { a ->
|
||||
val key = a.attr("href").substringAfterLast("/")
|
||||
val key = a.attr("href").removeSuffix('/').substringAfterLast('/').substringBefore("-")
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = a.text(),
|
||||
@@ -112,7 +123,7 @@ internal class Mangaowl(context: MangaLoaderContext) :
|
||||
manga.copy(
|
||||
tags = doc.body().select("div.comic-attrs div.column.my-2:contains(Genres) a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
|
||||
key = a.attr("href").removeSuffix("/").substringAfterLast('/').substringBefore("-"),
|
||||
title = a.text().toTitleCase().replace(",", ""),
|
||||
source = source,
|
||||
)
|
||||
@@ -130,52 +141,31 @@ internal class Mangaowl(context: MangaLoaderContext) :
|
||||
private fun getChapters(mangaUrl: String, doc: Document): List<MangaChapter> {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", sourceLocale)
|
||||
val script = doc.selectFirstOrThrow("script:containsData(chapters:)")
|
||||
val json =
|
||||
script.data().substringAfter("chapters:[").substringBeforeLast(')').substringBefore("],latest_chapter:")
|
||||
.split("},")
|
||||
val json = script.data().substringAfter("chapters:[")
|
||||
.substringBeforeLast(')')
|
||||
.substringBefore("],latest_chapter:")
|
||||
.split("},")
|
||||
val slug = mangaUrl.substringAfterLast("/")
|
||||
val chapter = ArrayList<MangaChapter>()
|
||||
var lastIndexed = 0
|
||||
json.mapIndexed { i, t ->
|
||||
return json.mapChapters { i, t ->
|
||||
if (t.contains("Chapter")) {
|
||||
val id = t.substringAfter("id:").substringBefore(",created_at")
|
||||
val url = "/reading/$slug/$id"
|
||||
val date = t.substringAfter("created_at:\"").substringBefore("\"")
|
||||
val name = t.substringAfter("name:\"").substringBefore("\"")
|
||||
chapter.add(
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = name,
|
||||
number = i + 1,
|
||||
url = url,
|
||||
uploadDate = dateFormat.tryParse(date),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
),
|
||||
val date = t.substringAfter("created_at:\"").substringBefore('"')
|
||||
val name = t.substringAfter("name:\"").substringBefore('"')
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = name,
|
||||
number = i + 1,
|
||||
url = url,
|
||||
uploadDate = dateFormat.tryParse(date),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
lastIndexed = i
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// last chapter
|
||||
val id = script.data().substringAfter("Sign in\",").substringBefore(",\"").split(",").last()
|
||||
val url = "/reading/$slug/$id"
|
||||
val date = script.data().substringAfter("$id,\"").substringBefore("\",")
|
||||
val name = script.data().substringAfter("$date\",\"").substringBefore("\",")
|
||||
chapter.add(
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = name,
|
||||
number = lastIndexed + 1,
|
||||
url = url,
|
||||
uploadDate = dateFormat.tryParse(date),
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
),
|
||||
)
|
||||
return chapter
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
|
||||
@@ -9,14 +9,28 @@ import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("MANHWA18", "Manhwa18", "en", type = ContentType.HENTAI)
|
||||
@MangaSourceParser("MANHWA18", "Manhwa18.net", "en", type = ContentType.HENTAI)
|
||||
class Manhwa18Parser(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.MANHWA18, pageSize = 18, searchPageSize = 18) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("manhwa18.net")
|
||||
|
||||
override val availableSortOrders: Set<SortOrder>
|
||||
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.ALPHABETICAL, SortOrder.NEWEST)
|
||||
get() = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.RATING,
|
||||
)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
MangaState.PAUSED,
|
||||
)
|
||||
|
||||
override val isTagsExclusionSupported = true
|
||||
|
||||
private val tagsMap = SuspendLazy(::parseTags)
|
||||
|
||||
@@ -29,6 +43,90 @@ class Manhwa18Parser(context: MangaLoaderContext) :
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-kiem?page=")
|
||||
append(page.toString())
|
||||
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&q=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("&accept_genres=")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append(
|
||||
filter.tags.joinToString(",") { it.key },
|
||||
)
|
||||
}
|
||||
|
||||
append("&reject_genres=")
|
||||
if (filter.tagsExclude.isNotEmpty()) {
|
||||
append(
|
||||
filter.tagsExclude.joinToString(",") { it.key },
|
||||
)
|
||||
}
|
||||
|
||||
append("&sort=")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.ALPHABETICAL -> "az"
|
||||
SortOrder.ALPHABETICAL_DESC -> "za"
|
||||
SortOrder.POPULARITY -> "top"
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.NEWEST -> "new"
|
||||
SortOrder.RATING -> "like"
|
||||
},
|
||||
)
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "1"
|
||||
MangaState.FINISHED -> "3"
|
||||
MangaState.PAUSED -> "2"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("&sort=update")
|
||||
}
|
||||
}
|
||||
|
||||
val docs = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return docs.select(".card-body .thumb-item-flow")
|
||||
.map {
|
||||
val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a")
|
||||
val absUrl = titleElement.attrAsAbsoluteUrl("href")
|
||||
Manga(
|
||||
id = generateUid(absUrl.toRelativeUrl(domain)),
|
||||
title = titleElement.text(),
|
||||
altTitle = null,
|
||||
url = absUrl.toRelativeUrl(domain),
|
||||
publicUrl = absUrl,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
largeCoverUrl = null,
|
||||
description = null,
|
||||
source = MangaSource.MANHWA18,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val cardInfoElement = docs.selectFirst("div.series-information")
|
||||
@@ -45,6 +143,7 @@ class Manhwa18Parser(context: MangaLoaderContext) :
|
||||
when (it.text().lowercase()) {
|
||||
"on going" -> MangaState.ONGOING
|
||||
"completed" -> MangaState.FINISHED
|
||||
"on hold" -> MangaState.PAUSED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -99,60 +198,6 @@ class Manhwa18Parser(context: MangaLoaderContext) :
|
||||
return cal.time.time
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val sortQuery = when (sortOrder) {
|
||||
SortOrder.ALPHABETICAL -> "az"
|
||||
SortOrder.POPULARITY -> "top"
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.NEWEST -> "new"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val tagQuery = tags?.joinToString(",") { it.key }.orEmpty()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/tim-kiem?page=")
|
||||
append(page)
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("&q=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
append("&accept_genres=$tagQuery")
|
||||
append("&sort=")
|
||||
append(sortQuery)
|
||||
}
|
||||
|
||||
val docs = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return docs.select(".card-body .thumb-item-flow")
|
||||
.map {
|
||||
val titleElement = it.selectFirstOrThrow(".thumb_attr.series-title > a")
|
||||
val absUrl = titleElement.attrAsAbsoluteUrl("href")
|
||||
Manga(
|
||||
id = generateUid(absUrl.toRelativeUrl(domain)),
|
||||
title = titleElement.text(),
|
||||
altTitle = null,
|
||||
url = absUrl.toRelativeUrl(domain),
|
||||
publicUrl = absUrl,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = it.selectFirst("div.img-in-ratio")?.attrAsAbsoluteUrl("data-bg").orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
largeCoverUrl = null,
|
||||
description = null,
|
||||
source = MangaSource.MANHWA18,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val chapterUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(chapterUrl).parseHtml()
|
||||
|
||||
@@ -21,29 +21,42 @@ class ManhwasMen(context: MangaLoaderContext) :
|
||||
override val availableSortOrders: Set<SortOrder>
|
||||
get() = EnumSet.of(SortOrder.POPULARITY)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/manga-list")
|
||||
append("?page=")
|
||||
append(page)
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
append(page.toString())
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&search=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("&genero=")
|
||||
append(tag?.key.orEmpty())
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("&genero=")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&estado=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "ongoing"
|
||||
MangaState.FINISHED -> "complete"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
@@ -80,6 +93,7 @@ class ManhwasMen(context: MangaLoaderContext) :
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
|
||||
return manga.copy(
|
||||
tags = doc.body().select(".genres a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
@@ -89,9 +103,9 @@ class ManhwasMen(context: MangaLoaderContext) :
|
||||
)
|
||||
},
|
||||
description = doc.select(".sinopsis").html(),
|
||||
state = when (doc.selectLast(".anime-type-peli")?.text()?.lowercase()) {
|
||||
state = when (doc.selectLast("span.anime-type-peli")?.text()?.lowercase()) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"completed" -> MangaState.FINISHED
|
||||
"complete" -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
chapters = doc.select(".episodes-list li").mapChapters(reversed = true) { i, li ->
|
||||
@@ -103,7 +117,7 @@ class ManhwasMen(context: MangaLoaderContext) :
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = parseChapterDate(
|
||||
SimpleDateFormat("dd/MM/yyyy", sourceLocale),
|
||||
simpleDateFormat,
|
||||
li.selectLastOrThrow(".flex-grow-1 span").text(),
|
||||
),
|
||||
branch = null,
|
||||
|
||||
@@ -15,20 +15,28 @@ internal class Po2Scans(context: MangaLoaderContext) : MangaParser(context, Mang
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
|
||||
override val configKeyDomain = ConfigKey.Domain("po2scans.com")
|
||||
|
||||
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val url = buildString {
|
||||
append("https://$domain/series")
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("?search=")
|
||||
append(query.urlEncoded())
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/series")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("?search=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select(".series-list").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = div.selectFirstOrThrow("h2").text(),
|
||||
|
||||
@@ -24,37 +24,43 @@ internal class Pururin(context: MangaLoaderContext) :
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("/search?q=")
|
||||
append(query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page)
|
||||
} else {
|
||||
append("/browse")
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/tags/content/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
append("?page=")
|
||||
append(page)
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED -> append("")
|
||||
SortOrder.POPULARITY -> append("most-viewed")
|
||||
SortOrder.RATING -> append("highest-rated")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
else -> append("")
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
append("/browse")
|
||||
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tags/content/")
|
||||
append(it.key)
|
||||
append("/")
|
||||
}
|
||||
|
||||
append("?page=")
|
||||
append(page)
|
||||
|
||||
append("&sort=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> append("")
|
||||
SortOrder.POPULARITY -> append("most-viewed")
|
||||
SortOrder.RATING -> append("highest-rated")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
else -> append("")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/browse?page=")
|
||||
append(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.koitharu.kotatsu.parsers.site.en
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("REAPERCOMICS", "ReaperComics", "en")
|
||||
internal class ReaperComics(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.REAPERCOMICS, pageSize = 30) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.ALPHABETICAL)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("reaperscans.com")
|
||||
|
||||
override val isSearchSupported = false
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED) // TODO
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
append("/")
|
||||
if (filter.sortOrder == SortOrder.UPDATED) {
|
||||
append("latest/")
|
||||
}
|
||||
append("comics?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/latest/comics?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
private fun parseMangaList(docs: Document): List<Manga> {
|
||||
|
||||
|
||||
return docs.select("main div.relative, main li.col-span-1").map {
|
||||
val a = it.selectFirstOrThrow("a")
|
||||
val url = a.attrAsAbsoluteUrl("href")
|
||||
Manga(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
title = (it.selectFirst("p a") ?: it.selectLast("a"))?.text().orEmpty(),
|
||||
altTitle = null,
|
||||
publicUrl = url,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = isNsfwSource,
|
||||
coverUrl = it.selectFirstOrThrow("img").src().orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy", sourceLocale)
|
||||
return manga.copy(
|
||||
description = doc.selectFirst("div.p-4 p.prose")?.html(),
|
||||
state = when (doc.selectFirst("dl.mt-2 div:contains(Status) dd")?.text()?.lowercase()) {
|
||||
"ongoing" -> MangaState.ONGOING
|
||||
"complete" -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
chapters = doc.select("div.p-2 div.pb-4 ul li").mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val chapterUrl = a.attrAsAbsoluteUrl("href").toRelativeUrl(domain)
|
||||
MangaChapter(
|
||||
id = generateUid(chapterUrl),
|
||||
name = li.selectFirst("div.truncate p.truncate")?.text().orEmpty(),
|
||||
number = i + 1,
|
||||
url = chapterUrl,
|
||||
scanlator = null,
|
||||
uploadDate = parseChapterDate(
|
||||
simpleDateFormat,
|
||||
li.selectFirst("div.truncate div.items-center")?.text(),
|
||||
),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||
val d = date?.lowercase() ?: return 0
|
||||
return when {
|
||||
d.endsWith(" ago") -> parseRelativeDate(date)
|
||||
else -> dateFormat.tryParse(date)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRelativeDate(date: String): Long {
|
||||
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
||||
val cal = Calendar.getInstance()
|
||||
return when {
|
||||
WordSet("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
WordSet("minute", "minutes").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
WordSet("hour", "hours").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
WordSet("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
WordSet("week", "weeks").anyWordIn(date) -> cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
|
||||
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.select("img.max-w-full").map { img ->
|
||||
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.es
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.Headers
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
@@ -20,26 +21,35 @@ internal class TempleScanEsp(context: MangaLoaderContext) :
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("templescanesp.net")
|
||||
|
||||
override val isSearchSupported = false
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (sortOrder == SortOrder.NEWEST) {
|
||||
append("/comics")
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
} else {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.sortOrder == SortOrder.NEWEST) {
|
||||
append("/comics?page=")
|
||||
append(page.toString())
|
||||
} else {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/comics?page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,49 +24,68 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
|
||||
private val chapterDateFormat = SimpleDateFormat("yyyy-MM-dd", sourceLocale)
|
||||
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT)
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.ALPHABETICAL_DESC,
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.RATING,
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
|
||||
val order =
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> "likes_count"
|
||||
SortOrder.UPDATED -> "release_date"
|
||||
SortOrder.NEWEST -> "creation"
|
||||
SortOrder.ALPHABETICAL -> "alphabetically"
|
||||
SortOrder.RATING -> "score"
|
||||
|
||||
}
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/library")
|
||||
if (query.isNullOrEmpty()) {
|
||||
append("?order_item=")
|
||||
append(order)
|
||||
append("&order_dir=desc")
|
||||
append("&filter_by=title")
|
||||
if (tags != null) {
|
||||
for (tag in tags) {
|
||||
append("&genders[]=${tag.key}")
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
append("?title=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
append("?order_item=")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> "likes_count&order_dir=desc"
|
||||
SortOrder.UPDATED -> "release_date&order_dir=desc"
|
||||
SortOrder.NEWEST -> "creation&order_dir=desc"
|
||||
SortOrder.ALPHABETICAL -> "alphabetically&order_dir=asc"
|
||||
SortOrder.ALPHABETICAL_DESC -> "alphabetically&order_dir=desc"
|
||||
SortOrder.RATING -> "score&order_dir=desc"
|
||||
},
|
||||
)
|
||||
append("&filter_by=title")
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
for (tag in filter.tags) {
|
||||
append("&genders[]=")
|
||||
append(tag.key)
|
||||
}
|
||||
}
|
||||
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
append("&erotic=")
|
||||
append(
|
||||
when (it) {
|
||||
ContentRating.SAFE -> "false"
|
||||
ContentRating.ADULT -> "true"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
append("?title=$query")
|
||||
}
|
||||
append("&_pg=1")
|
||||
append("&page=$page")
|
||||
}.toAbsoluteUrl(domain)
|
||||
|
||||
null -> {
|
||||
append("?order_item=release_date&order_dir=desc&filter_by=title")
|
||||
}
|
||||
}
|
||||
append("&_pg=1&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
val doc = webClient.httpGet(url, headers).parseHtml()
|
||||
val items = doc.body().select("div.element")
|
||||
return items.mapNotNull { item ->
|
||||
@@ -95,6 +114,13 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
val contents = doc.body().selectFirstOrThrow("section.element-header-content")
|
||||
return manga.copy(
|
||||
description = contents.selectFirst("p.element-description")?.html(),
|
||||
tags = contents.select("h6 a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
key = a.attr("href").substringBefore("&").substringAfterLast("="),
|
||||
title = a.text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
largeCoverUrl = contents.selectFirst(".book-thumbnail")?.attrAsAbsoluteUrlOrNull("src"),
|
||||
state = parseStatus(contents.select("span.book-status").text().orEmpty()),
|
||||
author = contents.selectFirst("h5.card-title")?.attr("title")?.substringAfter(", "),
|
||||
@@ -105,9 +131,9 @@ class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
} else {
|
||||
val chapters = ChaptersListBuilder(10)
|
||||
doc.select(regularChapterListSelector).reversed().forEachIndexed { i, item ->
|
||||
val chaptername = item.select("div.col-10.text-truncate").text().replace(" ", " ").trim()
|
||||
val scanelement = item.select("ul.chapter-list > li")
|
||||
scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, i)) }
|
||||
val chapterName = item.select("div.col-10.text-truncate").text().replace(" ", " ").trim()
|
||||
val scanElement = item.select("ul.chapter-list > li")
|
||||
scanElement.forEach { chapters.add(regularChapterFromElement(it, chapterName, i)) }
|
||||
}
|
||||
chapters.toList()
|
||||
},
|
||||
|
||||
@@ -25,9 +25,16 @@ internal abstract class FmreaderParser(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.ALPHABETICAL_DESC,
|
||||
)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(
|
||||
MangaState.ONGOING,
|
||||
MangaState.FINISHED,
|
||||
MangaState.ABANDONED,
|
||||
)
|
||||
|
||||
override val isTagsExclusionSupported = true
|
||||
|
||||
protected open val listUrl = "/manga-list.html"
|
||||
protected open val datePattern = "MMMM d, yyyy"
|
||||
@@ -40,58 +47,77 @@ internal abstract class FmreaderParser(
|
||||
|
||||
@JvmField
|
||||
protected val ongoing: Set<String> = setOf(
|
||||
"On going",
|
||||
"Incomplete",
|
||||
"En curso",
|
||||
"on going",
|
||||
"incomplete",
|
||||
"en curso",
|
||||
)
|
||||
|
||||
@JvmField
|
||||
protected val finished: Set<String> = setOf(
|
||||
"Completed",
|
||||
"Completado",
|
||||
"completed",
|
||||
"completado",
|
||||
)
|
||||
|
||||
@JvmField
|
||||
protected val abandoned: Set<String> = hashSetOf(
|
||||
"Canceled",
|
||||
"Cancelled",
|
||||
"Drop",
|
||||
"canceled",
|
||||
"cancelled",
|
||||
"drop",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append(listUrl)
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&name=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("&genre=")
|
||||
append(tag?.key.orEmpty())
|
||||
append(filter.tags.joinToString(",") { it.key })
|
||||
|
||||
append("&ungenre=")
|
||||
append(filter.tagsExclude.joinToString(",") { it.key })
|
||||
|
||||
|
||||
append("&sort=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("last_update")
|
||||
SortOrder.ALPHABETICAL -> append("name&sort_type=ASC")
|
||||
SortOrder.ALPHABETICAL_DESC -> append("name&sort_type=DESC")
|
||||
else -> append("last_update")
|
||||
}
|
||||
|
||||
append("&m_status=")
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "2"
|
||||
MangaState.FINISHED -> "1"
|
||||
MangaState.ABANDONED -> "3"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("last_update")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
else -> append("last_update")
|
||||
|
||||
null -> append("&sort=last_update")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
|
||||
}
|
||||
|
||||
protected open fun parseMangaList(doc: Document): List<Manga> {
|
||||
return doc.select("div.thumb-item-flow").map { div ->
|
||||
val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
@@ -99,8 +125,8 @@ internal abstract class FmreaderParser(
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg")
|
||||
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('")
|
||||
.substringBeforeLast("')"),
|
||||
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("(")
|
||||
.substringBefore(")"),
|
||||
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
@@ -121,7 +147,7 @@ internal abstract class FmreaderParser(
|
||||
val href = a.attr("href").substringAfter(tagPrefix).substringBeforeLast(".html")
|
||||
MangaTag(
|
||||
key = href,
|
||||
title = a.text(),
|
||||
title = a.text().toTitleCase(sourceLocale),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
@@ -140,7 +166,7 @@ internal abstract class FmreaderParser(
|
||||
val desc = doc.selectFirst(selectDesc)?.html()
|
||||
val stateDiv = doc.selectFirst(selectState)
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
when (it.text().lowercase()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
in finished -> MangaState.FINISHED
|
||||
in abandoned -> MangaState.ABANDONED
|
||||
|
||||
@@ -20,79 +20,67 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
|
||||
override val selectTag = "div.info-item:contains(Genre) span.info-value a"
|
||||
override val datePattern = "dd/MM/yyyy"
|
||||
override val selectPage = "div#chapter-content img"
|
||||
override val selectBodyTag = "div.genres-menu a"
|
||||
override val selectBodyTag = "div.advanced-wrapper .genre_label"
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/genre/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("last_update")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
else -> append("last_update")
|
||||
}
|
||||
} else {
|
||||
append(listUrl)
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
append("&q=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
}
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("last_update")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
else -> append("last_update")
|
||||
append("/tim-kiem?page=")
|
||||
append(page.toString())
|
||||
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&q=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
append("&accept_genres=")
|
||||
append(filter.tags.joinToString(",") { it.key })
|
||||
|
||||
append("&reject_genres=")
|
||||
append(filter.tagsExclude.joinToString(",") { it.key })
|
||||
|
||||
append("&sort=")
|
||||
append(
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.ALPHABETICAL -> "az"
|
||||
SortOrder.ALPHABETICAL_DESC -> "za"
|
||||
SortOrder.POPULARITY -> "top"
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.NEWEST -> "new"
|
||||
SortOrder.RATING -> "like"
|
||||
},
|
||||
)
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "1"
|
||||
MangaState.FINISHED -> "3"
|
||||
MangaState.PAUSED -> "2"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("&sort=update")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.thumb-item-flow").map { div ->
|
||||
val href = div.selectFirstOrThrow("div.series-title a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||
coverUrl = div.selectFirstOrThrow("div.img-in-ratio").attr("data-bg")
|
||||
?: div.selectFirstOrThrow("div.img-in-ratio").attr("style").substringAfter("('")
|
||||
.substringBeforeLast("')"),
|
||||
title = div.selectFirstOrThrow("div.series-title").text().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = null,
|
||||
state = null,
|
||||
source = source,
|
||||
isNsfw = isNsfwSource,
|
||||
)
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
|
||||
return doc.select(selectBodyTag).mapNotNullToSet { a ->
|
||||
val href = a.attr("href").substringAfterLast("/")
|
||||
return doc.select(selectBodyTag).mapNotNullToSet { label ->
|
||||
val key = label.attr("data-genre-id")
|
||||
MangaTag(
|
||||
key = href,
|
||||
title = a.text(),
|
||||
key = key,
|
||||
title = label.selectFirstOrThrow(".gerne-name").text(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
@@ -105,7 +93,7 @@ internal class Manhwa18Com(context: MangaLoaderContext) :
|
||||
val desc = doc.selectFirstOrThrow(selectDesc).html()
|
||||
val stateDiv = doc.selectFirst(selectState)
|
||||
val state = stateDiv?.let {
|
||||
when (it.text()) {
|
||||
when (it.text().lowercase()) {
|
||||
in ongoing -> MangaState.ONGOING
|
||||
in finished -> MangaState.FINISHED
|
||||
else -> null
|
||||
|
||||
@@ -14,37 +14,62 @@ internal class OlimpoScans(context: MangaLoaderContext) :
|
||||
override val selectAlt = "ul.manga-info li:contains(Otros nombres)"
|
||||
override val selectTag = "ul.manga-info li:contains(Género) a"
|
||||
override val tagPrefix = "lista-de-comics-genero-"
|
||||
override val isMultipleTagsSupported = false
|
||||
override val isTagsExclusionSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append(listUrl)
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append(listUrl)
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
append("&name=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("&genre=")
|
||||
append(tag?.key.orEmpty())
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/lista-de-comics-genero-")
|
||||
append(it.key)
|
||||
append(".html")
|
||||
}
|
||||
} else {
|
||||
append(listUrl)
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
append("&sort=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("last_update")
|
||||
SortOrder.ALPHABETICAL -> append("name&sort_type=ASC")
|
||||
SortOrder.ALPHABETICAL_DESC -> append("name&sort_type=DESC")
|
||||
else -> append("last_update")
|
||||
}
|
||||
}
|
||||
|
||||
append("&m_status=")
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "2"
|
||||
MangaState.FINISHED -> "1"
|
||||
MangaState.ABANDONED -> "3"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append(listUrl)
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
append("&sort=last_update")
|
||||
}
|
||||
}
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("last_update")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
else -> append("last_update")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
@@ -3,13 +3,7 @@ package org.koitharu.kotatsu.parsers.site.fmreader.ja
|
||||
import org.jsoup.nodes.Document
|
||||
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.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
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.*
|
||||
import org.koitharu.kotatsu.parsers.site.fmreader.FmreaderParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -27,40 +21,7 @@ internal class Klz9(context: MangaLoaderContext) :
|
||||
override val selectPage = "img"
|
||||
override val selectBodyTag = "div.panel-body a"
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/$listUrl")
|
||||
append("?page=")
|
||||
append(page.toString())
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
|
||||
append("&name=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("&genre=")
|
||||
append(tag?.key.orEmpty())
|
||||
}
|
||||
}
|
||||
append("&sort=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("last_update")
|
||||
SortOrder.ALPHABETICAL -> append("name")
|
||||
else -> append("last_update")
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
override fun parseMangaList(doc: Document): List<Manga> {
|
||||
return doc.select("div.thumb-item-flow").map { div ->
|
||||
val href = "/" + div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
@@ -112,7 +73,6 @@ internal class Klz9(context: MangaLoaderContext) :
|
||||
val docLoad = webClient.httpGet("https://$domain/app/manga/controllers/cont.listImg.php?cid=$cid").parseHtml()
|
||||
return docLoad.select(selectPage).map { img ->
|
||||
val url = img.src()?.toRelativeUrl(domain) ?: img.parseFailed("Image src not found")
|
||||
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
|
||||
@@ -32,35 +32,63 @@ internal abstract class FoolSlideParser(
|
||||
searchPaginator.firstPage = 1
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val doc = if (!query.isNullOrEmpty()) {
|
||||
val url = buildString {
|
||||
append("https://$domain/$searchUrl")
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
val q = query.urlEncoded()
|
||||
webClient.httpPost(url, "search=$q").parseHtml()
|
||||
} else {
|
||||
val url = buildString {
|
||||
append("https://$domain/$listUrl")
|
||||
// For some sites that don't have enough manga and page 2 links to page 1
|
||||
if (!pagination) {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val doc =
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
} else {
|
||||
append(page.toString())
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/")
|
||||
append(searchUrl)
|
||||
}
|
||||
|
||||
webClient.httpPost(url, "search=${filter.query.urlEncoded()}").parseHtml()
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/")
|
||||
append(listUrl)
|
||||
// For some sites that don't have enough manga and page 2 links to page 1
|
||||
if (!pagination) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
} else {
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/")
|
||||
append(listUrl)
|
||||
if (!pagination) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
} else {
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
}
|
||||
|
||||
return doc.select("div.list div.group").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
|
||||
@@ -1,135 +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,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
|
||||
val doc = if (!query.isNullOrEmpty()) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append('/')
|
||||
append(searchUrl)
|
||||
append("?q=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
} else {
|
||||
val url = buildString {
|
||||
append("https://$domain/$listUrl")
|
||||
// For some sites that don't have enough manga and page 2 links to page 1
|
||||
if (!pagination) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
} else {
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
|
||||
|
||||
@MangaSourceParser("HNISCANTRAD", "Hni Scantrad", "fr")
|
||||
@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr")
|
||||
internal class HniScantrad(context: MangaLoaderContext) :
|
||||
FoolSlideParser(context, MangaSource.HNISCANTRAD, "hni-scantrad.com") {
|
||||
override val pagination = false
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.foolslide.it
|
||||
|
||||
|
||||
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.foolslide.FoolSlideParser
|
||||
|
||||
|
||||
@MangaSourceParser("POWERMANGA", "PowerManga", "it")
|
||||
internal class PowerManga(context: MangaLoaderContext) :
|
||||
FoolSlideParser(context, MangaSource.POWERMANGA, "reader.powermanga.org") {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.foolslide.it
|
||||
|
||||
|
||||
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.foolslide.FoolSlideParser
|
||||
|
||||
|
||||
@MangaSourceParser("RAMAREADER", "RamaReader", "it")
|
||||
internal class Ramareader(context: MangaLoaderContext) :
|
||||
FoolSlideParser(context, MangaSource.RAMAREADER, "www.ramareader.it") {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.foolslide.it
|
||||
|
||||
|
||||
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.foolslide.FoolSlideParser
|
||||
|
||||
|
||||
@MangaSourceParser("READNIFTEAM", "ReadNifTeam", "it")
|
||||
internal class ReadNifteam(context: MangaLoaderContext) :
|
||||
FoolSlideParser(context, MangaSource.READNIFTEAM, "read-nifteam.info") {
|
||||
|
||||
@@ -24,39 +24,77 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
SortOrder.RATING,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.ALPHABETICAL,
|
||||
SortOrder.ALPHABETICAL_DESC,
|
||||
)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("bentomanga.com", "www.bentomanga.com")
|
||||
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
|
||||
|
||||
override val isTagsExclusionSupported: Boolean = true
|
||||
|
||||
init {
|
||||
paginator.firstPage = 0
|
||||
searchPaginator.firstPage = 0
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = urlBuilder()
|
||||
.host(domain)
|
||||
.addPathSegment("manga_list")
|
||||
.addQueryParameter("limit", page.toString())
|
||||
.addQueryParameter(
|
||||
"order_by",
|
||||
when (sortOrder) {
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.POPULARITY -> "views"
|
||||
SortOrder.RATING -> "top"
|
||||
SortOrder.NEWEST -> "create"
|
||||
SortOrder.ALPHABETICAL -> "name"
|
||||
},
|
||||
)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
url.addQueryParameter("withCategories", tags.joinToString(",") { it.key })
|
||||
}
|
||||
if (!query.isNullOrEmpty()) {
|
||||
url.addQueryParameter("search", query)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
url.addQueryParameter("search", filter.query)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> url.addQueryParameter("order_by", "update")
|
||||
.addQueryParameter("order", "desc")
|
||||
|
||||
SortOrder.POPULARITY -> url.addQueryParameter("order_by", "views")
|
||||
.addQueryParameter("order", "desc")
|
||||
|
||||
SortOrder.RATING -> url.addQueryParameter("order_by", "top")
|
||||
.addQueryParameter("order", "desc")
|
||||
|
||||
SortOrder.NEWEST -> url.addQueryParameter("order_by", "create")
|
||||
.addQueryParameter("order", "desc")
|
||||
|
||||
SortOrder.ALPHABETICAL -> url.addQueryParameter("order_by", "name")
|
||||
.addQueryParameter("order", "asc")
|
||||
|
||||
SortOrder.ALPHABETICAL_DESC -> url.addQueryParameter("order_by", "name")
|
||||
.addQueryParameter("order", "desc")
|
||||
}
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
url.addQueryParameter("withCategories", filter.tags.joinToString(",") { it.key })
|
||||
}
|
||||
|
||||
if (filter.tagsExclude.isNotEmpty()) {
|
||||
url.addQueryParameter("withoutCategories", filter.tagsExclude.joinToString(",") { it.key })
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
url.addQueryParameter(
|
||||
"state",
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "1"
|
||||
MangaState.FINISHED -> "2"
|
||||
MangaState.PAUSED -> "3"
|
||||
MangaState.ABANDONED -> "5"
|
||||
else -> "1"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
null -> url.addQueryParameter("order_by", "update")
|
||||
}
|
||||
val root = webClient.httpGet(url.build()).parseHtml().requireElementById("mangas_content")
|
||||
return root.select(".manga[data-manga]").map { div ->
|
||||
@@ -109,7 +147,7 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
|
||||
"En pause" -> MangaState.PAUSED
|
||||
else -> null
|
||||
},
|
||||
author = root.selectFirst(".datas_more-authors-people")?.textOrNull().assertNotNull("author"),
|
||||
author = root.selectFirst(".datas_more-authors-people")?.textOrNull(),
|
||||
chapters = run {
|
||||
val input = root.selectFirst("input[name=\"limit\"]") ?: return@run parseChapters(root)
|
||||
val max = input.attr("max").toInt()
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.site.fr
|
||||
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
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.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("FMTEAM", "FmTeam", "fr")
|
||||
internal class FmTeam(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.FMTEAM, 0) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
|
||||
override val configKeyDomain = ConfigKey.Domain("fmteam.fr")
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val jsonManga = if (!query.isNullOrEmpty()) {
|
||||
//3 letters minimum
|
||||
webClient.httpGet("https://$domain/api/search/${query.urlEncoded()}").parseJson().getJSONArray("comics")
|
||||
} else {
|
||||
webClient.httpGet("https://$domain/api/comics").parseJson().getJSONArray("comics")
|
||||
}
|
||||
|
||||
val manga = ArrayList<Manga>(jsonManga.length())
|
||||
for (i in 0 until jsonManga.length()) {
|
||||
val j = jsonManga.getJSONObject(i)
|
||||
val href = "/api" + j.getString("url")
|
||||
when {
|
||||
!tags.isNullOrEmpty() -> {
|
||||
val a = j.getJSONArray("genres").toString()
|
||||
var found = true
|
||||
tags.forEach {
|
||||
if (!a.contains(it.key, ignoreCase = true)) {
|
||||
found = false
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
manga.add(
|
||||
addManga(href, j),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
manga.add(
|
||||
addManga(href, j),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun addManga(href: String, j: JSONObject): Manga {
|
||||
return Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href.toAbsoluteUrl(domain),
|
||||
coverUrl = j.getString("thumbnail"),
|
||||
title = j.getString("title"),
|
||||
description = j.getString("description"),
|
||||
altTitle = j.getJSONArray("alt_titles").toString()
|
||||
.replace("[\"", "")
|
||||
.replace("\"]", "")
|
||||
.replace("\",\"", " , "),
|
||||
rating = j.getString("rating").toFloatOrNull()?.div(10f)
|
||||
?: RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
author = j.getString("author"),
|
||||
state = when (j.getString("status").lowercase()) {
|
||||
"en cours" -> MangaState.ONGOING
|
||||
"terminé" -> MangaState.FINISHED
|
||||
else -> null
|
||||
},
|
||||
source = source,
|
||||
isNsfw = when (j.getString("adult").toInt()) {
|
||||
0 -> false
|
||||
1 -> true
|
||||
else -> true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val json = webClient.httpGet(fullUrl).parseJson().getJSONObject("comic")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
val chapters = JSONArray(json.getJSONArray("chapters").toJSONList().reversed())
|
||||
|
||||
manga.copy(
|
||||
tags = json.getJSONArray("genres").toJSONList().mapNotNullToSet {
|
||||
MangaTag(
|
||||
key = it.getString("slug"),
|
||||
title = it.getString("name"),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
chapters = chapters.mapJSONIndexed { i, j ->
|
||||
val url = "/api" + j.getString("url").toRelativeUrl(domain)
|
||||
val name = j.getString("full_title")
|
||||
val date = j.getStringOrNull("updated_at")
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = name,
|
||||
number = i + 1,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = dateFormat.tryParse(date),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val jsonPages = webClient.httpGet(fullUrl).parseJson().getJSONObject("chapter").getJSONArray("pages").toString()
|
||||
val pages = jsonPages.replace("[", "").replace("]", "")
|
||||
.replace("\\", "").split("\",\"").drop(1)
|
||||
return pages.map { url ->
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.parsers.site.fr
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.Headers
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
@@ -22,6 +23,8 @@ internal class FuryoSociety(context: MangaLoaderContext) :
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("furyosociety.com")
|
||||
|
||||
override val isSearchSupported = false
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
@@ -35,21 +38,27 @@ internal class FuryoSociety(context: MangaLoaderContext) :
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (page == 1) {
|
||||
if (sortOrder == SortOrder.ALPHABETICAL) {
|
||||
append("/mangas")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
|
||||
}
|
||||
} else {
|
||||
return emptyList()
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
|
||||
append("/mangas")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.parsers.site.fr
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
@@ -16,9 +17,10 @@ import java.util.*
|
||||
internal class LegacyScansParser(context: MangaLoaderContext) :
|
||||
PagedMangaParser(context, MangaSource.LEGACY_SCANS, 18) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.POPULARITY,
|
||||
)
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
|
||||
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.PAUSED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("legacy-scans.com")
|
||||
|
||||
@@ -26,86 +28,108 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
|
||||
.add("User-Agent", UserAgents.CHROME_MOBILE)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val end = page * pageSize
|
||||
val start = end - (pageSize - 1)
|
||||
val url = if (!query.isNullOrEmpty()) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
buildString {
|
||||
append("https://api.$domain/misc/home/search?title=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
} else {
|
||||
buildString {
|
||||
append("https://api.$domain/misc/comic/search/query?status=&order=&genreNames=")
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
for (tag in tags) {
|
||||
append(tag.key)
|
||||
append(',')
|
||||
}
|
||||
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
append("&type=&start=")
|
||||
append(start)
|
||||
append("&end=")
|
||||
append(end)
|
||||
val url = buildString {
|
||||
append("https://api.$domain/misc/home/search?title=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
return parseMangaListQuery(webClient.httpGet(url).parseJson())
|
||||
}
|
||||
|
||||
}
|
||||
val json = webClient.httpGet(url).parseJson()
|
||||
return if (!query.isNullOrEmpty()) {
|
||||
json.getJSONArray("results").mapJSON { j ->
|
||||
val slug = j.getString("slug")
|
||||
val urlManga = "https://$domain/comics/$slug"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = "",
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
is MangaListFilter.Advanced -> {
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domain)
|
||||
append("/misc/comic/search/query?status=")
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "En cours"
|
||||
MangaState.FINISHED -> "Terminé"
|
||||
MangaState.ABANDONED -> "Abandonné"
|
||||
MangaState.PAUSED -> "En pause"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
append("&order=&genreNames=")
|
||||
append(filter.tags.joinToString(",") { it.key })
|
||||
append("&type=&start=")
|
||||
append(start)
|
||||
append("&end=")
|
||||
append(end)
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseJson())
|
||||
}
|
||||
} else {
|
||||
json.getJSONArray("comics").mapJSON { j ->
|
||||
val slug = j.getString("slug")
|
||||
val urlManga = "https://$domain/comics/$slug"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = "https://api.$domain/" + j.getString("cover"),
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domain)
|
||||
append("/misc/comic/search/query?status=&order=&genreNames=&type=&start=")
|
||||
append(start)
|
||||
append("&end=")
|
||||
append(end)
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseJson())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun parseMangaList(json: JSONObject): List<Manga> {
|
||||
return json.getJSONArray("comics").mapJSON { j ->
|
||||
val slug = j.getString("slug")
|
||||
val urlManga = "https://$domain/comics/$slug"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = "https://api.$domain/" + j.getString("cover"),
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMangaListQuery(json: JSONObject): List<Manga> {
|
||||
return json.getJSONArray("results").mapJSON { j ->
|
||||
val slug = j.getString("slug")
|
||||
val urlManga = "https://$domain/comics/$slug"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = "",
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.FRENCH)
|
||||
|
||||
return manga.copy(
|
||||
altTitle = null,
|
||||
tags = root.select("div.serieGenre span").mapNotNullToSet { span ->
|
||||
@@ -156,7 +180,6 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
|
||||
val script = doc.requireElementById("__NUXT_DATA__").data()
|
||||
.substringAfterLast("\"genres\"").substringBeforeLast("\"comics\"")
|
||||
.split("\",\"").drop(1)
|
||||
|
||||
return script.mapNotNullToSet { tag ->
|
||||
MangaTag(
|
||||
key = tag.substringBeforeLast("\",{"),
|
||||
|
||||
@@ -24,36 +24,50 @@ internal class LireScan(context: MangaLoaderContext) : PagedMangaParser(context,
|
||||
.add("User-Agent", UserAgents.CHROME_MOBILE)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val doc =
|
||||
if (!query.isNullOrEmpty()) { // search only works with 4 or more letters
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val q = query.urlEncoded().replace("%20", "+")
|
||||
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
|
||||
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
|
||||
} else {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/manga/")
|
||||
append(tag?.key.orEmpty())
|
||||
}
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
append('/')
|
||||
return emptyList()
|
||||
}
|
||||
val q = filter.query.urlEncoded().replace("%20", "+")
|
||||
val post = "do=search&subaction=search&search_start=0&full_search=0&result_from=1&story=$q"
|
||||
webClient.httpPost("https://$domain/index.php?do=search", post).parseHtml()
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/manga/")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
append('/')
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
append('/')
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
}
|
||||
|
||||
return doc.select("div.sect__content.grid-items div.item-poster").map { div ->
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.koitharu.kotatsu.parsers.site.fr
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
@@ -9,7 +11,6 @@ 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.mapJSON
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@@ -21,6 +22,11 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
|
||||
SortOrder.UPDATED,
|
||||
)
|
||||
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
|
||||
|
||||
override val isSearchSupported = false
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("lugnica-scans.com")
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
@@ -43,77 +49,103 @@ internal class LugnicaScans(context: MangaLoaderContext) : PagedMangaParser(cont
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
if (!query.isNullOrEmpty()) {
|
||||
throw IllegalArgumentException("Search is not supported by this source")
|
||||
}
|
||||
if (sortOrder == SortOrder.ALPHABETICAL) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/api/get/catalog?page=0&filter=all")
|
||||
}
|
||||
val json = webClient.httpGet(url).parseJsonArray()
|
||||
return json.mapJSON { j ->
|
||||
val urlManga = "https://$domain/api/get/card/${j.getString("slug")}"
|
||||
val img = "https://$domain/upload/min_cover/${j.getString("image")}"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga.toAbsoluteUrl(domain),
|
||||
rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = img,
|
||||
tags = setOf(),
|
||||
state = when (j.getString("status")) {
|
||||
"0" -> MangaState.ONGOING
|
||||
"1" -> MangaState.FINISHED
|
||||
"3" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/api/get/homegrid/")
|
||||
append(page)
|
||||
}
|
||||
val json = webClient.httpGet(url).parseJsonArray()
|
||||
return json.mapJSON { j ->
|
||||
val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}"
|
||||
val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("manga_title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga.toAbsoluteUrl(domain),
|
||||
rating = j.getString("manga_rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = img,
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
|
||||
}
|
||||
|
||||
}
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/api/get/catalog?page=0&filter=")
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
when (it) {
|
||||
MangaState.ONGOING -> append("0")
|
||||
MangaState.FINISHED -> append("1")
|
||||
MangaState.PAUSED -> append("4")
|
||||
MangaState.ABANDONED -> append("3")
|
||||
else -> append("")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return parseMangaListAlpha(webClient.httpGet(url).parseJsonArray())
|
||||
} else {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/api/get/homegrid/")
|
||||
append(page)
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseJsonArray())
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/api/get/homegrid/")
|
||||
append(page)
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseJsonArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMangaList(json: JSONArray): List<Manga> {
|
||||
return json.mapJSON { j ->
|
||||
val urlManga = "https://$domain/api/get/card/${j.getString("manga_slug")}"
|
||||
val img = "https://$domain/upload/min_cover/${j.getString("manga_image")}"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("manga_title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga.toAbsoluteUrl(domain),
|
||||
rating = j.getString("manga_rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = img,
|
||||
tags = setOf(),
|
||||
state = null,
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMangaListAlpha(json: JSONArray): List<Manga> {
|
||||
return json.mapJSON { j ->
|
||||
val urlManga = "https://$domain/api/get/card/${j.getString("slug")}"
|
||||
val img = "https://$domain/upload/min_cover/${j.getString("image")}"
|
||||
Manga(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
publicUrl = urlManga.toAbsoluteUrl(domain),
|
||||
rating = j.getString("rate").toFloatOrNull()?.div(5f) ?: RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
coverUrl = img,
|
||||
tags = setOf(),
|
||||
state = when (j.getString("status")) {
|
||||
"0" -> MangaState.ONGOING
|
||||
"1" -> MangaState.FINISHED
|
||||
"3" -> MangaState.ABANDONED
|
||||
else -> null
|
||||
},
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
|
||||
@@ -30,46 +30,45 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
if (page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (page == 1) {
|
||||
if (!query.isNullOrEmpty()) {
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/?s=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
append("&post_type=manga")
|
||||
}
|
||||
|
||||
} else if (!tags.isNullOrEmpty()) {
|
||||
append("/genres/")
|
||||
for (tag in tags) {
|
||||
append(tag.key)
|
||||
}
|
||||
} else {
|
||||
append("/tous-nos-mangas/")
|
||||
append("?order=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("popular")
|
||||
SortOrder.UPDATED -> append("update")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
SortOrder.NEWEST -> append("create")
|
||||
else -> append("update")
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("/genres/")
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append(it.key)
|
||||
}
|
||||
} else {
|
||||
append("/tous-nos-mangas/?order=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("popular")
|
||||
SortOrder.UPDATED -> append("update")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
SortOrder.NEWEST -> append("create")
|
||||
else -> append("update")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
null -> append("/tous-nos-mangas/?order=update")
|
||||
}
|
||||
}
|
||||
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
|
||||
return doc.select("div.postbody .bs .bsx").map { div ->
|
||||
val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
@@ -90,7 +89,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/tous-nos-mangas/").parseHtml()
|
||||
return doc.select("ul.genre li").mapNotNullToSet { li ->
|
||||
@@ -104,25 +102,18 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val chaptersDeferred = getChapters(doc)
|
||||
|
||||
val desc = doc.selectFirstOrThrow("div.desc").html()
|
||||
|
||||
val state = if (doc.select("div.spe span:contains(En cours)").isNullOrEmpty()) {
|
||||
MangaState.FINISHED
|
||||
} else {
|
||||
MangaState.ONGOING
|
||||
}
|
||||
|
||||
val alt = doc.body().select("div.infox span.alter").text()
|
||||
|
||||
val aut = doc.select("div.spe span")[2].text().replace("Auteur:", "")
|
||||
|
||||
manga.copy(
|
||||
tags = doc.select("div.spe span:contains(Genres) a").mapNotNullToSet { a ->
|
||||
MangaTag(
|
||||
@@ -140,7 +131,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun getChapters(doc: Document): List<MangaChapter> {
|
||||
return doc.body().requireElementById("chapter_list").select("li").mapChapters(reversed = true) { i, li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
@@ -161,10 +151,8 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
|
||||
val script = doc.selectFirstOrThrow("script:containsData(page_image)")
|
||||
val images = JSONArray(script.data().substringAfterLast("var pages = ").substringBefore(';'))
|
||||
|
||||
val pages = ArrayList<MangaPage>(images.length())
|
||||
for (i in 0 until images.length()) {
|
||||
val pageTake = images.getJSONObject(i)
|
||||
@@ -177,7 +165,6 @@ internal class ScansMangasMe(context: MangaLoaderContext) :
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,49 +20,54 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
|
||||
SortOrder.UPDATED,
|
||||
)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("scantrad-union.com")
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when {
|
||||
!query.isNullOrEmpty() -> {
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
append("/?s=")
|
||||
append(query.urlEncoded())
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
!tags.isNullOrEmpty() -> {
|
||||
append("/tag/")
|
||||
for (tag in tags) {
|
||||
append(tag.key)
|
||||
append(',')
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tag/")
|
||||
append(it.key)
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
}
|
||||
} else {
|
||||
if (filter.sortOrder == SortOrder.ALPHABETICAL) {
|
||||
append("/manga/page/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
}
|
||||
|
||||
if (filter.sortOrder == SortOrder.UPDATED && page > 1) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
}
|
||||
append("/page/")
|
||||
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/manga/page/")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (sortOrder == SortOrder.ALPHABETICAL) {
|
||||
append("/manga/")
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
if (sortOrder == SortOrder.UPDATED) {
|
||||
append("")
|
||||
}
|
||||
append("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,12 +182,10 @@ internal class ScantradUnion(context: MangaLoaderContext) : PagedMangaParser(con
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/").parseHtml()
|
||||
val body = doc.body()
|
||||
val root = body.select(".asp_gochosen")[1]
|
||||
val list = root?.select("option").orEmpty()
|
||||
val list = body.select(".asp_gochosen")[1].select("option").orEmpty()
|
||||
return list.mapToSet { li ->
|
||||
|
||||
MangaTag(
|
||||
key = li.text(),
|
||||
key = li.text().lowercase().replace(" ", "-"),
|
||||
title = li.text(),
|
||||
source = source,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
@@ -24,36 +25,42 @@ internal abstract class GalleryAdultsParser(
|
||||
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
if (tag?.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
append("/?")
|
||||
} else {
|
||||
append("/tag/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/?")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search/?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&")
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("/search/?q=")
|
||||
append(query.urlEncoded())
|
||||
append("&")
|
||||
} else {
|
||||
append("/?")
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
val tag = filter.tags.oneOrThrowIfMany()
|
||||
val lang = filter.locale
|
||||
if (tag != null && lang != null) {
|
||||
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED)
|
||||
}
|
||||
if (tag != null) {
|
||||
append("/tag/")
|
||||
append(tag.key)
|
||||
append("/?")
|
||||
} else if (filter.locale != null) {
|
||||
append("/language/")
|
||||
append(filter.locale.toLanguagePath())
|
||||
append("/?")
|
||||
} else {
|
||||
append("/?")
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("/?")
|
||||
}
|
||||
append("page=")
|
||||
append(page)
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
@@ -96,39 +103,30 @@ internal abstract class GalleryAdultsParser(
|
||||
}.awaitAll().flattenTo(ArraySet(360))
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.FRENCH,
|
||||
Locale.JAPANESE,
|
||||
Locale.CHINESE,
|
||||
Locale("es"),
|
||||
Locale("ru"),
|
||||
Locale("ko"),
|
||||
Locale.GERMAN,
|
||||
Locale("id"),
|
||||
Locale.ITALIAN,
|
||||
Locale("pt"),
|
||||
Locale("tr"),
|
||||
Locale("th"),
|
||||
Locale("vi"),
|
||||
)
|
||||
|
||||
protected open val pathTagUrl = "/tags/popular/?page="
|
||||
protected open val selectTags = ".tags_page ul.tags li"
|
||||
protected open val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/french",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
"/spanish",
|
||||
"/russian",
|
||||
"/korean",
|
||||
"/german",
|
||||
"/indonesian",
|
||||
"/italian",
|
||||
"/portuguese",
|
||||
"/turkish",
|
||||
"/thai",
|
||||
"/vietnamese",
|
||||
) // The "/" is used to move them up in the tag list and therefore also in the url.
|
||||
|
||||
private suspend fun getTags(page: Int): Set<MangaTag> {
|
||||
val url = "https://$domain$pathTagUrl$page"
|
||||
val root = webClient.httpGet(url).parseHtml().selectFirstOrThrow(selectTags)
|
||||
val tagLanguage = ArrayList<MangaTag>(listLanguage.size)
|
||||
for (language in listLanguage) {
|
||||
tagLanguage.add(
|
||||
MangaTag(
|
||||
key = "languageKey",
|
||||
title = language,
|
||||
source = source,
|
||||
),
|
||||
)
|
||||
}
|
||||
return root.parseTags() + tagLanguage
|
||||
return root.parseTags()
|
||||
}
|
||||
|
||||
protected open fun Element.parseTags() = select("a").mapToSet {
|
||||
@@ -136,7 +134,7 @@ internal abstract class GalleryAdultsParser(
|
||||
val name = it.html().substringBefore("<")
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
title = name.toTitleCase(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
@@ -150,6 +148,9 @@ internal abstract class GalleryAdultsParser(
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val urlChapters = doc.selectFirstOrThrow(selectUrlChapter).attr("href")
|
||||
val tag = doc.selectFirst(selectTag)?.parseTags()
|
||||
val branch = doc.select(selectLanguageChapter).joinToString(separator = " / ") {
|
||||
it.html().substringBefore("<")
|
||||
}
|
||||
return manga.copy(
|
||||
tags = tag.orEmpty(),
|
||||
author = doc.selectFirst(selectAuthor)?.html()?.substringBefore("<span"),
|
||||
@@ -161,7 +162,7 @@ internal abstract class GalleryAdultsParser(
|
||||
url = urlChapters,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = doc.selectFirst(selectLanguageChapter)?.html()?.substringBefore("<"),
|
||||
branch = branch,
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
@@ -195,4 +196,8 @@ internal abstract class GalleryAdultsParser(
|
||||
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.requireElementById(idImg).src() ?: doc.parseFailed("Image src not found")
|
||||
}
|
||||
|
||||
protected open fun Locale.toLanguagePath() = when (language) {
|
||||
else -> getDisplayLanguage(Locale.ENGLISH).lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.removeSuffix
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("ASMHENTAI", "AsmHentai", type = ContentType.HENTAI)
|
||||
internal class AsmHentai(context: MangaLoaderContext) :
|
||||
@@ -17,11 +20,12 @@ internal class AsmHentai(context: MangaLoaderContext) :
|
||||
override val pathTagUrl = "/tags/?page="
|
||||
override val selectAuthor = "div.tags:contains(Artists:) .tag_list a span.tag"
|
||||
override val idImg = "fimg"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
"/turkish",
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.JAPANESE,
|
||||
Locale.CHINESE,
|
||||
Locale("tr"),
|
||||
)
|
||||
|
||||
override fun Element.parseTags() = select("a").mapToSet {
|
||||
|
||||
@@ -6,10 +6,11 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("DOUJINDESUUK", "DoujinDesu.uk", type = ContentType.HENTAI)
|
||||
internal class DoujinDesuUk(context: MangaLoaderContext) :
|
||||
GalleryAdultsParser(context, MangaSource.DOUJINDESUUK, "doujindesu.uk", 50) {
|
||||
GalleryAdultsParser(context, MangaSource.DOUJINDESUUK, "doujindesu.uk", 25) {
|
||||
override val selectGallery = ".gallery"
|
||||
override val selectGalleryLink = "a"
|
||||
override val selectGalleryTitle = ".caption"
|
||||
@@ -19,10 +20,11 @@ internal class DoujinDesuUk(context: MangaLoaderContext) :
|
||||
override val selectAuthor = "div.tag-container:contains(Artists) a"
|
||||
override val selectLanguageChapter = "div.tag-container:contains(Languages) a"
|
||||
override val idImg = "image-container"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.JAPANESE,
|
||||
Locale.CHINESE,
|
||||
)
|
||||
|
||||
override fun parseMangaList(doc: Document): List<Manga> {
|
||||
|
||||
@@ -1,65 +1,92 @@
|
||||
package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
|
||||
import org.jsoup.internal.StringUtil
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("HENTAI3", "3Hentai", type = ContentType.HENTAI)
|
||||
internal class Hentai3(context: MangaLoaderContext) :
|
||||
GalleryAdultsParser(context, MangaSource.HENTAI3, "3hentai.net") {
|
||||
GalleryAdultsParser(context, MangaSource.HENTAI3, "3hentai.net", pageSize = 30) {
|
||||
|
||||
override val selectGallery = ".doujin "
|
||||
override val selectGalleryLink = "a"
|
||||
override val selectGalleryTitle = ".title"
|
||||
override val pathTagUrl = "/tags-popular/"
|
||||
override val selectTags = "span.filter-elem"
|
||||
override val selectTag = "div.tag-container:contains(Tags :) .filter-elem"
|
||||
override val selectAuthor = "div.tag-container:contains(Artistes :) .filter-elem"
|
||||
override val selectLanguageChapter = "div.tag-container:contains(Langues :) .filter-elem"
|
||||
override val selectTags = "div.tag-listing-container"
|
||||
override val selectTag = "div.tag-container:contains(Tags)"
|
||||
override val selectAuthor = "div.tag-container:contains(Artists) .filter-elem a"
|
||||
override val selectLanguageChapter = "div.tag-container:contains(Languages) a"
|
||||
override val selectUrlChapter = "#main-cover a"
|
||||
override val idImg = ".js-main-img"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/spanish",
|
||||
"/french",
|
||||
"/italian",
|
||||
"/portuguese",
|
||||
"/russian",
|
||||
"/japanese",
|
||||
|
||||
override val isMultipleTagsSupported = true
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.FRENCH,
|
||||
Locale.JAPANESE,
|
||||
Locale("es"),
|
||||
Locale("ru"),
|
||||
Locale.ITALIAN,
|
||||
Locale("pt"),
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
if (tag?.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
append("/")
|
||||
append(page)
|
||||
} else {
|
||||
append("/tags/")
|
||||
append(tag?.key.orEmpty())
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
|
||||
append("/search?q=")
|
||||
append(buildQuery(filter.tags, filter.locale))
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("&sort=popular")
|
||||
}
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
} else if (filter.locale != null) {
|
||||
append("/language/")
|
||||
append(filter.locale.toLanguagePath())
|
||||
append("/")
|
||||
append(page.toString())
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("?sort=popular")
|
||||
}
|
||||
} else if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tags/")
|
||||
append(it.key)
|
||||
}
|
||||
append("/")
|
||||
append(page.toString())
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("?sort=popular")
|
||||
}
|
||||
} else {
|
||||
append("/")
|
||||
append(page)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/")
|
||||
append(page)
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("/search/?q=")
|
||||
append(query.urlEncoded())
|
||||
append("&page=")
|
||||
append(page)
|
||||
} else {
|
||||
append("/")
|
||||
append(page)
|
||||
}
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
@@ -69,4 +96,19 @@ internal class Hentai3(context: MangaLoaderContext) :
|
||||
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found")
|
||||
}
|
||||
|
||||
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
|
||||
val joiner = StringUtil.StringJoiner(" ")
|
||||
tags.forEach { tag ->
|
||||
joiner.add("tag:\"")
|
||||
joiner.append(tag.key)
|
||||
joiner.append("\"")
|
||||
}
|
||||
language?.let { lc ->
|
||||
joiner.add("language:\"")
|
||||
joiner.append(lc.toLanguagePath())
|
||||
joiner.append("\"")
|
||||
}
|
||||
return joiner.complete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
@@ -8,10 +9,11 @@ import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.oneOrThrowIfMany
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("HENTAIENVY", "HentaiEnvy", type = ContentType.HENTAI)
|
||||
internal class HentaiEnvy(context: MangaLoaderContext) :
|
||||
GalleryAdultsParser(context, MangaSource.HENTAIENVY, "hentaienvy.com") {
|
||||
GalleryAdultsParser(context, MangaSource.HENTAIENVY, "hentaienvy.com", pageSize = 24) {
|
||||
override val selectGalleryLink = "a"
|
||||
override val selectGalleryTitle = "div.title"
|
||||
override val selectTags = ".tags_items"
|
||||
@@ -19,48 +21,60 @@ internal class HentaiEnvy(context: MangaLoaderContext) :
|
||||
override val selectAuthor = ".gt_right_tags ul:contains(Artists:) a"
|
||||
override val selectLanguageChapter = ".gt_right_tags ul:contains(Languages:) a"
|
||||
override val idImg = "fimg"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/french",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
"/spanish",
|
||||
"/russian",
|
||||
"/korean",
|
||||
"/german",
|
||||
"/portuguese",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
if (tag?.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
append("/?")
|
||||
} else {
|
||||
append("/tag/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/?")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search/?s_key=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&")
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("/search/?s_key=")
|
||||
append(query.urlEncoded())
|
||||
append("&")
|
||||
} else {
|
||||
append("/?")
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
if (filter.locale != null) {
|
||||
throw IllegalArgumentException(ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED)
|
||||
}
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tag/")
|
||||
append(it.key)
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("/popular")
|
||||
}
|
||||
append("/?")
|
||||
}
|
||||
} else if (filter.locale != null) {
|
||||
append("/language/")
|
||||
append(filter.locale.toLanguagePath())
|
||||
append("/?")
|
||||
} else {
|
||||
append("/?")
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("/?")
|
||||
}
|
||||
append("page=")
|
||||
append(page)
|
||||
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.FRENCH,
|
||||
Locale.JAPANESE,
|
||||
Locale.CHINESE,
|
||||
Locale("es"),
|
||||
Locale("ru"),
|
||||
Locale("ko"),
|
||||
Locale.GERMAN,
|
||||
Locale("pt"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("HENTAIERA", "HentaiEra", type = ContentType.HENTAI)
|
||||
internal class HentaiEra(context: MangaLoaderContext) :
|
||||
@@ -14,14 +15,20 @@ internal class HentaiEra(context: MangaLoaderContext) :
|
||||
override val selectTag = ".galleries_info li:contains(Tags) div.info_tags"
|
||||
override val selectAuthor = ".galleries_info li:contains(Artists) span.item_name"
|
||||
override val selectLanguageChapter = ".galleries_info li:contains(Languages) div.info_tags .item_name"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/japanese",
|
||||
"/spanish",
|
||||
"/french",
|
||||
"/korean",
|
||||
"/german",
|
||||
"/russian",
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
|
||||
override val isMultipleTagsSupported = true
|
||||
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.FRENCH,
|
||||
Locale.JAPANESE,
|
||||
Locale("es"),
|
||||
Locale("ru"),
|
||||
Locale("ko"),
|
||||
Locale.GERMAN,
|
||||
)
|
||||
|
||||
override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {
|
||||
@@ -34,43 +41,89 @@ internal class HentaiEra(context: MangaLoaderContext) :
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
if (tag?.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
append("/?")
|
||||
} else {
|
||||
append("/tag/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/?")
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search/?key=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&")
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("/search/?key=")
|
||||
append(query.urlEncoded())
|
||||
append("&")
|
||||
} else {
|
||||
append("/?")
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
|
||||
append("/search/?key=")
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append(
|
||||
buildQuery(filter.tags, filter.locale)
|
||||
.replace("<=1&dl=0&pp=0&tr=0", "<=0&dl=0&pp=1&tr=0"),
|
||||
)
|
||||
} else {
|
||||
append(buildQuery(filter.tags, filter.locale))
|
||||
}
|
||||
append("&")
|
||||
} else if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tag/")
|
||||
append(it.key)
|
||||
}
|
||||
append("/")
|
||||
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
append("?")
|
||||
} else if (filter.locale != null) {
|
||||
append("/language/")
|
||||
append(filter.locale.toLanguagePath())
|
||||
append("/")
|
||||
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
append("?")
|
||||
} else {
|
||||
append("/?")
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("/?")
|
||||
}
|
||||
append("page=")
|
||||
append(page)
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
private fun buildQuery(tags: Collection<MangaTag>, locale: Locale?): String {
|
||||
val queryDefault =
|
||||
"&search=&mg=1&dj=1&ws=1&is=1&ac=1&gc=1&en=0&jp=0&es=0&fr=0&kr=0&de=0&ru=0<=1&dl=0&pp=0&tr=0"
|
||||
val tag = tags.joinToString(" ", postfix = " ") { it.key }
|
||||
val queryMod = when (val lp = locale?.toLanguagePath()) {
|
||||
"english" -> queryDefault.replace("en=0", "en=1")
|
||||
"japanese" -> queryDefault.replace("jp=0", "jp=1")
|
||||
"spanish" -> queryDefault.replace("es=0", "es=1")
|
||||
"french" -> queryDefault.replace("fr=0", "fr=1")
|
||||
"korean" -> queryDefault.replace("kr=0", "kr=1")
|
||||
"russian" -> queryDefault.replace("ru=0", "ru=1")
|
||||
"german" -> queryDefault.replace("de=0", "de=1")
|
||||
null -> "&search=&mg=1&dj=1&ws=1&is=1&ac=1&gc=1&en=1&jp=1&es=1&fr=1&kr=1&de=1&ru=1<=1&dl=0&pp=0&tr=0"
|
||||
else -> throw IllegalArgumentException("Unsupported locale: $lp")
|
||||
}
|
||||
return tag + queryMod
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val urlChapters = doc.selectFirstOrThrow("#cover a, .cover a, .left_cover a").attr("href")
|
||||
val tag = doc.selectFirst(selectTag)?.parseTags()
|
||||
val branch = doc.select(selectLanguageChapter).joinToString(separator = " / ") {
|
||||
it.text()
|
||||
}
|
||||
return manga.copy(
|
||||
tags = tag.orEmpty(),
|
||||
author = doc.selectFirst(selectAuthor)?.text(),
|
||||
@@ -82,7 +135,7 @@ internal class HentaiEra(context: MangaLoaderContext) :
|
||||
url = urlChapters,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = doc.selectFirst(selectLanguageChapter)?.text(),
|
||||
branch = branch,
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
|
||||
import org.jsoup.internal.StringUtil
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("HENTAIFORCE", "HentaiForce", type = ContentType.HENTAI)
|
||||
internal class HentaiForce(context: MangaLoaderContext) :
|
||||
GalleryAdultsParser(context, MangaSource.HENTAIFORCE, "hentaiforce.net") {
|
||||
GalleryAdultsParser(context, MangaSource.HENTAIFORCE, "hentaiforce.net", pageSize = 30) {
|
||||
override val selectGallery = ".gallery"
|
||||
override val selectGalleryLink = "a.gallery-thumb"
|
||||
override val pathTagUrl = "/tags/popular/"
|
||||
@@ -18,20 +20,25 @@ internal class HentaiForce(context: MangaLoaderContext) :
|
||||
override val selectAuthor = "div.tag-container:contains(Artists:) a"
|
||||
override val selectLanguageChapter = "div.tag-container:contains(Languages:) a"
|
||||
override val idImg = ".gallery-reader-img-wrapper img"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/french",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
"/spanish",
|
||||
"/russian",
|
||||
"/korean",
|
||||
"/german",
|
||||
"/indonesian",
|
||||
"/italian",
|
||||
"/portuguese",
|
||||
"/thai",
|
||||
"/vietnamese",
|
||||
|
||||
override val isMultipleTagsSupported = true
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.FRENCH,
|
||||
Locale.JAPANESE,
|
||||
Locale.CHINESE,
|
||||
Locale("es"),
|
||||
Locale("ru"),
|
||||
Locale("ko"),
|
||||
Locale.GERMAN,
|
||||
Locale("id"),
|
||||
Locale.ITALIAN,
|
||||
Locale("pt"),
|
||||
Locale("th"),
|
||||
Locale("vi"),
|
||||
)
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String {
|
||||
@@ -39,35 +46,70 @@ internal class HentaiForce(context: MangaLoaderContext) :
|
||||
return doc.selectFirstOrThrow(idImg).src() ?: doc.parseFailed("Image src not found")
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
if (tag?.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
append("/")
|
||||
} else {
|
||||
append("/tag/")
|
||||
append(tag?.key.orEmpty())
|
||||
append("/")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&page=")
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("search?q=")
|
||||
append(query.urlEncoded())
|
||||
append("&page=")
|
||||
} else {
|
||||
append("/page/")
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
|
||||
append("/search?q=")
|
||||
append(buildQuery(filter.tags, filter.locale))
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("&sort=popular")
|
||||
}
|
||||
append("&page=")
|
||||
} else if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tag/")
|
||||
append(it.key)
|
||||
}
|
||||
append("/")
|
||||
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
append("?")
|
||||
} else if (filter.locale != null) {
|
||||
append("/language/")
|
||||
append(filter.locale.toLanguagePath())
|
||||
append("/")
|
||||
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
append("?")
|
||||
} else {
|
||||
append("/page/")
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("/page/")
|
||||
}
|
||||
append(page)
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
|
||||
val joiner = StringUtil.StringJoiner(" ")
|
||||
tags.forEach { tag ->
|
||||
joiner.add("tag:\"")
|
||||
joiner.append(tag.key)
|
||||
joiner.append("\"")
|
||||
}
|
||||
language?.let { lc ->
|
||||
joiner.add("language:\"")
|
||||
joiner.append(lc.toLanguagePath())
|
||||
joiner.append("\"")
|
||||
}
|
||||
return joiner.complete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
|
||||
import org.jsoup.internal.StringUtil
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("HENTAIFOX", "HentaiFox", type = ContentType.HENTAI)
|
||||
internal class HentaiFox(context: MangaLoaderContext) :
|
||||
@@ -14,65 +17,113 @@ internal class HentaiFox(context: MangaLoaderContext) :
|
||||
override val selectTags = ".list_tags"
|
||||
override val selectTag = "ul.tags"
|
||||
override val selectLanguageChapter = "ul.languages a.tag_btn"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/french",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
"/spanish",
|
||||
"/russian",
|
||||
"/korean",
|
||||
"/indonesian",
|
||||
"/italian",
|
||||
"/portuguese",
|
||||
"/turkish",
|
||||
"/thai",
|
||||
"/vietnamese",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override val isMultipleTagsSupported = true
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
if (tag?.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
} else {
|
||||
append("/tag/")
|
||||
append(tag?.key.orEmpty())
|
||||
}
|
||||
if (page > 1) {
|
||||
append("/pag/")
|
||||
append(page)
|
||||
append("/")
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search/?q=")
|
||||
append(filter.query.urlEncoded())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("/search/?q=")
|
||||
append(query.urlEncoded())
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page)
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
|
||||
append("/search/?q=")
|
||||
append(buildQuery(filter.tags, filter.locale))
|
||||
if (page > 1) {
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("&sort=popular")
|
||||
}
|
||||
} else if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tag/")
|
||||
append(it.key)
|
||||
}
|
||||
append("/")
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("/pag/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
}
|
||||
} else if (filter.locale != null) {
|
||||
append("/language/")
|
||||
append(filter.locale.toLanguagePath())
|
||||
append("/")
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("/pag/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
}
|
||||
} else {
|
||||
if (page > 2) {
|
||||
append("/pag/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
} else if (page > 1) {
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (page > 2) {
|
||||
append("/pag/")
|
||||
append(page)
|
||||
append("/")
|
||||
} else if (page > 1) {
|
||||
append("/page/")
|
||||
append(page)
|
||||
append("/")
|
||||
|
||||
null -> {
|
||||
if (page > 2) {
|
||||
append("/pag/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
} else if (page > 1) {
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
append("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override fun Element.parseTags() = select("a").mapToSet {
|
||||
val key = it.attr("href").removeSuffix('/').substringAfterLast('/')
|
||||
val name = it.selectFirst(".list_tag")?.text() ?: it.html().substringBefore("<")
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
|
||||
val joiner = StringUtil.StringJoiner(" ")
|
||||
tags.forEach { tag ->
|
||||
joiner.add(tag.key)
|
||||
}
|
||||
language?.let { lc ->
|
||||
joiner.add(lc.toLanguagePath())
|
||||
}
|
||||
return joiner.complete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.removeSuffix
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("HENTAIROX", "HentaiRox", type = ContentType.HENTAI)
|
||||
internal class HentaiRox(context: MangaLoaderContext) :
|
||||
@@ -16,14 +19,15 @@ internal class HentaiRox(context: MangaLoaderContext) :
|
||||
override val selectTag = "li:contains(Tags:)"
|
||||
override val selectAuthor = "li:contains(Artists:) span.item_name"
|
||||
override val selectLanguageChapter = "li:contains(Languages:) .item_name"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/french",
|
||||
"/japanese",
|
||||
"/spanish",
|
||||
"/russian",
|
||||
"/korean",
|
||||
"/german",
|
||||
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.FRENCH,
|
||||
Locale.JAPANESE,
|
||||
Locale("es"),
|
||||
Locale("ru"),
|
||||
Locale("ko"),
|
||||
Locale.GERMAN,
|
||||
)
|
||||
|
||||
override fun Element.parseTags() = select("a.tag, .gallery_title a").mapToSet {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
|
||||
import org.jsoup.internal.StringUtil
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("NHENTAI", "NHentai.net", type = ContentType.HENTAI)
|
||||
internal class NHentaiParser(context: MangaLoaderContext) :
|
||||
@@ -14,50 +16,71 @@ internal class NHentaiParser(context: MangaLoaderContext) :
|
||||
override val selectGalleryLink = "a"
|
||||
override val selectGalleryTitle = ".caption"
|
||||
override val pathTagUrl = "/tags/popular?page="
|
||||
override val selectTags = "#tag-container a"
|
||||
override val selectTags = "#tag-container"
|
||||
override val selectTag = ".tag-container:contains(Tags:) span.tags"
|
||||
override val selectAuthor = "#tags div.tag-container:contains(Artists:) span.name"
|
||||
override val selectLanguageChapter =
|
||||
".tag-container:contains(Languages:) span.tags a:not(.tag-17249) span.name" // tag-17249 = translated
|
||||
override val idImg = "image-container"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
if (query.isNullOrEmpty() && tags != null && tags.size > 1) {
|
||||
return getListPage(page, buildQuery(tags), emptySet(), sortOrder)
|
||||
}
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY)
|
||||
|
||||
override val isMultipleTagsSupported = true
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
val tag = tags.single()
|
||||
if (tag.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
append("/?")
|
||||
} else {
|
||||
append("/tag/")
|
||||
append(tag.key)
|
||||
append("/?")
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
append("/search/?q=pages:>0 ")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&")
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
append("/search/?q=")
|
||||
append(query.urlEncoded())
|
||||
append("&")
|
||||
} else {
|
||||
append("/?")
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
if (filter.tags.size > 1 || (filter.tags.isNotEmpty() && filter.locale != null)) {
|
||||
append("/search/?q=")
|
||||
append(buildQuery(filter.tags, filter.locale).urlEncoded())
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("&sort=popular")
|
||||
}
|
||||
append("&")
|
||||
} else if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/tag/")
|
||||
append(it.key)
|
||||
}
|
||||
append("/")
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
|
||||
append("?")
|
||||
} else if (filter.locale != null) {
|
||||
append("/language/")
|
||||
append(filter.locale.toLanguagePath())
|
||||
append("/")
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("popular/")
|
||||
}
|
||||
|
||||
append("?")
|
||||
} else {
|
||||
if (filter.sortOrder == SortOrder.POPULARITY) {
|
||||
append("/?sort=popular&")
|
||||
} else {
|
||||
append("/?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> append("/?")
|
||||
}
|
||||
append("page=")
|
||||
append(page)
|
||||
append(page.toString())
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
@@ -78,7 +101,24 @@ internal class NHentaiParser(context: MangaLoaderContext) :
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildQuery(tags: Collection<MangaTag>) = tags.joinToString(separator = " ") { tag ->
|
||||
"tag:\"${tag.key}\""
|
||||
override suspend fun getAvailableLocales(): Set<Locale> = setOf(
|
||||
Locale.ENGLISH,
|
||||
Locale.JAPANESE,
|
||||
Locale.CHINESE,
|
||||
)
|
||||
|
||||
private fun buildQuery(tags: Collection<MangaTag>, language: Locale?): String {
|
||||
val joiner = StringUtil.StringJoiner(" ")
|
||||
tags.forEach { tag ->
|
||||
joiner.add("tag:\"")
|
||||
joiner.append(tag.key)
|
||||
joiner.append("\"")
|
||||
}
|
||||
language?.let { lc ->
|
||||
joiner.add("language:\"")
|
||||
joiner.append(lc.toLanguagePath())
|
||||
joiner.append("\"")
|
||||
}
|
||||
return joiner.complete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.site.galleryadults.all
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.site.galleryadults.GalleryAdultsParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
@MangaSourceParser("NHENTAIUK", "NHentai.uk", type = ContentType.HENTAI)
|
||||
internal class NHentaiUk(context: MangaLoaderContext) :
|
||||
GalleryAdultsParser(context, MangaSource.NHENTAIUK, "nhentai.uk", 50) {
|
||||
override val selectGallery = ".gallery"
|
||||
override val selectGalleryLink = "a"
|
||||
override val selectGalleryTitle = ".caption"
|
||||
override val pathTagUrl = "/tags/popular?p="
|
||||
override val selectTags = "#tag-container"
|
||||
override val selectTag = "div.tag-container:contains(Tags:) span.tags"
|
||||
override val selectAuthor = "div.tag-container:contains(Artists:) a"
|
||||
override val selectLanguageChapter = "div.tag-container:contains(Languages:) a"
|
||||
override val idImg = "image-container"
|
||||
override val listLanguage = arrayOf(
|
||||
"/english",
|
||||
"/french",
|
||||
"/japanese",
|
||||
"/chinese",
|
||||
"/spanish",
|
||||
"/russian",
|
||||
"/korean",
|
||||
"/german",
|
||||
"/italian",
|
||||
"/portuguese",
|
||||
"/turkish",
|
||||
)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
if (tag?.key == "languageKey") {
|
||||
append("/language")
|
||||
append(tag.title)
|
||||
append("/?p=")
|
||||
} else {
|
||||
append("/tag/")
|
||||
append(tag?.key)
|
||||
append("/?p=")
|
||||
}
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
throw IllegalArgumentException("Search is not supported by this source")
|
||||
} else {
|
||||
append("/home?p=")
|
||||
}
|
||||
append(page)
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String {
|
||||
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val root = doc.body()
|
||||
return root.requireElementById(idImg).selectFirstOrThrow("img").src() ?: root.parseFailed("Image src not found")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.koitharu.kotatsu.parsers.site.gattsu
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
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 java.util.*
|
||||
|
||||
internal abstract class GattsuParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaSource,
|
||||
domain: String,
|
||||
pageSize: Int = 20,
|
||||
) : PagedMangaParser(context, source, pageSize) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
protected open val tagPrefix = "tag"
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
append("/?s=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/$tagPrefix/")
|
||||
append(it.key)
|
||||
}
|
||||
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("/page/")
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
}
|
||||
|
||||
protected open fun parseMangaList(doc: Document): List<Manga> {
|
||||
return doc.select("div.lista ul li, div.videos div.video").mapNotNull { li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
val href = a.attrAsAbsoluteUrl("href")
|
||||
if (!href.contains(domain)) {
|
||||
//Some sources include ads in manga lists
|
||||
return@mapNotNull null
|
||||
}
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href,
|
||||
title = li.selectLastOrThrow(".thumb-titulo, .video-titulo").text(),
|
||||
coverUrl = li.selectFirst("img")?.src().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
description = null,
|
||||
state = null,
|
||||
author = null,
|
||||
isNsfw = isNsfwSource,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected open val tagUrl = "generos"
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/$tagUrl/").parseHtml()
|
||||
return doc.selectLastOrThrow(".meio-conteudo p, div.lista-tags ul").parseTags()
|
||||
}
|
||||
|
||||
protected open fun Element.parseTags() = select("a").mapToSet {
|
||||
val key = it.attr("href").removeSuffix("/").substringAfterLast("/")
|
||||
val name = it.selectFirst(".tag-titulo")?.text() ?: key
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = name,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val urlChapter = doc.selectFirstOrThrow("ul.post-fotos li a, ul.paginaPostBotoes a").attrAsAbsoluteUrl("href")
|
||||
return manga.copy(
|
||||
description = doc.selectFirst("div.post-texto")?.html(),
|
||||
tags = doc.selectFirst(".post-itens li:contains(Tags), .paginaPostInfo li:contains(Categorias)")
|
||||
?.parseTags().orEmpty(),
|
||||
author = doc.selectFirst(".post-itens li:contains(Autor) a, .paginaPostInfo li:contains(Artista) a")
|
||||
?.text(),
|
||||
chapters = listOf(
|
||||
MangaChapter(
|
||||
id = manga.id,
|
||||
name = manga.title,
|
||||
number = 1,
|
||||
url = urlChapter,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val totalPages =
|
||||
doc.selectLastOrThrow("div.galeria-paginacao span").text().substringAfterLast("- ").substringBeforeLast(')')
|
||||
.toInt()
|
||||
val rawUrl = chapter.url.substringBeforeLast("=")
|
||||
return (1..totalPages).map {
|
||||
val url = "$rawUrl=$it"
|
||||
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()
|
||||
return doc.selectFirstOrThrow("div.galeria-foto img").src() ?: doc.parseFailed("Image src not found")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.parsers.site.gattsu.pt
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
|
||||
|
||||
@MangaSourceParser("HENTAISEASON", "HentaiSeason", "pt", ContentType.HENTAI)
|
||||
internal class HentaiSeason(context: MangaLoaderContext) :
|
||||
GattsuParser(context, MangaSource.HENTAISEASON, "hentaiseason.com")
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.parsers.site.gattsu.pt
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
|
||||
|
||||
@MangaSourceParser("HENTAITOKYO", "HentaiTokyo", "pt", ContentType.HENTAI)
|
||||
internal class HentaiTokyo(context: MangaLoaderContext) :
|
||||
GattsuParser(context, MangaSource.HENTAITOKYO, "hentaitokyo.net") {
|
||||
override val tagUrl = "tags"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.parsers.site.gattsu.pt
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
||||
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsAbsoluteUrl
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.selectLastOrThrow
|
||||
import org.koitharu.kotatsu.parsers.util.src
|
||||
|
||||
@MangaSourceParser("MUNDOHENTAIOFICIAL", "MundoHentaiOficial", "pt", ContentType.HENTAI)
|
||||
internal class MundoHentaiOficial(context: MangaLoaderContext) :
|
||||
GattsuParser(context, MangaSource.MUNDOHENTAIOFICIAL, "mundohentaioficial.com") {
|
||||
|
||||
override val tagUrl = "tags"
|
||||
|
||||
override fun parseMangaList(doc: Document): List<Manga> {
|
||||
return doc.select("div.lista ul li, div.videos div.video").mapNotNull { li ->
|
||||
val a = li.selectLastOrThrow("a")
|
||||
val href = a.attrAsAbsoluteUrl("href")
|
||||
if (!href.contains(domain)) {
|
||||
//Some sources include ads in manga lists
|
||||
return@mapNotNull null
|
||||
}
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
url = href,
|
||||
publicUrl = href,
|
||||
title = li.selectLastOrThrow(".thumb-titulo, .video-titulo").text(),
|
||||
coverUrl = li.selectFirst("img")?.src().orEmpty(),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
description = null,
|
||||
state = null,
|
||||
author = null,
|
||||
isNsfw = isNsfwSource,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.koitharu.kotatsu.parsers.site.gattsu.pt
|
||||
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.site.gattsu.GattsuParser
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
|
||||
@MangaSourceParser("UNIVERSOHENTAI", "UniversoHentai", "pt", ContentType.HENTAI)
|
||||
internal class UniversoHentai(context: MangaLoaderContext) :
|
||||
GattsuParser(context, MangaSource.UNIVERSOHENTAI, "universohentai.com") {
|
||||
|
||||
override val tagPrefix = "category"
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://$domain/tags/").parseHtml()
|
||||
return doc.requireElementById("menu-topo").parseTags()
|
||||
}
|
||||
|
||||
override fun Element.parseTags() = select("a").mapNotNullToSet {
|
||||
if (!it.attr("href").contains("/category/")) return@mapNotNullToSet null
|
||||
val key = it.attr("href").removeSuffix("/").substringAfterLast("/")
|
||||
MangaTag(
|
||||
key = key,
|
||||
title = it.text(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val images = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml().requireElementById("galeria")
|
||||
.select(".galeria-foto img")
|
||||
return images.map { img ->
|
||||
val urlImages = img.src() ?: img.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
id = generateUid(urlImages),
|
||||
url = urlImages,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.koitharu.kotatsu.parsers.site.guya
|
||||
|
||||
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.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
internal abstract class GuyaParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaSource,
|
||||
domain: String,
|
||||
pageSize: Int = 0,
|
||||
) : PagedMangaParser(context, source, pageSize) {
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
if (page > 1) return emptyList()
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/api/get_all_series/")
|
||||
}
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
return parseMangaList(webClient.httpGet(url).parseJson(), filter.query)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
|
||||
return parseMangaList(webClient.httpGet(url).parseJson(), "")
|
||||
}
|
||||
|
||||
protected open fun parseMangaList(json: JSONObject, query: String): List<Manga> {
|
||||
val manga = ArrayList<Manga>(json.length())
|
||||
val keys: Iterator<String> = json.keys()
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
if (json.get(key) is JSONObject) {
|
||||
if (query.isNotEmpty()) {
|
||||
if (key.lowercase().contains(query.lowercase())) manga.add(addManga(json.getJSONObject(key), key))
|
||||
} else manga.add(addManga(json.getJSONObject(key), key))
|
||||
}
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun addManga(j: JSONObject, name: String): Manga {
|
||||
val url = "https://$domain/read/manga/" + j.getString("slug")
|
||||
val apiUrl = "https://$domain/api/series/" + j.getString("slug")
|
||||
return Manga(
|
||||
id = generateUid(apiUrl),
|
||||
url = apiUrl,
|
||||
publicUrl = url,
|
||||
title = name,
|
||||
coverUrl = j.getString("cover").toAbsoluteUrl(domain),
|
||||
altTitle = null,
|
||||
rating = RATING_UNKNOWN,
|
||||
tags = emptySet(),
|
||||
description = j.getString("description"),
|
||||
state = null,
|
||||
author = j.getString("author"),
|
||||
isNsfw = isNsfwSource,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val json = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseJson().getJSONObject("chapters")
|
||||
val slug = manga.url.removeSuffix('/').substringAfterLast('/')
|
||||
val keys: Iterator<String> = json.keys()
|
||||
val chapters = ArrayList<MangaChapter>()
|
||||
var i = 0
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
++i
|
||||
val chapter = json.getJSONObject(key)
|
||||
val url = "https://$domain/api/series/$slug/$key"
|
||||
chapters.add(
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = chapter.getString("title"),
|
||||
number = i,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = 0,
|
||||
branch = null,
|
||||
source = source,
|
||||
),
|
||||
)
|
||||
}
|
||||
return manga.copy(chapters = chapters)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val key = chapter.url.substringAfterLast('/')
|
||||
val url = chapter.url.substringBeforeLast('/')
|
||||
val slug = url.substringAfterLast('/')
|
||||
val chapterPages = webClient.httpGet(url.toAbsoluteUrl(domain)).parseJson()
|
||||
.getJSONObject("chapters").getJSONObject(key)
|
||||
val images = chapterPages.getJSONObject("groups")
|
||||
val folder = chapterPages.getString("folder")
|
||||
val keysPages: Iterator<String> = images.keys()
|
||||
val firstKey = keysPages.next()
|
||||
val jsonPages = images.getJSONArray(firstKey)
|
||||
val pages = ArrayList<MangaPage>(jsonPages.length())
|
||||
for (i in 0 until jsonPages.length()) {
|
||||
val urlPage = "https://$domain/media/manga/$slug/chapters/$folder/$firstKey/" + jsonPages.getString(i)
|
||||
pages.add(
|
||||
MangaPage(
|
||||
id = generateUid(urlPage),
|
||||
url = urlPage,
|
||||
preview = null,
|
||||
source = source,
|
||||
),
|
||||
)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.guya.all
|
||||
|
||||
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.guya.GuyaParser
|
||||
|
||||
@MangaSourceParser("MAHOUSHOUJOBU", "MahouShoujobu")
|
||||
internal class MahouShoujobu(context: MangaLoaderContext) :
|
||||
GuyaParser(context, MangaSource.MAHOUSHOUJOBU, "mahoushoujobu.com")
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.guya.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.site.guya.GuyaParser
|
||||
|
||||
@MangaSourceParser("DANKE", "DankeFursLesen", "en")
|
||||
internal class Danke(context: MangaLoaderContext) :
|
||||
GuyaParser(context, MangaSource.DANKE, "danke.moe")
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.parsers.site.guya.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.site.guya.GuyaParser
|
||||
|
||||
@MangaSourceParser("GUYACUBARI", "GuyaCubari", "en")
|
||||
internal class GuyaCubari(context: MangaLoaderContext) :
|
||||
GuyaParser(context, MangaSource.GUYACUBARI, "guya.cubari.moe")
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.parsers.site.guya.en
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.guya.GuyaParser
|
||||
|
||||
@MangaSourceParser("HACHIRUMI", "Hachirumi", "en", ContentType.HENTAI)
|
||||
internal class Hachirumi(context: MangaLoaderContext) :
|
||||
GuyaParser(context, MangaSource.HACHIRUMI, "hachirumi.com")
|
||||
@@ -27,6 +27,9 @@ internal abstract class HeanCms(
|
||||
SortOrder.POPULARITY,
|
||||
)
|
||||
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||
.build()
|
||||
@@ -34,50 +37,55 @@ internal abstract class HeanCms(
|
||||
protected open val pathManga = "series"
|
||||
|
||||
//For some sources, you need to send a json. For the moment, this part only works in Get. ( ex source need json gloriousscan.com , omegascans.org )
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
|
||||
var firstTag = false
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domain)
|
||||
append("/query?query_string=")
|
||||
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
|
||||
append("&series_status=All&order=desc&orderBy=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("total_views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("created_at")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
else -> append("latest")
|
||||
}
|
||||
|
||||
append("&series_type=Comic&page=")
|
||||
append(page)
|
||||
append("&perPage=12&tags_ids=")
|
||||
append("[".urlEncoded())
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
for (tag in tags) {
|
||||
// Just to make it fit [1,2,44] ect
|
||||
if (!firstTag) {
|
||||
firstTag = true
|
||||
} else {
|
||||
append(",")
|
||||
}
|
||||
append(tag.key)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&series_status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "Ongoing"
|
||||
MangaState.FINISHED -> "Completed"
|
||||
MangaState.ABANDONED -> "Dropped"
|
||||
MangaState.PAUSED -> "Hiatus"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
append("&orderBy=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("total_views&order=desc")
|
||||
SortOrder.UPDATED -> append("latest&order=desc")
|
||||
SortOrder.NEWEST -> append("created_at&order=desc")
|
||||
SortOrder.ALPHABETICAL -> append("title&order=desc")
|
||||
SortOrder.ALPHABETICAL_DESC -> append("title&order=asc")
|
||||
else -> append("latest&order=desc")
|
||||
}
|
||||
append("&series_type=Comic&perPage=12")
|
||||
append("&tags_ids=")
|
||||
append("[".urlEncoded())
|
||||
append(filter.tags.joinToString(",") { it.key })
|
||||
append("]".urlEncoded())
|
||||
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
append("]".urlEncoded())
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
val json = webClient.httpGet(url).parseJson()
|
||||
|
||||
return json.getJSONArray("data").mapJSON { j ->
|
||||
val slug = j.getString("series_slug")
|
||||
val urlManga = "https://$domain/$pathManga/$slug"
|
||||
@@ -90,7 +98,7 @@ internal abstract class HeanCms(
|
||||
id = generateUid(urlManga),
|
||||
title = j.getString("title"),
|
||||
altTitle = null,
|
||||
url = urlManga,
|
||||
url = urlManga.toRelativeUrl(domain),
|
||||
publicUrl = urlManga,
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = false,
|
||||
@@ -107,8 +115,10 @@ internal abstract class HeanCms(
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected open val datePattern = "yyyy-MM-dd"
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
@@ -172,7 +182,7 @@ internal abstract class HeanCms(
|
||||
return tags.mapNotNullToSet {
|
||||
MangaTag(
|
||||
key = it.substringAfter("id\":").substringBefore(",\""),
|
||||
title = it.substringAfter("name\":\"").substringBefore("\"}]"),
|
||||
title = it.substringAfter("name\":\"").substringBefore("\"}]").toTitleCase(sourceLocale),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,19 +2,10 @@ package org.koitharu.kotatsu.parsers.site.heancms.es
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
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.*
|
||||
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
|
||||
@MangaSourceParser("YUGEN_MANGAS_ES", "YugenMangas.lat", "es", ContentType.HENTAI)
|
||||
internal class YugenMangasEs(context: MangaLoaderContext) :
|
||||
@@ -22,46 +13,56 @@ internal class YugenMangasEs(context: MangaLoaderContext) :
|
||||
|
||||
private val domainAlt = "yugenmangas.net"
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
var firstTag = false
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domainAlt)
|
||||
append("/query?query_string=")
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
append("&series_status=All&order=desc&orderBy=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("total_views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("created_at")
|
||||
SortOrder.ALPHABETICAL -> append("title")
|
||||
else -> append("latest")
|
||||
}
|
||||
append("&series_type=Comic&page=")
|
||||
append(page)
|
||||
append("&perPage=12&tags_ids=")
|
||||
append("[".urlEncoded())
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
for (tag in tags) {
|
||||
// Just to make it fit [1,2,44] ect
|
||||
if (!firstTag) {
|
||||
firstTag = true
|
||||
} else {
|
||||
append(",")
|
||||
}
|
||||
append(tag.key)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&series_status=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "Ongoing"
|
||||
MangaState.FINISHED -> "Completed"
|
||||
MangaState.ABANDONED -> "Dropped"
|
||||
MangaState.PAUSED -> "Hiatus"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
append("&orderBy=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("total_views&order=desc")
|
||||
SortOrder.UPDATED -> append("latest&order=desc")
|
||||
SortOrder.NEWEST -> append("created_at&order=desc")
|
||||
SortOrder.ALPHABETICAL -> append("title&order=desc")
|
||||
SortOrder.ALPHABETICAL_DESC -> append("title&order=asc")
|
||||
else -> append("latest&order=desc")
|
||||
}
|
||||
append("&series_type=Comic&perPage=12")
|
||||
append("&tags_ids=")
|
||||
append("[".urlEncoded())
|
||||
append(filter.tags.joinToString(",") { it.key })
|
||||
append("]".urlEncoded())
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
append("]".urlEncoded())
|
||||
|
||||
append("&page=")
|
||||
append(page.toString())
|
||||
}
|
||||
|
||||
val json = webClient.httpGet(url).parseJson()
|
||||
|
||||
return json.getJSONArray("data").mapJSON { j ->
|
||||
val slug = j.getString("series_slug")
|
||||
val urlManga = "https://$domain/series/$slug"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.site.heancms.pt
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.site.heancms.HeanCms
|
||||
|
||||
@MangaSourceParser("REAPERSCANSPT", "ReaperScans.net", "pt")
|
||||
internal class ReaperScansPt(context: MangaLoaderContext) :
|
||||
HeanCms(context, MangaSource.REAPERSCANSPT, "reaperscans.net") {
|
||||
override val configKeyDomain = ConfigKey.Domain("reaperscans.net", "reaperbr.online")
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.site.heancmsalt
|
||||
|
||||
import org.koitharu.kotatsu.parsers.ErrorMessages
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
@@ -22,6 +23,8 @@ internal abstract class HeanCmsAlt(
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
|
||||
|
||||
override val isSearchSupported = false
|
||||
|
||||
protected open val listUrl = "/comics"
|
||||
protected open val datePattern = "MMMM d, yyyy"
|
||||
|
||||
@@ -33,23 +36,24 @@ internal abstract class HeanCmsAlt(
|
||||
protected open val selectManga = "div.grid.grid-cols-2 div:not([class]):contains(M)"
|
||||
protected open val selectMangaTitle = "h5"
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
// No search or tag
|
||||
if (!query.isNullOrEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append(listUrl)
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
throw IllegalArgumentException(ErrorMessages.SEARCH_NOT_SUPPORTED)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
if (page > 1) {
|
||||
append("?page=")
|
||||
append(page)
|
||||
append(page.toString())
|
||||
}
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
@@ -125,10 +129,10 @@ internal abstract class HeanCmsAlt(
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||
protected open fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||
val d = date?.lowercase() ?: return 0
|
||||
return when {
|
||||
d.startsWith("hace ") -> parseRelativeDate(date)
|
||||
d.startsWith("hace ") || d.endsWith(" antes") -> parseRelativeDate(date)
|
||||
else -> dateFormat.tryParse(date)
|
||||
}
|
||||
}
|
||||
@@ -151,7 +155,7 @@ internal abstract class HeanCmsAlt(
|
||||
)
|
||||
}.timeInMillis
|
||||
|
||||
WordSet("mes").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
WordSet("mes", "meses").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
WordSet("año").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.koitharu.kotatsu.parsers.site.heancmsalt.es
|
||||
|
||||
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.site.heancmsalt.HeanCmsAlt
|
||||
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.generateUid
|
||||
import org.koitharu.kotatsu.parsers.util.mapChapters
|
||||
import org.koitharu.kotatsu.parsers.util.parseHtml
|
||||
import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@MangaSourceParser("BRAKEOUT", "Brakeout", "es")
|
||||
internal class Brakeout(context: MangaLoaderContext) :
|
||||
HeanCmsAlt(context, MangaSource.BRAKEOUT, "brakeout.xyz", 10) {
|
||||
override val selectManga = "div.grid.grid-cols-2 figure"
|
||||
override val selectMangaTitle = "figcaption"
|
||||
|
||||
override val selectDesc = "#section-sinopsis p"
|
||||
override val selectChapter = ".grid-capitulos div.contenedor-capitulo-miniatura"
|
||||
override val selectChapterTitle = "#name"
|
||||
override val selectChapterDate = "time"
|
||||
override val datePattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||
return manga.copy(
|
||||
altTitle = doc.selectFirst(selectAlt)?.text().orEmpty(),
|
||||
description = doc.selectFirstOrThrow(selectDesc).html(),
|
||||
chapters = doc.select(selectChapter)
|
||||
.mapChapters(reversed = true) { i, div ->
|
||||
val a = div.selectFirstOrThrow("a")
|
||||
val dateText = div.selectFirstOrThrow(selectChapterDate).text()
|
||||
val url = a.attrAsRelativeUrl("href").toAbsoluteUrl(domain)
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = div.selectFirstOrThrow(selectChapterTitle).text(),
|
||||
number = i + 1,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = parseChapterDate(
|
||||
dateFormat,
|
||||
dateText,
|
||||
),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override val selectPage = "section img.readImg"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.site.id
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
@@ -18,6 +19,84 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context,
|
||||
override val availableSortOrders: Set<SortOrder>
|
||||
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.POPULARITY)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||
|
||||
|
||||
override val headers: Headers = Headers.Builder()
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.add("Referer", "https://$domain/")
|
||||
.build()
|
||||
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = urlBuilder().apply {
|
||||
addPathSegment("manga")
|
||||
addPathSegment("page")
|
||||
addPathSegment("$page/")
|
||||
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
addQueryParameter("title", filter.query)
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
addQueryParameter("title", "")
|
||||
addQueryParameter(
|
||||
"order",
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.POPULARITY -> "popular"
|
||||
SortOrder.ALPHABETICAL -> "title"
|
||||
SortOrder.NEWEST -> "latest"
|
||||
else -> "latest"
|
||||
},
|
||||
)
|
||||
|
||||
filter.tags.forEach {
|
||||
addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded())
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
addEncodedQueryParameter(
|
||||
"statusx",
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "Publishing"
|
||||
MangaState.FINISHED -> "Finished"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> addQueryParameter("order", "update")
|
||||
}
|
||||
}.build()
|
||||
|
||||
return webClient.httpGet(url).parseHtml()
|
||||
.requireElementById("archives")
|
||||
.selectFirstOrThrow("div.entries")
|
||||
.select(".entry")
|
||||
.map {
|
||||
val titleTag = it.selectFirstOrThrow(".metadata > a")
|
||||
val relativeUrl = titleTag.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = titleTag.attr("title"),
|
||||
altTitle = null,
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
largeCoverUrl = null,
|
||||
description = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().selectFirstOrThrow("#archive")
|
||||
val chapterDateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", sourceLocale)
|
||||
@@ -58,56 +137,6 @@ class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val url = urlBuilder().apply {
|
||||
addPathSegment("manga")
|
||||
addPathSegment("page")
|
||||
addPathSegment("$page/")
|
||||
val order = when (sortOrder) {
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.POPULARITY -> "popular"
|
||||
SortOrder.ALPHABETICAL -> "title"
|
||||
SortOrder.NEWEST -> "latest"
|
||||
else -> throw IllegalArgumentException("Sort order not supported")
|
||||
}
|
||||
addQueryParameter("order", order)
|
||||
addQueryParameter("title", query.orEmpty())
|
||||
tags?.forEach {
|
||||
addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded())
|
||||
}
|
||||
}.build()
|
||||
|
||||
return webClient.httpGet(url).parseHtml()
|
||||
.requireElementById("archives")
|
||||
.selectFirstOrThrow("div.entries")
|
||||
.select(".entry")
|
||||
.map {
|
||||
val titleTag = it.selectFirstOrThrow(".metadata > a")
|
||||
val relativeUrl = titleTag.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = titleTag.attr("title"),
|
||||
altTitle = null,
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = it.selectFirst(".thumbnail > img")?.src().orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
largeCoverUrl = null,
|
||||
description = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val id = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
.requireElementById("reader")
|
||||
|
||||
@@ -39,26 +39,37 @@ class NicovideoSeigaParser(context: MangaLoaderContext) :
|
||||
SortOrder.POPULARITY,
|
||||
)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain("nicovideo.jp")
|
||||
|
||||
@InternalParsersApi
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val page = (offset / 20f).toIntUp().inc()
|
||||
val domain = getDomain("seiga")
|
||||
val url = when {
|
||||
!query.isNullOrEmpty() -> return if (offset == 0) getSearchList(query, page) else emptyList()
|
||||
tags.isNullOrEmpty() -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}"
|
||||
tags.size == 1 -> "https://$domain/manga/list?category=${tags.first().key}&page=$page" +
|
||||
"&sort=${getSortKey(sortOrder)}"
|
||||
val url =
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
return if (offset == 0) getSearchList(filter.query, page) else emptyList()
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany().let {
|
||||
"https://$domain/manga/list?category=${it?.key}&page=$page&sort=${getSortKey(filter.sortOrder)}"
|
||||
}
|
||||
|
||||
} else {
|
||||
"https://$domain/manga/list?page=$page&sort=${getSortKey(filter.sortOrder)}"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
null -> "https://$domain/manga/list?page=$page"
|
||||
}
|
||||
|
||||
tags.size > 1 -> throw IllegalArgumentException("This source supports only 1 category")
|
||||
else -> "https://$domain/manga/list?page=$page&sort=${getSortKey(sortOrder)}"
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
val comicList = doc.body().select("#comic_list > ul > li") ?: doc.parseFailed("Container not found")
|
||||
val items = comicList.select("div > .description > div > div")
|
||||
@@ -145,12 +156,12 @@ class NicovideoSeigaParser(context: MangaLoaderContext) :
|
||||
|
||||
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://${getDomain("seiga")}/manga/list").parseHtml()
|
||||
val root = doc.body().selectOrThrow("#mg_category_list > ul > li")
|
||||
val root = doc.body().selectOrThrow("#mg_category_list > ul > li").drop(1)
|
||||
return root.mapToSet { li ->
|
||||
val a = li.selectFirstOrThrow("a")
|
||||
MangaTag(
|
||||
title = a.text(),
|
||||
key = a.attrAsRelativeUrlOrNull("href").orEmpty(),
|
||||
key = a.attrAsRelativeUrl("href").substringAfter("category=").substringBefore("&"),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.koitharu.kotatsu.parsers.site.likemanga
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
@@ -22,44 +24,75 @@ internal abstract class LikeMangaParser(
|
||||
|
||||
override val availableSortOrders: Set<SortOrder> =
|
||||
EnumSet.of(SortOrder.UPDATED, SortOrder.POPULARITY, SortOrder.NEWEST)
|
||||
|
||||
override val availableStates: Set<MangaState> =
|
||||
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||
|
||||
override val isMultipleTagsSupported = false
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/?act=search&f")
|
||||
append("[sortby]".urlEncoded())
|
||||
append("=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("hot")
|
||||
SortOrder.UPDATED -> append("lastest-chap")
|
||||
SortOrder.NEWEST -> append("lastest-manga")
|
||||
else -> append("lastest-chap")
|
||||
append("/?act=search")
|
||||
|
||||
when (filter) {
|
||||
is MangaListFilter.Search -> {
|
||||
append("&f")
|
||||
append("[keyword]".urlEncoded())
|
||||
append("=")
|
||||
append(filter.query.urlEncoded())
|
||||
}
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
append("&f")
|
||||
append("[sortby]".urlEncoded())
|
||||
append("=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("hot")
|
||||
SortOrder.UPDATED -> append("lastest-chap")
|
||||
SortOrder.NEWEST -> append("lastest-manga")
|
||||
else -> append("lastest-chap")
|
||||
}
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
append("&f")
|
||||
append("[genres]".urlEncoded())
|
||||
append("=")
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append(it.key)
|
||||
}
|
||||
}
|
||||
|
||||
filter.states.oneOrThrowIfMany()?.let {
|
||||
append("&f")
|
||||
append("[status]".urlEncoded())
|
||||
append("=")
|
||||
append(
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "in-process"
|
||||
MangaState.FINISHED -> "complete"
|
||||
MangaState.PAUSED -> "pause"
|
||||
else -> "all"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("&f")
|
||||
append("[sortby]".urlEncoded())
|
||||
append("=lastest-chap")
|
||||
}
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
append("&pageNum=")
|
||||
append(page)
|
||||
}
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&f")
|
||||
append("[genres]".urlEncoded())
|
||||
append("=")
|
||||
append(tag?.key.orEmpty())
|
||||
}
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append("&f")
|
||||
append("[keyword]".urlEncoded())
|
||||
append("=")
|
||||
append(query.urlEncoded())
|
||||
}
|
||||
|
||||
}
|
||||
val doc = webClient.httpGet(url).parseHtml()
|
||||
return doc.select("div.card-body div.video").map { div ->
|
||||
@@ -157,7 +190,8 @@ internal abstract class LikeMangaParser(
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = name,
|
||||
number = chapNum.toInt(),
|
||||
number = chapNum.toFloatOrNull() ?: 0f,
|
||||
volume = 0,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = parseChapterDate(
|
||||
@@ -201,15 +235,39 @@ internal abstract class LikeMangaParser(
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||
return doc.select(".reading-detail img").map { img ->
|
||||
val url = img.src() ?: img.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
val testJson = doc.selectFirst("div.reading input#next_img_token")
|
||||
if (testJson != null) {
|
||||
val jsonRaw = testJson.attr("value").split(".")[1]
|
||||
val jsonData = JSONObject(context.decodeBase64(jsonRaw).toString(Charsets.UTF_8))
|
||||
val jsonImg = context.decodeBase64(jsonData.getString("data")).toString(Charsets.UTF_8)
|
||||
val images = jsonImg.replace("\\", "").replace("[", "").replace("]", "").replace("\"", "").split(",")
|
||||
val baseUrl = doc.selectFirstOrThrow(".reading-detail img").src()
|
||||
val cdn = baseUrl?.substringBefore("manga/", "")?.ifEmpty {
|
||||
baseUrl.toHttpUrl().resolve("/").toString()
|
||||
}
|
||||
return images.map { img ->
|
||||
val url = concatUrl(cdn.orEmpty(), img)
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
return doc.select(".reading-detail img").map { img ->
|
||||
val url = img.src() ?: img.parseFailed("Image src not found")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||
|
||||
@@ -33,6 +33,10 @@ internal abstract class MadaraParser(
|
||||
SortOrder.RATING,
|
||||
)
|
||||
|
||||
override val availableStates: Set<MangaState> = EnumSet.allOf(MangaState::class.java)
|
||||
|
||||
override val availableContentRating: Set<ContentRating> = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT)
|
||||
|
||||
protected open val tagPrefix = "manga-genre/"
|
||||
protected open val datePattern = "MMMM d, yyyy"
|
||||
protected open val stylePage = "?style=list"
|
||||
@@ -149,79 +153,173 @@ internal abstract class MadaraParser(
|
||||
"En attente",
|
||||
)
|
||||
|
||||
@JvmField
|
||||
protected val upcoming: Set<String> = hashSetOf(
|
||||
"Upcoming",
|
||||
"upcoming",
|
||||
"لم تُنشَر بعد",
|
||||
"Prochainement",
|
||||
"À venir",
|
||||
)
|
||||
|
||||
// Change these values only if the site does not support manga listings via ajax
|
||||
protected open val withoutAjax = false
|
||||
|
||||
// can be changed to retrieve tags see getTags
|
||||
protected open val listUrl = "manga/"
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val tag = tags.oneOrThrowIfMany()
|
||||
val doc = if (withoutAjax) {
|
||||
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||
if (withoutAjax) {
|
||||
val pages = page + 1
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
val pages = page + 1
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("/$tagPrefix")
|
||||
append(tag?.key.orEmpty())
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?s=")
|
||||
append(filter.query.urlEncoded())
|
||||
append("&post_type=wp-manga")
|
||||
}
|
||||
append("?")
|
||||
} else {
|
||||
append("/page/")
|
||||
append(pages)
|
||||
append("/?s=")
|
||||
if (!query.isNullOrEmpty()) {
|
||||
append(query.urlEncoded())
|
||||
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
if (filter.tags.isNotEmpty()) {
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
append("/$tagPrefix")
|
||||
append(it.key)
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?")
|
||||
}
|
||||
} else {
|
||||
|
||||
if (pages > 1) {
|
||||
append("/page/")
|
||||
append(pages.toString())
|
||||
}
|
||||
append("/?s=&post_type=wp-manga")
|
||||
filter.states.forEach {
|
||||
append("&status[]=")
|
||||
when (it) {
|
||||
MangaState.ONGOING -> append("on-going")
|
||||
MangaState.FINISHED -> append("end")
|
||||
MangaState.ABANDONED -> append("canceled")
|
||||
MangaState.PAUSED -> append("on-hold")
|
||||
MangaState.UPCOMING -> append("upcoming")
|
||||
}
|
||||
}
|
||||
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
append("&adult=")
|
||||
append(
|
||||
when (it) {
|
||||
ContentRating.SAFE -> "0"
|
||||
ContentRating.ADULT -> "1"
|
||||
else -> ""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
append("&")
|
||||
}
|
||||
|
||||
append("m_orderby=")
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("new-manga")
|
||||
SortOrder.ALPHABETICAL -> append("alphabet")
|
||||
SortOrder.RATING -> append("rating")
|
||||
else -> append("latest")
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
append("?s&post_type=wp-manga&m_orderby=latest")
|
||||
}
|
||||
append("&post_type=wp-manga&")
|
||||
/// &status[]= ( on-going - end - canceled - on-hold - upcoming )
|
||||
}
|
||||
append("m_orderby=")
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> append("views")
|
||||
SortOrder.UPDATED -> append("latest")
|
||||
SortOrder.NEWEST -> append("new-manga")
|
||||
SortOrder.ALPHABETICAL -> append("alphabet")
|
||||
SortOrder.RATING -> append("rating")
|
||||
}
|
||||
}
|
||||
webClient.httpGet(url).parseHtml()
|
||||
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||
} else {
|
||||
val payload = if (sortOrder == SortOrder.RATING) {
|
||||
val payload = if (filter?.sortOrder == SortOrder.RATING) {
|
||||
createRequestTemplate(ratingRequest)
|
||||
} else {
|
||||
createRequestTemplate(defaultRequest)
|
||||
}
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views"
|
||||
SortOrder.UPDATED -> payload["vars[meta_key]"] = "_latest_update"
|
||||
SortOrder.NEWEST -> payload["vars[meta_key]"] = ""
|
||||
SortOrder.ALPHABETICAL -> {
|
||||
payload["vars[orderby]"] = "post_title"
|
||||
payload["vars[order]"] = "ASC"
|
||||
|
||||
payload["page"] = page.toString()
|
||||
|
||||
when (filter) {
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
payload["vars[s]"] = filter.query.urlEncoded()
|
||||
}
|
||||
|
||||
SortOrder.RATING -> {}
|
||||
}
|
||||
payload["page"] = page.toString()
|
||||
payload["vars[wp-manga-genre]"] = tag?.key.orEmpty()
|
||||
payload["vars[s]"] = query?.urlEncoded().orEmpty()
|
||||
/// payload["vars[meta_query][0][0][value][]"] = ( on-going - end - canceled - on-hold - upcoming )
|
||||
webClient.httpPost(
|
||||
"https://$domain/wp-admin/admin-ajax.php",
|
||||
payload,
|
||||
).parseHtml()
|
||||
}
|
||||
is MangaListFilter.Advanced -> {
|
||||
|
||||
filter.tags.oneOrThrowIfMany()?.let {
|
||||
payload["vars[wp-manga-genre]"] = it.key
|
||||
}
|
||||
|
||||
when (filter.sortOrder) {
|
||||
SortOrder.POPULARITY -> payload["vars[meta_key]"] = "_wp_manga_views"
|
||||
SortOrder.UPDATED -> payload["vars[meta_key]"] = "_latest_update"
|
||||
SortOrder.NEWEST -> payload["vars[meta_key]"] = ""
|
||||
SortOrder.ALPHABETICAL -> {
|
||||
payload["vars[orderby]"] = "post_title"
|
||||
payload["vars[order]"] = "ASC"
|
||||
}
|
||||
|
||||
SortOrder.RATING -> {}
|
||||
else -> payload["vars[meta_key]"] = "_latest_update"
|
||||
}
|
||||
|
||||
filter.states.forEach {
|
||||
payload["vars[meta_query][0][0][value][]"] =
|
||||
when (it) {
|
||||
MangaState.ONGOING -> "on-going"
|
||||
MangaState.FINISHED -> "end"
|
||||
MangaState.ABANDONED -> "canceled"
|
||||
MangaState.PAUSED -> "on-hold"
|
||||
MangaState.UPCOMING -> "upcoming"
|
||||
}
|
||||
}
|
||||
|
||||
filter.contentRating.oneOrThrowIfMany()?.let {
|
||||
payload["vars[meta_query][0][1][key]"] = "manga_adult_content"
|
||||
payload["vars[meta_query][0][1][value]"] =
|
||||
when (it) {
|
||||
ContentRating.SAFE -> ""
|
||||
ContentRating.ADULT -> "a%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22yes%22%3B%7D"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
payload["vars[meta_key]"] = "_latest_update"
|
||||
}
|
||||
}
|
||||
|
||||
return parseMangaList(
|
||||
webClient.httpPost(
|
||||
"https://$domain/wp-admin/admin-ajax.php",
|
||||
payload,
|
||||
).parseHtml(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun parseMangaList(doc: Document): List<Manga> {
|
||||
return doc.select("div.row.c-tabs-item__content").ifEmpty {
|
||||
doc.select("div.page-item-detail")
|
||||
}.map { div ->
|
||||
@@ -249,6 +347,7 @@ internal abstract class MadaraParser(
|
||||
in finished -> MangaState.FINISHED
|
||||
in abandoned -> MangaState.ABANDONED
|
||||
in paused -> MangaState.PAUSED
|
||||
in upcoming -> MangaState.UPCOMING
|
||||
else -> null
|
||||
},
|
||||
source = source,
|
||||
@@ -275,9 +374,9 @@ internal abstract class MadaraParser(
|
||||
}
|
||||
MangaTag(
|
||||
key = href,
|
||||
title = a.ownText().trim().ifEmpty {
|
||||
a.selectFirst(".menu-image-title")?.text()?.trim() ?: return@mapNotNullToSet null
|
||||
}.toTitleCase(),
|
||||
title = a.ownText().ifEmpty {
|
||||
a.selectFirst(".menu-image-title")?.textOrNull()
|
||||
}?.toTitleCase(sourceLocale) ?: return@mapNotNullToSet null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user