diff --git a/.github/ISSUE_TEMPLATE/01_report_issue.yml b/.github/ISSUE_TEMPLATE/01_report_issue.yml
deleted file mode 100644
index 07dad314..00000000
--- a/.github/ISSUE_TEMPLATE/01_report_issue.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-name: 🐞 Issue report
-description: Report an issue in UMA
-labels: [Bug]
-body:
-
- - type: input
- id: component
- attributes:
- label: Source name
- description: |
- You can find the extension name and version in source settings.
- placeholder: |
- Example: "Not Real Scans"
- validations:
- required: true
-
- - type: input
- id: language
- attributes:
- label: Source language
- placeholder: |
- Example: "English"
- validations:
- required: true
-
- - type: textarea
- id: reproduce-steps
- attributes:
- label: Steps to reproduce
- description: Provide an example of the issue.
- placeholder: |
- Example:
- 1. First step
- 2. Second step
- 3. Issue here
- validations:
- required: true
-
- - type: textarea
- id: error-logs
- attributes:
- label: Error logs or stack trace
- description: |
- If applicable, paste any error messages or stack traces.
- placeholder: |
- Paste error output here...
-
- - type: input
- id: app-version
- attributes:
- label: Usagi / Kotatsu fork version
- description: |
- You can find your Mihon/Mihon fork version number in **About**.
- placeholder: |
- Example: "App 0.0.1v"
- validations:
- required: true
-
- - type: input
- id: android-version
- attributes:
- label: Android version
- description: |
- You can find this somewhere in your Android settings.
- placeholder: |
- Example: "Android 11"
- validations:
- required: true
-
- - type: textarea
- id: other-details
- attributes:
- label: Other details
- placeholder: |
- Additional details and attachments.
-
- - type: checkboxes
- id: acknowledgements
- attributes:
- label: Acknowledgements
- description: Your issue will be closed if you haven't done these steps.
- options:
- - label: I have searched [existing issues](https://github.com/InvalidDavid/UMA/issues) both open & closed, and confirm that this is a new unreported issue.
- required: true
- - label: I have written a short but informative title.
- required: true
- - label: I have updated the app to to the latest version.
- required: true
- - label: I have provided clear steps to reproduce the issue.
- required: true
- - label: I have included relevant error logs or stack traces.
- required: true
- - label: I will correctly fill out all of the requested information in this form.
- required: true
-
- - type: textarea
- attributes:
- label:
- description: Do **not** modify. This is a reminder for other users to vote.
- value: |
- ---
-
- Add a :+1: [reaction] to [issues you find important].
-
- [reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
- [issues you find important]: https://github.com/InvalidDavid/UMA/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
diff --git a/.github/ISSUE_TEMPLATE/02_source_requests.yml b/.github/ISSUE_TEMPLATE/02_source_requests.yml
deleted file mode 100644
index 6a1cd967..00000000
--- a/.github/ISSUE_TEMPLATE/02_source_requests.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-name: 🌐 Source request
-description: Suggest a new source in UMA
-labels: [Source request]
-body:
-
- - type: input
- id: name
- attributes:
- label: Source name
- placeholder: |
- Example: "Not a real scan"
- validations:
- required: true
-
- - type: input
- id: link
- attributes:
- label: Source link
- placeholder: |
- Example: "https://google.net"
- validations:
- required: true
-
- - type: input
- id: category
- attributes:
- label: Source language
- placeholder: |
- Example: "English / EN", "spanish / ES"
- validations:
- required: true
-
- - type: textarea
- id: other-details
- attributes:
- label: Other details
- placeholder: |
- Additional details and attachments.
- Example:
- "18+/NSFW = yes"
-
- - type: checkboxes
- id: acknowledgements
- attributes:
- label: Acknowledgements
- description: Your issue will be closed if you haven't done these steps.
- options:
- - label: I have searched [existing issues](https://github.com/InvalidDavid/UMA/issues) both open & closed, and confirm that this is a new unreported feature request.
- required: true
- - label: I have written a meaningful title describing the feature.
- required: true
- - label: I have written a meaningful title with the source name.
- required: true
- - label: I will correctly fill out all of the requested information in this form.
- required: true
-
- - type: textarea
- attributes:
- label:
- description: Do **not** modify. This is a reminder for other users to vote.
- value: |
- ---
-
- Add a :+1: [reaction] to [issues you find important].
-
- [reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
- [issues you find important]: https://github.com/InvalidDavid/UMA/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
deleted file mode 100644
index 3ba13e0c..00000000
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ /dev/null
@@ -1 +0,0 @@
-blank_issues_enabled: false
diff --git a/.github/summary.yaml b/.github/summary.yaml
index f0d18c2f..edeeedd8 100644
--- a/.github/summary.yaml
+++ b/.github/summary.yaml
@@ -1 +1 @@
-total: 76
\ No newline at end of file
+total: 233
\ No newline at end of file
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2349a736..ff63bacb 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -6,7 +6,7 @@ on:
branches:
- master
paths:
- - 'src/main/kotlin/org/koitharu/kotatsu/parsers/**'
+ - 'src/main/kotlin/tsuki/**'
jobs:
publish:
@@ -46,4 +46,4 @@ jobs:
tag_name: ${{ steps.meta.outputs.sha_short }}
name: "${{ steps.meta.outputs.sha_short }}"
files: build/libs/uma.jar
- token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.idea/runConfigurations/buildJar.xml b/.idea/runConfigurations/buildJar.xml
index 09571555..3eb34e73 100644
--- a/.idea/runConfigurations/buildJar.xml
+++ b/.idea/runConfigurations/buildJar.xml
@@ -21,4 +21,4 @@
false
-
\ No newline at end of file
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8a39b726..96941724 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,6 +50,7 @@ This is a living document that evolves over time. If you find issues or have sug
- [Common Patterns & Best Practices](#common-patterns--best-practices)
- [URL Handling](#url-handling)
- [Error Handling](#error-handling)
+ - [Testing Your Parser](#testing-your-parser)
- [Building & Testing](#building--testing)
- [Submitting a Pull Request](#submitting-a-pull-request)
- [Troubleshooting](#troubleshooting)
diff --git a/build.gradle.kts b/build.gradle.kts
index 81ed09e1..cfd7984a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,7 +10,7 @@ plugins {
}
group = "org.usagi"
-version = "1.0.1"
+version = "1.0.0"
tasks.test {
useJUnitPlatform()
@@ -34,7 +34,7 @@ tasks.withType().configureEach
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
- "-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi",
+ "-opt-in=tsuki.InternalParsersApi",
)
}
}
@@ -54,18 +54,14 @@ publishing {
dependencies {
implementation(libs.kotlinx.coroutines.core)
- implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp)
implementation(libs.okio)
implementation(libs.json)
implementation(libs.androidx.collection)
- implementation("com.squareup.okhttp3:okhttp-brotli:5.4.0")
- api(libs.core.parsers)
+ api(libs.tsuki)
api(libs.jsoup)
- compileOnly(libs.android.stubs)
-
ksp(project(":plugins-ksp"))
testImplementation(libs.junit.api)
@@ -73,18 +69,21 @@ dependencies {
testImplementation(libs.junit.params)
testRuntimeOnly(libs.junit.launcher)
testImplementation(libs.kotlinx.coroutines.test)
- testImplementation(libs.quickjs)
}
tasks.register("buildJar") {
+ description = "Build all sources to a JAR file"
dependsOn("dexJar")
}
tasks.register("dexJar") {
+ description = "Dex classes after build"
dependsOn(tasks.jar)
inputJar.set(tasks.jar.flatMap { it.archiveFile })
outputJar.set(layout.projectDirectory.file("build/libs/uma.jar"))
classpath.from(configurations.runtimeClasspath)
}
-tasks.register("generateTestsReport")
+tasks.register("generateTestsReport") {
+ description = "Generate a HTML file to get tests report"
+}
diff --git a/gradle.properties b/gradle.properties
index df06b5a1..83f3a9a7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,3 +22,6 @@ org.gradle.vfs.watch=true
org.gradle.workers.max=4
systemProp.org.gradle.unsafe.configuration-cache=false
Xannotation-default-target=param-property
+
+# Enabled parallel sync for Gradle 9.4+
+org.gradle.tooling.parallel=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e1bb9889..e1d8f1c6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,5 @@
[versions]
collection = "1.6.0"
-core = "9b51187e96"
kotlin = "2.2.10"
ksp = "2.2.10-2.0.2"
coroutines = "1.10.2"
@@ -11,11 +10,9 @@ json = "20251224"
# Keep Android 5, 6 compatible (with SDK 21 - 23)
#noinspection NewerVersionAvailable
jsoup = "1.17.2"
-quickjs = "1.1.0"
korte = "4.0.10"
-serialization = "1.9.0"
simplexml = "2.7.1"
-stubs = "4.1.1.4"
+tsuki = "1.0.5"
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
@@ -23,12 +20,10 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
[libraries]
-android-stubs = { module = "com.google.android:android", version.ref = "stubs" }
-core-parsers = { module = "com.github.UsagiApp:core-parsers", version.ref = "core"}
+tsuki = { module = "com.github.UsagiApp:Tsuki", version.ref = "tsuki"}
ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
-kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
@@ -38,6 +33,5 @@ okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
json = { module = "org.json:json", version.ref = "json" }
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
-quickjs = { module = "io.webfolder:quickjs", version.ref = "quickjs" }
korte = { module = "com.soywiz.korlibs.korte:korte-jvm", version.ref = "korte" }
-simplexml = { module = "org.simpleframework:simple-xml", version.ref = "simplexml" }
\ No newline at end of file
+simplexml = { module = "org.simpleframework:simple-xml", version.ref = "simplexml" }
diff --git a/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt b/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt
index bacd2079..bfb3674c 100644
--- a/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt
+++ b/plugins-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt
@@ -20,21 +20,17 @@ class ParserProcessor(
private val sourceNamePattern = Regex("[A-Z_][A-Z0-9_]{3,}")
override fun process(resolver: Resolver): List {
- val kotatsuSymbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.MangaSourceParser")
- val tachiyomiSymbols = resolver.getSymbolsWithAnnotation("org.koitharu.kotatsu.parsers.TachiyomiSource")
-
+ val kotatsuSymbols = resolver.getSymbolsWithAnnotation("tsuki.MangaSourceParser")
val kotatsuRet = kotatsuSymbols.filterNot { it.validate() }.toList()
- val tachiyomiRet = tachiyomiSymbols.filterNot { it.validate() }.toList()
-
- if (!kotatsuSymbols.iterator().hasNext() && !tachiyomiSymbols.iterator().hasNext()) {
- return kotatsuRet + tachiyomiRet
+ if (!kotatsuSymbols.iterator().hasNext()) {
+ return kotatsuRet
}
val dependencies = Dependencies.ALL_FILES
val factoryFile =
try {
codeGenerator.createNewFile(
dependencies = dependencies,
- packageName = "org.koitharu.kotatsu.parsers",
+ packageName = "tsuki",
fileName = "MangaParserFactory",
)
} catch (e: FileAlreadyExistsException) {
@@ -45,7 +41,7 @@ class ParserProcessor(
try {
codeGenerator.createNewFile(
dependencies = dependencies,
- packageName = "org.koitharu.kotatsu.parsers.model",
+ packageName = "tsuki.model",
fileName = "MangaSource",
)
} catch (e: FileAlreadyExistsException) {
@@ -54,29 +50,27 @@ class ParserProcessor(
}
val totalCount = sourcesFile?.writer().use { sourcesWriter ->
factoryFile?.writer().use { factoryWriter ->
- writeContent(sourcesWriter, factoryWriter, kotatsuSymbols, tachiyomiSymbols)
+ writeContent(sourcesWriter, factoryWriter, kotatsuSymbols)
}
}
writeSummary(totalCount)
- return kotatsuRet + tachiyomiRet
+ return kotatsuRet
}
private fun writeContent(
sourcesWriter: Writer?,
factoryWriter: Writer?,
kotatsuSymbols: Sequence,
- tachiyomiSymbols: Sequence,
): Int {
if (sourcesWriter == null && factoryWriter == null) {
return 0
}
factoryWriter?.write(
"""
- package org.koitharu.kotatsu.parsers
+ package tsuki
- import org.koitharu.kotatsu.parsers.model.MangaParserSource
- import org.koitharu.kotatsu.parsers.core.MangaParserWrapper
- import org.koitharu.kotatsu.parsers.compat.TachiyomiSourceAdapter
+ import tsuki.model.MangaParserSource
+ import tsuki.core.MangaParserWrapper
internal fun MangaParserSource.newParser(context: MangaLoaderContext): MangaParser = when (this) {
@@ -84,7 +78,7 @@ class ParserProcessor(
)
sourcesWriter?.write(
"""
- package org.koitharu.kotatsu.parsers.model
+ package tsuki.model
public enum class MangaParserSource(
override val title: String,
@@ -103,18 +97,11 @@ class ParserProcessor(
.onEach { it.accept(kotatsuVisitor, Unit) }
.count()
- // Process Tachiyomi source bridges
- val tachiyomiVisitor = TachiyomiVisitor(sourcesWriter, factoryWriter, kotatsuVisitor.titles)
- val tachiyomiCount = tachiyomiSymbols
- .filter { it is KSClassDeclaration && it.validate() }
- .onEach { it.accept(tachiyomiVisitor, Unit) }
- .count()
-
factoryWriter?.write(
- """
+ $$"""
}.let {
require(it.source == this) {
- "Cannot instantiate manga parser: ${'$'}name mapped to ${'$'}{it.source}"
+ "Cannot instantiate manga parser: $name mapped to ${it.source}"
}
MangaParserWrapper(it)
}
@@ -126,7 +113,7 @@ class ParserProcessor(
}
""".trimIndent(),
)
- return kotatsuCount + tachiyomiCount
+ return kotatsuCount
}
private fun writeSummary(totalCount: Int) {
@@ -197,91 +184,4 @@ class ParserProcessor(
)
}
}
-
- /**
- * Visitor for @TachiyomiSource annotations.
- * Generates factory entries that instantiate the Tachiyomi HttpSource and wrap it with TachiyomiSourceAdapter.
- */
- private inner class TachiyomiVisitor(
- private val sourcesWriter: Writer?,
- private val factoryWriter: Writer?,
- private val titles: HashMap,
- ) : KSVisitorVoid() {
-
- override fun visitClassDeclaration(
- classDeclaration: KSClassDeclaration,
- data: Unit,
- ) {
- val annotation = classDeclaration.annotations.single { it.shortName.asString() == "TachiyomiSource" }
-
- // Get annotation arguments with defaults
- val nameArg = annotation.arguments.find { it.name?.asString() == "name" }?.value as? String ?: ""
- val titleArg = annotation.arguments.find { it.name?.asString() == "title" }?.value as? String ?: ""
- val localeArg = annotation.arguments.find { it.name?.asString() == "locale" }?.value as? String ?: ""
- val typeArg = annotation.arguments.find { it.name?.asString() == "type" }?.value?.toString()
- val type = if (typeArg.isNullOrBlank() || typeArg == "null") "ContentType.MANGA" else typeArg
-
- // Auto-detect values from class if not provided
- val className = classDeclaration.simpleName.asString()
- val qualifiedClassName = checkNotNull(classDeclaration.qualifiedName?.asString()) {
- "Class qualified name is null"
- }
-
- // Generate source name from class name (UPPER_CASE)
- val sourceName = nameArg.takeIf { it.isNotBlank() }
- ?: className.uppercase().replace(Regex("[^A-Z0-9_]"), "_").let {
- // Ensure it starts with a letter
- if (it.firstOrNull()?.isLetter() == true) it else "SOURCE_$it"
- }
-
- // Use provided title or class name
- val sourceTitle = titleArg.takeIf { it.isNotBlank() } ?: className
-
- // Auto-detect locale from package name if not provided
- val locale = localeArg.takeIf { it.isNotBlank() }
- ?: detectLocaleFromPackage(qualifiedClassName)
-
- val localeString = "\"$locale\""
- val localeObj = if (locale.isEmpty()) null else Locale(locale)
- val localeTitle = localeObj?.getDisplayLanguage(localeObj)
-
- if (localeObj != null && localeObj !in availableLocales) {
- logger.error("Tachiyomi source $sourceName has wrong locale: $localeTitle")
- }
-
- if (!sourceNamePattern.matches(sourceName)) {
- logger.error("Tachiyomi source name must be uppercase: $sourceName")
- }
-
- val prevTitleClass = titles.put(sourceTitle, qualifiedClassName)
- if (prevTitleClass != null) {
- logger.warn("Source title duplication: \"$sourceTitle\" is assigned to both $prevTitleClass and $qualifiedClassName")
- }
-
- // Factory: wrap the Tachiyomi source with TachiyomiSourceAdapter
- factoryWriter?.write(
- "\tMangaParserSource.$sourceName -> TachiyomiSourceAdapter(context, MangaParserSource.$sourceName, $qualifiedClassName())\n"
- )
-
- // Enum entry
- val localeComment = localeTitle?.toTitleCase(localeObj)?.let { " /* $it */" }.orEmpty()
- sourcesWriter?.write(
- "\t$sourceName(\"$sourceTitle\", $localeString$localeComment, $type, false),\n",
- )
- }
-
- private fun detectLocaleFromPackage(qualifiedName: String): String {
- // Common locale patterns in package names
- val localePatterns = mapOf(
- ".vi." to "vi", ".en." to "en", ".ja." to "ja", ".ko." to "ko",
- ".zh." to "zh", ".es." to "es", ".fr." to "fr", ".de." to "de",
- ".it." to "it", ".pt." to "pt", ".ru." to "ru", ".ar." to "ar",
- ".th." to "th", ".id." to "id", ".tr." to "tr", ".pl." to "pl",
- ".all." to "", ".multi." to ""
- )
-
- return localePatterns.entries.find { qualifiedName.contains(it.key) }?.value
- ?: ""
- }
- }
}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt
deleted file mode 100644
index 3f418bae..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/compat/TachiyomiSourceAdapter.kt
+++ /dev/null
@@ -1,560 +0,0 @@
-@file:Suppress("warnings", "unused")
-
-package org.koitharu.kotatsu.parsers.compat
-
-import android.app.Application
-import eu.kanade.tachiyomi.network.NetworkHelper
-import eu.kanade.tachiyomi.source.model.MangasPage
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import okhttp3.Headers
-import okhttp3.Interceptor
-import okhttp3.Response
-import org.koitharu.kotatsu.parsers.InternalParsersApi
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
-import org.koitharu.kotatsu.parsers.config.ConfigKey
-import org.koitharu.kotatsu.parsers.core.PagedMangaParser
-import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaChapter
-import org.koitharu.kotatsu.parsers.model.MangaListFilter
-import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
-import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
-import org.koitharu.kotatsu.parsers.model.MangaPage
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.model.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.util.generateUid
-import org.koitharu.kotatsu.parsers.util.mapChapters
-import uy.kohesive.injekt.Injekt
-
-/**
- * Adapter that wraps a Tachiyomi [HttpSource] and exposes it as a parser via [PagedMangaParser].
- * Enables Tachiyomi extension sources to be used within Usagi and Kotatsu ecosystem (if possible).
- */
-@OptIn(InternalParsersApi::class)
-open class TachiyomiSourceAdapter(
- context: MangaLoaderContext,
- source: MangaParserSource,
- protected val tachiyomiSource: HttpSource,
-) : PagedMangaParser(context, source, DEFAULT_PAGE_SIZE) {
-
- /** Disable login button, it's useless */
- override val authorizationProvider: MangaParserAuthProvider? = null
-
- /** Default for source settings. */
- protected open val splitByTranslationsKey = ConfigKey.SplitByTranslations(false)
- protected open val showSuspiciousContentKey = ConfigKey.ShowSuspiciousContent(false)
- protected open val preferredImageServerKey = ConfigKey.PreferredImageServer(emptyMap(), null)
-
- init {
- /** Register Application context so Injekt.get() works. */
- runCatching {
- val activityThreadClass = Class.forName("android.app.ActivityThread")
- val currentApplicationMethod = activityThreadClass.getMethod("currentApplication")
- val app = currentApplicationMethod.invoke(null) as? Application
- if (app != null) {
- Injekt.addSingleton(app)
- } else {
- Injekt.addSingleton(Application())
- }
- Injekt.addSingleton(context)
- }.onFailure {
- Injekt.addSingleton(Application())
- Injekt.addSingleton(context)
- }
-
- /** Setup intercepted client for domain and User-Agent swapping */
- val originalDomain = runCatching { java.net.URI(tachiyomiSource.baseUrl).host }.getOrNull()
- val interceptedClient = context.httpClient.newBuilder()
- .addInterceptor { chain ->
- var request = chain.request()
- val builder = request.newBuilder()
- val customUa = config[userAgentKey]
- if (originalDomain != null && request.url.host == originalDomain) {
- val newUrl = request.url.newBuilder()
- .host(domain)
- .build()
- builder.url(newUrl)
- }
- if (customUa.isNotBlank()) {
- builder.header("User-Agent", customUa)
- }
- chain.proceed(builder.build())
- }
- .build()
-
- runCatching {
- tachiyomiSource.javaClass.getMethod("setClient", okhttp3.OkHttpClient::class.java)
- .invoke(tachiyomiSource, interceptedClient)
- }
-
- NetworkHelper.setClient(interceptedClient)
- }
-
- override fun onCreateConfig(keys: MutableCollection>) {
- super.onCreateConfig(keys)
- keys.add(userAgentKey)
- }
-
- override val configKeyDomain: ConfigKey.Domain by lazy {
- val host = runCatching {
- java.net.URI(tachiyomiSource.baseUrl).host
- ?: tachiyomiSource.baseUrl.toHostOnly()
- }.getOrDefault(tachiyomiSource.baseUrl.toHostOnly())
- ConfigKey.Domain(host)
- }
-
- // ========================== Sort / Order ===============================
- override val availableSortOrders: Set = buildSet {
- add(SortOrder.UPDATED)
- add(SortOrder.POPULARITY)
- if (tachiyomiSource.supportsLatest) {
- add(SortOrder.NEWEST)
- }
- }
-
- /**
- * Lazily computed: true if the source's filter list contains any Filter.Text entry,
- * which typically means author/artist/year text inputs are supported.
- */
- private val hasTextFilter: Boolean by lazy {
- tachiyomiSource.getFilterList().any { it is eu.kanade.tachiyomi.source.model.Filter.Text }
- }
-
- /** Mapping filters, allow to search + use all filters through tags */
- @InternalParsersApi
- override val filterCapabilities: MangaListFilterCapabilities
- get() = MangaListFilterCapabilities(
- isSearchSupported = true,
- isMultipleTagsSupported = true,
- isAuthorSearchSupported = hasTextFilter,
- )
-
-
- // ============================== List ===================================
-
- open override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List {
- val query = filter.query
-
- val mangasPage: MangasPage = withContext(Dispatchers.IO) {
- when {
- !query.isNullOrEmpty() || filter.hasNonSearchOptions() -> {
- val tachiyomiFilters = tachiyomiSource.getFilterList()
-
- // --- Map Kotatsu tag filters → Tachiyomi CheckBox / Group ---
- filter.tags.forEach { tag ->
- tachiyomiFilters.forEach { f ->
- if (f is eu.kanade.tachiyomi.source.model.Filter.CheckBox && f.name == tag.title) {
- f.state = true
- } else if (f is eu.kanade.tachiyomi.source.model.Filter.Group<*>) {
- f.state.forEach { sub ->
- if (sub is eu.kanade.tachiyomi.source.model.Filter.CheckBox && sub.name == tag.title) {
- sub.state = true
- }
- }
- } else if (f is eu.kanade.tachiyomi.source.model.Filter.Select<*> && !isStatusFilterSelect(f)) {
- // For generic Select filters, select the matching option by name
- val idx = f.values.indexOfFirst { it.toString() == tag.title }
- if (idx >= 0) f.state = idx
- }
- }
- }
-
- // --- Map Kotatsu state filters → Tachiyomi StatusFilter (Group or Select) ---
- val states = filter.states
- if (states.isNotEmpty()) {
- tachiyomiFilters.forEach { f ->
- when {
- f is eu.kanade.tachiyomi.source.model.Filter.Group<*> && isStatusFilterGroup(f) -> {
- f.state.forEach { sub ->
- if (sub is eu.kanade.tachiyomi.source.model.Filter.CheckBox) {
- val mappedState = mapNameToState(sub.name)
- if (mappedState != null && mappedState in states) {
- sub.state = true
- }
- }
- }
- }
- f is eu.kanade.tachiyomi.source.model.Filter.Select<*> && isStatusFilterSelect(f) -> {
- // Pick the first matching option for single-select status
- val values = f.values
- for (i in 1 until values.size) {
- val mappedState = mapNameToState(values[i].toString())
- if (mappedState != null && mappedState in states) {
- f.state = i
- break
- }
- }
- }
- else -> {}
- }
- }
- }
-
- tachiyomiSource.fetchSearchManga(page, query ?: "", tachiyomiFilters)
- .toBlocking().single()
- }
- order == SortOrder.NEWEST && tachiyomiSource.supportsLatest -> {
- tachiyomiSource.fetchLatestUpdates(page).toBlocking().single()
- }
- else -> {
- tachiyomiSource.fetchPopularManga(page).toBlocking().single()
- }
- }
- }
-
- return mangasPage.mangas.map { it.toManga() }
- }
-
- // ============================== Details ================================
-
- open override suspend fun getDetails(manga: Manga): Manga {
- val sManga = manga.toSManga()
-
- return withContext(Dispatchers.IO) {
- val details = tachiyomiSource.fetchMangaDetails(sManga).toBlocking().single()
- val chapters = tachiyomiSource.fetchChapterList(sManga).toBlocking().single()
-
- /** Auto-detect chapter ordering: Tachiyomi default is newest-first,
- but some sources return oldest-first. Detect by comparing chapter_number
- of first two entries. If not available, fall back to date comparison. */
- val isNewestFirst = detectNewestFirst(chapters)
-
- manga.copy(
- title = details.title.ifEmpty { manga.title },
- altTitles = emptySet(),
- coverUrl = details.thumbnail_url ?: manga.coverUrl,
- largeCoverUrl = details.thumbnail_url,
- description = details.description,
- authors = setOfNotNull(details.author, details.artist)
- .filter { it.isNotBlank() }.toSet(),
- tags = parseGenreTags(details.genre),
- state = mapStatus(details.status),
- chapters = chapters.mapChapters { i, sChapter ->
- sChapter.toMangaChapter(i, chapters.size, isNewestFirst)
- }.sortedBy { it.number },
- )
- }
- }
-
- /**
- * Detects whether [chapters] are ordered newest-first (index 0 = latest).
- * Priority:
- * 1. chapter_number: if first > second → newest-first
- * 2. date_upload: if first > second → newest-first
- * 3. Default: true (Tachiyomi convention)
- * It helps rearrange the chapter list in asc order, avoiding confusion.
- */
- private fun detectNewestFirst(chapters: List): Boolean {
- if (chapters.size < 2) return true
- val a = chapters[0]
- val b = chapters[1]
- if (a.chapter_number > 0f && b.chapter_number > 0f) {
- return a.chapter_number > b.chapter_number
- }
- if (a.date_upload > 0L && b.date_upload > 0L) {
- return a.date_upload > b.date_upload
- }
- return true
- }
-
- // ============================== Pages ==================================
-
- open override suspend fun getPages(chapter: MangaChapter): List {
- val sChapter = SChapter.create().apply {
- url = chapter.url
- name = chapter.title ?: ""
- chapter_number = chapter.number
- }
-
- val pages = withContext(Dispatchers.IO) {
- tachiyomiSource.fetchPageList(sChapter).toBlocking().single()
- }
-
- return pages.map { page ->
- val imgUrl = page.imageUrl ?: page.url
- MangaPage(
- id = generateUid(imgUrl.ifEmpty { "${chapter.url}#${page.index}" }),
- url = imgUrl,
- preview = null,
- source = source,
- )
- }
- }
-
- open override suspend fun getPageUrl(page: MangaPage): String {
- val url = page.url
- if (url.startsWith("http://") || url.startsWith("https://")) return url
- return "https://$domain$url"
- }
-
- // ============================== Filter Options =========================
-
- open override suspend fun getFilterOptions(): MangaListFilterOptions = withContext(Dispatchers.IO) {
- val tachiyomiFilters = tachiyomiSource.getFilterList()
- val tags = mutableSetOf()
- val states = mutableSetOf()
-
- tachiyomiFilters.forEach { filter ->
- when (filter) {
- is eu.kanade.tachiyomi.source.model.Filter.CheckBox -> {
- tags.add(MangaTag(title = filter.name, key = filter.name.lowercase(), source = source))
- }
-
- // --- Group (multi-checkbox list, e.g. GenreList or StatusFilter) ---
- is eu.kanade.tachiyomi.source.model.Filter.Group<*> -> {
- if (isStatusFilterGroup(filter)) {
- // Map each child checkbox to a MangaState
- filter.state.forEach { sub ->
- if (sub is eu.kanade.tachiyomi.source.model.Filter.CheckBox) {
- mapNameToState(sub.name)?.let { states.add(it) }
- }
- }
- } else {
- // Generic group → flatten children as tags
- filter.state.forEach { sub ->
- when (sub) {
- is eu.kanade.tachiyomi.source.model.Filter.CheckBox -> {
- tags.add(
- MangaTag(
- title = sub.name,
- key = sub.name.lowercase(),
- source = source,
- ),
- )
- }
-
- else -> {} // ignore nested selects / text inside groups
- }
- }
- }
- }
-
- // --- Select (single-choice dropdown, e.g. GenreFilter / StatusFilter in fastscan) ---
- is eu.kanade.tachiyomi.source.model.Filter.Select<*> -> {
- if (isStatusFilterSelect(filter)) {
- // Map each option to MangaState (skip index 0 which is usually "All")
- val values = filter.values
- for (i in 1 until values.size) {
- mapNameToState(values[i].toString())?.let { states.add(it) }
- }
- } else {
- // Generic select → extract non-"All" options as tags
- val values = filter.values
- for (i in 1 until values.size) {
- val name = values[i].toString()
- if (name.isNotBlank()) {
- tags.add(MangaTag(title = name, key = name.lowercase(), source = source))
- }
- }
- }
- }
-
- // Filter.Text / Header / Separator — intentionally ignored for tag/state population
- else -> {}
- }
- }
-
- MangaListFilterOptions(availableTags = tags, availableStates = states)
- }
-
- /**
- * Heuristic: returns true when a [eu.kanade.tachiyomi.source.model.Filter.Group] represents a status/state selector.
- * Matches group names containing words like "status", "tình trạng", "trang thai", etc.
- */
- private fun isStatusFilterGroup(filter: eu.kanade.tachiyomi.source.model.Filter.Group<*>): Boolean {
- val name = filter.name.lowercase()
- return STATUS_KEYWORDS.any { name.contains(it) }
- }
-
- /**
- * Heuristic: returns true when a [eu.kanade.tachiyomi.source.model.Filter.Select] represents a status/state selector.
- */
- private fun isStatusFilterSelect(filter: eu.kanade.tachiyomi.source.model.Filter.Select<*>): Boolean {
- val name = filter.name.lowercase()
- return STATUS_KEYWORDS.any { name.contains(it) }
- }
-
- /**
- * Maps common status label strings (in multiple languages) to a [MangaState].
- * Returns null if no mapping is found.
- */
- protected open fun mapNameToState(name: String): MangaState? {
- val lower = name.lowercase()
- return when {
- ONGOING_KEYWORDS.any { lower.contains(it) } -> MangaState.ONGOING
- FINISHED_KEYWORDS.any { lower.contains(it) } -> MangaState.FINISHED
- PAUSED_KEYWORDS.any { lower.contains(it) } -> MangaState.PAUSED
- ABANDONED_KEYWORDS.any { lower.contains(it) } -> MangaState.ABANDONED
- else -> null
- }
- }
-
- // ============================== Headers ================================
-
- open override fun getRequestHeaders(): Headers = tachiyomiSource.headers
-
- // ============================== Interceptor ============================
-
- open override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
-
- // ============================== Private Helpers ========================
-
- protected open fun SManga.toManga(): Manga {
- val mangaUrl = this.url
- val publicUrl = if (mangaUrl.startsWith("http")) mangaUrl
- else "${tachiyomiSource.baseUrl}$mangaUrl"
-
- return Manga(
- id = generateUid(mangaUrl),
- title = this.title,
- altTitles = emptySet(),
- url = mangaUrl,
- publicUrl = publicUrl,
- rating = RATING_UNKNOWN,
- contentRating = null,
- coverUrl = this.thumbnail_url,
- tags = parseGenreTags(this.genre),
- state = mapStatus(this.status),
- authors = setOfNotNull(this.author, this.artist)
- .filter { it.isNotBlank() }.toSet(),
- source = source,
- )
- }
-
- protected open fun Manga.toSManga(): SManga = SManga.create().apply {
- url = this@toSManga.url
- title = this@toSManga.title
- thumbnail_url = this@toSManga.coverUrl
- }
-
- protected open fun SChapter.toMangaChapter(index: Int, total: Int, isNewestFirst: Boolean = true): MangaChapter {
- // When chapter_number is set by the source, use it directly.
- // Otherwise derive from position: newest-first → total-index, oldest-first → index+1
- val number = if (this.chapter_number > 0f) {
- this.chapter_number
- } else if (isNewestFirst) {
- (total - index).toFloat() // index 0 = newest = highest number
- } else {
- (index + 1).toFloat() // index 0 = oldest = chapter 1
- }
- return MangaChapter(
- id = generateUid(this.url),
- title = this.name.ifBlank { null },
- number = number,
- volume = 0,
- url = this.url,
- scanlator = this.scanlator,
- uploadDate = this.date_upload,
- branch = null,
- source = source,
- )
- }
-
- protected open fun parseGenreTags(genreStr: String?): Set {
- if (genreStr.isNullOrBlank()) return emptySet()
- return genreStr.split(",")
- .map { it.trim() }
- .filter { it.isNotBlank() }
- .mapTo(mutableSetOf()) { genre ->
- MangaTag(title = genre, key = genre.lowercase(), source = source)
- }
- }
-
- protected open fun mapStatus(status: Int): MangaState? = when (status) {
- SManga.ONGOING -> MangaState.ONGOING
- SManga.COMPLETED, SManga.PUBLISHING_FINISHED -> MangaState.FINISHED
- SManga.ON_HIATUS -> MangaState.PAUSED
- SManga.CANCELLED -> MangaState.ABANDONED
- else -> null
- }
-
- protected open fun String.toHostOnly(): String =
- removePrefix("https://").removePrefix("http://").substringBefore("/")
-
- companion object {
- const val DEFAULT_PAGE_SIZE = 20
-
- /** Filter names (lowercased) that indicate a status/state filter. */
- private val STATUS_KEYWORDS = listOf(
- "status",
- "tình trạng",
- "tinh trang",
- "trạng thái",
- "trang thai",
- "durum",
- "état",
- "estado",
- "zustand",
- )
-
- /** Label fragments (lowercased) that map to [MangaState.ONGOING]. */
- private val ONGOING_KEYWORDS = listOf(
- "ongoing",
- "on-going",
- "on going",
- "updating",
- "publishing",
- "airing",
- "đang", // Vietnamese "đang tiến hành"
- "dang tien hanh",
- "devam", // Turkish
- "en cours", // French
- "en curso", // Spanish
- "lançando", // Portuguese
- "выходит", // Russian
- "продолжается",
- )
-
- /** Label fragments (lowercased) that map to [MangaState.FINISHED]. */
- private val FINISHED_KEYWORDS = listOf(
- "completed",
- "complete",
- "finished",
- "end",
- "full",
- "hoàn", // Vietnamese "hoàn thành" / "hoàn tất"
- "hoan thanh",
- "trọn bộ",
- "tron bo",
- "tamamland", // Turkish
- "achevé",
- "terminé",
- "завершено", // Russian
- )
-
- /** Label fragments (lowercased) that map to [MangaState.PAUSED]. */
- private val PAUSED_KEYWORDS = listOf(
- "hiatus",
- "on hold",
- "on-hold",
- "paused",
- "beklemede", // Turkish
- "durduruldu",
- "en pause",
- "заморожено", // Russian
- )
-
- /** Label fragments (lowercased) that map to [MangaState.ABANDONED]. */
- private val ABANDONED_KEYWORDS = listOf(
- "canceled",
- "cancelled",
- "dropped",
- "drop",
- "abandoned",
- "discontinued",
- "iptal", // Turkish
- "đã hủy",
- "da huy",
- "abandonné",
- "заброшено", // Russian
- )
- }
-}
-
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/MangaFire.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/MangaFire.kt
deleted file mode 100644
index c9f84c20..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/MangaFire.kt
+++ /dev/null
@@ -1,504 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.all
-
-import okhttp3.Interceptor
-import org.jsoup.Jsoup
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.config.ConfigKey
-import org.koitharu.kotatsu.parsers.core.PagedMangaParser
-import org.koitharu.kotatsu.parsers.model.ContentType
-import org.koitharu.kotatsu.parsers.model.Demographic
-import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaChapter
-import org.koitharu.kotatsu.parsers.model.MangaListFilter
-import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
-import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
-import org.koitharu.kotatsu.parsers.model.MangaPage
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.model.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.network.OkHttpWebClient
-import org.koitharu.kotatsu.parsers.util.generateUid
-import org.koitharu.kotatsu.parsers.util.parseJson
-import java.util.EnumSet
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
-
-internal abstract class MangaFireParser(
- context: MangaLoaderContext,
- source: MangaParserSource,
- private val siteLang: String,
-) : PagedMangaParser(context, source, 30), Interceptor, MangaParserAuthProvider {
-
- override val configKeyDomain = ConfigKey.Domain("mangafire.to")
-
- override val availableSortOrders: Set = EnumSet.of(
- SortOrder.UPDATED, // chapter update
- SortOrder.POPULARITY, // most views
- SortOrder.RATING, // rating score
- SortOrder.NEWEST, // created manga
- SortOrder.ALPHABETICAL, // title asc
- SortOrder.RELEVANCE, // relevance sc
- SortOrder.POPULARITY_WEEK,
- SortOrder.POPULARITY_MONTH,
- )
-
- private val apiClient by lazy {
- val newHttpClient = context.httpClient.newBuilder()
- .addInterceptor { chain ->
- val request = chain.request().newBuilder()
- .header("Referer", "https://$domain/")
- .header("Accept", "application/json")
- .build()
- chain.proceed(request)
- }
- .build()
- OkHttpWebClient(newHttpClient, source)
- }
-
- override fun intercept(chain: Interceptor.Chain) = chain.proceed(
- chain.request().newBuilder()
- .header("Referer", "https://$domain/")
- .build()
- )
-
- override val filterCapabilities = MangaListFilterCapabilities(
- isMultipleTagsSupported = true,
- isTagsExclusionSupported = true,
- isSearchSupported = true,
- isSearchWithFiltersSupported = true,
- isYearRangeSupported = true,
- )
-
- companion object {
- val GENRE_MAP = mapOf(
- "Action" to "1",
- "Adult" to "268929",
- "Adventure" to "78",
- "Avant Garde" to "3",
- "Boys Love" to "4",
- "Comedy" to "5",
- "Crime" to "268921",
- "Demons" to "77",
- "Drama" to "6",
- "Ecchi" to "7",
- "Fantasy" to "79",
- "Girls Love" to "9",
- "Gourmet" to "10",
- "Harem" to "11",
- "Hentai" to "268930",
- "Historical" to "268922",
- "Horror" to "530",
- "Isekai" to "13",
- "Iyashikei" to "531",
- "Josei" to "15",
- "Kids" to "532",
- "Magic" to "539",
- "Magical Girls" to "268923",
- "Mahou Shoujo" to "533",
- "Martial Arts" to "534",
- "Mature" to "268931",
- "Mecha" to "19",
- "Medical" to "268924",
- "Military" to "535",
- "Music" to "21",
- "Mystery" to "22",
- "Parody" to "23",
- "Philosophical" to "268925",
- "Psychological" to "536",
- "Reverse Harem" to "25",
- "Romance" to "26",
- "School" to "73",
- "Sci-Fi" to "28",
- "Seinen" to "537",
- "Shoujo" to "30",
- "Shounen" to "31",
- "Slice of Life" to "538",
- "Smut" to "268932",
- "Space" to "33",
- "Sports" to "34",
- "Super Power" to "75",
- "Superhero" to "268926",
- "Supernatural" to "76",
- "Suspense" to "37",
- "Thriller" to "38",
- "Tragedy" to "268927",
- "Vampire" to "39",
- "Wuxia" to "268928"
- )
- }
-
- private val tags by lazy {
- GENRE_MAP.entries.map { (title, id) ->
- MangaTag(title, id, source)
- }.toSet()
- }
-
- override suspend fun getFilterOptions() = MangaListFilterOptions(
- availableTags = tags,
- availableStates = EnumSet.of(
- MangaState.ONGOING, MangaState.FINISHED,
- MangaState.ABANDONED, MangaState.PAUSED, MangaState.UPCOMING,
- ),
- availableContentTypes = EnumSet.of(
- ContentType.MANGA, ContentType.MANHWA, ContentType.MANHUA, ContentType.OTHER,
- ),
- availableDemographics = EnumSet.of(Demographic.SHOUNEN, Demographic.SHOUJO, Demographic.SEINEN, Demographic.JOSEI),
- )
-
- override suspend fun getListPage(
- page: Int,
- order: SortOrder,
- filter: MangaListFilter,
- ): List {
- val urlBuilder = okhttp3.HttpUrl.Builder()
- .scheme("https")
- .host(domain)
- .addPathSegments("api/titles")
- .addQueryParameter("page", page.toString())
- .addQueryParameter("limit", "50")
-
- if (!filter.query.isNullOrBlank()) {
- urlBuilder.addQueryParameter("keyword", filter.query)
- }
-
- if (filter.yearFrom > 0) {
- urlBuilder.addQueryParameter("year_from", filter.yearFrom.toString())
- }
- if (filter.yearTo > 0) {
- urlBuilder.addQueryParameter("year_to", filter.yearTo.toString())
- }
-
- filter.types.forEach { type ->
- val value = when (type) {
- ContentType.MANGA -> "manga"
- ContentType.MANHWA -> "manhwa"
- ContentType.MANHUA -> "manhua"
- ContentType.OTHER -> "other"
- else -> null
- }
- value?.let { urlBuilder.addQueryParameter("types[]", it) }
- }
-
- filter.demographics.forEach { demo ->
- val id = when (demo) {
- Demographic.JOSEI -> "268919"
- Demographic.SEINEN -> "268920"
- Demographic.SHOUJO -> "268917"
- Demographic.SHOUNEN -> "268918"
- else -> null
- }
- id?.let { urlBuilder.addQueryParameter("demographics[]", it) }
- }
-
- filter.tags.forEach { urlBuilder.addQueryParameter("genres_in[]", it.key) }
- filter.tagsExclude.forEach { urlBuilder.addQueryParameter("genres_ex[]", it.key) }
-
- filter.states.forEach { state ->
- val apiState = when (state) {
- MangaState.ONGOING -> "releasing"
- MangaState.FINISHED -> "finished"
- MangaState.ABANDONED -> "discontinued"
- MangaState.PAUSED -> "on_hiatus"
- MangaState.UPCOMING -> "not_yet_released"
- else -> null
- }
- apiState?.let { urlBuilder.addQueryParameter("statuses[]", it) }
- }
-
- val sortParam = when (order) {
- SortOrder.UPDATED -> "chapter_updated_at" to "desc"
- SortOrder.POPULARITY -> "views_total" to "desc"
- SortOrder.RATING -> "score" to "desc"
- SortOrder.NEWEST -> "created_at" to "desc"
- SortOrder.ALPHABETICAL -> "title" to "asc"
- SortOrder.RELEVANCE -> "relevance" to "desc"
- SortOrder.POPULARITY_WEEK -> "views_7d" to "desc"
- SortOrder.POPULARITY_MONTH -> "views_30d" to "desc"
- else -> null
- }
-
- sortParam?.let { (field, dir) ->
- urlBuilder.addQueryParameter("order[$field]", dir)
- }
-
- val url = urlBuilder.build().toString()
-
- val response = apiClient.httpGet(url).parseJson()
- val items = response.getJSONArray("items")
- val mangas = mutableListOf()
- for (i in 0 until items.length()) {
- val obj = items.getJSONObject(i)
- val hid = obj.getString("hid")
- val slug = obj.optString("slug", null)
- val title = obj.getString("title")
- val poster = obj.optJSONObject("poster")
- val cover = poster?.optString("large")
- ?: poster?.optString("medium")
- ?: poster?.optString("small") ?: ""
- val urlPath = "/title/$hid${slug?.let { "-$it" } ?: ""}"
- mangas.add(
- Manga(
- id = generateUid(urlPath),
- url = urlPath,
- publicUrl = "https://$domain$urlPath",
- title = title,
- coverUrl = cover,
- source = source,
- altTitles = emptySet(),
- largeCoverUrl = null,
- authors = emptySet(),
- contentRating = null,
- rating = RATING_UNKNOWN,
- state = null,
- tags = emptySet(),
- )
- )
- }
- return mangas
- }
-
- override suspend fun getDetails(manga: Manga): Manga {
- detailsCache[manga.url]?.let { return it }
-
- val result = coroutineScope {
- val hid = extractHid(manga.url)
-
- val detailsJson = apiClient.httpGet("https://$domain/api/titles/$hid").parseJson()
- val data = detailsJson.getJSONObject("data")
-
- val hasVolumes = data.optBoolean("hasVolumes", false)
-
- val chaptersDeferred = async { fetchChapters(hid, hasVolumes) }
-
- val title = data.getString("title")
- val poster = data.optJSONObject("poster")
- val cover = poster?.optString("large")
- ?: poster?.optString("medium")
- ?: poster?.optString("small")
- val synopsisHtml = data.optString("synopsisHtml", null)
- val status = data.optString("status", null)
- val type = data.optString("type", null)
- val authorsList = data.optJSONArray("authors")?.let { arr ->
- (0 until arr.length()).map { arr.getJSONObject(it).getString("title") }
- }.orEmpty()
- val artistsList = data.optJSONArray("artists")?.let { arr ->
- (0 until arr.length()).map { arr.getJSONObject(it).getString("title") }
- }.orEmpty()
- val genres = data.optJSONArray("genres")?.let { arr ->
- (0 until arr.length()).map { arr.getJSONObject(it).getString("title") }
- }
- val themes = data.optJSONArray("themes")?.let { arr ->
- (0 until arr.length()).map { arr.getJSONObject(it).getString("title") }
- }
- val altTitlesArray = data.optJSONArray("altTitles")?.let { arr ->
- (0 until arr.length()).map { arr.getString(it) }
- } ?: emptyList()
-
- val rawRating = data.optDouble("rating", -1.0)
- val rating = if (rawRating >= 0.0) (rawRating / 10.0).toFloat() else RATING_UNKNOWN
-
- val synopsisText = synopsisHtml?.let { Jsoup.parseBodyFragment(it).text() } ?: ""
-
- val genreList = buildList {
- type?.let { add(it.replaceFirstChar { c -> c.uppercase() }) }
- genres?.let { addAll(it) }
- themes?.let { addAll(it) }
- }
- val genreTags = genreList.mapNotNull { name ->
- tags.find { it.title == name }
- }.toSet()
-
- val chapters = chaptersDeferred.await()
-
- manga.copy(
- title = title,
- coverUrl = cover ?: manga.coverUrl,
- authors = (authorsList + artistsList).toSet(),
- description = synopsisText.trim(),
- rating = rating,
- state = when (status?.lowercase()) {
- "releasing" -> MangaState.ONGOING
- "finished" -> MangaState.FINISHED
- "discontinued" -> MangaState.ABANDONED
- "on_hiatus" -> MangaState.PAUSED
- "not_yet_released" -> MangaState.UPCOMING
- else -> null
- },
- tags = genreTags,
- altTitles = altTitlesArray.toSet(),
- chapters = chapters,
- )
- }
-
- detailsCache.put(manga.url, result)
- return result
- }
-
- @get:Synchronized
- private val detailsCache = object : LinkedHashMap(16, 0.75f, true) {
- override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean {
- return size > 10
- }
- }
-
- private suspend fun fetchChapters(hid: String, hasVolumes: Boolean): List {
- val chapters = mutableListOf()
- val base = "https://$domain/api/titles/$hid"
-
- var page = 1
- var lastPage: Int
- do {
- val json = apiClient.httpGet(
- "$base/chapters?language=$siteLang&sort=number&order=desc&page=$page&limit=200"
- ).parseJson()
- val items = json.getJSONArray("items")
- val meta = json.optJSONObject("meta")
- lastPage = meta?.optInt("lastPage", 1) ?: 1
-
- for (i in 0 until items.length()) {
- val ch = items.getJSONObject(i)
- if (ch.getString("language") != siteLang) continue
-
- val id = ch.getInt("id")
- val number = ch.getDouble("number").toFloat()
- val name = ch.optString("name", null)
- val createdAt = ch.optLong("createdAt", 0L) * 1000L
- val type = ch.getString("type")
- val chapterUrl = "/title/$hid/$id"
- val displayName = buildString {
- append("Ch. ")
- append(number.toString().removeSuffix(".0"))
- if (!name.isNullOrBlank()) append(" - $name")
- }
- chapters.add(
- MangaChapter(
- id = generateUid(chapterUrl),
- title = displayName,
- number = number,
- volume = 0,
- url = chapterUrl,
- scanlator = null,
- uploadDate = createdAt,
- branch = type,
- source = source,
- )
- )
- }
- page++
- } while (page <= lastPage)
-
- if (hasVolumes) {
- try {
- val volJson = apiClient.httpGet("$base/volumes?language=$siteLang").parseJson()
- val volItems = volJson.getJSONArray("items")
- for (i in 0 until volItems.length()) {
- val vol = volItems.getJSONObject(i)
- if (vol.getString("language") != siteLang) continue
-
- val volId = vol.getInt("id")
- val volNumber = vol.getDouble("number").toFloat()
- val volName = vol.optString("name", "").takeIf { it.isNotBlank() }
- val chapterCount = vol.optInt("chapterCount", 0)
-
- val title = buildString {
- append("Vol. ")
- append(volNumber.toString().removeSuffix(".0"))
- if (volName != null) append(" - $volName")
- }
- val name = if (chapterCount > 0) "$chapterCount chapters" else ""
-
- chapters.add(
- MangaChapter(
- id = generateUid("/title/$hid/vol/$volId"),
- title = title,
- number = volNumber,
- volume = 0,
- url = "/title/$hid/vol/$volId",
- scanlator = name,
- uploadDate = 0L,
- branch = "Volume",
- source = source,
- )
- )
- }
- } catch (_: Exception) {
- }
- }
-
- val distinctBranches = chapters.map { it.branch }.distinct()
- val useGroups = distinctBranches.size > 1
-
- return chapters
- .map { chapter ->
- chapter.copy(
- branch = if (useGroups) (chapter.branch ?: "").replaceFirstChar { it.uppercase() } else null
- )
- }
- .sortedBy { it.number }
- }
-
- override suspend fun getPages(chapter: MangaChapter): List {
- val chapterId = chapter.url.substringAfterLast("/") // numeric ID
- val isVolume = chapter.url.contains("/vol/")
- val endpoint = if (isVolume) "volumes" else "chapters"
-
- val response = apiClient.httpGet("https://$domain/api/$endpoint/$chapterId").parseJson()
- val pagesArray = response.getJSONObject("data").getJSONArray("pages")
- val pages = ArrayList(pagesArray.length())
- for (i in 0 until pagesArray.length()) {
- val pageObj = pagesArray.getJSONObject(i)
- val url = pageObj.getString("url")
- pages.add(
- MangaPage(
- id = generateUid(url),
- url = url,
- preview = null,
- source = source,
- )
- )
- }
- return pages
- }
-
- override suspend fun getRelatedManga(seed: Manga): List = emptyList()
-
- override val authUrl: String get() = "https://$domain"
- override suspend fun isAuthorized(): Boolean = true
- override suspend fun getUsername(): String = ""
-
-
- private fun extractHid(url: String): String {
- val lastPart = url.removeSuffix("/").substringAfterLast("/")
- return when {
- lastPart.contains(".") -> lastPart.substringAfterLast(".")
- lastPart.contains("-") -> lastPart.substringBefore("-")
- else -> lastPart
- }
- }
-
- @MangaSourceParser("MANGAFIRE_EN", "MangaFire (English)", "en")
- class English(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_EN, "en")
-
- @MangaSourceParser("MANGAFIRE_ES", "MangaFire (Spanish)", "es")
- class Spanish(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_ES, "es")
-
- @MangaSourceParser("MANGAFIRE_ESLA", "MangaFire Spanish (Latin)", "es")
- class SpanishLatim(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_ESLA, "es-la")
-
- @MangaSourceParser("MANGAFIRE_FR", "MangaFire (French)", "fr")
- class French(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_FR, "fr")
-
- @MangaSourceParser("MANGAFIRE_JA", "MangaFire (Japanese)", "ja")
- class Japanese(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_JA, "ja")
-
- @MangaSourceParser("MANGAFIRE_PT", "MangaFire (Portuguese)", "pt")
- class Portuguese(context: MangaLoaderContext) : MangaFireParser(context, MangaParserSource.MANGAFIRE_PT, "pt")
-
- @MangaSourceParser("MANGAFIRE_PTBR", "MangaFire Portuguese (Brazil)", "pt")
- class PortugueseBR(context: MangaLoaderContext) :
- MangaFireParser(context, MangaParserSource.MANGAFIRE_PTBR, "pt-br")
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/MangaPlus.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/MangaPlus.kt
deleted file mode 100644
index bf7ea2fc..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/MangaPlus.kt
+++ /dev/null
@@ -1,293 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.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.MangaSourceParser
-import org.koitharu.kotatsu.parsers.config.ConfigKey
-import org.koitharu.kotatsu.parsers.core.SinglePageMangaParser
-import org.koitharu.kotatsu.parsers.model.*
-import org.koitharu.kotatsu.parsers.util.*
-import org.koitharu.kotatsu.parsers.util.json.asTypedList
-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.suspendlazy.suspendLazy
-import java.util.*
-
-internal abstract class MangaPlusParser(
- context: MangaLoaderContext,
- source: MangaParserSource,
- private val sourceLang: String
-):
- SinglePageMangaParser(context, source), Interceptor {
-
- private val apiUrl = "https://jumpg-webapi.tokyo-cdn.com/api"
- override val configKeyDomain = ConfigKey.Domain("mangaplus.shueisha.co.jp")
-
- override fun onCreateConfig(keys: MutableCollection>) {
- super.onCreateConfig(keys)
- keys.add(userAgentKey)
- }
-
- override val availableSortOrders: Set = EnumSet.of(
- SortOrder.POPULARITY,
- SortOrder.UPDATED,
- SortOrder.ALPHABETICAL,
- )
-
- override val filterCapabilities: MangaListFilterCapabilities
- get() = MangaListFilterCapabilities(
- isSearchSupported = true,
- )
-
- override suspend fun getFilterOptions() = MangaListFilterOptions()
-
- private val extraHeaders = Headers.headersOf("Session-Token", UUID.randomUUID().toString())
-
- override suspend fun getList(order: SortOrder, filter: MangaListFilter): List {
- return when {
- filter.query.isNullOrEmpty() -> {
- when (order) {
- SortOrder.POPULARITY -> getPopularList()
- SortOrder.UPDATED -> getLatestList()
- else -> getAllTitleList()
- }
- }
- else -> getAllTitleList(filter.query)
- }
- }
-
- private suspend fun getPopularList(): List {
- val json = apiCall("/title_list/ranking")
-
- return json.getJSONObject("titleRankingView")
- .getJSONArray("titles")
- .asTypedList()
- .toMangaList()
- }
-
- private suspend fun getLatestList(): List {
- val json = apiCall("/title_list/updated")
-
- return json.getJSONObject("titleUpdatedView")
- .getJSONArray("latestTitle")
- .mapJSON { it.getJSONObject("title") }
- .toMangaList()
- }
-
- private val allTitleCache = suspendLazy {
- apiCall("/title_list/allV2")
- .getJSONObject("allTitlesViewV2")
- .getJSONArray("AllTitlesGroup")
- .mapJSON { it.getJSONArray("titles").asTypedList() }
- .flatten()
- }
-
- private suspend fun getAllTitleList(query: String? = null): List {
- return allTitleCache.get().toMangaList(query)
- }
-
- private fun List.toMangaList(query: String? = null): List {
- 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)
-
- 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"),
- altTitles = emptySet(),
- authors = setOf(author),
- contentRating = null,
- 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
- val author = title.getString("author")
- .split("/").joinToString(transform = String::trim)
-
- return manga.copy(
- title = title.getString("name"),
- publicUrl = "/titles/${title.getInt("titleId")}".toAbsoluteUrl(domain),
- coverUrl = title.getString("portraitImageUrl"),
- authors = setOf(author),
- description = buildString {
- json.getString("overview").let(::append)
- json.getStringOrNull("viewingPeriodDescription")
- ?.takeIf { !completed }
- ?.let { append("
", 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 {
- val chapterList = chapterListGroup
- .asTypedList()
- .flatMap {
- it.optJSONArray("firstChapterList")?.asTypedList().orEmpty() +
- it.optJSONArray("lastChapterList")?.asTypedList().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,
- title = subtitle,
- number = chapter.getString("name")
- .substringAfter("#")
- .toFloatOrNull() ?: -1f,
- volume = 0,
- 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 {
- 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,
- )
- }
- }
-
- 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
- }
-
- return response.map { responseBody ->
- val contentType = response.headers["Content-Type"] ?: "image/jpeg"
- val image = responseBody.bytes().decodeXorCipher(encryptionKey)
- image.toResponseBody(contentType.toMediaTypeOrNull())
- }
- }
-
- 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")
- .asTypedList()
- .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, MangaParserSource.MANGAPLUSPARSER_EN, "ENGLISH")
-
- @MangaSourceParser("MANGAPLUSPARSER_ES", "MANGA Plus (Spanish)", "es")
- class Spanish(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_ES, "SPANISH")
-
- @MangaSourceParser("MANGAPLUSPARSER_FR", "MANGA Plus (French)", "fr")
- class French(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_FR, "FRENCH")
-
- @MangaSourceParser("MANGAPLUSPARSER_ID", "MANGA Plus (Indonesian)", "id")
- class Indonesian(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_ID, "INDONESIAN")
-
- @MangaSourceParser("MANGAPLUSPARSER_PTBR", "MANGA Plus Portuguese (Brazil)", "pt")
- class Portuguese(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_PTBR, "PORTUGUESE_BR")
-
- @MangaSourceParser("MANGAPLUSPARSER_RU", "MANGA Plus (Russian)", "ru")
- class Russian(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_RU, "RUSSIAN")
-
- @MangaSourceParser("MANGAPLUSPARSER_TH", "MANGA Plus (Thai)", "th")
- class Thai(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_TH, "THAI")
-
- @MangaSourceParser("MANGAPLUSPARSER_VI", "MANGA Plus (Vietnamese)", "vi")
- class Vietnamese(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_VI, "VIETNAMESE")
-
- @MangaSourceParser("MANGAPLUSPARSER_DE", "MANGA Plus (German)", "de")
- class German(context: MangaLoaderContext) : MangaPlusParser(context, MangaParserSource.MANGAPLUSPARSER_DE, "GERMAN")
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/BuonDua.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/BuonDua.kt
deleted file mode 100644
index 4e555f96..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/BuonDua.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.all.hentais
-
-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.ContentType
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.GalleryParser
-
-@MangaSourceParser("BUONDUA", "Buon Dua", type = ContentType.OTHER)
-internal class BuonDua(context: MangaLoaderContext) :
- GalleryParser(context, MangaParserSource.BUONDUA, "buondua.com") {
-
- override val configKeyDomain: ConfigKey.Domain = ConfigKey.Domain(
- "buondua.com",
- "buondua.us",
- )
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/MangaCrazy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/MangaCrazy.kt
deleted file mode 100644
index fa042ea0..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/MangaCrazy.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.all.hentais
-
-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.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-import java.util.Locale
-
-@MangaSourceParser("MANGACRAZY", "MangaCrazy", "", ContentType.HENTAI)
-internal class MangaCrazy(context: MangaLoaderContext) :
- MadaraParser(context, MangaParserSource.MANGACRAZY, "mangacrazy.net") {
- override val sourceLocale: Locale = Locale.ENGLISH
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/Novelcrow.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/Novelcrow.kt
deleted file mode 100644
index be1dfa16..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/all/hentais/Novelcrow.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.all.hentais
-
-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.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-
-@MangaSourceParser("NOVELCROW", "NovelCrow", "en", ContentType.HENTAI)
-internal class Novelcrow(context: MangaLoaderContext) :
- MadaraParser(context, MangaParserSource.NOVELCROW, "novelcrow.com", 24) {
- override val tagPrefix = "comic-genre/"
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/CoffeeManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/CoffeeManga.kt
deleted file mode 100644
index 86488f68..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/CoffeeManga.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-
-@MangaSourceParser("COFFEEMANGA", "CoffeeManga", "en")
-internal class CoffeeManga(context: MangaLoaderContext) :
- MadaraParser(context, MangaParserSource.COFFEEMANGA, "coffeemanga.ink")
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/EnThunderScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/EnThunderScans.kt
deleted file mode 100644
index 03153da3..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/EnThunderScans.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MangaReaderParser
-
-@MangaSourceParser("ENTHUNDERSCANS", "EnThunderScans", "en")
-internal class EnThunderScans(context: MangaLoaderContext) :
- MangaReaderParser(
- context,
- MangaParserSource.ENTHUNDERSCANS,
- "en-thunderscans.com",
- pageSize = 30,
- searchPageSize = 10,
- ) {
- override val listUrl = "/comics"
-
- override val filterCapabilities: MangaListFilterCapabilities
- get() = super.filterCapabilities.copy(
- isTagsExclusionSupported = false,
- )
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/KingofShojo.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/KingofShojo.kt
deleted file mode 100644
index 7e31556c..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/KingofShojo.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MangaThemesia
-
-@MangaSourceParser("KINGOFSHOJO", "King of Shojo", "en")
-internal class KingofShojo(context: MangaLoaderContext) :
- MangaThemesia(context, MangaParserSource.KINGOFSHOJO, "kingofshojo.com")
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/Mangaread.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/Mangaread.kt
deleted file mode 100644
index 7b3f82d0..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/Mangaread.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-
-@MangaSourceParser("MANGAREAD", "MangaRead", "en")
-internal class MangaRead(context: MangaLoaderContext) :
- MadaraParser(context, MangaParserSource.MANGAREAD, "www.mangaread.org") {
- override val tagPrefix = "genres/"
- override val datePattern = "dd.MM.yyyy"
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/ManhwaNex.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/ManhwaNex.kt
deleted file mode 100644
index ffc91246..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/ManhwaNex.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-
-@MangaSourceParser("MANHWANEX", "ManhwaNex", "en")
-internal class ManhwaNex(context: MangaLoaderContext):
- MadaraParser(context, MangaParserSource.MANHWANEX, "manhwanex.com") {
-
- override val postReq = false
-
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/ManhwaTop.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/ManhwaTop.kt
deleted file mode 100644
index b264ee08..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/ManhwaTop.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-
-@MangaSourceParser("MANHWATOP", "ManhwaTop", "en")
-internal class ManhwaTop(context: MangaLoaderContext) :
- MadaraParser(context, MangaParserSource.MANHWATOP, "manhwatop.com")
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/Novel24h.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/Novel24h.kt
deleted file mode 100644
index 6a3530ea..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/Novel24h.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en
-
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-
-@MangaSourceParser("TWENTYFOURHNOVEL", "24HNovel", "en")
-internal class Novel24hParser(context: MangaLoaderContext) :
- MadaraParser(
- context = context,
- source = MangaParserSource.TWENTYFOURHNOVEL,
- domain = "24hnovel.com"
- ) {
- override val withoutAjax = true
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/ComicLand.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/ComicLand.kt
deleted file mode 100644
index 19cc6732..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/ComicLand.kt
+++ /dev/null
@@ -1,286 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en.hentais
-
-import okhttp3.Interceptor
-import okhttp3.Response
-import org.json.JSONObject
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.config.ConfigKey
-import org.koitharu.kotatsu.parsers.core.PagedMangaParser
-import org.koitharu.kotatsu.parsers.model.*
-import org.koitharu.kotatsu.parsers.util.generateUid
-import org.koitharu.kotatsu.parsers.util.urlEncoded
-
-@MangaSourceParser("COMICLAND", "ComicLand", "en", ContentType.HENTAI)
-internal class ComicLand(context: MangaLoaderContext) : PagedMangaParser(
- context, MangaParserSource.COMICLAND, 20) {
-
- override val configKeyDomain =
- ConfigKey.Domain("comicland.org")
-
- override fun intercept(chain: Interceptor.Chain): Response {
- val request = chain.request()
-
- val newRequest = request.newBuilder()
- .header(
- "Origin",
- "https://comicland.org"
- )
- .header(
- "Referer",
- "https://comicland.org/"
- )
- .build()
-
- return chain.proceed(newRequest)
- }
-
- override val availableSortOrders =
- setOf(
- SortOrder.UPDATED, // Recommended
- SortOrder.POPULARITY, // Popular
- SortOrder.NEWEST // Official/Ongoing equivalent
- )
-
- override val filterCapabilities =
- MangaListFilterCapabilities(
- isMultipleTagsSupported = false,
- isTagsExclusionSupported = false,
- isSearchSupported = true,
- isSearchWithFiltersSupported = true,
- isYearSupported = false,
- isAuthorSearchSupported = false
- )
-
- private val api =
- "https://api.comicland.org/api"
-
-
- override suspend fun getListPage(
- page: Int,
- order: SortOrder,
- filter: MangaListFilter
- ): List {
-
-
- val offset = page * 20
- val endpoint =
- if (!filter.query.isNullOrBlank()) {
- "$api/comic/search?q=${filter.query!!.urlEncoded()}"
- } else {
- val category =
- filter.tags
- .firstOrNull()
- ?.key
- when(category) {
- "official" ->
- "$api/comics/official"
- "ongoing" ->
- "$api/comics"
- "popular" ->
- "$api/comics/popular"
- else ->
- "$api/comics"
- }
- }
-
- val separator =
- if (endpoint.contains("?")) "&" else "?"
-
- val response =
- webClient.httpGet(
- "$endpoint${separator}offset=$offset&limit=20"
- )
-
- val root =
- JSONObject(response.body.string())
-
- val data =
- root.optJSONObject("data")
- ?: return emptyList()
-
- val array =
- data.optJSONArray("list")
- ?: data.optJSONArray("items")
- ?: return emptyList()
-
- val result = ArrayList()
- for (i in 0 until array.length()) {
- val comic =
- array.getJSONObject(i)
- val slug =
- comic.optString("slug")
- result.add(
- Manga(
- id = generateUid(slug),
- title = comic.optString("title"),
- url = slug,
- publicUrl = "https://comicland.org/comic/$slug",
- coverUrl = comic.optString("cover_url"),
- altTitles = emptySet(),
- rating = RATING_UNKNOWN,
- tags = emptySet(),
- authors = emptySet(),
- state = null,
- contentRating = ContentRating.ADULT,
- source = source
- )
- )
- }
- return result
- }
-
- override suspend fun getDetails(
- manga: Manga
- ): Manga {
- val response =
- webClient.httpGet(
- "$api/comic/detail?slug=${manga.url}"
- )
-
- val root =
- JSONObject(response.body.string())
-
- val comic =
- root.optJSONObject("data")
- ?: return manga
-
- val chapters =
- ArrayList()
-
- val chapterArray =
- comic.optJSONArray("chapters")
-
- if (chapterArray != null) {
- for (i in 0 until chapterArray.length()) {
- val chapter =
- chapterArray.getJSONObject(i)
-
- val index =
- chapter.optDouble(
- "chapter_index",
- 0.0
- )
- chapters.add(
- MangaChapter(
- id = generateUid("${comic.optString("slug")}/$index"),
- title = chapter.optString("title"),
- number = index.toFloat(),
- volume = 0,
- url ="/comic/${comic.optString("slug")}/chapter/${index.toString().removeSuffix(".0")}",
- scanlator = null,
- uploadDate = 0,
- branch = null,
- source = source
- )
- )
- }
- }
- return manga.copy(
- title =
- comic.optString(
- "title",
- manga.title
- ),
- description =
- comic.optString(
- "description",
- ""
- ),
- coverUrl =
- comic.optString(
- "cover_url",
- manga.coverUrl
- ),
- authors =
- parseNames(
- comic.optJSONArray("authors")
- ),
- tags = emptySet(),
- chapters = chapters
- )
- }
-
-
- private fun parseNames(
- array: org.json.JSONArray?
- ): Set {
- if (array == null)
- return emptySet()
- return buildSet {
- for (i in 0 until array.length()) {
- add(
- array.getJSONObject(i)
- .optString("name")
- )
- }
- }
- }
-
-
- override suspend fun getPages(
- chapter: MangaChapter
- ): List {
-
- val slug =
- chapter.url
- .substringAfter("/comic/")
- .substringBefore("/chapter/")
-
- val index =
- chapter.url
- .substringAfter("/chapter/")
-
- val response =
- webClient.httpGet(
- "$api/chapter/pages_by_index?slug=$slug&index=$index"
- )
-
- val root =
- JSONObject(response.body.string())
-
- val data =
- root.optJSONObject("data")
- ?: return emptyList()
-
- val pages =
- data.optJSONArray("pages")
- ?: return emptyList()
-
- return buildList {
- for (i in 0 until pages.length()) {
- val image =
- pages.getString(i)
- add(
- MangaPage(
- id = generateUid(image),
- url = image,
- preview = null,
- source = source
- )
- )
- }
- }
- }
-
- private val categories = mapOf(
- "recommended" to "/comics",
- "official" to "/comics/official",
- "ongoing" to "/comics?status=ongoing",
- "popular" to "/comics/popular"
- )
-
- override suspend fun getFilterOptions(): MangaListFilterOptions {
- return MangaListFilterOptions(
- availableTags = categories.map { (key, _) ->
- MangaTag(
- title = key.replaceFirstChar { it.uppercase() },
- key = key,
- source = source
- )
- }.toSet(),
-
- availableContentRating = emptySet()
- )
- }
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/Hiperdex.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/Hiperdex.kt
deleted file mode 100644
index 620224f9..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/Hiperdex.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en.hentais
-
-import okhttp3.Headers
-import org.koitharu.kotatsu.parsers.Broken
-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.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.HiperParser
-
-@Broken
-@MangaSourceParser("HIPERDEX", "Hiperdex", "en", ContentType.HENTAI)
-internal class Hiperdex(context: MangaLoaderContext) :
- HiperParser(context, MangaParserSource.HIPERDEX, "hiperdex.com") {
-
- override fun getRequestHeaders(): Headers = super.getRequestHeaders().newBuilder()
- .set("x-svc-gate", "f5pabmx7sdek")
- .build()
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/Manga18.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/Manga18.kt
deleted file mode 100644
index b6ae5367..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/Manga18.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en.hentais
-
-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.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.Manga18Parser
-
-@MangaSourceParser("MANGA18", "Manga18", "en", ContentType.HENTAI)
-internal class Manga18(context: MangaLoaderContext) :
- Manga18Parser(context, MangaParserSource.MANGA18, "manga18.club")
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/MangaDistrict.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/MangaDistrict.kt
deleted file mode 100644
index 20995789..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/MangaDistrict.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en.hentais
-
-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.MangaChapter
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.model.MangaTag
-import org.koitharu.kotatsu.parsers.model.SortOrder
-import org.koitharu.kotatsu.parsers.parsers.MadaraParser
-import org.koitharu.kotatsu.parsers.util.*
-import java.text.SimpleDateFormat
-import java.util.EnumSet
-
-@MangaSourceParser("MANGADISTRICT", "MangaDistrict", "en", ContentType.HENTAI)
-internal class MangaDistrict(context: MangaLoaderContext) :
- MadaraParser(context, MangaParserSource.MANGADISTRICT, "mangadistrict.com", pageSize = 30) {
-
- override val tagPrefix = "publication-genre/"
- override val withoutAjax: Boolean = true
- override val datePattern: String = "MMMM d, yyyy"
- override val stylePage: String = "?style=list"
-
- override val availableSortOrders: Set = EnumSet.of(
- SortOrder.RELEVANCE,
- SortOrder.NEWEST,
- SortOrder.ALPHABETICAL,
- SortOrder.RATING,
- SortOrder.POPULARITY,
- SortOrder.UPDATED,
- )
-
- override suspend fun getChapters(manga: Manga, doc: Document): List {
- val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
-
- val chapters = doc.body()
- .select("li.wp-manga-chapter")
- .mapNotNull { li ->
- val a = li.selectFirstOrThrow("a")
- val href = a.attrAsRelativeUrl("href")
- val link = href + stylePage
-
- val name = a.selectFirst("p")?.text()
- ?: a.ownText()
-
- val dateText =
- li.selectFirst("a.c-new-tag")?.attr("title")
- ?: li.selectFirst("span.chapter-release-date i")?.text()
-
- MangaChapter(
- id = generateUid(href),
- title = name,
- number = extractChapterNumber(name) ?: 0f,
- volume = 0,
- url = link,
- uploadDate = parseChapterDate(dateFormat, dateText),
- scanlator = null,
- branch = null,
- source = source,
- )
- }
- .sortedWith(
- compareBy { it.number }
- .thenBy { it.title }
- )
-
- return chapters
- }
-
- private fun extractChapterNumber(name: String): Float? {
- val match = Regex(
- """(?:chapter|ch\.?)\s*(\d+(?:\.\d+)?)""",
- RegexOption.IGNORE_CASE
- ).find(name)
-
- return match
- ?.groupValues
- ?.getOrNull(1)
- ?.toFloatOrNull()
- }
-
- override suspend fun fetchAvailableTags(): Set {
- val doc = webClient.httpGet("https://$domain/series/").parseHtml()
- val elements = doc.select("header ul.second-menu li a, div.genres_wrap ul li a")
-
- return elements.mapNotNullToSet { a ->
- val href = a.attr("href")
- .removeSuffix("/")
- .substringAfterLast(tagPrefix, "")
-
- if (href.isBlank()) return@mapNotNullToSet null
-
- MangaTag(
- key = href,
- title = a.text()
- .replace(Regex("""\s*\(\d+\)"""), "")
- .replace(Regex("""[^\x20-\x7E]"""), "")
- .trim()
- .toTitleCase(),
- source = source,
- )
- }
- }
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/OmegaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/OmegaScan.kt
deleted file mode 100644
index bff2c95b..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/en/hentais/OmegaScan.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.en.hentais
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.*
-import org.koitharu.kotatsu.parsers.parsers.HeanCmsParser
-
-@MangaSourceParser("OMEGASCANS", "OmegaScans", "en", ContentType.HENTAI)
-internal class OmegaScans(context: MangaLoaderContext) :
- HeanCmsParser(context, MangaParserSource.OMEGASCANS, "omegascans.org")
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/id/hentais/DoujinDesu.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/id/hentais/DoujinDesu.kt
deleted file mode 100644
index b6c01ea9..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/id/hentais/DoujinDesu.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.id.hentais
-
-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.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.DoujinDesuParser
-import java.util.EnumSet
-
-@MangaSourceParser("DOUJINDESU", "DoujinDesu", "id", ContentType.HENTAI)
-internal class DoujinDesu(context: MangaLoaderContext) :
- DoujinDesuParser(context, MangaParserSource.DOUJINDESU) {
-
- override val defaultTypes: String = "doujinshi,manga,manhwa"
-
- override val availableContentTypes: Set = EnumSet.of(
- ContentType.MANGA,
- ContentType.DOUJINSHI,
- ContentType.MANHWA
- )
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/pt/HiperToon.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/pt/HiperToon.kt
deleted file mode 100644
index b5dbcac1..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/pt/HiperToon.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.pt
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.parsers.HiperParser
-
-@MangaSourceParser("HIPERTOON", "Hipertoon", "pt")
-internal class HiperToon(context: MangaLoaderContext) :
- HiperParser(context, MangaParserSource.HIPERTOON, "hipertoon.com")
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/zh/hentai/Xiutaku.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/zh/hentai/Xiutaku.kt
deleted file mode 100644
index 17c4fc08..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/kotatsu/zh/hentai/Xiutaku.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.kotatsu.zh.hentai
-
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaSourceParser
-import org.koitharu.kotatsu.parsers.model.MangaParserSource
-import org.koitharu.kotatsu.parsers.model.ContentType
-import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
-import org.koitharu.kotatsu.parsers.parsers.GalleryParser
-
-@MangaSourceParser("XIUTAKU", "Xiutaku", "zh", type = ContentType.OTHER)
-internal class Xiutaku(context: MangaLoaderContext) :
- GalleryParser(context, MangaParserSource.XIUTAKU, "xiutaku.com") {
-
- override suspend fun getFilterOptions(): MangaListFilterOptions =
- MangaListFilterOptions(availableTags = fetchTags())
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/mistscans/MistScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/mistscans/MistScans.kt
deleted file mode 100644
index 795f0bcf..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/mistscans/MistScans.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.mistscans
-
-import org.koitharu.kotatsu.parsers.site.tachiyomi.parsers.Keyoapp
-import eu.kanade.tachiyomi.source.model.SManga
-import org.jsoup.nodes.Element
-import org.koitharu.kotatsu.parsers.TachiyomiSource
-
-@TachiyomiSource("MISTSCANS", "Mist Scans", "en")
-class MistScans : Keyoapp() {
-
- override fun popularMangaSelector() = ".series-splide .splide__slide"
-
- override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
- val a = element.selectFirst("a[href]")!!
- title = a.attr("title")
- setUrlWithoutDomain(a.attr("abs:href"))
- thumbnail_url = element.getImageUrl("*[style*=background-image]")
- }
-
- override val name: String
- get() = "Mist Scans"
- override val lang: String
- get() = "en"
- override val baseUrl: String
- get() = "https://mistscans.com"
-}
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/qiscan/QiScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/qiscan/QiScan.kt
deleted file mode 100644
index 522caf2b..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/qiscan/QiScan.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.qiscan
-
-import org.koitharu.kotatsu.parsers.site.tachiyomi.parsers.ezmanhwa.EZManhwa
-import org.koitharu.kotatsu.parsers.site.tachiyomi.parsers.ezmanhwa.EZManhwaSortFilter
-import org.koitharu.kotatsu.parsers.site.tachiyomi.parsers.ezmanhwa.EZManhwaStatusFilter
-import org.koitharu.kotatsu.parsers.site.tachiyomi.parsers.ezmanhwa.EZManhwaTypeFilter
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.model.FilterList
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.Request
-import org.koitharu.kotatsu.parsers.TachiyomiSource
-
-
-@TachiyomiSource("QISCANS", "QiScans", "en")
-class QiScans : EZManhwa("QiScans", "https://qimanga.com", "https://api.qimanga.com/api/v1") {
-
- override fun headersBuilder() = super.headersBuilder()
- .set("Origin", baseUrl)
- .set("Sec-Fetch-Dest", "empty")
- .set("Sec-Fetch-Mode", "cors")
- .set("Sec-Fetch-Site", "same-site")
-
- // QiScans search endpoint ignores filters — only send the query when searching.
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- val isSearch = query.isNotBlank()
- val endpoint = if (isSearch) "$apiUrl/series/search" else "$apiUrl/series"
- val url = endpoint.toHttpUrl().newBuilder().apply {
- addQueryParameter("page", page.toString())
- addQueryParameter("perPage", "20")
- if (isSearch) {
- addQueryParameter("q", query)
- } else {
- var sortAdded = false
- for (filter in filters) {
- when (filter) {
- is EZManhwaSortFilter -> {
- addQueryParameter("sort", filter.value)
- sortAdded = true
- }
- is EZManhwaStatusFilter -> if (filter.value.isNotBlank()) addQueryParameter("status", filter.value)
- is EZManhwaTypeFilter -> if (filter.value.isNotBlank()) addQueryParameter("type", filter.value)
- is QiScansGenreFilter -> if (filter.value.isNotBlank()) addQueryParameter("genre", filter.value)
- else -> {}
- }
- }
- if (!sortAdded) addQueryParameter("sort", "latest")
- }
- }.build()
- return GET(url, headers)
- }
-
- override fun getFilterList() = FilterList(
- EZManhwaSortFilter(),
- EZManhwaStatusFilter(),
- EZManhwaTypeFilter(),
- QiScansGenreFilter(),
- )
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/qiscan/QiScansGenreFilter.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/qiscan/QiScansGenreFilter.kt
deleted file mode 100644
index d4c29a0b..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/qiscan/QiScansGenreFilter.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.qiscan
-
-import eu.kanade.tachiyomi.source.model.Filter
-
-class QiScansGenreFilter : Filter.Select("Genre", DISPLAY) {
- val value get() = VALUES[state]
-
- companion object {
- private val ENTRIES = arrayOf(
- "All" to "",
- "Acting" to "acting",
- "Action" to "action",
- "Action (Alt)" to "action-582",
- "Adventure" to "adventure",
- "Adventure (Alt)" to "adventure-589",
- "Apocalypse" to "apocalypce",
- "Comedy" to "comedy",
- "Cooking" to "cooking",
- "Crazy MC" to "crazy-mc",
- "Cultivation" to "cultivation",
- "Drama" to "drama",
- "Ecchi" to "ecchi",
- "Fantasy" to "fantasy",
- "Fantasy (Alt)" to "fantasy-747",
- "Fight" to "fight",
- "Gender Bender" to "gender-bender",
- "Harem" to "harem",
- "Hidden" to "hidden",
- "Historical" to "historical",
- "Horror" to "horror",
- "Josei" to "josei",
- "Live" to "live",
- "Magic" to "magic",
- "Manhua" to "manhua",
- "Martial Arts" to "martial-arts",
- "Mature" to "mature",
- "Mecha" to "mecha",
- "Medieval Area" to "medieval-area",
- "Munchkin" to "munchkin",
- "Murim" to "murim",
- "Mystery" to "mystery",
- "Myth" to "myth",
- "Politics" to "politics",
- "Psychological" to "psychological",
- "Reincarnation" to "reincarnation",
- "Revenge" to "revenge",
- "Romance" to "romance",
- "School Life" to "school-life",
- "Sci-Fi" to "sci-fi",
- "Seinen" to "seinen",
- "Shounen" to "shounen",
- "Slice of Life" to "slice-of-life",
- "Sports" to "sports",
- "Supernatural" to "supernatural",
- "Superpower" to "superpower",
- "System" to "system",
- "Taming" to "taming",
- "Tower" to "tower",
- "Tragedy" to "tragedy",
- "Urban" to "urban",
- "Vampires" to "vampiers",
- "Virtual Reality" to "virtual-reality",
- "Wuxia" to "wuxia",
- )
- val DISPLAY = ENTRIES.map { it.first }.toTypedArray()
- val VALUES = ENTRIES.map { it.second }.toTypedArray()
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/Dto.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/Dto.kt
deleted file mode 100644
index 031a927c..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/Dto.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.scansgg
-
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import keiyoushi.utils.tryParse
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import java.text.SimpleDateFormat
-
-@Serializable
-class ResponseDto(
- val data: T,
- val meta: MetaDto? = null,
-)
-
-@Serializable
-class MetaDto(
- @SerialName("has_more") val hasMore: Boolean = false,
-)
-
-@Serializable
-class SeriesDto(
- private val id: Int,
- private val title: String,
- private val summary: String? = null,
- private val cover: String? = null,
- private val author: List? = null,
- private val artist: List? = null,
- private val tags: List? = null,
- private val status: Int? = null,
-) {
- fun toSManga(cdnUrl: String, tagsMap: Map? = null) = SManga.create().apply {
- url = id.toString()
- this.title = this@SeriesDto.title
- thumbnail_url = cover?.let { "$cdnUrl/covers/$it" }
- if (tagsMap != null) {
- description = summary
- author = this@SeriesDto.author?.joinToString()
- artist = this@SeriesDto.artist?.joinToString()
- genre = tags?.mapNotNull { tagsMap[it] }?.joinToString()
- this.status = when (this@SeriesDto.status) {
- 1 -> SManga.ONGOING
- 2 -> SManga.COMPLETED
- 3, 4, 5 -> SManga.CANCELLED
- else -> SManga.UNKNOWN
- }
- }
- }
-}
-
-@Serializable
-class ChapterDto(
- private val id: Int,
- private val number: Float,
- private val title: String? = null,
- @SerialName("created_at") private val createdAt: String? = null,
- @SerialName("group_id") private val groupId: Int? = null,
- private val group: GroupDto? = null,
-) {
- fun toSChapter(seriesIdStr: String, dateFormat: SimpleDateFormat) = SChapter.create().apply {
- // SChapter.url is used as a full API path to avoid passing multiple fields later
- url = "/chapter-navigation?series_id=$seriesIdStr&chapter_id=$id&group_id=${groupId ?: 0}"
- name = buildString {
- append("Chapter ")
- append(number.toString().removeSuffix(".0"))
- if (!title.isNullOrEmpty()) {
- append(" - ")
- append(title)
- }
- }
- date_upload = dateFormat.tryParse(createdAt)
- scanlator = group?.title
- }
-}
-
-@Serializable
-class GroupDto(
- val title: String? = null,
-)
-
-@Serializable
-class PageListDto(
- private val chapter: ChapterDataDto? = null,
-) {
- fun toPages(cdnUrl: String): List {
- val chapterId = chapter?.id ?: return emptyList()
- return chapter.pages?.map {
- Page(it.position, imageUrl = "$cdnUrl/pages/$chapterId/${it.path}")
- } ?: emptyList()
- }
-}
-
-@Serializable
-class ChapterDataDto(
- val id: Int? = null, // Capturing the chapter id to use it in the image path
- val pages: List? = null,
-)
-
-@Serializable
-class PageDto(
- val position: Int,
- val path: String,
-)
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/Filters.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/Filters.kt
deleted file mode 100644
index 947dddf6..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/Filters.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.scansgg
-
-import eu.kanade.tachiyomi.source.model.Filter
-
-class TypeFilter : Filter.Group("Type", types.map { TypeCheckBox(it.first, it.second) })
-class TypeCheckBox(name: String, val id: Int) : Filter.CheckBox(name)
-
-private val types = listOf(
- Pair("Comic", 1),
- Pair("Manga", 2),
- Pair("Manhwa", 3),
- Pair("Manhua", 4),
- Pair("Webtoon", 5),
-)
-
-class StatusFilter : Filter.Group("Status", statuses.map { StatusCheckBox(it.first, it.second) })
-class StatusCheckBox(name: String, val id: Int) : Filter.CheckBox(name)
-
-private val statuses = listOf(
- Pair("Ongoing", 1),
- Pair("Completed", 2),
- Pair("Hiatus", 3),
- Pair("Cancelled", 4),
- Pair("Dropped", 5),
-)
-
-class TagFilter : Filter.Group("Tags", tagsMap.map { TagCheckBox(it.value, it.key) })
-class TagCheckBox(name: String, val id: Int) : Filter.CheckBox(name)
-
-val tagsMap = mapOf(
- 49 to "Regression", 48 to "Male Protagonist", 47 to "Survival",
- 46 to "Avant Garde", 45 to "Award Winning", 44 to "Lolicon",
- 43 to "Mahou Shoujo", 42 to "Doujinshi", 41 to "Girls Love",
- 40 to "Hentai", 39 to "Mecha", 38 to "Shotacon", 37 to "Ecchi",
- 36 to "Music", 35 to "Smut", 34 to "Erotica", 33 to "Adult",
- 32 to "Gourmet", 31 to "Yuri", 30 to "Shoujo Ai", 29 to "Yaoi",
- 28 to "Shounen Ai", 27 to "Boys Love", 26 to "Harem", 25 to "Tragedy",
- 24 to "Gender Bender", 23 to "Suspense", 22 to "Psychological",
- 21 to "Mature", 20 to "Horror", 19 to "Mystery", 18 to "Martial Arts",
- 17 to "Sci-fi", 16 to "Adventure", 15 to "Supernatural", 14 to "Sports",
- 13 to "Shounen", 12 to "Historical", 11 to "Seinen", 10 to "Action",
- 9 to "Josei", 8 to "Thriller", 7 to "School Life", 6 to "Slice Of Life",
- 5 to "Drama", 4 to "Comedy", 3 to "Shoujo", 2 to "Romance", 1 to "Fantasy",
-)
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/ScansGG.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/ScansGG.kt
deleted file mode 100644
index 840c6eee..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/scansgg/ScansGG.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.scansgg
-
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.source.model.MangasPage
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import keiyoushi.utils.firstInstanceOrNull
-import keiyoushi.utils.parseAs
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.Request
-import okhttp3.Response
-import org.koitharu.kotatsu.parsers.TachiyomiSource
-import rx.Observable
-import java.text.SimpleDateFormat
-import java.util.Locale
-import java.util.TimeZone
-import eu.kanade.tachiyomi.network.asObservable
-
-@TachiyomiSource("SCANSGG", "ScansGG", "en")
-class ScansGG : HttpSource() {
-
- override val name = "ScansGG"
- override val baseUrl = "https://scans.gg"
- override val lang = "en"
- override val supportsLatest = true
-
- private val apiUrl = "https://api.scans.gg"
- private val cdnUrl = "https://cdn.scans.gg/uploads"
-
- override fun headersBuilder() = super.headersBuilder()
- .add("Origin", baseUrl)
- .add("Referer", "$baseUrl/")
-
- private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT).apply {
- timeZone = TimeZone.getTimeZone("UTC")
- }
-
- // ============================== Popular ==============================
-
- override fun popularMangaRequest(page: Int): Request {
- val url = "$apiUrl/series".toHttpUrl().newBuilder()
- .addQueryParameter("limit", POPULAR_LIMIT.toString())
- .addQueryParameter("offset", ((page - 1) * POPULAR_LIMIT).toString())
- .build()
- return GET(url, headers)
- }
-
- override fun popularMangaParse(response: Response): MangasPage {
- val dto = response.parseAs>>()
- val mangas = dto.data.map { it.toSManga(cdnUrl) }
-
- // The /series endpoint doesn't return the meta pagination object,
- // so we check if the returned items match our limit
- return MangasPage(mangas, mangas.size == POPULAR_LIMIT)
- }
-
- // ============================== Latest ===============================
-
- override fun latestUpdatesRequest(page: Int): Request {
- val url = "$apiUrl/chapters".toHttpUrl().newBuilder()
- .addQueryParameter("page", page.toString())
- .addQueryParameter("limit", LATEST_LIMIT.toString())
- .addQueryParameter("chapters", "true")
- .addQueryParameter("series_details", "true")
- .addQueryParameter("group_details", "true")
- .addQueryParameter("sort", "date")
- .build()
- return GET(url, headers)
- }
-
- override fun latestUpdatesParse(response: Response): MangasPage {
- val dto = response.parseAs>>()
- val mangas = dto.data.map { it.toSManga(cdnUrl) }
- return MangasPage(mangas, dto.meta?.hasMore == true)
- }
-
- // ============================== Search ===============================
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- val url = "$apiUrl/series".toHttpUrl().newBuilder().apply {
- addQueryParameter("limit", POPULAR_LIMIT.toString())
- addQueryParameter("offset", ((page - 1) * POPULAR_LIMIT).toString())
- if (query.isNotEmpty()) {
- addQueryParameter("q", query)
- }
-
- val types = filters.firstInstanceOrNull()?.state?.filter { it.state }?.map { it.id } ?: emptyList()
- val statuses = filters.firstInstanceOrNull()?.state?.filter { it.state }?.map { it.id } ?: emptyList()
- val tags = filters.firstInstanceOrNull()?.state?.filter { it.state }?.map { it.id } ?: emptyList()
-
- addQueryParameter("q_type", types.joinToString(",", "[", "]"))
- addQueryParameter("q_status", statuses.joinToString(",", "[", "]"))
- addQueryParameter("q_tags", tags.joinToString(",", "[", "]"))
- }.build()
-
- return GET(url, headers)
- }
-
- override fun searchMangaParse(response: Response) = popularMangaParse(response)
-
- // ============================== Details ==============================
-
- override fun getMangaUrl(manga: SManga): String = "$baseUrl/series/${manga.url}"
-
- override fun mangaDetailsRequest(manga: SManga): Request {
- val url = "$apiUrl/series".toHttpUrl().newBuilder()
- .addQueryParameter("id", manga.url)
- .addQueryParameter("trackers", "true")
- .addQueryParameter("sources", "true")
- .build()
- return GET(url, headers)
- }
-
- override fun mangaDetailsParse(response: Response): SManga {
- val dto = response.parseAs>()
- return dto.data.toSManga(cdnUrl, tagsMap)
- }
-
- // ============================= Chapters ==============================
-
-// ============================= Chapters ==============================
-
- override fun getChapterUrl(chapter: SChapter): String {
- val url = (apiUrl + chapter.url).toHttpUrl()
- val seriesId = url.queryParameter("series_id") ?: return baseUrl
- return "$baseUrl/series/$seriesId"
- }
-
- override fun fetchChapterList(manga: SManga): Observable> =
- client.newCall(chapterListRequest(manga))
- .asObservable()
- .map(::chapterListParseApi)
-
- override fun chapterListRequest(manga: SManga): Request {
- val url = "$apiUrl/chapters".toHttpUrl().newBuilder()
- .addQueryParameter("series_id", manga.url)
- .addQueryParameter("limit", CHAPTER_LIMIT.toString())
- .addQueryParameter("page", "1")
- .addQueryParameter("group_details", "true")
- .build()
-
- return GET(url, headers)
- }
-
- private fun chapterListParseApi(response: Response): List {
- val mangaId = response.request.url.queryParameter("series_id")
- ?: throw Exception("Missing series id")
-
- val dto = response.parseAs>>()
-
- return dto.data.map {
- it.toSChapter(mangaId, dateFormat)
- }
- }
-
- override fun chapterListParse(response: Response): List =
- throw UnsupportedOperationException()
- // =============================== Pages ===============================
-
- override fun pageListRequest(chapter: SChapter): Request = GET(apiUrl + chapter.url, headers)
-
- override fun pageListParse(response: Response): List {
- val dto = response.parseAs>()
- return dto.data.toPages(cdnUrl)
- }
-
- override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
-
- // ============================== Filters ==============================
-
- override fun getFilterList() = FilterList(
- TypeFilter(),
- StatusFilter(),
- TagFilter(),
- )
-
- companion object {
- private const val POPULAR_LIMIT = 21
- private const val LATEST_LIMIT = 14
- private const val CHAPTER_LIMIT = 100
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Constants.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Constants.kt
deleted file mode 100644
index 0dfe0ea5..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Constants.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.vortexscan
-
-import java.util.Locale
-
-const val PER_PAGE = 18
-
-const val SERIES_PATH_SEGMENT = "series"
-
-const val API_QUERY_PATH = "/api/query"
-const val API_POST_PATH = "/api/post"
-const val API_CHAPTER_PATH = "/api/chapter"
-
-const val STATUS_FILTER_KEY = "seriesStatus"
-val STATUS_OPTIONS: Options = listOf(
- "All" to "",
- "Ongoing" to "ONGOING",
- "Completed" to "COMPLETED",
- "Cancelled" to "CANCELLED",
- "Dropped" to "DROPPED",
- "Coming soon" to "COMING_SOON",
- "Mass released" to "MASS_RELEASED",
-)
-
-const val TYPE_FILTER_KEY = "seriesType"
-val TYPE_OPTIONS: Options = listOf(
- "All" to "",
- "Manga" to "MANGA",
- "Manhua" to "MANHUA",
- "Manhwa" to "MANHWA",
-)
-
-const val SORT_FILTER_KEY = "orderBy"
-val SORT_OPTIONS: Options = listOf(
- "Last chapter added" to "lastChapterAddedAt",
- "Views" to "totalViews",
- "Created at" to "createdAt",
- "Chapter count" to "chaptersCount",
- "Alphabetical" to "postTitle",
-)
-
-const val SORT_DIRECTION_FILTER_KEY = "orderDirection"
-val SORT_DIRECTION_OPTIONS: Options = listOf(
- "Descending" to "desc",
- "Ascending" to "asc",
-)
-
-const val GENRE_INCLUDE_FILTER_KEY = "genreIds"
-const val GENRE_EXCLUDE_FILTER_KEY = "excludedGenreIds"
-
-val GENRE_OPTIONS: Options = listOf(
- "Action" to "1",
- "Drama" to "2",
- "Shounen" to "3",
- "Sports" to "4",
- "Manhwa" to "5",
- "Martial Arts" to "6",
- "Comedy" to "7",
- "Fantasy" to "8",
- "Horror" to "9",
- "Seinen" to "10",
- "Supernatural" to "11",
- "Mature" to "12",
- "Adventure" to "13",
- "Monsters" to "14",
- "System" to "15",
- "Reincarnation" to "16",
- "Revenge" to "17",
- "Slice Of Life" to "18",
- "Historical" to "19",
- "Romance" to "20",
- "Josei" to "21",
- "Shoujo" to "22",
- "School Life" to "23",
- "terror" to "24",
- "elf" to "25",
- "shojo" to "26",
- "Video Games" to "27",
- "Fantas" to "28",
- "WEB COMIC" to "29",
- "Webtoons" to "30",
- "Murim" to "31",
- "Restaurant" to "32",
- "Webtoon" to "33",
- "+100 Chapter" to "34",
- "Tower" to "35",
- "Legendary " to "36",
- "Dungeons" to "37",
- "bully" to "38",
- "orphan" to "39",
- "Sci-Fi" to "40",
- "Gore" to "41",
- "Isekai" to "42",
- "magic" to "43",
- "blood" to "44",
- "war" to "45",
- "magic and sword" to "46",
- "academy" to "47",
- "violence" to "48",
- "Harem" to "49",
- "Myth" to "50",
- "OverpoweredMC" to "51",
- "TerritoryManagement" to "52",
- "Swordsman" to "53",
- "Necromancer" to "54",
- "Mage" to "55",
- "JackOfAllTrades" to "56",
- "Artifacts" to "57",
- "CharacterGrowth" to "58",
- "Mercenary" to "59",
- "Elementals" to "60",
- "Genius" to "61",
- "Psychological" to "62",
- "Tragedy" to "63",
- "Gender Bender" to "64",
-).sortedBy { it.first.trim().lowercase(Locale.ROOT) }
-
-const val POPULAR_ORDER_BY = "totalViews"
-const val LATEST_ORDER_BY = "lastChapterAddedAt"
-const val ORDER_DESC = "desc"
-
-const val SHOW_LOCKED_CHAPTERS_PREF_KEY = "pref_show_locked_chapters"
-const val SHOW_LOCKED_CHAPTERS_DEFAULT = false
-
-const val MISSING_TITLE_MESSAGE = "Series title is missing"
-const val LOCKED_CHAPTER_MESSAGE = "Unlock chapter in WebView"
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Dto.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Dto.kt
deleted file mode 100644
index 6b296ff6..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Dto.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.vortexscan
-
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.JsonPrimitive
-
-@Serializable
-class SearchResponseDto(
- val posts: List,
- val totalCount: Int,
-)
-
-@Serializable
-class PostSummaryDto(
- val id: Int,
- val slug: String,
- val postTitle: String,
- val featuredImage: String? = null,
- val seriesStatus: String? = null,
- val genres: List = emptyList(),
-)
-
-@Serializable
-class GenreDto(
- val name: String,
-)
-
-@Serializable
-class PostResponseDto(
- val post: PostDto,
-)
-
-@Serializable
-class PostDto(
- val id: Int,
- val slug: String,
- val postTitle: String,
- val postContent: String? = null,
- val alternativeTitles: String? = null,
- val author: String? = null,
- val artist: String? = null,
- val featuredImage: String? = null,
- val seriesType: String? = null,
- val seriesStatus: String? = null,
- val genres: List = emptyList(),
- val chapters: List = emptyList(),
-)
-
-@Serializable
-class PostChapterDto(
- val id: Int,
- val slug: String,
- val number: JsonPrimitive,
- val title: String? = null,
- val createdAt: String,
- val isLocked: Boolean? = null,
- val isAccessible: Boolean? = null,
-)
-
-@Serializable
-class ChapterResponseDto(
- val chapter: ChapterDto,
-)
-
-@Serializable
-class ChapterDto(
- val isLocked: Boolean? = null,
- val isAccessible: Boolean? = null,
- val images: List = emptyList(),
-)
-
-@Serializable
-class ChapterImageDto(
- val url: String,
- val order: Int? = null,
-)
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Filters.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Filters.kt
deleted file mode 100644
index 217083bc..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Filters.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.vortexscan
-
-import eu.kanade.tachiyomi.source.model.Filter
-import okhttp3.HttpUrl
-
-typealias Options = List>
-
-interface UrlPartFilter {
- fun addUrlParameter(url: HttpUrl.Builder)
-}
-
-abstract class SelectFilter(
- name: String,
- private val urlParameter: String,
- private val options: Options,
- defaultValue: String? = null,
-) : Filter.Select(
- name,
- options.map { it.first }.toTypedArray(),
- options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
-),
- UrlPartFilter {
- override fun addUrlParameter(url: HttpUrl.Builder) {
- options[state].second.takeIf { it.isNotBlank() }?.let {
- url.addQueryParameter(urlParameter, it)
- }
- }
-}
-
-class StatusFilter(
- title: String,
- key: String,
- options: Options,
-) : SelectFilter(title, key, options)
-
-class TypeFilter(
- title: String,
- key: String,
- options: Options,
-) : SelectFilter(title, key, options)
-
-class SortFilter(
- title: String,
- key: String,
- options: Options,
- defaultValue: String? = null,
-) : SelectFilter(title, key, options, defaultValue)
-
-class SortDirectionFilter(
- title: String,
- key: String,
- options: Options,
- defaultValue: String? = null,
-) : SelectFilter(title, key, options, defaultValue)
-
-class GenreFilter(
- name: String,
- genres: Options,
-) : Filter.Group(
- name,
- genres.map { GenreValue(it.first, it.second) },
-),
- UrlPartFilter {
- override fun addUrlParameter(url: HttpUrl.Builder) {
- val included = state
- .filter { it.state == Filter.TriState.STATE_INCLUDE }
- .map { it.id }
-
- val excluded = state
- .filter { it.state == Filter.TriState.STATE_EXCLUDE }
- .map { it.id }
-
- if (included.isNotEmpty()) {
- url.addQueryParameter(GENRE_INCLUDE_FILTER_KEY, included.joinToString(","))
- }
-
- if (excluded.isNotEmpty()) {
- url.addQueryParameter(GENRE_EXCLUDE_FILTER_KEY, excluded.joinToString(","))
- }
- }
-}
-
-class GenreValue(name: String, val id: String) : Filter.TriState(name)
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Helpers.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Helpers.kt
deleted file mode 100644
index 4b6e49e2..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Helpers.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.vortexscan
-
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-
-class DeepLink(
- val mangaSlug: String,
- val chapterSlug: String?,
-)
-
-fun parseDeepLink(query: String, baseHost: String): DeepLink? {
- val url = query.toHttpUrlOrNull() ?: return null
- if (url.host != baseHost) return null
-
- val pathSegments = url.pathSegments
- if (pathSegments.getOrNull(0) != SERIES_PATH_SEGMENT) return null
-
- val mangaSlug = pathSegments.getOrNull(1)
- ?.takeIf { it.isNotBlank() }
- ?: return null
-
- val chapterSlug = pathSegments.getOrNull(2)
- ?.takeIf { it.isNotBlank() }
-
- return DeepLink(mangaSlug, chapterSlug)
-}
-
-fun extractMangaSlug(rawUrl: String): String {
- val pathSegments = rawUrl.substringBefore('#').trim('/').split('/').filter { it.isNotBlank() }
-
- return when {
- pathSegments.size >= 2 && pathSegments[0] == SERIES_PATH_SEGMENT -> pathSegments[1]
- pathSegments.isNotEmpty() -> pathSegments[0]
- else -> throw Exception("Invalid manga url")
- }
-}
-
-fun extractChapterSlugs(rawUrl: String): Pair? {
- val pathSegments = rawUrl.substringBefore('#').trim('/').split('/').filter { it.isNotBlank() }
-
- return when {
- pathSegments.size >= 3 && pathSegments[0] == SERIES_PATH_SEGMENT -> pathSegments[1] to pathSegments[2]
- pathSegments.size >= 2 -> pathSegments[pathSegments.size - 2] to pathSegments.last()
- else -> null
- }
-}
-
-fun buildSlugSearchTerms(slug: String): List {
- val withSpaces = slug.replace('-', ' ')
- val withoutApostrophe = withSpaces.replace("'", " ")
-
- return listOf(
- withSpaces,
- withoutApostrophe,
- collapseSpaces(withoutApostrophe),
- slug,
- ).map { it.trim() }
- .filter { it.isNotBlank() }
- .distinct()
-}
-
-private fun collapseSpaces(text: String): String {
- var value = text
- while (value.contains(" ")) {
- value = value.replace(" ", " ")
- }
- return value
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Mapper.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Mapper.kt
deleted file mode 100644
index 3cde0ce5..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/Mapper.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.vortexscan
-
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import keiyoushi.utils.tryParse
-import org.jsoup.Jsoup
-import java.text.SimpleDateFormat
-
-fun PostSummaryDto.toSMangaSummary(): SManga {
- val cleanTitle = postTitle.trim()
- if (cleanTitle.isEmpty()) throw Exception(MISSING_TITLE_MESSAGE)
-
- return SManga.create().apply {
- url = "$slug#$id"
- title = cleanTitle
- thumbnail_url = featuredImage
- genre = genres.joinToString { it.name }
- status = mapStatus(seriesStatus)
- }
-}
-
-fun PostDto.toSMangaDetailsModel(): SManga {
- val cleanTitle = postTitle.trim()
- if (cleanTitle.isEmpty()) throw Exception(MISSING_TITLE_MESSAGE)
-
- return SManga.create().apply {
- url = "$slug#$id"
- title = cleanTitle
- description = buildDescription(this@toSMangaDetailsModel)
- author = this@toSMangaDetailsModel.author?.trim()?.takeUnless { it.isEmpty() }
- artist = this@toSMangaDetailsModel.artist?.trim()?.takeUnless { it.isEmpty() }
- genre = buildGenre(seriesType, genres)
- status = mapStatus(seriesStatus)
- thumbnail_url = featuredImage
- initialized = true
- }
-}
-
-fun PostChapterDto.toSChapterModel(mangaSlug: String, dateFormat: SimpleDateFormat): SChapter {
- val rawNumber = number.toString().trim('"').trim()
- val fallbackNumber = slug.substringAfter("chapter-", "")
- val chapterNumberText = if (rawNumber.isNotEmpty()) rawNumber else fallbackNumber
- val chapterTitle = title?.trim().orEmpty()
-
- return SChapter.create().apply {
- url = "$mangaSlug/$slug#$id"
- name = buildString {
- if (isAccessible == false || isLocked == true) {
- append("🔒 ")
- }
-
- append("Chapter")
- if (chapterNumberText.isNotEmpty()) {
- append(' ')
- append(chapterNumberText)
- }
-
- if (chapterTitle.isNotEmpty()) {
- append(" - ")
- append(chapterTitle)
- }
- }
- chapterNumberText.toFloatOrNull()?.let { chapter_number = it }
- date_upload = dateFormat.tryParse(createdAt)
- }
-}
-
-private fun buildDescription(post: PostDto): String? {
- val synopsis = post.postContent
- ?.takeIf { it.isNotBlank() }
- ?.replace("
", "\n")
- ?.replace("
", "\n")
- ?.replace("
", "\n")
- ?.let { Jsoup.parse(it).text() }
- ?.trim()
- ?.takeIf { it.isNotEmpty() }
-
- val alternativeTitles = post.alternativeTitles
- ?.lineSequence()
- ?.map { it.trim() }
- ?.filter { it.isNotEmpty() }
- ?.toList()
- ?.takeIf { it.isNotEmpty() }
- ?.joinToString("\n")
-
- return buildString {
- synopsis?.let { append(it) }
- alternativeTitles?.let {
- if (isNotEmpty()) append("\n\n")
- append("Alternative titles:\n")
- append(it)
- }
- }.takeIf { it.isNotBlank() }
-}
-
-private fun buildGenre(seriesType: String?, genres: List): String? {
- val values = mutableListOf()
-
- when (seriesType?.uppercase()) {
- "MANGA" -> values += "Manga"
- "MANHUA" -> values += "Manhua"
- "MANHWA" -> values += "Manhwa"
- }
-
- genres.mapTo(values) { it.name }
-
- val out = values.distinct().joinToString()
- return out.takeIf { it.isNotBlank() }
-}
-
-private fun mapStatus(status: String?): Int = when (status) {
- "ONGOING", "COMING_SOON", "MASS_RELEASED" -> SManga.ONGOING
- "COMPLETED" -> SManga.COMPLETED
- "CANCELLED", "DROPPED" -> SManga.CANCELLED
- else -> SManga.UNKNOWN
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/VortexScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/VortexScan.kt
deleted file mode 100644
index 188e56c7..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/en/vortexscan/VortexScan.kt
+++ /dev/null
@@ -1,290 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.en.vortexscan
-
-import androidx.preference.PreferenceScreen
-import androidx.preference.SwitchPreferenceCompat
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.ConfigurableSource
-import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.source.model.MangasPage
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import keiyoushi.utils.getPreferencesLazy
-import keiyoushi.utils.parseAs
-import okhttp3.Headers
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.Request
-import okhttp3.Response
-import org.koitharu.kotatsu.parsers.TachiyomiSource
-import rx.Observable
-import java.text.SimpleDateFormat
-import java.util.Locale
-
-@TachiyomiSource("VORTEXSCANS", "Vortex Scans", "en")
-class VortexScans :
- HttpSource(),
- ConfigurableSource {
-
- private lateinit var summaryOn: String
- private lateinit var summaryOff: String
- override val name = "Vortex Scans"
- override val baseUrl = "https://vortexscans.org"
- private val apiUrl = "https://api.vortexscans.org"
- override val lang = "en"
- override val supportsLatest = true
-
- private val preferences by getPreferencesLazy()
-
- private val baseHost = baseUrl.toHttpUrl().host
- private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
- private val showLockedChapters
- get() = preferences.getBoolean(SHOW_LOCKED_CHAPTERS_PREF_KEY, SHOW_LOCKED_CHAPTERS_DEFAULT)
-
- override fun headersBuilder() = super.headersBuilder()
- .set("Referer", "$baseUrl/")
-
- private val apiHeaders: Headers by lazy {
- headersBuilder()
- .set("Accept", "application/json, text/plain, */*")
- .set("Origin", baseUrl)
- .build()
- }
-
- // ========================= Popular =========================
-
- override fun popularMangaRequest(page: Int): Request = queryRequest(
- page = page,
- query = "",
- orderBy = POPULAR_ORDER_BY,
- orderDirection = ORDER_DESC,
- )
-
- override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
-
- // ========================= Latest =========================
-
- override fun latestUpdatesRequest(page: Int): Request = queryRequest(
- page = page,
- query = "",
- orderBy = LATEST_ORDER_BY,
- orderDirection = ORDER_DESC,
- )
-
- override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
-
- // ========================= Search =========================
-
- override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
- if (page == 1) {
- val deepLink = parseDeepLink(query, baseHost)
- if (deepLink != null) {
- val summary = findPostBySlug(deepLink.mangaSlug)
- ?: throw Exception("Series not found")
-
- val manga = summary.toSMangaSummary()
-
- return fetchMangaDetails(manga)
- .map { MangasPage(listOf(it), false) }
- }
- }
-
- return super.fetchSearchManga(page, query, filters)
- }
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = queryRequest(
- page = page,
- query = query.trim(),
- filters = filters.filterIsInstance(),
- )
-
- override fun searchMangaParse(response: Response): MangasPage {
- val payload = response.parseAs()
- val currentPage = response.request.url.queryParameter("page")?.toIntOrNull() ?: 1
- val hasNextPage = payload.totalCount > (currentPage * PER_PAGE)
-
- val mangas = payload.posts.map { it.toSMangaSummary() }
- return MangasPage(mangas, hasNextPage)
- }
-
- // ========================= Filters =========================
-
- override fun getFilterList() = FilterList(
- StatusFilter("Status", STATUS_FILTER_KEY, STATUS_OPTIONS),
- TypeFilter("Type", TYPE_FILTER_KEY, TYPE_OPTIONS),
- SortFilter("Sort by", SORT_FILTER_KEY, SORT_OPTIONS),
- SortDirectionFilter("Sort direction", SORT_DIRECTION_FILTER_KEY, SORT_DIRECTION_OPTIONS),
- GenreFilter("Genres", GENRE_OPTIONS),
- )
-
- // ========================= Details =========================
-
- override fun getMangaUrl(manga: SManga): String {
- val slug = extractMangaSlug(manga.url)
- return "$baseUrl/$SERIES_PATH_SEGMENT/$slug"
- }
-
- override fun mangaDetailsRequest(manga: SManga): Request = postRequest(resolvePostId(manga))
-
- override fun mangaDetailsParse(response: Response): SManga {
- val payload = response.parseAs()
- return payload.post.toSMangaDetailsModel()
- }
-
- // ========================= Chapters =========================
-
- override fun getChapterUrl(chapter: SChapter): String {
- val slugs = extractChapterSlugs(chapter.url)
- ?: return "$baseUrl/$SERIES_PATH_SEGMENT/${chapter.url.substringBefore('#').trim('/')}"
-
- return "$baseUrl/$SERIES_PATH_SEGMENT/${slugs.first}/${slugs.second}"
- }
-
- override fun chapterListRequest(manga: SManga): Request = postRequest(resolvePostId(manga))
-
- override fun chapterListParse(response: Response): List {
- val payload = response.parseAs()
- val mangaSlug = payload.post.slug
-
- return payload.post.chapters
- .filter { chapter ->
- showLockedChapters || (chapter.isAccessible != false && chapter.isLocked != true)
- }
- .map { it.toSChapterModel(mangaSlug, dateFormat) }
- }
-
- override fun setupPreferenceScreen(screen: PreferenceScreen) {
- SwitchPreferenceCompat(screen.context).apply {
- key = SHOW_LOCKED_CHAPTERS_PREF_KEY
- title = "Show inaccessible chapters"
- summaryOn = "Locked/inaccessible chapters will be shown in the chapter list."
- summaryOff = "Locked/inaccessible chapters will be hidden from the chapter list."
- setDefaultValue(SHOW_LOCKED_CHAPTERS_DEFAULT)
- }.also(screen::addPreference)
- }
-
- // ========================= Pages =========================
-
- override fun pageListRequest(chapter: SChapter): Request = chapterRequest(resolveChapterId(chapter))
-
- override fun pageListParse(response: Response): List {
- val payload = response.parseAs()
- val chapter = payload.chapter
-
- if (chapter.isAccessible == false || chapter.isLocked == true) {
- throw Exception(LOCKED_CHAPTER_MESSAGE)
- }
-
- val images = chapter.images.sortedBy { it.order ?: Int.MAX_VALUE }
- if (images.isEmpty()) {
- throw Exception("No pages found")
- }
-
- return images.mapIndexed { index, image ->
- Page(index = index, imageUrl = image.url)
- }
- }
-
- override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
-
- private fun queryRequest(
- page: Int,
- query: String,
- orderBy: String? = null,
- orderDirection: String? = null,
- filters: List = emptyList(),
- ): Request {
- val url = apiUrl.toHttpUrl().newBuilder().apply {
- encodedPath(API_QUERY_PATH)
- addQueryParameter("page", page.toString())
- addQueryParameter("perPage", PER_PAGE.toString())
- addQueryParameter("searchTerm", query)
-
- filters.forEach { it.addUrlParameter(this) }
-
- if (!orderBy.isNullOrBlank()) {
- setQueryParameter(SORT_FILTER_KEY, orderBy)
- }
-
- if (!orderDirection.isNullOrBlank()) {
- setQueryParameter(SORT_DIRECTION_FILTER_KEY, orderDirection)
- }
- }.build()
-
- return GET(url, apiHeaders)
- }
-
- private fun postRequest(postId: Int): Request {
- val url = apiUrl.toHttpUrl().newBuilder().apply {
- encodedPath(API_POST_PATH)
- addQueryParameter("postId", postId.toString())
- }.build()
-
- return GET(url, apiHeaders)
- }
-
- private fun chapterRequest(chapterId: Int): Request {
- val url = apiUrl.toHttpUrl().newBuilder().apply {
- encodedPath(API_CHAPTER_PATH)
- addQueryParameter("chapterId", chapterId.toString())
- }.build()
-
- return GET(url, apiHeaders)
- }
-
- private fun resolvePostId(manga: SManga): Int {
- manga.url.substringAfter('#', "").toIntOrNull()?.let { return it }
-
- val slug = extractMangaSlug(manga.url)
- val summary = findPostBySlug(slug)
- return summary?.id ?: throw Exception("Unable to resolve series id")
- }
-
- private fun resolveChapterId(chapter: SChapter): Int {
- chapter.url.substringAfter('#', "").toIntOrNull()?.let { return it }
-
- val slugs = extractChapterSlugs(chapter.url)
- ?: throw Exception("Unable to resolve chapter id")
-
- val summary = findPostBySlug(slugs.first)
- ?: throw Exception("Unable to resolve series id")
-
- return try {
- client.newCall(postRequest(summary.id)).execute().use { response ->
- if (!response.isSuccessful) {
- throw Exception("Unable to resolve chapter id")
- }
-
- val payload = response.parseAs()
- val chapterDto = payload.post.chapters.firstOrNull { it.slug == slugs.second }
- ?: throw Exception("Unable to resolve chapter id")
-
- chapterDto.id
- }
- } catch (_: Exception) {
- throw Exception("Unable to resolve chapter id")
- }
- }
-
- private fun findPostBySlug(slug: String): PostSummaryDto? {
- val normalizedSlug = slug.trim().trim('/').lowercase()
- val terms = buildSlugSearchTerms(normalizedSlug)
-
- for (term in terms) {
- try {
- client.newCall(queryRequest(page = 1, query = term)).execute().use { response ->
- if (response.isSuccessful) {
- val payload = response.parseAs()
- val post = payload.posts.firstOrNull { it.slug.equals(normalizedSlug, ignoreCase = true) }
- if (post != null) {
- return post
- }
- }
- }
- } catch (_: Exception) {
- }
- }
-
- return null
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/mangaoni/Filters.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/mangaoni/Filters.kt
deleted file mode 100644
index 6ad673c8..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/mangaoni/Filters.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.es.mangaoni
-
-import eu.kanade.tachiyomi.source.model.Filter
-
-open class UriPartFilter(displayName: String, private val vals: Array>) : Filter.Select(displayName, vals.map { it.first }.toTypedArray()) {
- fun toUriPart() = vals[state].second
-}
-
-class StatusFilter :
- UriPartFilter(
- "Estado",
- arrayOf(
- Pair("Estado", "false"),
- Pair("En desarrollo", "1"),
- Pair("Completo", "0"),
- ),
- )
-
-class TypeFilter :
- UriPartFilter(
- "Tipo",
- arrayOf(
- Pair("Todo", "false"),
- Pair("Mangas", "0"),
- Pair("Manhwas", "1"),
- Pair("One Shot", "2"),
- Pair("Manhuas", "3"),
- Pair("Novelas", "4"),
- ),
- )
-
-class GenreFilter :
- UriPartFilter(
- "Géneros",
- arrayOf(
- Pair("Todos", "false"),
- Pair("Comedia", "1"),
- Pair("Drama", "2"),
- Pair("Acción", "3"),
- Pair("Escolar", "4"),
- Pair("Romance", "5"),
- Pair("Ecchi", "6"),
- Pair("Aventura", "7"),
- Pair("Shōnen", "8"),
- Pair("Shōjo", "9"),
- Pair("Deportes", "10"),
- Pair("Psicológico", "11"),
- Pair("Fantasía", "12"),
- Pair("Mecha", "13"),
- Pair("Gore", "14"),
- Pair("Yaoi", "15"),
- Pair("Yuri", "16"),
- Pair("Misterio", "17"),
- Pair("Sobrenatural", "18"),
- Pair("Seinen", "19"),
- Pair("Ficción", "20"),
- Pair("Harem", "21"),
- Pair("Webtoon", "25"),
- Pair("Histórico", "27"),
- Pair("Músical", "30"),
- Pair("Ciencia ficción", "31"),
- Pair("Shōjo-ai", "32"),
- Pair("Josei", "33"),
- Pair("Magia", "34"),
- Pair("Artes Marciales", "35"),
- Pair("Horror", "36"),
- Pair("Demonios", "37"),
- Pair("Supervivencia", "38"),
- Pair("Recuentos de la vida", "39"),
- Pair("Shōnen ai", "40"),
- Pair("Militar", "41"),
- Pair("Eroge", "42"),
- Pair("Isekai", "43"),
- ),
- )
-
-class AdultContentFilter :
- UriPartFilter(
- "Contenido +18",
- arrayOf(
- Pair("Mostrar todo", "false"),
- Pair("Mostrar solo +18", "1"),
- Pair("No mostrar +18", "0"),
- ),
- )
-
-class SortBy :
- Filter.Sort(
- "Ordenar por",
- arrayOf("Visitas", "Recientes", "Alfabético"),
- Selection(0, false),
- ) {
- fun toUriPart() = when (state?.index) {
- 0 -> "visitas"
- 1 -> "id"
- 2 -> "nombre"
- else -> "visitas"
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/mangaoni/MangaOni.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/mangaoni/MangaOni.kt
deleted file mode 100644
index 874747b2..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/mangaoni/MangaOni.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.es.mangaoni
-
-import android.content.SharedPreferences
-import android.net.Uri
-import android.util.Base64
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.ConfigurableSource
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.source.model.MangasPage
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.util.asJsoup
-import keiyoushi.utils.firstInstanceOrNull
-import keiyoushi.utils.getPreferencesLazy
-import keiyoushi.utils.tryParse
-import okhttp3.Request
-import okhttp3.Response
-import org.koitharu.kotatsu.parsers.TachiyomiSource
-import java.nio.charset.Charset
-import java.text.SimpleDateFormat
-import java.util.Locale
-
-@TachiyomiSource("MANGAONI", "MangaOni", "es")
-class MangaOni :
- HttpSource(),
- ConfigurableSource {
-
- override val name = "MangaOni"
-
- override val id: Long = 2202687009511923782
-
- override val baseUrl = "https://manga-oni.com"
-
- override val lang = "es"
-
- override val supportsLatest = true
-
- private val preferences: SharedPreferences by getPreferencesLazy()
-
- private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
-
- override fun popularMangaRequest(page: Int) = GET(
- url = "$baseUrl/directorio?genero=false&estado=false&filtro=visitas&tipo=false&adulto=${if (hideNSFWContent()) "0" else "false"}&orden=desc&p=$page",
- headers = headers,
- )
-
- override fun popularMangaParse(response: Response): MangasPage {
- val document = response.asJsoup()
-
- val mangas = document.select("#article-div a").map { element ->
- SManga.create().apply {
- setUrlWithoutDomain(element.attr("abs:href"))
- thumbnail_url = element.select("img").attr("abs:data-src")
- title = element.select("div:eq(1)").text()
- }
- }
-
- val hasNextPage = document.selectFirst("ul.pagination a[rel=next]") != null
-
- return MangasPage(mangas, hasNextPage)
- }
-
- override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/recientes?p=$page", headers)
-
- override fun latestUpdatesParse(response: Response): MangasPage {
- val document = response.asJsoup()
-
- val mangas = document.select("div._1bJU3").map { element ->
- SManga.create().apply {
- thumbnail_url = element.select("img").attr("abs:data-src")
- element.selectFirst("a[data-test=latest-update-name]")?.also {
- title = it.text()
- setUrlWithoutDomain(it.attr("abs:href"))
- }
- }
- }
-
- val hasNextPage = document.selectFirst("ul.pagination a[rel=next]") != null
-
- return MangasPage(mangas, hasNextPage)
- }
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- val uri = Uri.parse("$baseUrl/${if (query.isNotBlank()) "buscar" else "directorio"}").buildUpon()
-
- if (query.isNotBlank()) {
- uri.appendQueryParameter("q", query)
- } else {
- val adultContent = filters.firstInstanceOrNull()?.toUriPart()
- ?: if (hideNSFWContent()) "0" else "false"
- uri.appendQueryParameter("adulto", adultContent)
-
- filters.firstInstanceOrNull()?.let { uri.appendQueryParameter("estado", it.toUriPart()) }
- filters.firstInstanceOrNull()?.let { uri.appendQueryParameter("tipo", it.toUriPart()) }
- filters.firstInstanceOrNull()?.let { uri.appendQueryParameter("genero", it.toUriPart()) }
- filters.firstInstanceOrNull()?.let {
- uri.appendQueryParameter("filtro", it.toUriPart())
- uri.appendQueryParameter("orden", if (it.state?.ascending == true) "asc" else "desc")
- }
- }
- uri.appendQueryParameter("p", page.toString())
- return GET(uri.toString(), headers)
- }
-
- override fun searchMangaParse(response: Response): MangasPage {
- if (!response.isSuccessful) throw Exception("Búsqueda fallida ${response.code}")
-
- val document = response.asJsoup()
-
- val mangas = if (document.location().startsWith("$baseUrl/directorio")) {
- document.select("#article-div a").map { element ->
- SManga.create().apply {
- setUrlWithoutDomain(element.attr("abs:href"))
- thumbnail_url = element.select("img").attr("abs:data-src")
- title = element.select("div:eq(1)").text()
- }
- }
- } else {
- document.select("#article-div > div").map { element ->
- SManga.create().apply {
- thumbnail_url = element.select("img").attr("abs:src")
- title = element.select("a").firstOrNull { it.text().isNotBlank() }?.text()
- ?: throw Exception("Title not found")
- element.selectFirst("a")?.also {
- setUrlWithoutDomain(it.attr("abs:href"))
- }
- }
- }
- }
-
- val hasNextPage = document.selectFirst("ul.pagination a[rel=next]") != null
-
- return MangasPage(mangas, hasNextPage)
- }
-
- override fun mangaDetailsParse(response: Response): SManga {
- val document = response.asJsoup()
-
- return SManga.create().apply {
- title = document.selectFirst("h1")?.text() ?: throw Exception("Title not found")
- thumbnail_url = document.select("img[src*=cover]").attr("abs:src")
- description = document.select("div#sinopsis").lastOrNull()?.ownText()
- author = document.select("div#info-i").text().let {
- if (it.contains("Autor", true)) {
- it.substringAfter("Autor:").substringBefore("Fecha:").trim()
- } else {
- "N/A"
- }
- }
- artist = author
- genre = document.select("div#categ a").joinToString(", ") { it.text() }
- status = when (document.selectFirst("strong:contains(Estado) + span")?.text()) {
- "En desarrollo" -> SManga.ONGOING
- "Finalizado" -> SManga.COMPLETED
- else -> SManga.UNKNOWN
- }
- }
- }
-
- override fun chapterListParse(response: Response): List {
- val document = response.asJsoup()
-
- return document.select("div#c_list a").map { element ->
- SChapter.create().apply {
- name = element.text()
- setUrlWithoutDomain(element.attr("abs:href"))
- chapter_number = element.select("span").attr("data-num").toFloatOrNull() ?: -1f
- date_upload = dateFormat.tryParse(element.select("span").attr("datetime"))
- }
- }
- }
-
- override fun pageListParse(response: Response): List {
- val document = response.asJsoup()
-
- val encoded = document.selectFirst("script:containsData(unicap)")
- ?.data()?.substringAfter("'")?.substringBefore("'")
- ?: throw Exception("unicap not found")
- val drop = encoded.length % 4
- val decoded = Base64.decode(encoded.dropLast(drop), Base64.DEFAULT).toString(Charset.defaultCharset())
- val path = decoded.substringBefore("||")
-
- return decoded.substringAfter("[").substringBefore("]").split(",").mapIndexed { i, file ->
- Page(i, imageUrl = path + file.removeSurrounding("\""))
- }
- }
-
- override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
-
- override fun getFilterList(): FilterList {
- val filterList = mutableListOf(
- Filter.Header("NOTA: Se ignoran si se usa el buscador"),
- Filter.Separator(),
- SortBy(),
- StatusFilter(),
- TypeFilter(),
- GenreFilter(),
- )
-
- if (!hideNSFWContent()) {
- filterList.add(AdultContentFilter())
- }
- return FilterList(filterList)
- }
-
- override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
- val contentPref = android.preference.CheckBoxPreference(screen.context).apply {
- key = CONTENT_PREF
- title = CONTENT_PREF_TITLE
- summary = CONTENT_PREF_SUMMARY
- setDefaultValue(CONTENT_PREF_DEFAULT_VALUE)
- }
- }
-
- private fun hideNSFWContent(): Boolean = preferences.getBoolean(CONTENT_PREF, CONTENT_PREF_DEFAULT_VALUE)
-
- companion object {
- private const val CONTENT_PREF = "showNSFWContent"
- private const val CONTENT_PREF_TITLE = "Ocultar contenido +18"
- private const val CONTENT_PREF_SUMMARY = "Ocultar el contenido erótico en mangas populares y filtros, no funciona en los mangas recientes ni búsquedas textuales."
- private const val CONTENT_PREF_DEFAULT_VALUE = false
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/olympusscan/Dto.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/olympusscan/Dto.kt
deleted file mode 100644
index beef1a42..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/olympusscan/Dto.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.es.olympusscan
-
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import java.text.ParseException
-import java.text.SimpleDateFormat
-
-@Serializable
-class RankingDto(
- val data: List,
- @SerialName("current_page") private val currentPage: Int,
- @SerialName("last_page") private val lastPage: Int,
-) {
- fun hasNextPage() = currentPage < lastPage
-}
-
-@Serializable
-class PayloadSeriesDto(val data: PayloadSeriesDataDto)
-
-@Serializable
-class PayloadSeriesDataDto(
- val series: SeriesDto,
-)
-
-@Serializable
-class SeriesDto(
- val data: List,
- @SerialName("current_page") private val currentPage: Int,
- @SerialName("last_page") private val lastPage: Int,
-) {
- fun hasNextPage() = currentPage < lastPage
-}
-
-@Serializable
-class PayloadMangaDto(val data: List)
-
-@Serializable
-class MangaDto(
- val id: Int,
- val name: String,
- val slug: String,
- private val cover: String? = null,
- val type: String? = null,
- private val summary: String? = null,
- private val status: MangaStatusDto? = null,
- private val genres: List? = null,
-) {
- fun toSManga() = SManga.create().apply {
- title = name
- url = id.toString()
- thumbnail_url = cover
- }
-
- fun toSMangaDetails() = toSManga().apply {
- description = summary
- status = parseStatus()
- genre = genres?.joinToString { it.name.trim() }
- }
-
- private fun parseStatus(): Int {
- val status = this.status ?: return SManga.UNKNOWN
- return when (status.id) {
- 1 -> SManga.ONGOING
- 3 -> SManga.ON_HIATUS
- 4 -> SManga.COMPLETED
- 5 -> SManga.CANCELLED
- else -> SManga.UNKNOWN
- }
- }
-}
-
-@Serializable
-class NewChaptersDto(
- val data: List,
- @SerialName("current_page") private val currentPage: Int,
- @SerialName("last_page") private val lastPage: Int,
-) {
- fun hasNextPage() = currentPage < lastPage
-}
-
-@Serializable
-class LatestMangaDto(
- val id: Int,
- private val name: String,
- val slug: String,
- private val cover: String? = null,
- val type: String? = null,
-) {
- fun toSManga() = SManga.create().apply {
- title = name
- url = id.toString()
- thumbnail_url = cover
- }
-}
-
-@Serializable
-class MangaDetailDto(
- var data: MangaDto,
-)
-
-@Serializable
-class PayloadChapterDto(var data: List, val meta: MetaDto)
-
-@Serializable
-class ChapterDto(
- private val id: Int,
- private val name: String,
- @SerialName("published_at") private val date: String,
-) {
- fun toSChapter(mangaId: String, dateFormat: SimpleDateFormat) = SChapter.create().apply {
- name = "Capitulo ${this@ChapterDto.name}"
- url = "$mangaId/$id"
- date_upload = try {
- dateFormat.parse(date)!!.time
- } catch (e: ParseException) {
- 0L
- }
- }
-}
-
-@Serializable
-class MetaDto(val total: Int)
-
-@Serializable
-class PayloadPagesDto(val chapter: PageDto)
-
-@Serializable
-class PageDto(val pages: List)
-
-@Serializable
-class MangaStatusDto(
- val id: Int,
-)
-
-@Serializable
-class GenresStatusesDto(
- val genres: List,
- val statuses: List,
-)
-
-@Serializable
-class FilterDto(
- val id: Int,
- val name: String,
-)
-
-@Serializable
-class BookmarksWrapperDto(
- private val data: List = emptyList(),
- val meta: BookmarksMetaDto,
-) {
- fun getBookmarks() = data.filter { it.type == "comic" && it.id != null && it.slug != null }
-}
-
-@Serializable
-class BookmarkDto(
- val id: Int?,
- val slug: String?,
- val type: String?,
-)
-
-@Serializable
-class BookmarksMetaDto(
- @SerialName("current_page") private val currentPage: Int,
- @SerialName("last_page") private val lastPage: Int,
-) {
- fun hasNextPage() = currentPage < lastPage
-}
\ No newline at end of file
diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/olympusscan/OlympusScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/olympusscan/OlympusScan.kt
deleted file mode 100644
index 823932cd..00000000
--- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tachiyomi/es/olympusscan/OlympusScan.kt
+++ /dev/null
@@ -1,338 +0,0 @@
-package org.koitharu.kotatsu.parsers.site.tachiyomi.es.olympusscan
-
-import android.content.SharedPreferences
-import android.widget.Toast
-import androidx.preference.EditTextPreference
-import androidx.preference.PreferenceScreen
-import androidx.preference.SwitchPreferenceCompat
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.ConfigurableSource
-import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.source.model.MangasPage
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.util.asJsoup
-import org.koitharu.kotatsu.parsers.util.rateLimit
-import keiyoushi.utils.getPreferences
-import keiyoushi.utils.parseAs
-import keiyoushi.utils.toJsonString
-import kotlinx.serialization.SerializationException
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.Request
-import okhttp3.Response
-import org.koitharu.kotatsu.parsers.TachiyomiSource
-import rx.Observable
-import java.text.SimpleDateFormat
-import java.util.Locale
-import java.util.TimeZone
-import kotlin.time.Duration.Companion.seconds
-
-@TachiyomiSource("OLYMPUSSCAN", "Olympus Scanlation", "es")
-class OlympusScan :
- HttpSource(),
- ConfigurableSource {
- private val fetchedDomainUrlHost by lazy { fetchedDomainUrl.toHttpUrl().host }
- private val apiBaseUrlHost by lazy { apiBaseUrl.toHttpUrl().host }
-
- override val versionId = 3
- private val isCi = System.getenv("CI") == "true"
-
- override val baseUrl: String get() = when {
- isCi -> defaultBaseUrl
- else -> preferences.prefBaseUrl
- }
-
- private val defaultBaseUrl: String = "https://olympusxyz.com"
-
- private val fetchedDomainUrl: String by lazy {
- if (!preferences.fetchDomainPref()) return@lazy preferences.prefBaseUrl
- try {
- val initClient = network.client
- val headers = super.headersBuilder().build()
- val document = initClient.newCall(GET("https://olympus.pages.dev", headers)).execute().asJsoup()
- val domain = document.selectFirst("meta[property=og:url]")?.attr("content")
- ?: return@lazy preferences.prefBaseUrl
- val host = initClient.newCall(GET(domain, headers)).execute().request.url.host
- val newDomain = "https://$host"
- preferences.prefBaseUrl = newDomain
- newDomain
- } catch (_: Exception) {
- preferences.prefBaseUrl
- }
- }
-
- private val apiBaseUrl by lazy {
- fetchedDomainUrl.replace("https://", "https://panel.")
- }
-
- override val lang: String = "es"
- override val name: String = "Olympus Scanlation"
-
- override val supportsLatest: Boolean = true
-
- private val preferences: SharedPreferences = getPreferences {
- this.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
- if (domain != defaultBaseUrl) {
- this.edit()
- .putString(BASE_URL_PREF, defaultBaseUrl)
- .putString(DEFAULT_BASE_URL_PREF, defaultBaseUrl)
- .apply()
- }
- }
- }
-
- override val client by lazy {
- val client = network.client.newBuilder()
-// .rateLimit(1, 2.seconds) { it.host == fetchedDomainUrlHost }
-// .rateLimit(2, 1.seconds) { it.host == apiBaseUrlHost }
- .build()
-
- return@lazy client
- }
-
- override fun headersBuilder() = super.headersBuilder()
- .add("Referer", "$baseUrl/")
-
- private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US).apply {
- timeZone = TimeZone.getTimeZone("UTC")
- }
-
- @Volatile
- private var seriesList: List = emptyList()
-
- @Volatile
- private var lastFetchTime: Long = 0L
-
- @Synchronized
- private fun fetchSeriesList() {
- val now = System.currentTimeMillis()
-
- if (seriesList.isNotEmpty() && (now - lastFetchTime) < CACHE_DURATION_MS) {
- return
- }
-
- val result = client.newCall(GET("$baseUrl/api/series/list")).execute()
- if (!result.isSuccessful) {
- throw Exception("Failed to fetch series list: HTTP ${result.code}")
- }
-
- val series = result.parseAs()
-
- val comics = series.data.asSequence()
- .filter { it.type == "comic" }
- .toList()
-
- seriesList = comics
- lastFetchTime = now
-
- val newSlugMap = comics.associate { it.id to it.slug }
-
- preferences.slugMap += newSlugMap
- }
-
- override fun fetchPopularManga(page: Int): Observable {
- fetchSeriesList()
- return super.fetchPopularManga(page)
- }
-
- override fun popularMangaRequest(page: Int): Request {
- val apiUrl = "$baseUrl/api/rankings?page=$page&period=total_ranking".toHttpUrl().newBuilder()
- .build()
- return GET(apiUrl, headers)
- }
-
- override fun popularMangaParse(response: Response): MangasPage {
- val result = response.parseAs()
- val slugMap = preferences.slugMap.toMutableMap()
- val mangaList = result.data
- .filter { it.type == "comic" }
- .map {
- slugMap[it.id] = it.slug
- it.toSManga()
- }
- preferences.slugMap = slugMap
- return MangasPage(mangaList, hasNextPage = result.hasNextPage())
- }
-
- override fun fetchLatestUpdates(page: Int): Observable {
- fetchSeriesList()
- return super.fetchLatestUpdates(page)
- }
-
- override fun latestUpdatesRequest(page: Int): Request {
- val apiUrl = "$baseUrl/api/new-chapters?page=$page".toHttpUrl().newBuilder()
- .build()
- return GET(apiUrl, headers)
- }
-
- override fun latestUpdatesParse(response: Response): MangasPage {
- val result = response.parseAs()
- val slugMap = preferences.slugMap.toMutableMap()
- val mangaList = result.data.filter { it.type == "comic" }
- .map {
- slugMap[it.id] = it.slug
- it.toSManga()
- }
- preferences.slugMap = slugMap
- return MangasPage(mangaList, result.hasNextPage())
- }
-
- override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
- fetchSeriesList()
- return Observable.just(parseSearchManga(page, query))
- }
-
- private fun parseSearchManga(page: Int, query: String): MangasPage {
- val filteredList = seriesList.filter { it.name.contains(query, ignoreCase = true) }
- val paginatedList = filteredList.drop((page - 1) * 20).take(20)
- val hasNextPage = page * 20 < filteredList.size
- return MangasPage(paginatedList.map { it.toSManga() }, hasNextPage)
- }
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw Exception("La búsqueda no es compatible.")
-
- override fun searchMangaParse(response: Response) = throw Exception("La búsqueda no es compatible.")
-
- override fun getMangaUrl(manga: SManga): String {
- val slug = preferences.slugMap[manga.url.toInt()]!!
- return "$baseUrl/series/comic-$slug"
- }
-
- override fun fetchMangaDetails(manga: SManga): Observable {
- fetchSeriesList()
- return super.fetchMangaDetails(manga)
- }
-
- override fun mangaDetailsRequest(manga: SManga): Request {
- val slug = preferences.slugMap[manga.url.toInt()]!!
-
- val apiUrl = "$baseUrl/api/series/$slug?type=comic"
- return GET(url = apiUrl, headers = headers)
- }
-
- override fun mangaDetailsParse(response: Response): SManga {
- val result = response.parseAs()
- return result.data.toSMangaDetails()
- }
-
- override fun getChapterUrl(chapter: SChapter): String {
- val mangaId = chapter.url.substringBefore("/")
- val chapterId = chapter.url.substringAfter("/")
- val mangaSlug = preferences.slugMap[mangaId.toInt()]!!
- return "$baseUrl/capitulo/$chapterId/comic-$mangaSlug"
- }
-
- override fun fetchChapterList(manga: SManga): Observable> {
- fetchSeriesList()
- return super.fetchChapterList(manga)
- }
-
- override fun chapterListRequest(manga: SManga): Request {
- val mangaId = manga.url
- val mangaSlug = preferences.slugMap[mangaId.toInt()]!!
-
- return paginatedChapterListRequest(mangaSlug, mangaId, 1)
- }
-
- private fun paginatedChapterListRequest(mangaSlug: String, mangaId: String, page: Int): Request = GET(
- url = "$apiBaseUrl/api/series/$mangaSlug/chapters?page=$page&direction=desc&type=comic#$mangaId",
- headers = headers,
- )
-
- override fun chapterListParse(response: Response): List {
- val mangaId = response.request.url.fragment ?: ""
- val slug = response.request.url.toString()
- .substringAfter("/series/")
- .substringBefore("/chapters")
-
- val data = response.parseAs()
- var resultSize = data.data.size
- var page = 2
- while (data.meta.total > resultSize) {
- val newRequest = paginatedChapterListRequest(slug, mangaId, page)
- val newResponse = client.newCall(newRequest).execute()
- val newData = newResponse.parseAs()
- data.data += newData.data
- resultSize += newData.data.size
- page += 1
- }
- return data.data.map { it.toSChapter(mangaId, dateFormat) }.sortedBy { it.chapter_number }
- }
-
- override fun pageListRequest(chapter: SChapter): Request {
- val mangaId = chapter.url.substringBefore("/")
- val chapterId = chapter.url.substringAfter("/")
- val mangaSlug = preferences.slugMap[mangaId.toInt()]!!
-
- return GET("$baseUrl/api/capitulo/comic-$mangaSlug/$chapterId")
- }
-
- override fun pageListParse(response: Response): List = response.parseAs().chapter.pages.mapIndexed { i, img ->
- Page(i, imageUrl = img)
- }
-
- override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
-
- override fun setupPreferenceScreen(screen: PreferenceScreen) {
- SwitchPreferenceCompat(screen.context).apply {
- key = FETCH_DOMAIN_PREF
- title = "Buscar dominio automáticamente"
- summary = "Intenta buscar el dominio automáticamente al abrir la fuente."
- setDefaultValue(FETCH_DOMAIN_PREF_DEFAULT)
- }.also { screen.addPreference(it) }
-
- EditTextPreference(screen.context).apply {
- key = BASE_URL_PREF
- title = "Editar URL de la fuente"
- summary = "Para uso temporal, si la extensión se actualiza se perderá el cambio."
- dialogTitle = "Editar URL de la fuente"
- setDefaultValue(defaultBaseUrl)
- }.also { screen.addPreference(it) }
- }
-
- private var cachedBaseUrl: String? = null
- private var SharedPreferences.prefBaseUrl: String
- get() {
- if (cachedBaseUrl == null) {
- cachedBaseUrl = getString(BASE_URL_PREF, defaultBaseUrl)!!
- }
- return cachedBaseUrl!!
- }
- set(value) {
- cachedBaseUrl = value
- edit().putString(BASE_URL_PREF, value).apply()
- }
-
- private fun SharedPreferences.fetchDomainPref() = getBoolean(FETCH_DOMAIN_PREF, FETCH_DOMAIN_PREF_DEFAULT)
-
- private var slugMapCache: Map? = null
- private var SharedPreferences.slugMap: Map
- get() {
- slugMapCache?.let { return it }
- val json = getString(SLUG_MAP, "{}")!!
- slugMapCache = try {
- json.parseAs