diff --git a/REMOVED_SOURCES.md b/REMOVED_SOURCES.md
index 54ef583005..d56c5dfffa 100644
--- a/REMOVED_SOURCES.md
+++ b/REMOVED_SOURCES.md
@@ -66,3 +66,13 @@ Here is a list of known sources that were removed.
- SetsuScans https://github.com/tachiyomiorg/tachiyomi-extensions/issues/11040
- ShinobiScans https://github.com/tachiyomiorg/tachiyomi-extensions/issues/14457
- XXX Yaoi https://github.com/tachiyomiorg/tachiyomi-extensions/issues/9535
+
+### Requested removal by copyright holders
+
+By request of [Kakao Entertainment](https://www.kakaoent.com/):
+
+- 1st Kiss-Manga
+- Bato.to
+- Mangadex
+- NewToki / ManaToki
+- S2Manga
diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 78aed8f9d9..0000000000
Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 57b0e935be..0000000000
Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 60ac860b99..0000000000
Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 6201c85ab5..0000000000
Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index ed057db6bf..0000000000
Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png b/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png
deleted file mode 100644
index c39e43b09a..0000000000
Binary files a/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png and /dev/null differ
diff --git a/multisrc/overrides/madara/s2manga/src/S2Manga.kt b/multisrc/overrides/madara/s2manga/src/S2Manga.kt
deleted file mode 100644
index 0179493181..0000000000
--- a/multisrc/overrides/madara/s2manga/src/S2Manga.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package eu.kanade.tachiyomi.extension.en.s2manga
-
-import eu.kanade.tachiyomi.multisrc.madara.Madara
-
-class S2Manga : Madara("S2Manga", "https://www.s2manga.com", "en") {
-
- override fun headersBuilder() = super.headersBuilder()
- .add("Referer", "$baseUrl/")
-
- override val pageListParseSelector = "div.page-break img[src*=\"https\"]"
-}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
index 5e347bac4f..8e9be44455 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
@@ -422,7 +422,6 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("ROG Mangás", "https://rogmangas.com", "pt-BR", pkgName = "mangasoverall", className = "RogMangas", overrideVersionCode = 1),
SingleLang("Romantik Manga", "https://romantikmanga.com", "tr"),
SingleLang("Rüya Manga", "https://www.ruyamanga.com", "tr", className = "RuyaManga", overrideVersionCode = 1),
- SingleLang("S2Manga", "https://www.s2manga.com", "en", overrideVersionCode = 2),
SingleLang("Sagrado Império da Britannia", "https://imperiodabritannia.com", "pt-BR", className = "ImperioDaBritannia"),
SingleLang("SamuraiScan", "https://samuraiscan.com", "es", overrideVersionCode = 3),
SingleLang("Sawamics", "https://sawamics.com", "en"),
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3e0631f9ea..e1dbaa7e2e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,7 +19,7 @@ if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
*/
loadAllIndividualExtensions()
loadAllGeneratedMultisrcExtensions()
- // loadIndividualExtension("all", "mangadex")
+ // loadIndividualExtension("all", "komga")
// loadGeneratedMultisrcExtension("en", "guya")
} else {
// Running in CI (GitHub Actions)
diff --git a/src/all/batoto/AndroidManifest.xml b/src/all/batoto/AndroidManifest.xml
deleted file mode 100644
index c4ca310dd6..0000000000
--- a/src/all/batoto/AndroidManifest.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/all/batoto/CHANGELOG.md b/src/all/batoto/CHANGELOG.md
deleted file mode 100644
index e240bf4e9e..0000000000
--- a/src/all/batoto/CHANGELOG.md
+++ /dev/null
@@ -1,201 +0,0 @@
-## 1.3.30
-
-### Refactor
-
-* Replace CryptoJS with Native Kotlin Functions
-* Remove QuickJS dependency
-
-## 1.3.29
-
-### Refactor
-
-* Cleanup pageListParse function
-* Replace Duktape with QuickJS
-
-## 1.3.28
-
-### Features
-
-* Add mirror `batocc.com`
-* Add mirror `batotwo.com`
-* Add mirror `mangatoto.net`
-* Add mirror `mangatoto.org`
-* Add mirror `mycordant.co.uk`
-* Add mirror `dto.to`
-* Add mirror `hto.to`
-* Add mirror `mto.to`
-* Add mirror `wto.to`
-* Remove mirror `mycdhands.com`
-
-## 1.3.27
-
-### Features
-
-* Change default popular sort by `Most Views Totally`
-
-## 1.3.26
-
-### Fix
-
-* Update author and artist parsing
-
-## 1.3.25
-
-### Fix
-
-* Status parsing
-* Artist name parsing
-
-## 1.3.24
-
-### Fix
-
-* Bump versions for individual extension with URL handler activities
-
-## 1.2.23
-
-### Fix
-
-* Update pageListParse logic to handle website changes
-
-## 1.2.22
-
-### Features
-
-* Add `CHANGELOG.md` & `README.md`
-
-## 1.2.21
-
-### Fix
-
-* Update lang codes
-
-## 1.2.20
-
-### Features
-
-* Rework of search
-
-## 1.2.19
-
-### Features
-
-* Support for alternative chapter list
-* Personal lists filter
-
-## 1.2.18
-
-### Features
-
-* Utils lists filter
-* Letter matching filter
-
-## 1.2.17
-
-### Features
-
-* Add mirror `mycdhands.com`
-
-## 1.2.16
-
-### Features
-
-* Mirror support
-* URL intent updates
-
-## 1.2.15
-
-### Fix
-
-* Manga description
-
-## 1.2.14
-
-### Features
-
-* Escape entities
-
-## 1.2.13
-
-### Refactor
-
-* Replace Gson with kotlinx.serialization
-
-## 1.2.12
-
-### Fix
-
-* Infinity search
-
-## 1.2.11
-
-### Fix
-
-* No search result
-
-## 1.2.10
-
-### Features
-
-* Support for URL intent
-* Updated filters
-
-## 1.2.9
-
-### Fix
-
-* Chapter parsing
-
-## 1.2.8
-
-### Features
-
-* More chapter filtering
-
-## 1.2.7
-
-### Fix
-
-* Language filtering in latest
-* Parsing of seconds
-
-## 1.2.6
-
-### Features
-
-* Scanlator support
-
-### Fix
-
-* Date parsing
-
-## 1.2.5
-
-### Features
-
-* Update supported Language list
-
-## 1.2.4
-
-### Features
-
-* Support for excluding genres
-
-## 1.2.3
-
-### Fix
-
-* Typo in some genres
-
-## 1.2.2
-
-### Features
-
-* Reworked filter option
-
-## 1.2.1
-
-### Features
-
-* Conversion from Emerald to Bato.to
-* First version
diff --git a/src/all/batoto/README.md b/src/all/batoto/README.md
deleted file mode 100644
index 8f7c39cc5e..0000000000
--- a/src/all/batoto/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Bato.to
-
-Table of Content
-- [FAQ](#FAQ)
- - [Why are there Manga of diffrent languge than the selected one in Personal & Utils lists?](#why-are-there-manga-of-diffrent-languge-than-the-selected-one-in-personal--utils-lists)
- - [Bato.to is not loading anything?](#batoto-is-not-loading-anything)
-
-[Uncomment this if needed; and replace ( and ) with ( and )]: <> (- [Guides](#Guides))
-
-Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
-
-## FAQ
-
-### Why are there Manga of diffrent languge than the selected one in Personal & Utils lists?
-Personol & Utils lists have no way to difritiate between langueges.
-
-### Bato.to is not loading anything?
-Bato.to get blocked by some ISPs, try using a diffrent mirror of Bato.to from the settings.
-
-[Uncomment this if needed]: <> (## Guides)
diff --git a/src/all/batoto/build.gradle b/src/all/batoto/build.gradle
deleted file mode 100644
index 3589fc1cd6..0000000000
--- a/src/all/batoto/build.gradle
+++ /dev/null
@@ -1,17 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlinx-serialization'
-
-ext {
- extName = 'Bato.to'
- pkgNameSuffix = 'all.batoto'
- extClass = '.BatoToFactory'
- extVersionCode = 32
- isNsfw = true
-}
-
-apply from: "$rootDir/common.gradle"
-
-dependencies {
- implementation(project(':lib-cryptoaes'))
-}
diff --git a/src/all/batoto/res/mipmap-hdpi/ic_launcher.png b/src/all/batoto/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 0c371b3be2..0000000000
Binary files a/src/all/batoto/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/batoto/res/mipmap-mdpi/ic_launcher.png b/src/all/batoto/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 718a3b8b18..0000000000
Binary files a/src/all/batoto/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png b/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index e705165fcb..0000000000
Binary files a/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png b/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 7f9d6a195c..0000000000
Binary files a/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index d7d91a0db8..0000000000
Binary files a/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/batoto/res/web_hi_res_512.png b/src/all/batoto/res/web_hi_res_512.png
deleted file mode 100644
index 973b65efaa..0000000000
Binary files a/src/all/batoto/res/web_hi_res_512.png and /dev/null differ
diff --git a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt
deleted file mode 100644
index 8d04c0e6b7..0000000000
--- a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt
+++ /dev/null
@@ -1,974 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.batoto
-
-import android.app.Application
-import android.content.SharedPreferences
-import androidx.preference.CheckBoxPreference
-import androidx.preference.ListPreference
-import androidx.preference.PreferenceScreen
-import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
-import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.network.POST
-import eu.kanade.tachiyomi.network.asObservableSuccess
-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.ParsedHttpSource
-import eu.kanade.tachiyomi.util.asJsoup
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.jsonArray
-import kotlinx.serialization.json.jsonObject
-import kotlinx.serialization.json.jsonPrimitive
-import okhttp3.FormBody
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import org.jsoup.Jsoup
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import org.jsoup.parser.Parser
-import rx.Observable
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import uy.kohesive.injekt.injectLazy
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Locale
-import java.util.concurrent.TimeUnit
-
-open class BatoTo(
- final override val lang: String,
- private val siteLang: String,
-) : ConfigurableSource, ParsedHttpSource() {
-
- private val preferences: SharedPreferences by lazy {
- Injekt.get().getSharedPreferences("source_$id", 0x0000)
- }
-
- override val name: String = "Bato.to"
- override val baseUrl: String = getMirrorPref()!!
- override val id: Long = when (lang) {
- "zh-Hans" -> 2818874445640189582
- "zh-Hant" -> 38886079663327225
- "ro-MD" -> 8871355786189601023
- else -> super.id
- }
-
- override fun setupPreferenceScreen(screen: PreferenceScreen) {
- val mirrorPref = ListPreference(screen.context).apply {
- key = "${MIRROR_PREF_KEY}_$lang"
- title = MIRROR_PREF_TITLE
- entries = MIRROR_PREF_ENTRIES
- entryValues = MIRROR_PREF_ENTRY_VALUES
- setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
- summary = "%s"
-
- setOnPreferenceChangeListener { _, newValue ->
- val selected = newValue as String
- val index = findIndexOfValue(selected)
- val entry = entryValues[index] as String
- preferences.edit().putString("${MIRROR_PREF_KEY}_$lang", entry).commit()
- }
- }
- val altChapterListPref = CheckBoxPreference(screen.context).apply {
- key = "${ALT_CHAPTER_LIST_PREF_KEY}_$lang"
- title = ALT_CHAPTER_LIST_PREF_TITLE
- summary = ALT_CHAPTER_LIST_PREF_SUMMARY
- setDefaultValue(ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Boolean
- preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit()
- }
- }
- screen.addPreference(mirrorPref)
- screen.addPreference(altChapterListPref)
- }
-
- private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
- private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
-
- override val supportsLatest = true
- private val json: Json by injectLazy()
- override val client: OkHttpClient = network.cloudflareClient.newBuilder()
- .connectTimeout(10, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .build()
-
- override fun latestUpdatesRequest(page: Int): Request {
- return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page")
- }
-
- override fun latestUpdatesSelector(): String {
- return when (siteLang) {
- "" -> "div#series-list div.col"
- "en" -> "div#series-list div.col.no-flag"
- else -> "div#series-list div.col:has([data-lang=\"$siteLang\"])"
- }
- }
-
- override fun latestUpdatesFromElement(element: Element): SManga {
- val manga = SManga.create()
- val item = element.select("a.item-cover")
- val imgurl = item.select("img").attr("abs:src")
- manga.setUrlWithoutDomain(item.attr("href"))
- manga.title = element.select("a.item-title").text().removeEntities()
- manga.thumbnail_url = imgurl
- return manga
- }
-
- override fun latestUpdatesNextPageSelector() = "div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)"
-
- override fun popularMangaRequest(page: Int): Request {
- return GET("$baseUrl/browse?langs=$siteLang&sort=views_a&page=$page")
- }
-
- override fun popularMangaSelector() = latestUpdatesSelector()
-
- override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element)
-
- override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector()
-
- override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
- return when {
- query.startsWith("ID:") -> {
- val id = query.substringAfter("ID:")
- client.newCall(GET("$baseUrl/series/$id", headers)).asObservableSuccess()
- .map { response ->
- queryIDParse(response)
- }
- }
- query.isNotBlank() -> {
- val url = "$baseUrl/search".toHttpUrl().newBuilder()
- .addQueryParameter("word", query)
- .addQueryParameter("page", page.toString())
- filters.forEach { filter ->
- when (filter) {
- is LetterFilter -> {
- if (filter.state == 1) {
- url.addQueryParameter("mode", "letter")
- }
- }
- else -> { /* Do Nothing */ }
- }
- }
- client.newCall(GET(url.build().toString(), headers)).asObservableSuccess()
- .map { response ->
- queryParse(response)
- }
- }
- else -> {
- val url = "$baseUrl/browse".toHttpUrlOrNull()!!.newBuilder()
- var min = ""
- var max = ""
- filters.forEach { filter ->
- when (filter) {
- is UtilsFilter -> {
- if (filter.state != 0) {
- val filterUrl = "$baseUrl/_utils/comic-list?type=${filter.selected}"
- return client.newCall(GET(filterUrl, headers)).asObservableSuccess()
- .map { response ->
- queryUtilsParse(response)
- }
- }
- }
- is HistoryFilter -> {
- if (filter.state != 0) {
- val filterUrl = "$baseUrl/ajax.my.${filter.selected}.paging"
- return client.newCall(POST(filterUrl, headers, formBuilder().build())).asObservableSuccess()
- .map { response ->
- queryHistoryParse(response)
- }
- }
- }
- is LangGroupFilter -> {
- if (filter.selected.isEmpty()) {
- url.addQueryParameter("langs", siteLang)
- } else {
- val selection = "${filter.selected.joinToString(",")},$siteLang"
- url.addQueryParameter("langs", selection)
- }
- }
- is GenreGroupFilter -> {
- with(filter) {
- url.addQueryParameter(
- "genres",
- included.joinToString(",") + "|" + excluded.joinToString(","),
- )
- }
- }
- is StatusFilter -> url.addQueryParameter("release", filter.selected)
- is SortFilter -> {
- if (filter.state != null) {
- val sort = getSortFilter()[filter.state!!.index].value
- val value = when (filter.state!!.ascending) {
- true -> "az"
- false -> "za"
- }
- url.addQueryParameter("sort", "$sort.$value")
- }
- }
- is OriginGroupFilter -> {
- if (filter.selected.isNotEmpty()) {
- url.addQueryParameter("origs", filter.selected.joinToString(","))
- }
- }
- is MinChapterTextFilter -> min = filter.state
- is MaxChapterTextFilter -> max = filter.state
- else -> { /* Do Nothing */ }
- }
- }
- url.addQueryParameter("page", page.toString())
-
- if (max.isNotEmpty() or min.isNotEmpty()) {
- url.addQueryParameter("chapters", "$min-$max")
- }
-
- client.newCall(GET(url.build().toString(), headers)).asObservableSuccess()
- .map { response ->
- queryParse(response)
- }
- }
- }
- }
-
- private fun queryIDParse(response: Response): MangasPage {
- val document = response.asJsoup()
- val infoElement = document.select("div#mainer div.container-fluid")
- val manga = SManga.create()
- manga.title = infoElement.select("h3").text().removeEntities()
- manga.thumbnail_url = document.select("div.attr-cover img")
- .attr("abs:src")
- manga.url = infoElement.select("h3 a").attr("abs:href")
- return MangasPage(listOf(manga), false)
- }
-
- private fun queryParse(response: Response): MangasPage {
- val document = response.asJsoup()
- val mangas = document.select(latestUpdatesSelector())
- .map { element -> latestUpdatesFromElement(element) }
- val nextPage = document.select(latestUpdatesNextPageSelector()).first() != null
- return MangasPage(mangas, nextPage)
- }
-
- private fun queryUtilsParse(response: Response): MangasPage {
- val document = response.asJsoup()
- val mangas = document.select("tbody > tr")
- .map { element -> searchUtilsFromElement(element) }
- return MangasPage(mangas, false)
- }
-
- private fun queryHistoryParse(response: Response): MangasPage {
- val json = json.decodeFromString(response.body.string())
- val html = json.jsonObject["html"]!!.jsonPrimitive.content
-
- val document = Jsoup.parse(html, response.request.url.toString())
- val mangas = document.select(".my-history-item")
- .map { element -> searchHistoryFromElement(element) }
- return MangasPage(mangas, false)
- }
-
- private fun searchUtilsFromElement(element: Element): SManga {
- val manga = SManga.create()
- manga.setUrlWithoutDomain(element.select("td a").attr("href"))
- manga.title = element.select("td a").text()
- manga.thumbnail_url = element.select("img").attr("abs:src")
- return manga
- }
-
- private fun searchHistoryFromElement(element: Element): SManga {
- val manga = SManga.create()
- manga.setUrlWithoutDomain(element.select(".position-relative a").attr("href"))
- manga.title = element.select(".position-relative a").text()
- manga.thumbnail_url = element.select("img").attr("abs:src")
- return manga
- }
-
- open fun formBuilder() = FormBody.Builder().apply {
- add("_where", "browse")
- add("first", "0")
- add("limit", "0")
- add("prevPos", "null")
- }
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException("Not used")
- override fun searchMangaSelector() = throw UnsupportedOperationException("Not used")
- override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
- override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
-
- override fun mangaDetailsRequest(manga: SManga): Request {
- if (manga.url.startsWith("http")) {
- return GET(manga.url, headers)
- }
- return super.mangaDetailsRequest(manga)
- }
-
- override fun mangaDetailsParse(document: Document): SManga {
- val infoElement = document.select("div#mainer div.container-fluid")
- val manga = SManga.create()
- val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
- val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
- manga.title = infoElement.select("h3").text().removeEntities()
- manga.author = infoElement.select("div.attr-item:contains(author) span").text()
- manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
- manga.status = parseStatus(workStatus, uploadStatus)
- manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
- manga.description = infoElement.select("div.limit-html").text() + "\n" + infoElement.select(".episode-list > .alert-warning").text().trim()
- manga.thumbnail_url = document.select("div.attr-cover img")
- .attr("abs:src")
- return manga
- }
-
- private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
- workStatus == null -> SManga.UNKNOWN
- workStatus.contains("Ongoing") -> SManga.ONGOING
- workStatus.contains("Cancelled") -> SManga.CANCELLED
- workStatus.contains("Hiatus") -> SManga.ON_HIATUS
- workStatus.contains("Completed") -> when {
- uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
- else -> SManga.COMPLETED
- }
- else -> SManga.UNKNOWN
- }
-
- override fun fetchChapterList(manga: SManga): Observable> {
- val url = client.newCall(
- GET(
- when {
- manga.url.startsWith("http") -> manga.url
- else -> "$baseUrl${manga.url}"
- },
- ),
- ).execute().asJsoup()
- if (getAltChapterListPref() || checkChapterLists(url)) {
- val id = manga.url.substringBeforeLast("/").substringAfterLast("/").trim()
- return client.newCall(GET("$baseUrl/rss/series/$id.xml"))
- .asObservableSuccess()
- .map { altChapterParse(it, manga.title) }
- }
- return super.fetchChapterList(manga)
- }
-
- private fun altChapterParse(response: Response, title: String): List {
- return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser())
- .select("channel > item").map { item ->
- SChapter.create().apply {
- url = item.selectFirst("guid")!!.text()
- name = item.selectFirst("title")!!.text().substringAfter(title).trim()
- date_upload = SimpleDateFormat("E, dd MMM yyyy H:m:s Z", Locale.US).parse(item.selectFirst("pubDate")!!.text())?.time ?: 0L
- }
- }
- }
-
- private fun checkChapterLists(document: Document): Boolean {
- return document.select(".episode-list > .alert-warning").text().contains("This comic has been marked as deleted and the chapter list is not available.")
- }
-
- override fun chapterListRequest(manga: SManga): Request {
- if (manga.url.startsWith("http")) {
- return GET(manga.url, headers)
- }
- return super.chapterListRequest(manga)
- }
-
- override fun chapterListSelector() = "div.main div.p-2"
-
- override fun chapterFromElement(element: Element): SChapter {
- val chapter = SChapter.create()
- val urlElement = element.select("a.chapt")
- val group = element.select("div.extra > a:not(.ps-3)").text()
- val time = element.select("div.extra > i.ps-3").text()
- chapter.setUrlWithoutDomain(urlElement.attr("href"))
- chapter.name = urlElement.text()
- if (group != "") {
- chapter.scanlator = group
- }
- if (time != "") {
- chapter.date_upload = parseChapterDate(time)
- }
- return chapter
- }
-
- private fun parseChapterDate(date: String): Long {
- val value = date.split(' ')[0].toInt()
-
- return when {
- "secs" in date -> Calendar.getInstance().apply {
- add(Calendar.SECOND, value * -1)
- }.timeInMillis
- "mins" in date -> Calendar.getInstance().apply {
- add(Calendar.MINUTE, value * -1)
- }.timeInMillis
- "hours" in date -> Calendar.getInstance().apply {
- add(Calendar.HOUR_OF_DAY, value * -1)
- }.timeInMillis
- "days" in date -> Calendar.getInstance().apply {
- add(Calendar.DATE, value * -1)
- }.timeInMillis
- "weeks" in date -> Calendar.getInstance().apply {
- add(Calendar.DATE, value * 7 * -1)
- }.timeInMillis
- "months" in date -> Calendar.getInstance().apply {
- add(Calendar.MONTH, value * -1)
- }.timeInMillis
- "years" in date -> Calendar.getInstance().apply {
- add(Calendar.YEAR, value * -1)
- }.timeInMillis
- "sec" in date -> Calendar.getInstance().apply {
- add(Calendar.SECOND, value * -1)
- }.timeInMillis
- "min" in date -> Calendar.getInstance().apply {
- add(Calendar.MINUTE, value * -1)
- }.timeInMillis
- "hour" in date -> Calendar.getInstance().apply {
- add(Calendar.HOUR_OF_DAY, value * -1)
- }.timeInMillis
- "day" in date -> Calendar.getInstance().apply {
- add(Calendar.DATE, value * -1)
- }.timeInMillis
- "week" in date -> Calendar.getInstance().apply {
- add(Calendar.DATE, value * 7 * -1)
- }.timeInMillis
- "month" in date -> Calendar.getInstance().apply {
- add(Calendar.MONTH, value * -1)
- }.timeInMillis
- "year" in date -> Calendar.getInstance().apply {
- add(Calendar.YEAR, value * -1)
- }.timeInMillis
- else -> {
- return 0
- }
- }
- }
-
- override fun pageListRequest(chapter: SChapter): Request {
- if (chapter.url.startsWith("http")) {
- return GET(chapter.url, headers)
- }
- return super.pageListRequest(chapter)
- }
-
- override fun pageListParse(document: Document): List {
- val script = document.selectFirst("script:containsData(imgHttpLis):containsData(batoWord):containsData(batoPass)")?.html()
- ?: throw RuntimeException("Couldn't find script with image data.")
-
- val imgHttpLisString = script.substringAfter("const imgHttpLis =").substringBefore(";").trim()
- val imgHttpLis = json.parseToJsonElement(imgHttpLisString).jsonArray.map { it.jsonPrimitive.content }
- val batoWord = script.substringAfter("const batoWord =").substringBefore(";").trim()
- val batoPass = script.substringAfter("const batoPass =").substringBefore(";").trim()
-
- val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(batoPass)
- val imgAccListString = CryptoAES.decrypt(batoWord.removeSurrounding("\""), evaluatedPass)
- val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
-
- return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) ->
- Page(i, imageUrl = "$imgUrl?$imgAcc")
- }
- }
-
- override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
-
- private fun String.removeEntities(): String = Parser.unescapeEntities(this, true)
-
- override fun getFilterList() = FilterList(
- LetterFilter(getLetterFilter(), 0),
- Filter.Separator(),
- Filter.Header("NOTE: Ignored if using text search!"),
- Filter.Separator(),
- SortFilter(getSortFilter().map { it.name }.toTypedArray()),
- StatusFilter(getStatusFilter(), 0),
- GenreGroupFilter(getGenreFilter()),
- OriginGroupFilter(getOrginFilter()),
- LangGroupFilter(getLangFilter()),
- MinChapterTextFilter(),
- MaxChapterTextFilter(),
- Filter.Separator(),
- Filter.Header("NOTE: Filters below are incompatible with any other filters!"),
- Filter.Header("NOTE: Login Required!"),
- Filter.Separator(),
- UtilsFilter(getUtilsFilter(), 0),
- HistoryFilter(getHistoryFilter(), 0),
- )
- class SelectFilterOption(val name: String, val value: String)
- class CheckboxFilterOption(val value: String, name: String, default: Boolean = false) : Filter.CheckBox(name, default)
- class TriStateFilterOption(val value: String, name: String, default: Int = 0) : Filter.TriState(name, default)
-
- abstract class SelectFilter(name: String, private val options: List, default: Int = 0) : Filter.Select(name, options.map { it.name }.toTypedArray(), default) {
- val selected: String
- get() = options[state].value
- }
-
- abstract class CheckboxGroupFilter(name: String, options: List) : Filter.Group(name, options) {
- val selected: List
- get() = state.filter { it.state }.map { it.value }
- }
-
- abstract class TriStateGroupFilter(name: String, options: List) : Filter.Group(name, options) {
- val included: List
- get() = state.filter { it.isIncluded() }.map { it.value }
-
- val excluded: List
- get() = state.filter { it.isExcluded() }.map { it.value }
- }
-
- abstract class TextFilter(name: String) : Filter.Text(name)
-
- class SortFilter(sortables: Array) : Filter.Sort("Sort", sortables, Selection(5, false))
- class StatusFilter(options: List, default: Int) : SelectFilter("Status", options, default)
- class OriginGroupFilter(options: List) : CheckboxGroupFilter("Origin", options)
- class GenreGroupFilter(options: List) : TriStateGroupFilter("Genre", options)
- class MinChapterTextFilter : TextFilter("Min. Chapters")
- class MaxChapterTextFilter : TextFilter("Max. Chapters")
- class LangGroupFilter(options: List) : CheckboxGroupFilter("Languages", options)
- class LetterFilter(options: List, default: Int) : SelectFilter("Letter matching mode (Slow)", options, default)
- class UtilsFilter(options: List, default: Int) : SelectFilter("Utils comic list", options, default)
- class HistoryFilter(options: List, default: Int) : SelectFilter("Personal list", options, default)
-
- private fun getLetterFilter() = listOf(
- SelectFilterOption("Disabled", "disabled"),
- SelectFilterOption("Enabled", "enabled"),
- )
-
- private fun getSortFilter() = listOf(
- SelectFilterOption("Z-A", "title"),
- SelectFilterOption("Last Updated", "update"),
- SelectFilterOption("Newest Added", "create"),
- SelectFilterOption("Most Views Totally", "views_a"),
- SelectFilterOption("Most Views 365 days", "views_y"),
- SelectFilterOption("Most Views 30 days", "views_m"),
- SelectFilterOption("Most Views 7 days", "views_w"),
- SelectFilterOption("Most Views 24 hours", "views_d"),
- SelectFilterOption("Most Views 60 minutes", "views_h"),
- )
-
- private fun getHistoryFilter() = listOf(
- SelectFilterOption("None", ""),
- SelectFilterOption("My History", "history"),
- SelectFilterOption("My Updates", "updates"),
- )
-
- private fun getUtilsFilter() = listOf(
- SelectFilterOption("None", ""),
- SelectFilterOption("Comics: I Created", "i-created"),
- SelectFilterOption("Comics: I Modified", "i-modified"),
- SelectFilterOption("Comics: I Uploaded", "i-uploaded"),
- SelectFilterOption("Comics: Authorized to me", "i-authorized"),
- SelectFilterOption("Comics: Draft Status", "status-draft"),
- SelectFilterOption("Comics: Hidden Status", "status-hidden"),
- SelectFilterOption("Ongoing and Not updated in 30-60 days", "not-updated-30-60"),
- SelectFilterOption("Ongoing and Not updated in 60-90 days", "not-updated-60-90"),
- SelectFilterOption("Ongoing and Not updated in 90-180 days", "not-updated-90-180"),
- SelectFilterOption("Ongoing and Not updated in 180-360 days", "not-updated-180-360"),
- SelectFilterOption("Ongoing and Not updated in 360-1000 days", "not-updated-360-1000"),
- SelectFilterOption("Ongoing and Not updated more than 1000 days", "not-updated-1000"),
- )
-
- private fun getStatusFilter() = listOf(
- SelectFilterOption("All", ""),
- SelectFilterOption("Pending", "pending"),
- SelectFilterOption("Ongoing", "ongoing"),
- SelectFilterOption("Completed", "completed"),
- SelectFilterOption("Hiatus", "hiatus"),
- SelectFilterOption("Cancelled", "cancelled"),
- )
-
- private fun getOrginFilter() = listOf(
- // Values exported from publish.bato.to
- CheckboxFilterOption("zh", "Chinese"),
- CheckboxFilterOption("en", "English"),
- CheckboxFilterOption("ja", "Japanese"),
- CheckboxFilterOption("ko", "Korean"),
- CheckboxFilterOption("af", "Afrikaans"),
- CheckboxFilterOption("sq", "Albanian"),
- CheckboxFilterOption("am", "Amharic"),
- CheckboxFilterOption("ar", "Arabic"),
- CheckboxFilterOption("hy", "Armenian"),
- CheckboxFilterOption("az", "Azerbaijani"),
- CheckboxFilterOption("be", "Belarusian"),
- CheckboxFilterOption("bn", "Bengali"),
- CheckboxFilterOption("bs", "Bosnian"),
- CheckboxFilterOption("bg", "Bulgarian"),
- CheckboxFilterOption("my", "Burmese"),
- CheckboxFilterOption("km", "Cambodian"),
- CheckboxFilterOption("ca", "Catalan"),
- CheckboxFilterOption("ceb", "Cebuano"),
- CheckboxFilterOption("zh_hk", "Chinese (Cantonese)"),
- CheckboxFilterOption("zh_tw", "Chinese (Traditional)"),
- CheckboxFilterOption("hr", "Croatian"),
- CheckboxFilterOption("cs", "Czech"),
- CheckboxFilterOption("da", "Danish"),
- CheckboxFilterOption("nl", "Dutch"),
- CheckboxFilterOption("en_us", "English (United States)"),
- CheckboxFilterOption("eo", "Esperanto"),
- CheckboxFilterOption("et", "Estonian"),
- CheckboxFilterOption("fo", "Faroese"),
- CheckboxFilterOption("fil", "Filipino"),
- CheckboxFilterOption("fi", "Finnish"),
- CheckboxFilterOption("fr", "French"),
- CheckboxFilterOption("ka", "Georgian"),
- CheckboxFilterOption("de", "German"),
- CheckboxFilterOption("el", "Greek"),
- CheckboxFilterOption("gn", "Guarani"),
- CheckboxFilterOption("gu", "Gujarati"),
- CheckboxFilterOption("ht", "Haitian Creole"),
- CheckboxFilterOption("ha", "Hausa"),
- CheckboxFilterOption("he", "Hebrew"),
- CheckboxFilterOption("hi", "Hindi"),
- CheckboxFilterOption("hu", "Hungarian"),
- CheckboxFilterOption("is", "Icelandic"),
- CheckboxFilterOption("ig", "Igbo"),
- CheckboxFilterOption("id", "Indonesian"),
- CheckboxFilterOption("ga", "Irish"),
- CheckboxFilterOption("it", "Italian"),
- CheckboxFilterOption("jv", "Javanese"),
- CheckboxFilterOption("kn", "Kannada"),
- CheckboxFilterOption("kk", "Kazakh"),
- CheckboxFilterOption("ku", "Kurdish"),
- CheckboxFilterOption("ky", "Kyrgyz"),
- CheckboxFilterOption("lo", "Laothian"),
- CheckboxFilterOption("lv", "Latvian"),
- CheckboxFilterOption("lt", "Lithuanian"),
- CheckboxFilterOption("lb", "Luxembourgish"),
- CheckboxFilterOption("mk", "Macedonian"),
- CheckboxFilterOption("mg", "Malagasy"),
- CheckboxFilterOption("ms", "Malay"),
- CheckboxFilterOption("ml", "Malayalam"),
- CheckboxFilterOption("mt", "Maltese"),
- CheckboxFilterOption("mi", "Maori"),
- CheckboxFilterOption("mr", "Marathi"),
- CheckboxFilterOption("mo", "Moldavian"),
- CheckboxFilterOption("mn", "Mongolian"),
- CheckboxFilterOption("ne", "Nepali"),
- CheckboxFilterOption("no", "Norwegian"),
- CheckboxFilterOption("ny", "Nyanja"),
- CheckboxFilterOption("ps", "Pashto"),
- CheckboxFilterOption("fa", "Persian"),
- CheckboxFilterOption("pl", "Polish"),
- CheckboxFilterOption("pt", "Portuguese"),
- CheckboxFilterOption("pt_br", "Portuguese (Brazil)"),
- CheckboxFilterOption("ro", "Romanian"),
- CheckboxFilterOption("rm", "Romansh"),
- CheckboxFilterOption("ru", "Russian"),
- CheckboxFilterOption("sm", "Samoan"),
- CheckboxFilterOption("sr", "Serbian"),
- CheckboxFilterOption("sh", "Serbo-Croatian"),
- CheckboxFilterOption("st", "Sesotho"),
- CheckboxFilterOption("sn", "Shona"),
- CheckboxFilterOption("sd", "Sindhi"),
- CheckboxFilterOption("si", "Sinhalese"),
- CheckboxFilterOption("sk", "Slovak"),
- CheckboxFilterOption("sl", "Slovenian"),
- CheckboxFilterOption("so", "Somali"),
- CheckboxFilterOption("es", "Spanish"),
- CheckboxFilterOption("es_419", "Spanish (Latin America)"),
- CheckboxFilterOption("sw", "Swahili"),
- CheckboxFilterOption("sv", "Swedish"),
- CheckboxFilterOption("tg", "Tajik"),
- CheckboxFilterOption("ta", "Tamil"),
- CheckboxFilterOption("th", "Thai"),
- CheckboxFilterOption("ti", "Tigrinya"),
- CheckboxFilterOption("to", "Tonga"),
- CheckboxFilterOption("tr", "Turkish"),
- CheckboxFilterOption("tk", "Turkmen"),
- CheckboxFilterOption("uk", "Ukrainian"),
- CheckboxFilterOption("ur", "Urdu"),
- CheckboxFilterOption("uz", "Uzbek"),
- CheckboxFilterOption("vi", "Vietnamese"),
- CheckboxFilterOption("yo", "Yoruba"),
- CheckboxFilterOption("zu", "Zulu"),
- CheckboxFilterOption("_t", "Other"),
- )
-
- private fun getGenreFilter() = listOf(
- TriStateFilterOption("artbook", "Artbook"),
- TriStateFilterOption("cartoon", "Cartoon"),
- TriStateFilterOption("comic", "Comic"),
- TriStateFilterOption("doujinshi", "Doujinshi"),
- TriStateFilterOption("imageset", "Imageset"),
- TriStateFilterOption("manga", "Manga"),
- TriStateFilterOption("manhua", "Manhua"),
- TriStateFilterOption("manhwa", "Manhwa"),
- TriStateFilterOption("webtoon", "Webtoon"),
- TriStateFilterOption("western", "Western"),
-
- TriStateFilterOption("shoujo", "Shoujo(G)"),
- TriStateFilterOption("shounen", "Shounen(B)"),
- TriStateFilterOption("josei", "Josei(W)"),
- TriStateFilterOption("seinen", "Seinen(M)"),
- TriStateFilterOption("yuri", "Yuri(GL)"),
- TriStateFilterOption("yaoi", "Yaoi(BL)"),
- TriStateFilterOption("futa", "Futa(WL)"),
- TriStateFilterOption("bara", "Bara(ML)"),
-
- TriStateFilterOption("gore", "Gore"),
- TriStateFilterOption("bloody", "Bloody"),
- TriStateFilterOption("violence", "Violence"),
- TriStateFilterOption("ecchi", "Ecchi"),
- TriStateFilterOption("adult", "Adult"),
- TriStateFilterOption("mature", "Mature"),
- TriStateFilterOption("smut", "Smut"),
- TriStateFilterOption("hentai", "Hentai"),
-
- TriStateFilterOption("_4_koma", "4-Koma"),
- TriStateFilterOption("action", "Action"),
- TriStateFilterOption("adaptation", "Adaptation"),
- TriStateFilterOption("adventure", "Adventure"),
- TriStateFilterOption("age_gap", "Age Gap"),
- TriStateFilterOption("aliens", "Aliens"),
- TriStateFilterOption("animals", "Animals"),
- TriStateFilterOption("anthology", "Anthology"),
- TriStateFilterOption("beasts", "Beasts"),
- TriStateFilterOption("bodyswap", "Bodyswap"),
- TriStateFilterOption("cars", "cars"),
- TriStateFilterOption("cheating_infidelity", "Cheating/Infidelity"),
- TriStateFilterOption("childhood_friends", "Childhood Friends"),
- TriStateFilterOption("college_life", "College Life"),
- TriStateFilterOption("comedy", "Comedy"),
- TriStateFilterOption("contest_winning", "Contest Winning"),
- TriStateFilterOption("cooking", "Cooking"),
- TriStateFilterOption("crime", "crime"),
- TriStateFilterOption("crossdressing", "Crossdressing"),
- TriStateFilterOption("delinquents", "Delinquents"),
- TriStateFilterOption("dementia", "Dementia"),
- TriStateFilterOption("demons", "Demons"),
- TriStateFilterOption("drama", "Drama"),
- TriStateFilterOption("dungeons", "Dungeons"),
- TriStateFilterOption("emperor_daughte", "Emperor's Daughter"),
- TriStateFilterOption("fantasy", "Fantasy"),
- TriStateFilterOption("fan_colored", "Fan-Colored"),
- TriStateFilterOption("fetish", "Fetish"),
- TriStateFilterOption("full_color", "Full Color"),
- TriStateFilterOption("game", "Game"),
- TriStateFilterOption("gender_bender", "Gender Bender"),
- TriStateFilterOption("genderswap", "Genderswap"),
- TriStateFilterOption("ghosts", "Ghosts"),
- TriStateFilterOption("gyaru", "Gyaru"),
- TriStateFilterOption("harem", "Harem"),
- TriStateFilterOption("harlequin", "Harlequin"),
- TriStateFilterOption("historical", "Historical"),
- TriStateFilterOption("horror", "Horror"),
- TriStateFilterOption("incest", "Incest"),
- TriStateFilterOption("isekai", "Isekai"),
- TriStateFilterOption("kids", "Kids"),
- TriStateFilterOption("loli", "Loli"),
- TriStateFilterOption("magic", "Magic"),
- TriStateFilterOption("magical_girls", "Magical Girls"),
- TriStateFilterOption("martial_arts", "Martial Arts"),
- TriStateFilterOption("mecha", "Mecha"),
- TriStateFilterOption("medical", "Medical"),
- TriStateFilterOption("military", "Military"),
- TriStateFilterOption("monster_girls", "Monster Girls"),
- TriStateFilterOption("monsters", "Monsters"),
- TriStateFilterOption("music", "Music"),
- TriStateFilterOption("mystery", "Mystery"),
- TriStateFilterOption("netorare", "Netorare/NTR"),
- TriStateFilterOption("ninja", "Ninja"),
- TriStateFilterOption("office_workers", "Office Workers"),
- TriStateFilterOption("omegaverse", "Omegaverse"),
- TriStateFilterOption("oneshot", "Oneshot"),
- TriStateFilterOption("parody", "parody"),
- TriStateFilterOption("philosophical", "Philosophical"),
- TriStateFilterOption("police", "Police"),
- TriStateFilterOption("post_apocalyptic", "Post-Apocalyptic"),
- TriStateFilterOption("psychological", "Psychological"),
- TriStateFilterOption("regression", "Regression"),
- TriStateFilterOption("reincarnation", "Reincarnation"),
- TriStateFilterOption("reverse_harem", "Reverse Harem"),
- TriStateFilterOption("reverse_isekai", "Reverse Isekai"),
- TriStateFilterOption("romance", "Romance"),
- TriStateFilterOption("royal_family", "Royal Family"),
- TriStateFilterOption("royalty", "Royalty"),
- TriStateFilterOption("samurai", "Samurai"),
- TriStateFilterOption("school_life", "School Life"),
- TriStateFilterOption("sci_fi", "Sci-Fi"),
- TriStateFilterOption("shota", "Shota"),
- TriStateFilterOption("shoujo_ai", "Shoujo Ai"),
- TriStateFilterOption("shounen_ai", "Shounen Ai"),
- TriStateFilterOption("showbiz", "Showbiz"),
- TriStateFilterOption("slice_of_life", "Slice of Life"),
- TriStateFilterOption("sm_bdsm", "SM/BDSM/SUB-DOM"),
- TriStateFilterOption("space", "Space"),
- TriStateFilterOption("sports", "Sports"),
- TriStateFilterOption("super_power", "Super Power"),
- TriStateFilterOption("superhero", "Superhero"),
- TriStateFilterOption("supernatural", "Supernatural"),
- TriStateFilterOption("survival", "Survival"),
- TriStateFilterOption("thriller", "Thriller"),
- TriStateFilterOption("time_travel", "Time Travel"),
- TriStateFilterOption("tower_climbing", "Tower Climbing"),
- TriStateFilterOption("traditional_games", "Traditional Games"),
- TriStateFilterOption("tragedy", "Tragedy"),
- TriStateFilterOption("transmigration", "Transmigration"),
- TriStateFilterOption("vampires", "Vampires"),
- TriStateFilterOption("villainess", "Villainess"),
- TriStateFilterOption("video_games", "Video Games"),
- TriStateFilterOption("virtual_reality", "Virtual Reality"),
- TriStateFilterOption("wuxia", "Wuxia"),
- TriStateFilterOption("xianxia", "Xianxia"),
- TriStateFilterOption("xuanhuan", "Xuanhuan"),
- TriStateFilterOption("zombies", "Zombies"),
- // Hidden Genres
- TriStateFilterOption("shotacon", "shotacon"),
- TriStateFilterOption("lolicon", "lolicon"),
- TriStateFilterOption("award_winning", "Award Winning"),
- TriStateFilterOption("youkai", "Youkai"),
- TriStateFilterOption("uncategorized", "Uncategorized"),
- )
-
- private fun getLangFilter() = listOf(
- // Values exported from publish.bato.to
- CheckboxFilterOption("en", "English"),
- CheckboxFilterOption("ar", "Arabic"),
- CheckboxFilterOption("bg", "Bulgarian"),
- CheckboxFilterOption("zh", "Chinese"),
- CheckboxFilterOption("cs", "Czech"),
- CheckboxFilterOption("da", "Danish"),
- CheckboxFilterOption("nl", "Dutch"),
- CheckboxFilterOption("fil", "Filipino"),
- CheckboxFilterOption("fi", "Finnish"),
- CheckboxFilterOption("fr", "French"),
- CheckboxFilterOption("de", "German"),
- CheckboxFilterOption("el", "Greek"),
- CheckboxFilterOption("he", "Hebrew"),
- CheckboxFilterOption("hi", "Hindi"),
- CheckboxFilterOption("hu", "Hungarian"),
- CheckboxFilterOption("id", "Indonesian"),
- CheckboxFilterOption("it", "Italian"),
- CheckboxFilterOption("ja", "Japanese"),
- CheckboxFilterOption("ko", "Korean"),
- CheckboxFilterOption("ms", "Malay"),
- CheckboxFilterOption("pl", "Polish"),
- CheckboxFilterOption("pt", "Portuguese"),
- CheckboxFilterOption("pt_br", "Portuguese (Brazil)"),
- CheckboxFilterOption("ro", "Romanian"),
- CheckboxFilterOption("ru", "Russian"),
- CheckboxFilterOption("es", "Spanish"),
- CheckboxFilterOption("es_419", "Spanish (Latin America)"),
- CheckboxFilterOption("sv", "Swedish"),
- CheckboxFilterOption("th", "Thai"),
- CheckboxFilterOption("tr", "Turkish"),
- CheckboxFilterOption("uk", "Ukrainian"),
- CheckboxFilterOption("vi", "Vietnamese"),
- CheckboxFilterOption("af", "Afrikaans"),
- CheckboxFilterOption("sq", "Albanian"),
- CheckboxFilterOption("am", "Amharic"),
- CheckboxFilterOption("hy", "Armenian"),
- CheckboxFilterOption("az", "Azerbaijani"),
- CheckboxFilterOption("be", "Belarusian"),
- CheckboxFilterOption("bn", "Bengali"),
- CheckboxFilterOption("bs", "Bosnian"),
- CheckboxFilterOption("my", "Burmese"),
- CheckboxFilterOption("km", "Cambodian"),
- CheckboxFilterOption("ca", "Catalan"),
- CheckboxFilterOption("ceb", "Cebuano"),
- CheckboxFilterOption("zh_hk", "Chinese (Cantonese)"),
- CheckboxFilterOption("zh_tw", "Chinese (Traditional)"),
- CheckboxFilterOption("hr", "Croatian"),
- CheckboxFilterOption("en_us", "English (United States)"),
- CheckboxFilterOption("eo", "Esperanto"),
- CheckboxFilterOption("et", "Estonian"),
- CheckboxFilterOption("fo", "Faroese"),
- CheckboxFilterOption("ka", "Georgian"),
- CheckboxFilterOption("gn", "Guarani"),
- CheckboxFilterOption("gu", "Gujarati"),
- CheckboxFilterOption("ht", "Haitian Creole"),
- CheckboxFilterOption("ha", "Hausa"),
- CheckboxFilterOption("is", "Icelandic"),
- CheckboxFilterOption("ig", "Igbo"),
- CheckboxFilterOption("ga", "Irish"),
- CheckboxFilterOption("jv", "Javanese"),
- CheckboxFilterOption("kn", "Kannada"),
- CheckboxFilterOption("kk", "Kazakh"),
- CheckboxFilterOption("ku", "Kurdish"),
- CheckboxFilterOption("ky", "Kyrgyz"),
- CheckboxFilterOption("lo", "Laothian"),
- CheckboxFilterOption("lv", "Latvian"),
- CheckboxFilterOption("lt", "Lithuanian"),
- CheckboxFilterOption("lb", "Luxembourgish"),
- CheckboxFilterOption("mk", "Macedonian"),
- CheckboxFilterOption("mg", "Malagasy"),
- CheckboxFilterOption("ml", "Malayalam"),
- CheckboxFilterOption("mt", "Maltese"),
- CheckboxFilterOption("mi", "Maori"),
- CheckboxFilterOption("mr", "Marathi"),
- CheckboxFilterOption("mo", "Moldavian"),
- CheckboxFilterOption("mn", "Mongolian"),
- CheckboxFilterOption("ne", "Nepali"),
- CheckboxFilterOption("no", "Norwegian"),
- CheckboxFilterOption("ny", "Nyanja"),
- CheckboxFilterOption("ps", "Pashto"),
- CheckboxFilterOption("fa", "Persian"),
- CheckboxFilterOption("rm", "Romansh"),
- CheckboxFilterOption("sm", "Samoan"),
- CheckboxFilterOption("sr", "Serbian"),
- CheckboxFilterOption("sh", "Serbo-Croatian"),
- CheckboxFilterOption("st", "Sesotho"),
- CheckboxFilterOption("sn", "Shona"),
- CheckboxFilterOption("sd", "Sindhi"),
- CheckboxFilterOption("si", "Sinhalese"),
- CheckboxFilterOption("sk", "Slovak"),
- CheckboxFilterOption("sl", "Slovenian"),
- CheckboxFilterOption("so", "Somali"),
- CheckboxFilterOption("sw", "Swahili"),
- CheckboxFilterOption("tg", "Tajik"),
- CheckboxFilterOption("ta", "Tamil"),
- CheckboxFilterOption("ti", "Tigrinya"),
- CheckboxFilterOption("to", "Tonga"),
- CheckboxFilterOption("tk", "Turkmen"),
- CheckboxFilterOption("ur", "Urdu"),
- CheckboxFilterOption("uz", "Uzbek"),
- CheckboxFilterOption("yo", "Yoruba"),
- CheckboxFilterOption("zu", "Zulu"),
- CheckboxFilterOption("_t", "Other"),
- // Lang options from bato.to brows not in publish.bato.to
- CheckboxFilterOption("eu", "Basque"),
- CheckboxFilterOption("pt-PT", "Portuguese (Portugal)"),
- ).filterNot { it.value == siteLang }
-
- companion object {
- private const val MIRROR_PREF_KEY = "MIRROR"
- private const val MIRROR_PREF_TITLE = "Mirror"
- private val MIRROR_PREF_ENTRIES = arrayOf(
- "bato.to",
- "batocomic.com",
- "batocomic.net",
- "batocomic.org",
- "batotoo.com",
- "batotwo.com",
- "battwo.com",
- "comiko.net",
- "comiko.org",
- "mangatoto.com",
- "mangatoto.net",
- "mangatoto.org",
- "readtoto.com",
- "readtoto.net",
- "readtoto.org",
- "dto.to",
- "hto.to",
- "mto.to",
- "wto.to",
- "xbato.com",
- "xbato.net",
- "xbato.org",
- "zbato.com",
- "zbato.net",
- "zbato.org",
- )
- private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
- private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
-
- private const val ALT_CHAPTER_LIST_PREF_KEY = "ALT_CHAPTER_LIST"
- private const val ALT_CHAPTER_LIST_PREF_TITLE = "Alternative Chapter List"
- private const val ALT_CHAPTER_LIST_PREF_SUMMARY = "If checked, uses an alternate chapter list"
- private const val ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE = false
- }
-}
diff --git a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToFactory.kt b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToFactory.kt
deleted file mode 100644
index 2a4ee05ee6..0000000000
--- a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToFactory.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.batoto
-
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.SourceFactory
-
-class BatoToFactory : SourceFactory {
- override fun createSources(): List = languages.map { BatoTo(it.lang, it.siteLang) }
-}
-
-class LanguageOption(val lang: String, val siteLang: String = lang)
-private val languages = listOf(
- LanguageOption("all", ""),
- // Lang options from publish.bato.to
- LanguageOption("en"),
- LanguageOption("ar"),
- LanguageOption("bg"),
- LanguageOption("zh"),
- LanguageOption("cs"),
- LanguageOption("da"),
- LanguageOption("nl"),
- LanguageOption("fil"),
- LanguageOption("fi"),
- LanguageOption("fr"),
- LanguageOption("de"),
- LanguageOption("el"),
- LanguageOption("he"),
- LanguageOption("hi"),
- LanguageOption("hu"),
- LanguageOption("id"),
- LanguageOption("it"),
- LanguageOption("ja"),
- LanguageOption("ko"),
- LanguageOption("ms"),
- LanguageOption("pl"),
- LanguageOption("pt"),
- LanguageOption("pt-BR", "pt_br"),
- LanguageOption("ro"),
- LanguageOption("ru"),
- LanguageOption("es"),
- LanguageOption("es-419", "es_419"),
- LanguageOption("sv"),
- LanguageOption("th"),
- LanguageOption("tr"),
- LanguageOption("uk"),
- LanguageOption("vi"),
- LanguageOption("af"),
- LanguageOption("sq"),
- LanguageOption("am"),
- LanguageOption("hy"),
- LanguageOption("az"),
- LanguageOption("be"),
- LanguageOption("bn"),
- LanguageOption("bs"),
- LanguageOption("my"),
- LanguageOption("km"),
- LanguageOption("ca"),
- LanguageOption("ceb"),
- LanguageOption("zh-Hans", "zh_hk"),
- LanguageOption("zh-Hant", "zh_tw"),
- LanguageOption("hr"),
- LanguageOption("en-US", "en_us"),
- LanguageOption("eo"),
- LanguageOption("et"),
- LanguageOption("fo"),
- LanguageOption("ka"),
- LanguageOption("gn"),
- LanguageOption("gu"),
- LanguageOption("ht"),
- LanguageOption("ha"),
- LanguageOption("is"),
- LanguageOption("ig"),
- LanguageOption("ga"),
- LanguageOption("jv"),
- LanguageOption("kn"),
- LanguageOption("kk"),
- LanguageOption("ku"),
- LanguageOption("ky"),
- LanguageOption("lo"),
- LanguageOption("lv"),
- LanguageOption("lt"),
- LanguageOption("lb"),
- LanguageOption("mk"),
- LanguageOption("mg"),
- LanguageOption("ml"),
- LanguageOption("mt"),
- LanguageOption("mi"),
- LanguageOption("mr"),
- LanguageOption("mo", "ro-MD"),
- LanguageOption("mn"),
- LanguageOption("ne"),
- LanguageOption("no"),
- LanguageOption("ny"),
- LanguageOption("ps"),
- LanguageOption("fa"),
- LanguageOption("rm"),
- LanguageOption("sm"),
- LanguageOption("sr"),
- LanguageOption("sh"),
- LanguageOption("st"),
- LanguageOption("sn"),
- LanguageOption("sd"),
- LanguageOption("si"),
- LanguageOption("sk"),
- LanguageOption("sl"),
- LanguageOption("so"),
- LanguageOption("sw"),
- LanguageOption("tg"),
- LanguageOption("ta"),
- LanguageOption("ti"),
- LanguageOption("to"),
- LanguageOption("tk"),
- LanguageOption("ur"),
- LanguageOption("uz"),
- LanguageOption("yo"),
- LanguageOption("zu"),
- LanguageOption("other", "_t"),
- // Lang options from bato.to brows not in publish.bato.to
- LanguageOption("eu"),
- LanguageOption("pt-PT", "pt_pt"),
- // Lang options that got removed
- // Pair("xh", "xh"),
-)
diff --git a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToUrlActivity.kt b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToUrlActivity.kt
deleted file mode 100644
index 4f1bd140ed..0000000000
--- a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToUrlActivity.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.batoto
-
-import android.app.Activity
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.os.Bundle
-import android.util.Log
-import kotlin.system.exitProcess
-
-class BatoToUrlActivity : Activity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val host = intent?.data?.host
- val pathSegments = intent?.data?.pathSegments
-
- if (host != null && pathSegments != null) {
- val query = fromBatoTo(pathSegments)
-
- if (query == null) {
- Log.e("BatoToUrlActivity", "Unable to parse URI from intent $intent")
- finish()
- exitProcess(1)
- }
-
- val mainIntent = Intent().apply {
- action = "eu.kanade.tachiyomi.SEARCH"
- putExtra("query", query)
- putExtra("filter", packageName)
- }
-
- try {
- startActivity(mainIntent)
- } catch (e: ActivityNotFoundException) {
- Log.e("BatoToUrlActivity", e.toString())
- }
- }
-
- finish()
- exitProcess(0)
- }
-
- private fun fromBatoTo(pathSegments: MutableList): String? {
- return if (pathSegments.size >= 2) {
- val id = pathSegments[1]
- "ID:$id"
- } else {
- null
- }
- }
-}
diff --git a/src/all/mangadex/AndroidManifest.xml b/src/all/mangadex/AndroidManifest.xml
deleted file mode 100644
index 4faeb6138d..0000000000
--- a/src/all/mangadex/AndroidManifest.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/all/mangadex/README.md b/src/all/mangadex/README.md
deleted file mode 100644
index e1859cc123..0000000000
--- a/src/all/mangadex/README.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# MangaDex
-
-Table of Content
-- [FAQ](#FAQ)
- - [Version 5 API Rewrite](#version-5-api-rewrite)
-- [Guides](#Guides)
- - [How can I block particular Scanlator Groups?](#how-can-i-block-particular-scanlator-groups)
-
-Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
-
-## FAQ
-
-### Version 5 API Rewrite
-
-#### Why are all my manga saying "Manga ID format has changed, migrate from MangaDex to MangaDex to continue reading"?
-You need to [migrate](https://tachiyomi.org/help/guides/source-migration/) all your MangaDex manga from MangaDex to MangaDex as MangaDex has changed their manga ID system from IDs to UUIDs.
-
-#### Why can I not restore from a JSON backup?
-JSON backups are now unusable due to the ID change. You will have to manually re-add your manga.
-
-## Guides
-
-### What does the Status of a Manga in Tachiyomi mean?
-
-Please refer to the following table
-
-| Status in Tachiyomi | in MangaDex | Remarks |
-|---------------------|------------------------|---------|
-| Ongoing | Publication: Ongoing | |
-| Cancelled | Publication: Cancelled | This title was abruptly stopped and will not resume |
-| Publishing Finished | Publication: Completed | The title is finished in its original language. However, Translations remain |
-| On_Hiatus | Publication: Hiatus | The title is not currently receiving any new chapters |
-| Completed | Completed/Cancelled | All chapters are translated and available |
-| Unknown | Unknown | There is no info about the Status of this Entry |
-
-### How can I block particular Scanlator Groups?
-
-The **MangaDex** extension allows blocking **Scanlator Groups**. Chapters uploaded by a **Blocked Scanlator Group** will not show up in **Latest** or in **Manga feed** (chapters list). For now, you can only block Groups by entering their UUIDs manually.
-
-Follow the following steps to easily block a group from the Tachiyomi MangaDex extension:
-
-A. Finding the **UUIDs**:
-- Go to [https://mangadex.org](https://mangadex.org) and **Search** for the Scanlation Group that you wish to block and view their Group Details
-- Using the URL of this page, get the 16-digit alphanumeric string which will be the UUID for that scanlation group
-- For Example:
- * The Group *Tristan's test scans* has the URL
- - [https://mangadex.org/group/6410209a-0f39-4f51-a139-bc559ad61a4f/tristan-s-test-scans](https://mangadex.org/group/6410209a-0f39-4f51-a139-bc559ad61a4f/tristan-s-test-scans)
- - Therefore, their UUID will be `6410209a-0f39-4f51-a139-bc559ad61a4f`
- * Other Examples include:
- + Azuki Manga | `5fed0576-8b94-4f9a-b6a7-08eecd69800d`
- + Bilibili Comics | `06a9fecb-b608-4f19-b93c-7caab06b7f44`
- + Comikey | `8d8ecf83-8d42-4f8c-add8-60963f9f28d9`
- + INKR | `caa63201-4a17-4b7f-95ff-ed884a2b7e60`
- + MangaHot | `319c1b10-cbd0-4f55-a46e-c4ee17e65139`
- + MangaPlus | `4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb`
-
-B. Blocking a group using their UUID in Tachiyomi MangaDex extension `v1.2.150+`:
-1. Go to **Browse** → **Extensions**.
-1. Click on **MangaDex** extension and then **Settings** under your Language of choice.
-1. Tap on the option **Block Groups by UUID** and enter the UUIDs.
- - By Default, the following groups are blocked:
- ```
- Azuki Manga, Bilibili Comics, Comikey, INKR, MangaHot & MangaPlus
- ```
- - Which are entered as:
- ```
- 5fed0576-8b94-4f9a-b6a7-08eecd69800d, 06a9fecb-b608-4f19-b93c-7caab06b7f44,
- 8d8ecf83-8d42-4f8c-add8-60963f9f28d9, caa63201-4a17-4b7f-95ff-ed884a2b7e60,
- 319c1b10-cbd0-4f55-a46e-c4ee17e65139, 4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb
- ```
diff --git a/src/all/mangadex/assets/i18n/messages_en.properties b/src/all/mangadex/assets/i18n/messages_en.properties
deleted file mode 100644
index 435187ebc7..0000000000
--- a/src/all/mangadex/assets/i18n/messages_en.properties
+++ /dev/null
@@ -1,150 +0,0 @@
-alternative_titles=Alternative titles:
-alternative_titles_in_description=Alternative titles in description
-alternative_titles_in_description_summary=Include a manga's alternative titles at the end of its description
-block_group_by_uuid=Block groups by UUID
-block_group_by_uuid_summary=Chapters from blocked groups will not show up in Latest or Manga feed. Enter as a Comma-separated list of group UUIDs
-block_uploader_by_uuid=Block uploader by UUID
-block_uploader_by_uuid_summary=Chapters from blocked uploaders will not show up in Latest or Manga feed. Enter as a Comma-separated list of uploader UUIDs
-content=Content
-content_gore=Gore
-content_rating=Content rating
-content_rating_erotica=Erotica
-content_rating_genre=Content rating: %s
-content_rating_pornographic=Pornographic
-content_rating_safe=Safe
-content_rating_suggestive=Suggestive
-content_sexual_violence=Sexual violence
-cover_quality=Cover quality
-cover_quality_low=Low
-cover_quality_medium=Medium
-cover_quality_original=Original
-data_saver=Data saver
-data_saver_summary=Enables smaller, more compressed images
-excluded_tags_mode=Excluded tags mode
-filter_original_languages=Filter original languages
-filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse
-format=Format
-format_adaptation=Adaptation
-format_anthology=Anthology
-format_award_winning=Award Winning
-format_doujinshi=Doujinshi
-format_fan_colored=Fan Colored
-format_full_color=Full Color
-format_long_strip=Long Strip
-format_official_colored=Official Colored
-format_oneshot=Oneshot
-format_user_created=User Created
-format_web_comic=Web Comic
-format_yonkoma=4-Koma
-genre=Genre
-genre_action=Action
-genre_adventure=Adventure
-genre_boys_love=Boy's Love
-genre_comedy=Comedy
-genre_crime=Crime
-genre_drama=Drama
-genre_fantasy=Fantasy
-genre_girls_love=Girl's Love
-genre_historical=Historical
-genre_horror=Horror
-genre_isekai=Isekai
-genre_magical_girls=Magical Girls
-genre_mecha=Mecha
-genre_medical=Medical
-genre_mystery=Mystery
-genre_philosophical=Philosophical
-genre_romance=Romance
-genre_sci_fi=Sci-Fi
-genre_slice_of_life=Slice of Life
-genre_sports=Sports
-genre_superhero=Superhero
-genre_thriller=Thriller
-genre_tragedy=Tragedy
-genre_wuxia=Wuxia
-has_available_chapters=Has available chapters
-included_tags_mode=Included tags mode
-invalid_author_id=Not a valid author ID
-invalid_manga_id=Not a valid manga ID
-invalid_group_id=Not a valid group ID
-invalid_uuids=The text contains invalid UUIDs
-migrate_warning=Migrate this entry from MangaDex to MangaDex to update it
-mode_and=And
-mode_or=Or
-no_group=No Group
-no_series_in_list=No series in the list
-original_language=Original language
-original_language_filter_chinese=%s (Manhua)
-original_language_filter_japanese=%s (Manga)
-original_language_filter_korean=%s (Manhwa)
-publication_demographic=Publication demographic
-publication_demographic_josei=Josei
-publication_demographic_none=None
-publication_demographic_seinen=Seinen
-publication_demographic_shoujo=Shoujo
-publication_demographic_shounen=Shounen
-sort=Sort
-sort_alphabetic=Alphabetic
-sort_chapter_uploaded_at=Chapter uploaded at
-sort_content_created_at=Content created at
-sort_content_info_updated_at=Content info updated at
-sort_number_of_follows=Number of follows
-sort_rating=Rating
-sort_relevance=Relevance
-sort_year=Year
-standard_content_rating=Default content rating
-standard_content_rating_summary=Show content with the selected ratings by default
-standard_https_port=Use HTTPS port 443 only
-standard_https_port_summary=Enable to only request image servers that use port 443. This allows users with stricter firewall restrictions to access MangaDex images
-status=Status
-status_cancelled=Cancelled
-status_completed=Completed
-status_hiatus=Hiatus
-status_ongoing=Ongoing
-tags_mode=Tags mode
-theme=Theme
-theme_aliens=Aliens
-theme_animals=Animals
-theme_cooking=Cooking
-theme_crossdressing=Crossdressing
-theme_delinquents=Delinquents
-theme_demons=Demons
-theme_gender_swap=Genderswap
-theme_ghosts=Ghosts
-theme_gyaru=Gyaru
-theme_harem=Harem
-theme_incest=Incest
-theme_loli=Loli
-theme_mafia=Mafia
-theme_magic=Magic
-theme_martial_arts=Martial Arts
-theme_military=Military
-theme_monster_girls=Monster Girls
-theme_monsters=Monsters
-theme_music=Music
-theme_ninja=Ninja
-theme_office_workers=Office Workers
-theme_police=Police
-theme_post_apocalyptic=Post-Apocalyptic
-theme_psychological=Psychological
-theme_reincarnation=Reincarnation
-theme_reverse_harem=Reverse Harem
-theme_samurai=Samurai
-theme_school_life=School Life
-theme_shota=Shota
-theme_supernatural=Supernatural
-theme_survival=Survival
-theme_time_travel=Time Travel
-theme_traditional_games=Traditional Games
-theme_vampires=Vampires
-theme_video_games=Video Games
-theme_villainess=Villainess
-theme_virtual_reality=Virtual Reality
-theme_zombies=Zombies
-try_using_first_volume_cover=Attempt to use the first volume cover as cover
-try_using_first_volume_cover_summary=May need to manually refresh entries already in library. Otherwise, clear database to have new covers to show up.
-unable_to_process_chapter_request=Unable to process Chapter request. HTTP code: %d
-uploaded_by=Uploaded by %s
-set_custom_useragent=Set custom User-Agent
-set_custom_useragent_summary=Keep it as default
-set_custom_useragent_dialog=\n\nSpecify a custom user agent\n After each modification, the application needs to be restarted.\n\nDefault value:\n%s
-set_custom_useragent_error_invalid=Invalid User-Agent: %s
diff --git a/src/all/mangadex/assets/i18n/messages_es.properties b/src/all/mangadex/assets/i18n/messages_es.properties
deleted file mode 100644
index fc1dc27ab3..0000000000
--- a/src/all/mangadex/assets/i18n/messages_es.properties
+++ /dev/null
@@ -1,108 +0,0 @@
-block_group_by_uuid=Bloquear grupos por UUID
-block_group_by_uuid_summary=Los capítulos de los grupos bloqueados no aparecerán en Recientes o en el Feed de mangas. Introduce una coma para separar la lista de UUIDs
-block_uploader_by_uuid=Bloquear uploader por UUID
-block_uploader_by_uuid_summary=Los capítulos de los uploaders bloqueados no aparecerán en Recientes o en el Feed de mangas. Introduce una coma para separar la lista de UUIDs
-content=Contenido
-content_rating=Clasificación de contenido
-content_rating_erotica=Erótico
-content_rating_genre=Clasificación: %s
-content_rating_pornographic=Pornográfico
-content_rating_safe=Seguro
-content_rating_suggestive=Sugestivo
-content_sexual_violence=Violencia sexual
-cover_quality=Calidad de la portada
-cover_quality_low=Bajo
-cover_quality_medium=Medio
-data_saver=Ahorro de datos
-data_saver_summary=Utiliza imágenes más pequeñas y más comprimidas
-excluded_tags_mode=Modo de etiquetas excluidas
-filter_original_languages=Filtrar por lenguajes
-filter_original_languages_summary=Muestra solo el contenido publicado en los idiomas seleccionados en recientes y en la búsqueda
-format=Formato
-format_adaptation=Adaptación
-format_anthology=Antología
-format_award_winning=Ganador de premio
-format_fan_colored=Coloreado por fans
-format_full_color=Todo a color
-format_long_strip=Tira larga
-format_official_colored=Coloreo oficial
-format_user_created=Creado por usuario
-genre=Genero
-genre_action=Acción
-genre_adventure=Aventura
-genre_comedy=Comedia
-genre_crime=Crimen
-genre_fantasy=Fantasia
-genre_historical=Histórico
-genre_magical_girls=Chicas mágicas
-genre_medical=Medico
-genre_mystery=Misterio
-genre_philosophical=Filosófico
-genre_sci_fi=Ciencia ficción
-genre_slice_of_life=Recuentos de la vida
-genre_sports=Deportes
-genre_superhero=Superhéroes
-genre_tragedy=Tragedia
-has_available_chapters=Tiene capítulos disponibles
-included_tags_mode=Modo de etiquetas incluidas
-invalid_author_id=ID de autor inválida
-invalid_group_id=ID de grupo inválida
-migrate_warning=Migre la entrada MangaDex a MangaDex para actualizarla
-mode_and=Y
-mode_or=O
-no_group=Sin grupo
-no_series_in_list=No hay series en la lista
-original_language=Lenguaje original
-publication_demographic=Demografía
-publication_demographic_none=Ninguna
-sort=Ordenar
-sort_alphabetic=Alfabeticamente
-sort_chapter_uploaded_at=Capítulo subido en
-sort_content_created_at=Contenido creado en
-sort_content_info_updated_at=Información del contenido actualizada en
-sort_number_of_follows=Número de seguidores
-sort_rating=Calificación
-sort_relevance=Relevancia
-sort_year=Año
-standard_content_rating=Clasificación de contenido por defecto
-standard_content_rating_summary=Muestra el contenido con la clasificación de contenido seleccionada por defecto
-standard_https_port=Utilizar el puerto 443 de HTTPS
-standard_https_port_summary=Habilite esta opción solicitar las imágenes a los servidores que usan el puerto 443. Esto permite a los usuarios con restricciones estrictas de firewall acceder a las imagenes en MangaDex
-status=Estado
-status_cancelled=Cancelado
-status_completed=Completado
-status_hiatus=Pausado
-status_ongoing=Publicandose
-tags_mode=Modo de etiquetas
-theme=Tema
-theme_aliens=Alienígenas
-theme_animals=Animales
-theme_cooking=Cocina
-theme_crossdressing=Travestismo
-theme_delinquents=Delincuentes
-theme_demons=Demonios
-theme_gender_swap=Cambio de sexo
-theme_ghosts=Fantasmas
-theme_incest=Incesto
-theme_magic=Magia
-theme_martial_arts=Artes marciales
-theme_military=Militar
-theme_monster_girls=Chicas monstruo
-theme_monsters=Monstruos
-theme_music=Musica
-theme_office_workers=Oficinistas
-theme_police=Policial
-theme_post_apocalyptic=Post-apocalíptico
-theme_psychological=Psicológico
-theme_reincarnation=Reencarnación
-theme_reverse_harem=Harem inverso
-theme_school_life=Vida escolar
-theme_supernatural=Sobrenatural
-theme_survival=Supervivencia
-theme_time_travel=Viaje en el tiempo
-theme_traditional_games=Juegos tradicionales
-theme_vampires=Vampiros
-theme_villainess=Villana
-theme_virtual_reality=Realidad virtual
-unable_to_process_chapter_request=No se ha podido procesar la solicitud del capítulo. Código HTTP: %d
-uploaded_by=Subido por %s
\ No newline at end of file
diff --git a/src/all/mangadex/assets/i18n/messages_pt_br.properties b/src/all/mangadex/assets/i18n/messages_pt_br.properties
deleted file mode 100644
index ecd4ca7be2..0000000000
--- a/src/all/mangadex/assets/i18n/messages_pt_br.properties
+++ /dev/null
@@ -1,119 +0,0 @@
-alternative_titles=Títulos alternativos:
-alternative_titles_in_description=Títulos alternativos na descrição
-alternative_titles_in_description_summary=Inclui os títulos alternativos das séries no final de cada descrição
-block_group_by_uuid=Bloquear grupos por UUID
-block_group_by_uuid_summary=Capítulos de grupos bloqueados não irão aparecer no feed de Recentes ou Mangás. Digite uma lista de UUIDs dos grupos separados por vírgulas
-block_uploader_by_uuid=Bloquear uploaders por UUID
-block_uploader_by_uuid_summary=Capítulos de usuários bloqueados não irão aparecer no feed de Recentes ou Mangás. Digite uma lista de UUIDs dos usuários separados por vírgulas
-content=Conteúdo
-content_rating=Classificação de conteúdo
-content_rating_erotica=Erótico
-content_rating_genre=Classificação: %s
-content_rating_pornographic=Pornográfico
-content_rating_safe=Seguro
-content_rating_suggestive=Sugestivo
-content_sexual_violence=Violência sexual
-cover_quality=Qualidade da capa
-cover_quality_low=Baixa
-cover_quality_medium=Média
-data_saver=Economia de dados
-data_saver_summary=Utiliza imagens menores e mais compactadas
-excluded_tags_mode=Modo de exclusão de tags
-filter_original_languages=Filtrar os idiomas originais
-filter_original_languages_summary=Mostra somente conteúdos que foram publicados originalmente nos idiomas selecionados nas seções de recentes e navegar
-format=Formato
-format_adaptation=Adaptação
-format_anthology=Antologia
-format_award_winning=Premiado
-format_fan_colored=Colorizado por fãs
-format_full_color=Colorido
-format_long_strip=Vertical
-format_official_colored=Colorizado oficialmente
-format_user_created=Criado por usuários
-genre=Gênero
-genre_action=Ação
-genre_adventure=Aventura
-genre_comedy=Comédia
-genre_crime=Crime
-genre_fantasy=Fantasia
-genre_historical=Histórico
-genre_magical_girls=Garotas mágicas
-genre_medical=Médico
-genre_mystery=Mistério
-genre_philosophical=Filosófico
-genre_sci_fi=Ficção científica
-genre_slice_of_life=Cotidiano
-genre_sports=Esportes
-genre_superhero=Super-heroi
-genre_tragedy=Tragédia
-has_available_chapters=Há capítulos disponíveis
-included_tags_mode=Modo de inclusão de tags
-invalid_author_id=ID do autor inválido
-invalid_manga_id=ID do mangá inválido
-invalid_group_id=ID do grupo inválido
-invalid_uuids=O texto contém UUIDs inválidos
-migrate_warning=Migre esta entrada do MangaDex para o MangaDex para atualizar
-mode_and=E
-mode_or=Ou
-no_group=Sem grupo
-no_series_in_list=Sem séries na lista
-original_language=Idioma original
-original_language_filter_japanese=%s (Mangá)
-publication_demographic=Demografia da publicação
-publication_demographic_none=Nenhuma
-sort=Ordenar
-sort_alphabetic=Alfabeticamente
-sort_chapter_uploaded_at=Upload do capítulo
-sort_content_created_at=Criação do conteúdo
-sort_content_info_updated_at=Atualização das informações
-sort_number_of_follows=Número de seguidores
-sort_rating=Nota
-sort_relevance=Relevância
-sort_year=Ano de lançamento
-standard_content_rating=Classificação de conteúdo padrão
-standard_content_rating_summary=Mostra os conteúdos com as classificações selecionadas por padrão
-standard_https_port=Utilizar somente a porta 443 do HTTPS
-standard_https_port_summary=Ative para fazer requisições em somente servidores de imagem que usem a porta 443. Isso permite com que usuários com regras mais restritas de firewall possam acessar as imagens do MangaDex.
-status=Estado
-status_cancelled=Cancelado
-status_completed=Completo
-status_hiatus=Hiato
-status_ongoing=Em andamento
-tags_mode=Modo das tags
-theme=Tema
-theme_aliens=Alienígenas
-theme_animals=Animais
-theme_cooking=Culinária
-theme_delinquents=Delinquentes
-theme_demons=Demônios
-theme_gender_swap=Troca de gêneros
-theme_ghosts=Fantasmas
-theme_harem=Harém
-theme_incest=Incesto
-theme_mafia=Máfia
-theme_magic=Magia
-theme_martial_arts=Artes marciais
-theme_military=Militar
-theme_monster_girls=Garotas monstro
-theme_monsters=Monstros
-theme_music=Musical
-theme_office_workers=Funcionários de escritório
-theme_police=Policial
-theme_post_apocalyptic=Pós-apocalíptico
-theme_psychological=Psicológico
-theme_reincarnation=Reencarnação
-theme_reverse_harem=Harém reverso
-theme_school_life=Vida escolar
-theme_supernatural=Sobrenatural
-theme_survival=Sobrevivência
-theme_time_travel=Viagem no tempo
-theme_traditional_games=Jogos tradicionais
-theme_vampires=Vampiros
-theme_video_games=Videojuegos
-theme_villainess=Villainess
-theme_virtual_reality=Realidade virtual
-theme_zombies=Zumbis
-try_using_first_volume_cover=Tentar usar a capa do primeiro volume como capa
-try_using_first_volume_cover_summary=Pode ser necessário atualizar os itens já adicionados na biblioteca. Alternativamente, limpe o banco de dados para as novas capas aparecerem.
-unable_to_process_chapter_request=Não foi possível processar a requisição do capítulo. Código HTTP: %d
-uploaded_by=Enviado por %s
\ No newline at end of file
diff --git a/src/all/mangadex/assets/i18n/messages_ru.properties b/src/all/mangadex/assets/i18n/messages_ru.properties
deleted file mode 100644
index fbc49b49f0..0000000000
--- a/src/all/mangadex/assets/i18n/messages_ru.properties
+++ /dev/null
@@ -1,138 +0,0 @@
-block_group_by_uuid=Заблокировать группы по UUID
-block_group_by_uuid_summary=Главы от заблокированных групп не будут отображаться в последних обновлениях и в списке глав тайтла. Введите через запятую список UUID групп.
-block_uploader_by_uuid=Заблокировать загрузчика по UUID
-block_uploader_by_uuid_summary=Главы от заблокированных загрузчиков не будут отображаться в последних обновлениях и в списке глав тайтла. Введите через запятую список UUID загрузчиков.
-content=Неприемлемый контент
-content_gore=Жестокость
-content_rating=Рейтинг контента
-content_rating_erotica=Эротический
-content_rating_genre=Рейтинг контента: %s
-content_rating_pornographic=Порнографический
-content_rating_safe=Безопасный
-content_rating_suggestive=Намекающий
-content_sexual_violence=Сексуальное насилие
-cover_quality=Качество обложки
-cover_quality_low=Низкое
-cover_quality_medium=Среднее
-cover_quality_original=Оригинальное
-data_saver=Экономия трафика
-data_saver_summary=Использует меньшие по размеру, сжатые изображения
-excluded_tags_mode=Исключая
-filter_original_languages=Фильтр по языку оригинала
-filter_original_languages_summary=Показывать тайтлы которые изначально были выпущены только в выбранных языках в последних обновлениях и при поиске
-format=Формат
-format_adaptation=Адаптация
-format_anthology=Антология
-format_award_winning=Отмеченный наградами
-format_doujinshi=Додзинси
-format_fan_colored=Раскрашенная фанатами
-format_full_color=В цвете
-format_long_strip=Веб
-format_official_colored=Официально раскрашенная
-format_oneshot=Сингл
-format_user_created=Созданная пользователями
-format_web_comic=Веб-комикс
-format_yonkoma=Ёнкома
-genre=Жанр
-genre_action=Боевик
-genre_adventure=Приключения
-genre_boys_love=BL
-genre_comedy=Комедия
-genre_crime=Криминал
-genre_drama=Драма
-genre_fantasy=Фэнтези
-genre_girls_love=GL
-genre_historical=История
-genre_horror=Ужасы
-genre_isekai=Исекай
-genre_magical_girls=Махо-сёдзё
-genre_mecha=Меха
-genre_medical=Медицина
-genre_mystery=Мистика
-genre_philosophical=Философия
-genre_romance=Романтика
-genre_sci_fi=Научная фантастика
-genre_slice_of_life=Повседневность
-genre_sports=Спорт
-genre_superhero=Супергерои
-genre_thriller=Триллер
-genre_tragedy=Трагедия
-genre_wuxia=Культивация
-has_available_chapters=Есть главы
-included_tags_mode=Включая
-invalid_author_id=Недействительный ID автора
-invalid_group_id=Недействительный ID группы
-mode_and=И
-mode_or=Или
-no_group=Нет группы
-no_series_in_list=Лист пуст
-original_language=Язык оригинала
-original_language_filter_chinese=%s (Манхуа)
-original_language_filter_japanese=%s (Манга)
-original_language_filter_korean=%s (Манхва)
-publication_demographic=Целевая аудитория
-publication_demographic_josei=Дзёсэй
-publication_demographic_none=Нет
-publication_demographic_seinen=Сэйнэн
-publication_demographic_shoujo=Сёдзё
-publication_demographic_shounen=Сёнэн
-sort=Сортировать по
-sort_alphabetic=Алфавиту
-sort_chapter_uploaded_at=Загруженной главе
-sort_content_created_at=По дате создания
-sort_content_info_updated_at=По дате обновления
-sort_number_of_follows=Количеству фолловеров
-sort_rating=Популярности
-sort_relevance=Лучшему соответствию
-sort_year=Год
-standard_content_rating=Рейтинг контента по умолчанию
-standard_content_rating_summary=Показывать контент с выбранным рейтингом по умолчанию
-standard_https_port=Использовать только HTTPS порт 443
-standard_https_port_summary=Запрашивает изображения только с серверов которые используют порт 443. Это позволяет пользователям со строгими правилами брандмауэра загружать изображения с MangaDex.
-status=Статус
-status_cancelled=Отменён
-status_completed=Завершён
-status_hiatus=Приостановлен
-status_ongoing=Онгоинг
-tags_mode=Режим поиска
-theme=Теги
-theme_aliens=Инопланетяне
-theme_animals=Животные
-theme_cooking=Животные
-theme_crossdressing=Кроссдрессинг
-theme_delinquents=Хулиганы
-theme_demons=Демоны
-theme_gender_swap=Смена гендера
-theme_ghosts=Призраки
-theme_gyaru=Гяру
-theme_harem=Гарем
-theme_incest=Инцест
-theme_loli=Лоли
-theme_mafia=Мафия
-theme_magic=Магия
-theme_martial_arts=Боевые исскуства
-theme_military=Военные
-theme_monster_girls=Монстродевушки
-theme_monsters=Монстры
-theme_music=Музыка
-theme_ninja=Ниндзя
-theme_office_workers=Офисные работники
-theme_police=Полиция
-theme_post_apocalyptic=Постапокалиптика
-theme_psychological=Психология
-theme_reincarnation=Реинкарнация
-theme_reverse_harem=Обратный гарем
-theme_samurai=Самураи
-theme_school_life=Школа
-theme_shota=Шота
-theme_supernatural=Сверхъестественное
-theme_survival=Выживание
-theme_time_travel=Путешествие во времени
-theme_traditional_games=Путешествие во времени
-theme_vampires=Вампиры
-theme_video_games=Видеоигры
-theme_villainess=Злодейка
-theme_virtual_reality=Виртуальная реальность
-theme_zombies=Зомби
-unable_to_process_chapter_request=Не удалось обработать ссылку на главу. Ошибка: %d
-uploaded_by=Загрузил %s
\ No newline at end of file
diff --git a/src/all/mangadex/build.gradle b/src/all/mangadex/build.gradle
deleted file mode 100644
index 101966ce69..0000000000
--- a/src/all/mangadex/build.gradle
+++ /dev/null
@@ -1,17 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlinx-serialization'
-
-ext {
- extName = 'MangaDex'
- pkgNameSuffix = 'all.mangadex'
- extClass = '.MangaDexFactory'
- extVersionCode = 192
- isNsfw = true
-}
-
-dependencies {
- implementation(project(":lib-i18n"))
-}
-
-apply from: "$rootDir/common.gradle"
diff --git a/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 8de6d7f3b6..0000000000
Binary files a/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 04105b6cdf..0000000000
Binary files a/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 6f5065fa3d..0000000000
Binary files a/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index fc989680e5..0000000000
Binary files a/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 48d4d30371..0000000000
Binary files a/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/src/all/mangadex/res/web_hi_res_512.png b/src/all/mangadex/res/web_hi_res_512.png
deleted file mode 100644
index 24ff723c06..0000000000
Binary files a/src/all/mangadex/res/web_hi_res_512.png and /dev/null differ
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt
deleted file mode 100644
index 8fc471311b..0000000000
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.mangadex
-
-import eu.kanade.tachiyomi.lib.i18n.Intl
-import java.text.SimpleDateFormat
-import java.util.Locale
-import java.util.TimeZone
-import kotlin.time.Duration.Companion.minutes
-
-object MDConstants {
-
- val uuidRegex =
- Regex("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
-
- const val mangaLimit = 20
- const val latestChapterLimit = 100
-
- const val chapter = "chapter"
- const val manga = "manga"
- const val coverArt = "cover_art"
- const val scanlationGroup = "scanlation_group"
- const val user = "user"
- const val author = "author"
- const val artist = "artist"
- const val tag = "tag"
- const val list = "custom_list"
- const val legacyNoGroupId = "00e03853-1b96-4f41-9542-c71b8692033b"
-
- const val cdnUrl = "https://uploads.mangadex.org"
- const val apiUrl = "https://api.mangadex.org"
- const val apiMangaUrl = "$apiUrl/manga"
- const val apiChapterUrl = "$apiUrl/chapter"
- const val apiListUrl = "$apiUrl/list"
- const val atHomePostUrl = "https://api.mangadex.network/report"
- val whitespaceRegex = "\\s".toRegex()
-
- val mdAtHomeTokenLifespan = 5.minutes.inWholeMilliseconds
-
- val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
- .apply { timeZone = TimeZone.getTimeZone("UTC") }
-
- const val prefixIdSearch = "id:"
- const val prefixChSearch = "ch:"
- const val prefixGrpSearch = "grp:"
- const val prefixAuthSearch = "author:"
- const val prefixUsrSearch = "usr:"
- const val prefixListSearch = "list:"
-
- private const val coverQualityPref = "thumbnailQuality"
-
- fun getCoverQualityPreferenceKey(dexLang: String): String {
- return "${coverQualityPref}_$dexLang"
- }
-
- fun getCoverQualityPreferenceEntries(intl: Intl) =
- arrayOf(intl["cover_quality_original"], intl["cover_quality_medium"], intl["cover_quality_low"])
-
- fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg")
-
- fun getCoverQualityPreferenceDefaultValue() = getCoverQualityPreferenceEntryValues()[0]
-
- private const val dataSaverPref = "dataSaverV5"
-
- fun getDataSaverPreferenceKey(dexLang: String): String {
- return "${dataSaverPref}_$dexLang"
- }
-
- private const val standardHttpsPortPref = "usePort443"
-
- fun getStandardHttpsPreferenceKey(dexLang: String): String {
- return "${standardHttpsPortPref}_$dexLang"
- }
-
- private const val contentRatingPref = "contentRating"
- const val contentRatingPrefValSafe = "safe"
- const val contentRatingPrefValSuggestive = "suggestive"
- const val contentRatingPrefValErotica = "erotica"
- const val contentRatingPrefValPornographic = "pornographic"
- val contentRatingPrefDefaults = setOf(contentRatingPrefValSafe, contentRatingPrefValSuggestive)
- val allContentRatings = setOf(
- contentRatingPrefValSafe,
- contentRatingPrefValSuggestive,
- contentRatingPrefValErotica,
- contentRatingPrefValPornographic,
- )
-
- fun getContentRatingPrefKey(dexLang: String): String {
- return "${contentRatingPref}_$dexLang"
- }
-
- private const val originalLanguagePref = "originalLanguage"
- const val originalLanguagePrefValJapanese = MangaDexIntl.JAPANESE
- const val originalLanguagePrefValChinese = MangaDexIntl.CHINESE
- const val originalLanguagePrefValChineseHk = "zh-hk"
- const val originalLanguagePrefValKorean = MangaDexIntl.KOREAN
- val originalLanguagePrefDefaults = emptySet()
-
- fun getOriginalLanguagePrefKey(dexLang: String): String {
- return "${originalLanguagePref}_$dexLang"
- }
-
- private const val groupAzuki = "5fed0576-8b94-4f9a-b6a7-08eecd69800d"
- private const val groupBilibili = "06a9fecb-b608-4f19-b93c-7caab06b7f44"
- private const val groupComikey = "8d8ecf83-8d42-4f8c-add8-60963f9f28d9"
- private const val groupInkr = "caa63201-4a17-4b7f-95ff-ed884a2b7e60"
- private const val groupMangaHot = "319c1b10-cbd0-4f55-a46e-c4ee17e65139"
- private const val groupMangaPlus = "4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb"
- val defaultBlockedGroups = setOf(
- groupAzuki,
- groupBilibili,
- groupComikey,
- groupInkr,
- groupMangaHot,
- groupMangaPlus,
- )
- private const val blockedGroupsPref = "blockedGroups"
- fun getBlockedGroupsPrefKey(dexLang: String): String {
- return "${blockedGroupsPref}_$dexLang"
- }
-
- private const val blockedUploaderPref = "blockedUploader"
- fun getBlockedUploaderPrefKey(dexLang: String): String {
- return "${blockedUploaderPref}_$dexLang"
- }
-
- private const val hasSanitizedUuidsPref = "hasSanitizedUuids"
- fun getHasSanitizedUuidsPrefKey(dexLang: String): String {
- return "${hasSanitizedUuidsPref}_$dexLang"
- }
-
- private const val tryUsingFirstVolumeCoverPref = "tryUsingFirstVolumeCover"
- const val tryUsingFirstVolumeCoverDefault = false
- fun getTryUsingFirstVolumeCoverPrefKey(dexLang: String): String {
- return "${tryUsingFirstVolumeCoverPref}_$dexLang"
- }
-
- private const val altTitlesInDescPref = "altTitlesInDesc"
- fun getAltTitlesInDescPrefKey(dexLang: String): String {
- return "${altTitlesInDescPref}_$dexLang"
- }
-
- private const val customUserAgentPref = "customUserAgent"
- fun getCustomUserAgentPrefKey(dexLang: String): String {
- return "${customUserAgentPref}_$dexLang"
- }
-
- val defaultUserAgent = "Tachiyomi " + System.getProperty("http.agent")
-
- private const val tagGroupContent = "content"
- private const val tagGroupFormat = "format"
- private const val tagGroupGenre = "genre"
- private const val tagGroupTheme = "theme"
- val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
-
- const val tagAnthologyUuid = "51d83883-4103-437c-b4b1-731cb73d786c"
- const val tagOneShotUuid = "0234a31e-a729-4e28-9d6a-3f87c4966b9e"
-
- val romanizedLangCodes = mapOf(
- MangaDexIntl.JAPANESE to "ja-ro",
- MangaDexIntl.KOREAN to "ko-ro",
- MangaDexIntl.CHINESE to "zh-ro",
- "zh-hk" to "zh-ro",
- )
-}
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt
deleted file mode 100644
index 06b72b1d03..0000000000
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt
+++ /dev/null
@@ -1,903 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.mangadex
-
-import android.app.Application
-import android.content.SharedPreferences
-import android.os.Build
-import android.widget.Toast
-import androidx.preference.EditTextPreference
-import androidx.preference.ListPreference
-import androidx.preference.MultiSelectListPreference
-import androidx.preference.PreferenceScreen
-import androidx.preference.SwitchPreferenceCompat
-import eu.kanade.tachiyomi.AppInfo
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterListDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtListDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaListDto
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.network.asObservable
-import eu.kanade.tachiyomi.network.asObservableSuccess
-import eu.kanade.tachiyomi.network.interceptor.rateLimit
-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 kotlinx.serialization.decodeFromString
-import okhttp3.CacheControl
-import okhttp3.Headers
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.Request
-import okhttp3.Response
-import rx.Observable
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.util.Date
-
-abstract class MangaDex(final override val lang: String, private val dexLang: String = lang) :
- ConfigurableSource, HttpSource() {
-
- override val name = MangaDexIntl.MANGADEX_NAME
-
- override val baseUrl = "https://mangadex.org"
-
- override val supportsLatest = true
-
- private val preferences: SharedPreferences by lazy {
- Injekt.get().getSharedPreferences("source_$id", 0x0000)
- }
-
- private val helper = MangaDexHelper(lang)
-
- final override fun headersBuilder(): Headers.Builder {
- val extraHeader = "Android/${Build.VERSION.RELEASE} " +
- "Tachiyomi/${AppInfo.getVersionName()} " +
- "MangaDex/1.4.190"
-
- val builder = super.headersBuilder().apply {
- set("Referer", "$baseUrl/")
- set("Extra", extraHeader)
- }
-
- return builder
- }
-
- override val client = network.client.newBuilder()
- .rateLimit(3)
- .addInterceptor(MdAtHomeReportInterceptor(network.client, headers))
- .addInterceptor(MdUserAgentInterceptor(preferences, dexLang))
- .build()
-
- init {
- preferences.sanitizeExistingUuidPrefs()
- }
-
- // Popular manga section
-
- override fun popularMangaRequest(page: Int): Request {
- val url = MDConstants.apiMangaUrl.toHttpUrl().newBuilder()
- .addQueryParameter("order[followedCount]", "desc")
- .addQueryParameter("availableTranslatedLanguage[]", dexLang)
- .addQueryParameter("limit", MDConstants.mangaLimit.toString())
- .addQueryParameter("offset", helper.getMangaListOffset(page))
- .addQueryParameter("includes[]", MDConstants.coverArt)
- .addQueryParameter("contentRating[]", preferences.contentRating)
- .addQueryParameter("originalLanguage[]", preferences.originalLanguages)
- .build()
-
- return GET(url, headers, CacheControl.FORCE_NETWORK)
- }
-
- override fun popularMangaParse(response: Response): MangasPage {
- if (response.code == 204) {
- return MangasPage(emptyList(), false)
- }
-
- val mangaListDto = response.parseAs()
-
- val coverSuffix = preferences.coverQuality
- val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
-
- val mangaList = mangaListDto.data.map { mangaDataDto ->
- val fileName = firstVolumeCovers.getOrElse(mangaDataDto.id) {
- mangaDataDto.relationships
- .firstInstanceOrNull()
- ?.attributes?.fileName
- }
- helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
- }
-
- return MangasPage(mangaList, mangaListDto.hasNextPage)
- }
-
- // Latest manga section
-
- override fun latestUpdatesRequest(page: Int): Request {
- val url = MDConstants.apiChapterUrl.toHttpUrl().newBuilder()
- .addQueryParameter("offset", helper.getLatestChapterOffset(page))
- .addQueryParameter("limit", MDConstants.latestChapterLimit.toString())
- .addQueryParameter("translatedLanguage[]", dexLang)
- .addQueryParameter("order[publishAt]", "desc")
- .addQueryParameter("includeFutureUpdates", "0")
- .addQueryParameter("originalLanguage[]", preferences.originalLanguages)
- .addQueryParameter("contentRating[]", preferences.contentRating)
- .addQueryParameter(
- "excludedGroups[]",
- MDConstants.defaultBlockedGroups + preferences.blockedGroups,
- )
- .addQueryParameter("excludedUploaders[]", preferences.blockedUploaders)
- .addQueryParameter("includeFuturePublishAt", "0")
- .addQueryParameter("includeEmptyPages", "0")
- .build()
-
- return GET(url, headers, CacheControl.FORCE_NETWORK)
- }
-
- /**
- * The API endpoint can't sort by date yet, so not implemented.
- */
- override fun latestUpdatesParse(response: Response): MangasPage {
- val chapterListDto = response.parseAs()
-
- val mangaIds = chapterListDto.data
- .flatMap { it.relationships }
- .filterIsInstance()
- .map { it.id }
- .distinct()
- .toSet()
-
- val mangaApiUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder()
- .addQueryParameter("includes[]", MDConstants.coverArt)
- .addQueryParameter("limit", mangaIds.size.toString())
- .addQueryParameter("contentRating[]", preferences.contentRating)
- .addQueryParameter("ids[]", mangaIds)
- .build()
-
- val mangaRequest = GET(mangaApiUrl, headers, CacheControl.FORCE_NETWORK)
- val mangaResponse = client.newCall(mangaRequest).execute()
- val mangaListDto = mangaResponse.parseAs()
- val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
-
- val mangaDtoMap = mangaListDto.data.associateBy({ it.id }, { it })
-
- val coverSuffix = preferences.coverQuality
-
- val mangaList = mangaIds.mapNotNull { mangaDtoMap[it] }.map { mangaDataDto ->
- val fileName = firstVolumeCovers.getOrElse(mangaDataDto.id) {
- mangaDataDto.relationships
- .firstInstanceOrNull()
- ?.attributes?.fileName
- }
- helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
- }
-
- return MangasPage(mangaList, chapterListDto.hasNextPage)
- }
-
- // Search manga section
-
- override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
- return when {
- query.startsWith(MDConstants.prefixChSearch) ->
- getMangaIdFromChapterId(query.removePrefix(MDConstants.prefixChSearch))
- .flatMap { mangaId ->
- super.fetchSearchManga(
- page = page,
- query = MDConstants.prefixIdSearch + mangaId,
- filters = filters,
- )
- }
-
- query.startsWith(MDConstants.prefixUsrSearch) ->
- client
- .newCall(
- request = searchMangaUploaderRequest(
- page = page,
- uploader = query.removePrefix(MDConstants.prefixUsrSearch),
- ),
- )
- .asObservableSuccess()
- .map { latestUpdatesParse(it) }
-
- query.startsWith(MDConstants.prefixListSearch) ->
- client
- .newCall(
- request = searchMangaListRequest(
- list = query.removePrefix(MDConstants.prefixListSearch),
- ),
- )
- .asObservableSuccess()
- .map { searchMangaListParse(it, page, filters) }
-
- else -> super.fetchSearchManga(page, query.trim(), filters)
- }
- }
-
- private fun getMangaIdFromChapterId(id: String): Observable {
- return client.newCall(GET("${MDConstants.apiChapterUrl}/$id", headers))
- .asObservable()
- .map { response ->
- if (response.isSuccessful.not()) {
- throw Exception(helper.intl.format("unable_to_process_chapter_request", response.code))
- }
-
- response.parseAs().data!!.relationships
- .firstInstanceOrNull()!!.id
- }
- }
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- if (query.startsWith(MDConstants.prefixIdSearch)) {
- val mangaId = query.removePrefix(MDConstants.prefixIdSearch)
-
- if (!helper.containsUuid(mangaId)) {
- throw Exception(helper.intl["invalid_manga_id"])
- }
-
- val url = MDConstants.apiMangaUrl.toHttpUrl().newBuilder()
- .addQueryParameter("ids[]", query.removePrefix(MDConstants.prefixIdSearch))
- .addQueryParameter("includes[]", MDConstants.coverArt)
- .addQueryParameter("contentRating[]", MDConstants.allContentRatings)
- .build()
-
- return GET(url, headers, CacheControl.FORCE_NETWORK)
- }
-
- val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder()
- .addQueryParameter("limit", MDConstants.mangaLimit.toString())
- .addQueryParameter("offset", helper.getMangaListOffset(page))
- .addQueryParameter("includes[]", MDConstants.coverArt)
-
- when {
- query.startsWith(MDConstants.prefixGrpSearch) -> {
- val groupId = query.removePrefix(MDConstants.prefixGrpSearch)
-
- if (!helper.containsUuid(groupId)) {
- throw Exception(helper.intl["invalid_group_id"])
- }
-
- tempUrl.addQueryParameter("group", groupId)
- }
-
- query.startsWith(MDConstants.prefixAuthSearch) -> {
- val authorId = query.removePrefix(MDConstants.prefixAuthSearch)
-
- if (!helper.containsUuid(authorId)) {
- throw Exception(helper.intl["invalid_author_id"])
- }
-
- tempUrl.addQueryParameter("authorOrArtist", authorId)
- }
-
- else -> {
- val actualQuery = query.replace(MDConstants.whitespaceRegex, " ")
-
- if (actualQuery.isNotBlank()) {
- tempUrl.addQueryParameter("title", actualQuery)
- }
- }
- }
-
- val finalUrl = helper.mdFilters.addFiltersToUrl(
- url = tempUrl,
- filters = filters.ifEmpty { getFilterList() },
- dexLang = dexLang,
- )
-
- return GET(finalUrl, headers, CacheControl.FORCE_NETWORK)
- }
-
- override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
-
- private fun searchMangaListRequest(list: String): Request {
- return GET("${MDConstants.apiListUrl}/$list", headers, CacheControl.FORCE_NETWORK)
- }
-
- private fun searchMangaListParse(response: Response, page: Int, filters: FilterList): MangasPage {
- val listDto = response.parseAs()
- val listDtoFiltered = listDto.data!!.relationships.filterIsInstance()
- val amount = listDtoFiltered.count()
-
- if (amount < 1) {
- throw Exception(helper.intl["no_series_in_list"])
- }
-
- val minIndex = (page - 1) * MDConstants.mangaLimit
-
- val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder()
- .addQueryParameter("limit", MDConstants.mangaLimit.toString())
- .addQueryParameter("offset", "0")
- .addQueryParameter("includes[]", MDConstants.coverArt)
-
- val ids = listDtoFiltered
- .filterIndexed { i, _ -> i >= minIndex && i < (minIndex + MDConstants.mangaLimit) }
- .map(MangaDataDto::id)
- .toSet()
-
- tempUrl.addQueryParameter("ids[]", ids)
-
- val finalUrl = helper.mdFilters.addFiltersToUrl(
- url = tempUrl,
- filters = filters.ifEmpty { getFilterList() },
- dexLang = dexLang,
- )
-
- val mangaRequest = GET(finalUrl, headers, CacheControl.FORCE_NETWORK)
- val mangaResponse = client.newCall(mangaRequest).execute()
- val mangaList = searchMangaListParse(mangaResponse)
-
- val hasNextPage = amount.toFloat() / MDConstants.mangaLimit - (page.toFloat() - 1) > 1 &&
- ids.size == MDConstants.mangaLimit
-
- return MangasPage(mangaList, hasNextPage)
- }
-
- private fun searchMangaListParse(response: Response): List {
- // This check will be used as the source is doing additional requests to this
- // that are not parsed by the asObservableSuccess() method. It should throw the
- // HttpException from the app if it becomes available in a future version of extensions-lib.
- if (response.isSuccessful.not()) {
- throw Exception("HTTP error ${response.code}")
- }
-
- val mangaListDto = response.parseAs()
- val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty()
-
- val coverSuffix = preferences.coverQuality
-
- val mangaList = mangaListDto.data.map { mangaDataDto ->
- val fileName = firstVolumeCovers.getOrElse(mangaDataDto.id) {
- mangaDataDto.relationships
- .firstInstanceOrNull()
- ?.attributes?.fileName
- }
- helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
- }
-
- return mangaList
- }
-
- private fun searchMangaUploaderRequest(page: Int, uploader: String): Request {
- val url = MDConstants.apiChapterUrl.toHttpUrl().newBuilder()
- .addQueryParameter("offset", helper.getLatestChapterOffset(page))
- .addQueryParameter("limit", MDConstants.latestChapterLimit.toString())
- .addQueryParameter("translatedLanguage[]", dexLang)
- .addQueryParameter("order[publishAt]", "desc")
- .addQueryParameter("includeFutureUpdates", "0")
- .addQueryParameter("includeFuturePublishAt", "0")
- .addQueryParameter("includeEmptyPages", "0")
- .addQueryParameter("uploader", uploader)
- .addQueryParameter("originalLanguage[]", preferences.originalLanguages)
- .addQueryParameter("contentRating[]", preferences.contentRating)
- .addQueryParameter(
- "excludedGroups[]",
- MDConstants.defaultBlockedGroups + preferences.blockedGroups,
- )
- .addQueryParameter("excludedUploaders[]", preferences.blockedUploaders)
- .build()
-
- return GET(url, headers, CacheControl.FORCE_NETWORK)
- }
-
- // Manga Details section
-
- override fun getMangaUrl(manga: SManga): String {
- return baseUrl + manga.url + "/" + helper.titleToSlug(manga.title)
- }
-
- /**
- * Get the API endpoint URL for the entry details.
- *
- * @throws Exception if the url is the old format so people migrate
- */
- override fun mangaDetailsRequest(manga: SManga): Request {
- if (!helper.containsUuid(manga.url.trim())) {
- throw Exception(helper.intl["migrate_warning"])
- }
-
- val url = (MDConstants.apiUrl + manga.url).toHttpUrl().newBuilder()
- .addQueryParameter("includes[]", MDConstants.coverArt)
- .addQueryParameter("includes[]", MDConstants.author)
- .addQueryParameter("includes[]", MDConstants.artist)
- .build()
-
- return GET(url, headers, CacheControl.FORCE_NETWORK)
- }
-
- override fun mangaDetailsParse(response: Response): SManga {
- val manga = response.parseAs()
-
- return helper.createManga(
- manga.data!!,
- fetchSimpleChapterList(manga, dexLang),
- fetchFirstVolumeCover(manga),
- dexLang,
- preferences.coverQuality,
- preferences.altTitlesInDesc,
- )
- }
-
- /**
- * Get a quick-n-dirty list of the chapters to be used in determining the manga status.
- * Uses the 'aggregate' endpoint.
- *
- * @see MangaDexHelper.getPublicationStatus
- * @see AggregateDto
- */
- private fun fetchSimpleChapterList(manga: MangaDto, langCode: String): Map {
- val url = "${MDConstants.apiMangaUrl}/${manga.data!!.id}/aggregate?translatedLanguage[]=$langCode"
- val response = client.newCall(GET(url, headers)).execute()
-
- return runCatching { response.parseAs() }
- .getOrNull()?.volumes.orEmpty()
- }
-
- /**
- * Attempt to get the first volume cover if the setting is enabled.
- * Uses the 'covers' endpoint.
- *
- * @see CoverArtListDto
- */
- private fun fetchFirstVolumeCover(manga: MangaDto): String? {
- return fetchFirstVolumeCovers(listOf(manga.data!!))?.get(manga.data.id)
- }
-
- /**
- * Attempt to get the first volume cover if the setting is enabled.
- * Uses the 'covers' endpoint.
- *
- * @see CoverArtListDto
- */
- private fun fetchFirstVolumeCovers(mangaList: List): Map? {
- if (!preferences.tryUsingFirstVolumeCover || mangaList.isEmpty()) {
- return null
- }
-
- val safeMangaList = mangaList.filterNot { it.attributes?.originalLanguage.isNullOrEmpty() }
- val mangaMap = safeMangaList.associate { it.id to it.attributes!! }
- val locales = safeMangaList.mapNotNull { it.attributes!!.originalLanguage }.distinct()
- val limit = (mangaMap.size * locales.size).coerceAtMost(100)
-
- val apiUrl = "${MDConstants.apiUrl}/cover".toHttpUrl().newBuilder()
- .addQueryParameter("order[volume]", "asc")
- .addQueryParameter("manga[]", mangaMap.keys)
- .addQueryParameter("locales[]", locales.toSet())
- .addQueryParameter("limit", limit.toString())
- .addQueryParameter("offset", "0")
- .build()
-
- val result = runCatching {
- client.newCall(GET(apiUrl, headers)).execute().parseAs().data
- }
-
- val covers = result.getOrNull() ?: return null
-
- return covers
- .groupBy { it.relationships.firstInstanceOrNull()!!.id }
- .mapValues {
- it.value.find { c -> c.attributes?.locale == mangaMap[it.key]?.originalLanguage }
- }
- .filterValues { !it?.attributes?.fileName.isNullOrEmpty() }
- .mapValues { it.value!!.attributes!!.fileName!! }
- }
-
- // Chapter list section
-
- /**
- * Get the API endpoint URL for the first page of chapter list.
- *
- * @throws Exception if the url is the old format so people migrate
- */
- override fun chapterListRequest(manga: SManga): Request {
- if (!helper.containsUuid(manga.url)) {
- throw Exception(helper.intl["migrate_warning"])
- }
-
- return paginatedChapterListRequest(helper.getUUIDFromUrl(manga.url), 0)
- }
-
- /**
- * Required because the chapter list API endpoint is paginated.
- */
- private fun paginatedChapterListRequest(mangaId: String, offset: Int): Request {
- val url = helper.getChapterEndpoint(mangaId, offset, dexLang).toHttpUrl().newBuilder()
- .addQueryParameter("contentRating[]", MDConstants.allContentRatings)
- .addQueryParameter("excludedGroups[]", preferences.blockedGroups)
- .addQueryParameter("excludedUploaders[]", preferences.blockedUploaders)
- .build()
-
- return GET(url, headers, CacheControl.FORCE_NETWORK)
- }
-
- override fun chapterListParse(response: Response): List {
- if (response.code == 204) {
- return emptyList()
- }
-
- val chapterListResponse = response.parseAs()
-
- val chapterListResults = chapterListResponse.data.toMutableList()
-
- val mangaId = response.request.url.toString()
- .substringBefore("/feed")
- .substringAfter("${MDConstants.apiMangaUrl}/")
-
- var offset = chapterListResponse.offset
- var hasNextPage = chapterListResponse.hasNextPage
-
- // Max results that can be returned is 500 so need to make more API
- // calls if the chapter list response has a next page.
- while (hasNextPage) {
- offset += chapterListResponse.limit
-
- val newRequest = paginatedChapterListRequest(mangaId, offset)
- val newResponse = client.newCall(newRequest).execute()
- val newChapterList = newResponse.parseAs()
- chapterListResults.addAll(newChapterList.data)
-
- hasNextPage = newChapterList.hasNextPage
- }
-
- return chapterListResults
- .filterNot { it.attributes!!.isInvalid }
- .map(helper::createChapter)
- }
-
- override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
-
- override fun pageListRequest(chapter: SChapter): Request {
- if (!helper.containsUuid(chapter.url)) {
- throw Exception(helper.intl["migrate_warning"])
- }
-
- val chapterId = chapter.url.substringAfter("/chapter/")
- val atHomeRequestUrl = if (preferences.forceStandardHttps) {
- "${MDConstants.apiUrl}/at-home/server/$chapterId?forcePort443=true"
- } else {
- "${MDConstants.apiUrl}/at-home/server/$chapterId"
- }
-
- return helper.mdAtHomeRequest(atHomeRequestUrl, headers, CacheControl.FORCE_NETWORK)
- }
-
- override fun pageListParse(response: Response): List {
- val atHomeRequestUrl = response.request.url
- val atHomeDto = response.parseAs()
- val host = atHomeDto.baseUrl
-
- // Have to add the time, and url to the page because pages timeout within 30 minutes now.
- val now = Date().time
-
- val hash = atHomeDto.chapter.hash
- val pageSuffix = if (preferences.useDataSaver) {
- atHomeDto.chapter.dataSaver.map { "/data-saver/$hash/$it" }
- } else {
- atHomeDto.chapter.data.map { "/data/$hash/$it" }
- }
-
- return pageSuffix.mapIndexed { index, imgUrl ->
- val mdAtHomeMetadataUrl = "$host,$atHomeRequestUrl,$now"
- Page(index, mdAtHomeMetadataUrl, imgUrl)
- }
- }
-
- override fun imageRequest(page: Page): Request {
- return helper.getValidImageUrlForPage(page, headers, client)
- }
-
- override fun imageUrlParse(response: Response): String = ""
-
- @Suppress("UNCHECKED_CAST")
- override fun setupPreferenceScreen(screen: PreferenceScreen) {
- val coverQualityPref = ListPreference(screen.context).apply {
- key = MDConstants.getCoverQualityPreferenceKey(dexLang)
- title = helper.intl["cover_quality"]
- entries = MDConstants.getCoverQualityPreferenceEntries(helper.intl)
- entryValues = MDConstants.getCoverQualityPreferenceEntryValues()
- setDefaultValue(MDConstants.getCoverQualityPreferenceDefaultValue())
- summary = "%s"
-
- setOnPreferenceChangeListener { _, newValue ->
- val selected = newValue as String
- val index = findIndexOfValue(selected)
- val entry = entryValues[index] as String
-
- preferences.edit()
- .putString(MDConstants.getCoverQualityPreferenceKey(dexLang), entry)
- .commit()
- }
- }
-
- val tryUsingFirstVolumeCoverPref = SwitchPreferenceCompat(screen.context).apply {
- key = MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang)
- title = helper.intl["try_using_first_volume_cover"]
- summary = helper.intl["try_using_first_volume_cover_summary"]
- setDefaultValue(MDConstants.tryUsingFirstVolumeCoverDefault)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Boolean
-
- preferences.edit()
- .putBoolean(MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang), checkValue)
- .commit()
- }
- }
-
- val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
- key = MDConstants.getDataSaverPreferenceKey(dexLang)
- title = helper.intl["data_saver"]
- summary = helper.intl["data_saver_summary"]
- setDefaultValue(false)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Boolean
-
- preferences.edit()
- .putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue)
- .commit()
- }
- }
-
- val standardHttpsPortPref = SwitchPreferenceCompat(screen.context).apply {
- key = MDConstants.getStandardHttpsPreferenceKey(dexLang)
- title = helper.intl["standard_https_port"]
- summary = helper.intl["standard_https_port_summary"]
- setDefaultValue(false)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Boolean
-
- preferences.edit()
- .putBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), checkValue)
- .commit()
- }
- }
-
- val contentRatingPref = MultiSelectListPreference(screen.context).apply {
- key = MDConstants.getContentRatingPrefKey(dexLang)
- title = helper.intl["standard_content_rating"]
- summary = helper.intl["standard_content_rating_summary"]
- entries = arrayOf(
- helper.intl["content_rating_safe"],
- helper.intl["content_rating_suggestive"],
- helper.intl["content_rating_erotica"],
- helper.intl["content_rating_pornographic"],
- )
- entryValues = arrayOf(
- MDConstants.contentRatingPrefValSafe,
- MDConstants.contentRatingPrefValSuggestive,
- MDConstants.contentRatingPrefValErotica,
- MDConstants.contentRatingPrefValPornographic,
- )
- setDefaultValue(MDConstants.contentRatingPrefDefaults)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Set
-
- preferences.edit()
- .putStringSet(MDConstants.getContentRatingPrefKey(dexLang), checkValue)
- .commit()
- }
- }
-
- val originalLanguagePref = MultiSelectListPreference(screen.context).apply {
- key = MDConstants.getOriginalLanguagePrefKey(dexLang)
- title = helper.intl["filter_original_languages"]
- summary = helper.intl["filter_original_languages_summary"]
- entries = arrayOf(
- helper.intl.languageDisplayName(MangaDexIntl.JAPANESE),
- helper.intl.languageDisplayName(MangaDexIntl.CHINESE),
- helper.intl.languageDisplayName(MangaDexIntl.KOREAN),
- )
- entryValues = arrayOf(
- MDConstants.originalLanguagePrefValJapanese,
- MDConstants.originalLanguagePrefValChinese,
- MDConstants.originalLanguagePrefValKorean,
- )
- setDefaultValue(MDConstants.originalLanguagePrefDefaults)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Set
-
- preferences.edit()
- .putStringSet(MDConstants.getOriginalLanguagePrefKey(dexLang), checkValue)
- .commit()
- }
- }
-
- val blockedGroupsPref = EditTextPreference(screen.context).apply {
- key = MDConstants.getBlockedGroupsPrefKey(dexLang)
- title = helper.intl["block_group_by_uuid"]
- summary = helper.intl["block_group_by_uuid_summary"]
-
- setOnBindEditTextListener(helper::setupEditTextUuidValidator)
-
- setOnPreferenceChangeListener { _, newValue ->
- preferences.edit()
- .putString(MDConstants.getBlockedGroupsPrefKey(dexLang), newValue.toString())
- .commit()
- }
- }
-
- val blockedUploaderPref = EditTextPreference(screen.context).apply {
- key = MDConstants.getBlockedUploaderPrefKey(dexLang)
- title = helper.intl["block_uploader_by_uuid"]
- summary = helper.intl["block_uploader_by_uuid_summary"]
-
- setOnBindEditTextListener(helper::setupEditTextUuidValidator)
-
- setOnPreferenceChangeListener { _, newValue ->
- preferences.edit()
- .putString(MDConstants.getBlockedUploaderPrefKey(dexLang), newValue.toString())
- .commit()
- }
- }
-
- val altTitlesInDescPref = SwitchPreferenceCompat(screen.context).apply {
- key = MDConstants.getAltTitlesInDescPrefKey(dexLang)
- title = helper.intl["alternative_titles_in_description"]
- summary = helper.intl["alternative_titles_in_description_summary"]
- setDefaultValue(false)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Boolean
-
- preferences.edit()
- .putBoolean(MDConstants.getAltTitlesInDescPrefKey(dexLang), checkValue)
- .commit()
- }
- }
-
- val userAgentPref = EditTextPreference(screen.context).apply {
- key = MDConstants.getCustomUserAgentPrefKey(dexLang)
- title = helper.intl["set_custom_useragent"]
- summary = helper.intl["set_custom_useragent_summary"]
- dialogMessage = helper.intl.format(
- "set_custom_useragent_dialog",
- MDConstants.defaultUserAgent,
- )
-
- setDefaultValue(MDConstants.defaultUserAgent)
-
- setOnPreferenceChangeListener { _, newValue ->
- try {
- Headers.Builder().add("User-Agent", newValue as String)
- summary = newValue
- true
- } catch (e: Throwable) {
- val errorMessage = helper.intl.format("set_custom_useragent_error_invalid", e.message)
- Toast.makeText(screen.context, errorMessage, Toast.LENGTH_LONG).show()
- false
- }
- }
- }
-
- screen.addPreference(coverQualityPref)
- screen.addPreference(tryUsingFirstVolumeCoverPref)
- screen.addPreference(dataSaverPref)
- screen.addPreference(standardHttpsPortPref)
- screen.addPreference(altTitlesInDescPref)
- screen.addPreference(contentRatingPref)
- screen.addPreference(originalLanguagePref)
- screen.addPreference(blockedGroupsPref)
- screen.addPreference(blockedUploaderPref)
- screen.addPreference(userAgentPref)
- }
-
- override fun getFilterList(): FilterList =
- helper.mdFilters.getMDFilterList(preferences, dexLang, helper.intl)
-
- private fun HttpUrl.Builder.addQueryParameter(name: String, value: Set?) = apply {
- value?.forEach { addQueryParameter(name, it) }
- }
-
- private inline fun Response.parseAs(): T = use {
- helper.json.decodeFromString(body.string())
- }
-
- private inline fun List<*>.firstInstanceOrNull(): T? =
- firstOrNull { it is T } as? T?
-
- private val SharedPreferences.contentRating
- get() = getStringSet(
- MDConstants.getContentRatingPrefKey(dexLang),
- MDConstants.contentRatingPrefDefaults,
- )
-
- private val SharedPreferences.originalLanguages: Set
- get() {
- val prefValues = getStringSet(
- MDConstants.getOriginalLanguagePrefKey(dexLang),
- MDConstants.originalLanguagePrefDefaults,
- )
-
- val originalLanguages = prefValues.orEmpty().toMutableSet()
-
- if (MDConstants.originalLanguagePrefValChinese in originalLanguages) {
- originalLanguages.add(MDConstants.originalLanguagePrefValChineseHk)
- }
-
- return originalLanguages
- }
-
- private val SharedPreferences.coverQuality
- get() = getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "")
-
- private val SharedPreferences.tryUsingFirstVolumeCover
- get() = getBoolean(
- MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang),
- MDConstants.tryUsingFirstVolumeCoverDefault,
- )
-
- private val SharedPreferences.blockedGroups
- get() = getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")
- ?.split(",")
- ?.map(String::trim)
- ?.filter(String::isNotEmpty)
- ?.sorted()
- .orEmpty()
- .toSet()
-
- private val SharedPreferences.blockedUploaders
- get() = getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "")
- ?.split(",")
- ?.map(String::trim)
- ?.filter(String::isNotEmpty)
- ?.sorted()
- .orEmpty()
- .toSet()
-
- private val SharedPreferences.forceStandardHttps
- get() = getBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), false)
-
- private val SharedPreferences.useDataSaver
- get() = getBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), false)
-
- private val SharedPreferences.altTitlesInDesc
- get() = getBoolean(MDConstants.getAltTitlesInDescPrefKey(dexLang), false)
-
- private val SharedPreferences.customUserAgent
- get() = getString(
- MDConstants.getCustomUserAgentPrefKey(dexLang),
- MDConstants.defaultUserAgent,
- )
-
- /**
- * Previous versions of the extension allowed invalid UUID values to be stored in the
- * preferences. This method clear invalid UUIDs in case the user have updated from
- * a previous version with that behaviour.
- */
- private fun SharedPreferences.sanitizeExistingUuidPrefs() {
- if (getBoolean(MDConstants.getHasSanitizedUuidsPrefKey(dexLang), false)) {
- return
- }
-
- val blockedGroups = getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")!!
- .split(",")
- .map(String::trim)
- .filter(helper::isUuid)
- .joinToString(", ")
-
- val blockedUploaders = getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "")!!
- .split(",")
- .map(String::trim)
- .filter(helper::isUuid)
- .joinToString(", ")
-
- edit()
- .putString(MDConstants.getBlockedGroupsPrefKey(dexLang), blockedGroups)
- .putString(MDConstants.getBlockedUploaderPrefKey(dexLang), blockedUploaders)
- .putBoolean(MDConstants.getHasSanitizedUuidsPrefKey(dexLang), true)
- .apply()
- }
-}
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt
deleted file mode 100644
index 10a39bc6ca..0000000000
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.mangadex
-
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.SourceFactory
-
-class MangaDexFactory : SourceFactory {
- override fun createSources(): List = listOf(
- MangaDexEnglish(),
- MangaDexAlbanian(),
- MangaDexArabic(),
- MangaDexAzerbaijani(),
- MangaDexBengali(),
- MangaDexBulgarian(),
- MangaDexBurmese(),
- MangaDexCatalan(),
- MangaDexChineseSimplified(),
- MangaDexChineseTraditional(),
- MangaDexCroatian(),
- MangaDexCzech(),
- MangaDexDanish(),
- MangaDexDutch(),
- MangaDexEsperanto(),
- MangaDexEstonian(),
- MangaDexFilipino(),
- MangaDexFinnish(),
- MangaDexFrench(),
- MangaDexGeorgian(),
- MangaDexGerman(),
- MangaDexGreek(),
- MangaDexHebrew(),
- MangaDexHindi(),
- MangaDexHungarian(),
- MangaDexIndonesian(),
- MangaDexItalian(),
- MangaDexJapanese(),
- MangaDexKazakh(),
- MangaDexKorean(),
- MangaDexLatin(),
- MangaDexLithuanian(),
- MangaDexMalay(),
- MangaDexMongolian(),
- MangaDexNepali(),
- MangaDexNorwegian(),
- MangaDexPersian(),
- MangaDexPolish(),
- MangaDexPortugueseBrazil(),
- MangaDexPortuguesePortugal(),
- MangaDexRomanian(),
- MangaDexRussian(),
- MangaDexSerbian(),
- MangaDexSlovak(),
- MangaDexSpanishLatinAmerica(),
- MangaDexSpanishSpain(),
- MangaDexSwedish(),
- MangaDexTamil(),
- MangaDexTelugu(),
- MangaDexThai(),
- MangaDexTurkish(),
- MangaDexUkrainian(),
- MangaDexVietnamese(),
- )
-}
-
-class MangaDexAlbanian : MangaDex("sq")
-class MangaDexArabic : MangaDex("ar")
-class MangaDexAzerbaijani : MangaDex("az")
-class MangaDexBengali : MangaDex("bn")
-class MangaDexBulgarian : MangaDex("bg")
-class MangaDexBurmese : MangaDex("my")
-class MangaDexCatalan : MangaDex("ca")
-class MangaDexChineseSimplified : MangaDex("zh-Hans", "zh")
-class MangaDexChineseTraditional : MangaDex("zh-Hant", "zh-hk")
-class MangaDexCroatian : MangaDex("hr")
-class MangaDexCzech : MangaDex("cs")
-class MangaDexDanish : MangaDex("da")
-class MangaDexDutch : MangaDex("nl")
-class MangaDexEnglish : MangaDex("en")
-class MangaDexEsperanto : MangaDex("eo")
-class MangaDexEstonian : MangaDex("et")
-class MangaDexFilipino : MangaDex("fil", "tl")
-class MangaDexFinnish : MangaDex("fi")
-class MangaDexFrench : MangaDex("fr")
-class MangaDexGeorgian : MangaDex("ka")
-class MangaDexGerman : MangaDex("de")
-class MangaDexGreek : MangaDex("el")
-class MangaDexHebrew : MangaDex("he")
-class MangaDexHindi : MangaDex("hi")
-class MangaDexHungarian : MangaDex("hu")
-class MangaDexIndonesian : MangaDex("id")
-class MangaDexItalian : MangaDex("it")
-class MangaDexJapanese : MangaDex("ja")
-class MangaDexKazakh : MangaDex("kk")
-class MangaDexKorean : MangaDex("ko")
-class MangaDexLatin : MangaDex("la")
-class MangaDexLithuanian : MangaDex("lt")
-class MangaDexMalay : MangaDex("ms")
-class MangaDexMongolian : MangaDex("mn")
-class MangaDexNepali : MangaDex("ne")
-class MangaDexNorwegian : MangaDex("no")
-class MangaDexPersian : MangaDex("fa")
-class MangaDexPolish : MangaDex("pl")
-class MangaDexPortugueseBrazil : MangaDex("pt-BR", "pt-br")
-class MangaDexPortuguesePortugal : MangaDex("pt")
-class MangaDexRomanian : MangaDex("ro")
-class MangaDexRussian : MangaDex("ru")
-class MangaDexSerbian : MangaDex("sr")
-class MangaDexSlovak : MangaDex("sk")
-class MangaDexSpanishLatinAmerica : MangaDex("es-419", "es-la")
-class MangaDexSpanishSpain : MangaDex("es")
-class MangaDexSwedish : MangaDex("sv")
-class MangaDexTamil : MangaDex("ta")
-class MangaDexTelugu : MangaDex("te")
-class MangaDexThai : MangaDex("th")
-class MangaDexTurkish : MangaDex("tr")
-class MangaDexUkrainian : MangaDex("uk")
-class MangaDexVietnamese : MangaDex("vi")
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt
deleted file mode 100644
index 05b452a01b..0000000000
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt
+++ /dev/null
@@ -1,400 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.mangadex
-
-import android.content.SharedPreferences
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ContentRatingDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.PublicationDemographicDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.StatusDto
-import eu.kanade.tachiyomi.lib.i18n.Intl
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.source.model.FilterList
-import okhttp3.HttpUrl
-
-class MangaDexFilters {
-
- internal fun getMDFilterList(
- preferences: SharedPreferences,
- dexLang: String,
- intl: Intl,
- ): FilterList = FilterList(
- HasAvailableChaptersFilter(intl),
- OriginalLanguageList(intl, getOriginalLanguage(preferences, dexLang, intl)),
- ContentRatingList(intl, getContentRating(preferences, dexLang, intl)),
- DemographicList(intl, getDemographics(intl)),
- StatusList(intl, getStatus(intl)),
- SortFilter(intl, getSortables(intl)),
- TagsFilter(intl, getTagFilters(intl)),
- TagList(intl["content"], getContents(intl)),
- TagList(intl["format"], getFormats(intl)),
- TagList(intl["genre"], getGenres(intl)),
- TagList(intl["theme"], getThemes(intl)),
- )
-
- private interface UrlQueryFilter {
- fun addQueryParameter(url: HttpUrl.Builder, dexLang: String)
- }
-
- private class HasAvailableChaptersFilter(intl: Intl) :
- Filter.CheckBox(intl["has_available_chapters"]),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- if (state) {
- url.addQueryParameter("hasAvailableChapters", "true")
- url.addQueryParameter("availableTranslatedLanguage[]", dexLang)
- }
- }
- }
-
- private class OriginalLanguage(
- name: String,
- val isoCode: String,
- state: Boolean = false,
- ) : Filter.CheckBox(name, state)
- private class OriginalLanguageList(intl: Intl, originalLanguage: List) :
- Filter.Group(intl["original_language"], originalLanguage),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- state.filter(OriginalLanguage::state)
- .forEach { lang ->
- // dex has zh and zh-hk for chinese manhua
- if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) {
- url.addQueryParameter(
- "originalLanguage[]",
- MDConstants.originalLanguagePrefValChineseHk,
- )
- }
-
- url.addQueryParameter("originalLanguage[]", lang.isoCode)
- }
- }
- }
-
- private fun getOriginalLanguage(
- preferences: SharedPreferences,
- dexLang: String,
- intl: Intl,
- ): List {
- val originalLanguages = preferences.getStringSet(
- MDConstants.getOriginalLanguagePrefKey(dexLang),
- setOf(),
- )!!
-
- return listOf(
- OriginalLanguage(
- name = intl.format(
- "original_language_filter_japanese",
- intl.languageDisplayName(MangaDexIntl.JAPANESE),
- ),
- isoCode = MDConstants.originalLanguagePrefValJapanese,
- state = MDConstants.originalLanguagePrefValJapanese in originalLanguages,
- ),
- OriginalLanguage(
- name = intl.format(
- "original_language_filter_chinese",
- intl.languageDisplayName(MangaDexIntl.CHINESE),
- ),
- isoCode = MDConstants.originalLanguagePrefValChinese,
- state = MDConstants.originalLanguagePrefValChinese in originalLanguages,
- ),
- OriginalLanguage(
- name = intl.format(
- "original_language_filter_korean",
- intl.languageDisplayName(MangaDexIntl.KOREAN),
- ),
- isoCode = MDConstants.originalLanguagePrefValKorean,
- state = MDConstants.originalLanguagePrefValKorean in originalLanguages,
- ),
- )
- }
-
- private class ContentRating(name: String, val value: String) : Filter.CheckBox(name)
- private class ContentRatingList(intl: Intl, contentRating: List) :
- Filter.Group(intl["content_rating"], contentRating),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- state.filter(ContentRating::state)
- .forEach { url.addQueryParameter("contentRating[]", it.value) }
- }
- }
-
- private fun getContentRating(
- preferences: SharedPreferences,
- dexLang: String,
- intl: Intl,
- ): List {
- val contentRatings = preferences.getStringSet(
- MDConstants.getContentRatingPrefKey(dexLang),
- MDConstants.contentRatingPrefDefaults,
- )
-
- return listOf(
- ContentRating(intl["content_rating_safe"], ContentRatingDto.SAFE.value).apply {
- state = contentRatings?.contains(MDConstants.contentRatingPrefValSafe) ?: true
- },
- ContentRating(intl["content_rating_suggestive"], ContentRatingDto.SUGGESTIVE.value).apply {
- state = contentRatings?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true
- },
- ContentRating(intl["content_rating_erotica"], ContentRatingDto.EROTICA.value).apply {
- state = contentRatings?.contains(MDConstants.contentRatingPrefValErotica) ?: false
- },
- ContentRating(intl["content_rating_pornographic"], ContentRatingDto.PORNOGRAPHIC.value).apply {
- state = contentRatings?.contains(MDConstants.contentRatingPrefValPornographic) ?: false
- },
- )
- }
-
- private class Demographic(name: String, val value: String) : Filter.CheckBox(name)
- private class DemographicList(intl: Intl, demographics: List) :
- Filter.Group(intl["publication_demographic"], demographics),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- state.filter(Demographic::state)
- .forEach { url.addQueryParameter("publicationDemographic[]", it.value) }
- }
- }
-
- private fun getDemographics(intl: Intl) = listOf(
- Demographic(intl["publication_demographic_none"], PublicationDemographicDto.NONE.value),
- Demographic(intl["publication_demographic_shounen"], PublicationDemographicDto.SHOUNEN.value),
- Demographic(intl["publication_demographic_shoujo"], PublicationDemographicDto.SHOUJO.value),
- Demographic(intl["publication_demographic_seinen"], PublicationDemographicDto.SEINEN.value),
- Demographic(intl["publication_demographic_josei"], PublicationDemographicDto.JOSEI.value),
- )
-
- private class Status(name: String, val value: String) : Filter.CheckBox(name)
- private class StatusList(intl: Intl, status: List) :
- Filter.Group(intl["status"], status),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- state.filter(Status::state)
- .forEach { url.addQueryParameter("status[]", it.value) }
- }
- }
-
- private fun getStatus(intl: Intl) = listOf(
- Status(intl["status_ongoing"], StatusDto.ONGOING.value),
- Status(intl["status_completed"], StatusDto.COMPLETED.value),
- Status(intl["status_hiatus"], StatusDto.HIATUS.value),
- Status(intl["status_cancelled"], StatusDto.CANCELLED.value),
- )
-
- data class Sortable(val title: String, val value: String) {
- override fun toString(): String = title
- }
-
- private fun getSortables(intl: Intl) = arrayOf(
- Sortable(intl["sort_alphabetic"], "title"),
- Sortable(intl["sort_chapter_uploaded_at"], "latestUploadedChapter"),
- Sortable(intl["sort_number_of_follows"], "followedCount"),
- Sortable(intl["sort_content_created_at"], "createdAt"),
- Sortable(intl["sort_content_info_updated_at"], "updatedAt"),
- Sortable(intl["sort_relevance"], "relevance"),
- Sortable(intl["sort_year"], "year"),
- Sortable(intl["sort_rating"], "rating"),
- )
-
- class SortFilter(intl: Intl, private val sortables: Array) :
- Filter.Sort(
- intl["sort"],
- sortables.map(Sortable::title).toTypedArray(),
- Selection(5, false),
- ),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- if (state != null) {
- val query = sortables[state!!.index].value
- val value = if (state!!.ascending) "asc" else "desc"
-
- url.addQueryParameter("order[$query]", value)
- }
- }
- }
-
- internal class Tag(val id: String, name: String) : Filter.TriState(name)
-
- private class TagList(collection: String, tags: List) :
- Filter.Group(collection, tags),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- state.forEach { tag ->
- if (tag.isIncluded()) {
- url.addQueryParameter("includedTags[]", tag.id)
- } else if (tag.isExcluded()) {
- url.addQueryParameter("excludedTags[]", tag.id)
- }
- }
- }
- }
-
- private fun getContents(intl: Intl): List {
- val tags = listOf(
- Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", intl["content_gore"]),
- Tag("97893a4c-12af-4dac-b6be-0dffb353568e", intl["content_sexual_violence"]),
- )
-
- return tags.sortIfTranslated(intl)
- }
-
- private fun getFormats(intl: Intl): List {
- val tags = listOf(
- Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", intl["format_yonkoma"]),
- Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", intl["format_adaptation"]),
- Tag("51d83883-4103-437c-b4b1-731cb73d786c", intl["format_anthology"]),
- Tag("0a39b5a1-b235-4886-a747-1d05d216532d", intl["format_award_winning"]),
- Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", intl["format_doujinshi"]),
- Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", intl["format_fan_colored"]),
- Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", intl["format_full_color"]),
- Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", intl["format_long_strip"]),
- Tag("320831a8-4026-470b-94f6-8353740e6f04", intl["format_official_colored"]),
- Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", intl["format_oneshot"]),
- Tag("891cf039-b895-47f0-9229-bef4c96eccd4", intl["format_user_created"]),
- Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", intl["format_web_comic"]),
- )
-
- return tags.sortIfTranslated(intl)
- }
-
- private fun getGenres(intl: Intl): List {
- val tags = listOf(
- Tag("391b0423-d847-456f-aff0-8b0cfc03066b", intl["genre_action"]),
- Tag("87cc87cd-a395-47af-b27a-93258283bbc6", intl["genre_adventure"]),
- Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", intl["genre_boys_love"]),
- Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", intl["genre_comedy"]),
- Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", intl["genre_crime"]),
- Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", intl["genre_drama"]),
- Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", intl["genre_fantasy"]),
- Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", intl["genre_girls_love"]),
- Tag("33771934-028e-4cb3-8744-691e866a923e", intl["genre_historical"]),
- Tag("cdad7e68-1419-41dd-bdce-27753074a640", intl["genre_horror"]),
- Tag("ace04997-f6bd-436e-b261-779182193d3d", intl["genre_isekai"]),
- Tag("81c836c9-914a-4eca-981a-560dad663e73", intl["genre_magical_girls"]),
- Tag("50880a9d-5440-4732-9afb-8f457127e836", intl["genre_mecha"]),
- Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", intl["genre_medical"]),
- Tag("ee968100-4191-4968-93d3-f82d72be7e46", intl["genre_mystery"]),
- Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", intl["genre_philosophical"]),
- Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", intl["genre_romance"]),
- Tag("256c8bd9-4904-4360-bf4f-508a76d67183", intl["genre_sci_fi"]),
- Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", intl["genre_slice_of_life"]),
- Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", intl["genre_sports"]),
- Tag("7064a261-a137-4d3a-8848-2d385de3a99c", intl["genre_superhero"]),
- Tag("07251805-a27e-4d59-b488-f0bfbec15168", intl["genre_thriller"]),
- Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", intl["genre_tragedy"]),
- Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", intl["genre_wuxia"]),
- )
-
- return tags.sortIfTranslated(intl)
- }
-
- private fun getThemes(intl: Intl): List {
- val tags = listOf(
- Tag("e64f6742-c834-471d-8d72-dd51fc02b835", intl["theme_aliens"]),
- Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", intl["theme_animals"]),
- Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", intl["theme_cooking"]),
- Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", intl["theme_crossdressing"]),
- Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", intl["theme_delinquents"]),
- Tag("39730448-9a5f-48a2-85b0-a70db87b1233", intl["theme_demons"]),
- Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", intl["theme_gender_swap"]),
- Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", intl["theme_ghosts"]),
- Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", intl["theme_gyaru"]),
- Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", intl["theme_harem"]),
- Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", intl["theme_incest"]),
- Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", intl["theme_loli"]),
- Tag("85daba54-a71c-4554-8a28-9901a8b0afad", intl["theme_mafia"]),
- Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", intl["theme_magic"]),
- Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", intl["theme_martial_arts"]),
- Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", intl["theme_military"]),
- Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", intl["theme_monster_girls"]),
- Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", intl["theme_monsters"]),
- Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", intl["theme_music"]),
- Tag("489dd859-9b61-4c37-af75-5b18e88daafc", intl["theme_ninja"]),
- Tag("92d6d951-ca5e-429c-ac78-451071cbf064", intl["theme_office_workers"]),
- Tag("df33b754-73a3-4c54-80e6-1a74a8058539", intl["theme_police"]),
- Tag("9467335a-1b83-4497-9231-765337a00b96", intl["theme_post_apocalyptic"]),
- Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", intl["theme_psychological"]),
- Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", intl["theme_reincarnation"]),
- Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", intl["theme_reverse_harem"]),
- Tag("81183756-1453-4c81-aa9e-f6e1b63be016", intl["theme_samurai"]),
- Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", intl["theme_school_life"]),
- Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", intl["theme_shota"]),
- Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", intl["theme_supernatural"]),
- Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", intl["theme_survival"]),
- Tag("292e862b-2d17-4062-90a2-0356caa4ae27", intl["theme_time_travel"]),
- Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", intl["theme_traditional_games"]),
- Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", intl["theme_vampires"]),
- Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", intl["theme_video_games"]),
- Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", intl["theme_villainess"]),
- Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", intl["theme_virtual_reality"]),
- Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", intl["theme_zombies"]),
- )
-
- return tags.sortIfTranslated(intl)
- }
-
- // to get all tags from dex https://api.mangadex.org/manga/tag
- internal fun getTags(intl: Intl): List {
- return getContents(intl) + getFormats(intl) + getGenres(intl) + getThemes(intl)
- }
-
- private data class TagMode(val title: String, val value: String) {
- override fun toString(): String = title
- }
-
- private fun getTagModes(intl: Intl) = arrayOf(
- TagMode(intl["mode_and"], "AND"),
- TagMode(intl["mode_or"], "OR"),
- )
-
- private class TagInclusionMode(intl: Intl, modes: Array) :
- Filter.Select(intl["included_tags_mode"], modes, 0),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- url.addQueryParameter("includedTagsMode", values[state].value)
- }
- }
-
- private class TagExclusionMode(intl: Intl, modes: Array) :
- Filter.Select(intl["excluded_tags_mode"], modes, 1),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- url.addQueryParameter("excludedTagsMode", values[state].value)
- }
- }
-
- private class TagsFilter(intl: Intl, innerFilters: FilterList) :
- Filter.Group>(intl["tags_mode"], innerFilters),
- UrlQueryFilter {
-
- override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
- state.filterIsInstance()
- .forEach { filter -> filter.addQueryParameter(url, dexLang) }
- }
- }
-
- private fun getTagFilters(intl: Intl): FilterList = FilterList(
- TagInclusionMode(intl, getTagModes(intl)),
- TagExclusionMode(intl, getTagModes(intl)),
- )
-
- internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): HttpUrl {
- filters.filterIsInstance()
- .forEach { filter -> filter.addQueryParameter(url, dexLang) }
-
- return url.build()
- }
-
- private fun List.sortIfTranslated(intl: Intl): List = apply {
- if (intl.chosenLanguage == MangaDexIntl.ENGLISH) {
- return this
- }
-
- return sortedWith(compareBy(intl.collator, Tag::name))
- }
-}
diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt
deleted file mode 100644
index 1a72db3bb9..0000000000
--- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt
+++ /dev/null
@@ -1,490 +0,0 @@
-package eu.kanade.tachiyomi.extension.all.mangadex
-
-import android.text.Editable
-import android.text.TextWatcher
-import android.util.Log
-import android.widget.Button
-import android.widget.EditText
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ArtistDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AttributesDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AuthorArtistAttributesDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.AuthorDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterAttributesDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDataDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ContentRatingDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtAttributesDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.EntityDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListAttributesDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDataDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaAttributesDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ScanlationGroupAttributes
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.ScanlationGroupDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.StatusDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.TagAttributesDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.TagDto
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.UnknownEntity
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.UserAttributes
-import eu.kanade.tachiyomi.extension.all.mangadex.dto.UserDto
-import eu.kanade.tachiyomi.lib.i18n.Intl
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.source.model.SChapter
-import eu.kanade.tachiyomi.source.model.SManga
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.modules.SerializersModule
-import kotlinx.serialization.modules.plus
-import kotlinx.serialization.modules.polymorphic
-import kotlinx.serialization.modules.subclass
-import okhttp3.CacheControl
-import okhttp3.Headers
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import org.jsoup.parser.Parser
-import java.util.Date
-import java.util.Locale
-import java.util.concurrent.TimeUnit
-
-class MangaDexHelper(lang: String) {
-
- val mdFilters = MangaDexFilters()
-
- val json = Json {
- isLenient = true
- ignoreUnknownKeys = true
- allowSpecialFloatingPointValues = true
- prettyPrint = true
- serializersModule += SerializersModule {
- polymorphic(EntityDto::class) {
- subclass(AuthorDto::class)
- subclass(ArtistDto::class)
- subclass(ChapterDataDto::class)
- subclass(CoverArtDto::class)
- subclass(ListDataDto::class)
- subclass(MangaDataDto::class)
- subclass(ScanlationGroupDto::class)
- subclass(TagDto::class)
- subclass(UserDto::class)
- defaultDeserializer { UnknownEntity.serializer() }
- }
-
- polymorphic(AttributesDto::class) {
- subclass(AuthorArtistAttributesDto::class)
- subclass(ChapterAttributesDto::class)
- subclass(CoverArtAttributesDto::class)
- subclass(ListAttributesDto::class)
- subclass(MangaAttributesDto::class)
- subclass(ScanlationGroupAttributes::class)
- subclass(TagAttributesDto::class)
- subclass(UserAttributes::class)
- }
- }
- }
-
- val intl = Intl(
- language = lang,
- baseLanguage = MangaDexIntl.ENGLISH,
- availableLanguages = MangaDexIntl.AVAILABLE_LANGS,
- classLoader = this::class.java.classLoader!!,
- createMessageFileName = { lang ->
- when (lang) {
- MangaDexIntl.SPANISH_LATAM -> Intl.createDefaultMessageFileName(MangaDexIntl.SPANISH)
- MangaDexIntl.PORTUGUESE -> Intl.createDefaultMessageFileName(MangaDexIntl.BRAZILIAN_PORTUGUESE)
- else -> Intl.createDefaultMessageFileName(lang)
- }
- },
- )
-
- /**
- * Gets the UUID from the url
- */
- fun getUUIDFromUrl(url: String) = url.substringAfterLast("/")
-
- /**
- * Get chapters for manga (aka manga/$id/feed endpoint)
- */
- fun getChapterEndpoint(mangaId: String, offset: Int, langCode: String) =
- "${MDConstants.apiMangaUrl}/$mangaId/feed".toHttpUrl().newBuilder()
- .addQueryParameter("includes[]", MDConstants.scanlationGroup)
- .addQueryParameter("includes[]", MDConstants.user)
- .addQueryParameter("limit", "500")
- .addQueryParameter("offset", offset.toString())
- .addQueryParameter("translatedLanguage[]", langCode)
- .addQueryParameter("order[volume]", "desc")
- .addQueryParameter("order[chapter]", "desc")
- .addQueryParameter("includeFuturePublishAt", "0")
- .addQueryParameter("includeEmptyPages", "0")
- .toString()
-
- /**
- * Check if the manga url is a valid uuid
- */
- fun containsUuid(url: String) = url.contains(MDConstants.uuidRegex)
-
- /**
- * Check if the string is a valid uuid
- */
- fun isUuid(text: String) = MDConstants.uuidRegex matches text
-
- /**
- * Get the manga offset pages are 1 based, so subtract 1
- */
- fun getMangaListOffset(page: Int): String = (MDConstants.mangaLimit * (page - 1)).toString()
-
- /**
- * Get the latest chapter offset pages are 1 based, so subtract 1
- */
- fun getLatestChapterOffset(page: Int): String =
- (MDConstants.latestChapterLimit * (page - 1)).toString()
-
- /**
- * Remove any HTML characters in manga or chapter name to actual
- * characters. For example ♥ will show ♥.
- */
- private fun String.removeEntities(): String {
- return Parser.unescapeEntities(this, false)
- }
-
- /**
- * Remove any HTML characters in description to actual characters.
- * It also removes Markdown syntax for links, italic and bold.
- */
- private fun String.removeEntitiesAndMarkdown(): String {
- return removeEntities()
- .substringBefore("---")
- .replace(markdownLinksRegex, "$1")
- .replace(markdownItalicBoldRegex, "$1")
- .replace(markdownItalicRegex, "$1")
- .trim()
- }
-
- /**
- * Maps MangaDex status to Tachiyomi status.
- * Adapted from the MangaDex handler from TachiyomiSY.
- */
- fun getPublicationStatus(attr: MangaAttributesDto, volumes: Map): Int {
- val chaptersList = volumes.values
- .flatMap { it.chapters.values }
- .map { it.chapter }
-
- val tempStatus = when (attr.status) {
- StatusDto.ONGOING -> SManga.ONGOING
- StatusDto.CANCELLED -> SManga.CANCELLED
- StatusDto.COMPLETED -> SManga.PUBLISHING_FINISHED
- StatusDto.HIATUS -> SManga.ON_HIATUS
- else -> SManga.UNKNOWN
- }
-
- val publishedOrCancelled = tempStatus == SManga.PUBLISHING_FINISHED ||
- tempStatus == SManga.CANCELLED
-
- val isOneShot = attr.tags.any { it.id == MDConstants.tagOneShotUuid } &&
- attr.tags.none { it.id == MDConstants.tagAnthologyUuid }
-
- return when {
- chaptersList.contains(attr.lastChapter) && publishedOrCancelled -> SManga.COMPLETED
- isOneShot && volumes["none"]?.chapters?.get("none") != null -> SManga.COMPLETED
- else -> tempStatus
- }
- }
-
- private fun parseDate(dateAsString: String): Long =
- MDConstants.dateFormatter.parse(dateAsString)?.time ?: 0
-
- /**
- * Chapter URL where we get the token, last request time.
- */
- private val tokenTracker = hashMapOf()
-
- companion object {
- val USE_CACHE = CacheControl.Builder()
- .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
- .build()
-
- val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
- val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
- val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
-
- val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
-
- val trailingHyphenRegex = "-+$".toRegex()
- }
-
- /**
- * Check the token map to see if the MD@Home host is still valid.
- */
- fun getValidImageUrlForPage(page: Page, headers: Headers, client: OkHttpClient): Request {
- val (host, tokenRequestUrl, time) = page.url.split(",")
-
- val mdAtHomeServerUrl =
- when (Date().time - time.toLong() > MDConstants.mdAtHomeTokenLifespan) {
- false -> host
- true -> {
- val tokenLifespan = Date().time - (tokenTracker[tokenRequestUrl] ?: 0)
- val cacheControl = if (tokenLifespan > MDConstants.mdAtHomeTokenLifespan) {
- CacheControl.FORCE_NETWORK
- } else {
- USE_CACHE
- }
- getMdAtHomeUrl(tokenRequestUrl, client, headers, cacheControl)
- }
- }
-
- return GET(mdAtHomeServerUrl + page.imageUrl, headers)
- }
-
- /**
- * Get the MD@Home URL.
- */
- private fun getMdAtHomeUrl(
- tokenRequestUrl: String,
- client: OkHttpClient,
- headers: Headers,
- cacheControl: CacheControl,
- ): String {
- val request = mdAtHomeRequest(tokenRequestUrl, headers, cacheControl)
- val response = client.newCall(request).execute()
-
- // This check is for the error that causes pages to fail to load.
- // It should never be entered, but in case it is, we retry the request.
- if (response.code == 504) {
- Log.wtf("MangaDex", "Failed to read cache for \"$tokenRequestUrl\"")
- return getMdAtHomeUrl(tokenRequestUrl, client, headers, CacheControl.FORCE_NETWORK)
- }
-
- return response.use { json.decodeFromString(it.body.string()).baseUrl }
- }
-
- /**
- * create an md at home Request
- */
- fun mdAtHomeRequest(
- tokenRequestUrl: String,
- headers: Headers,
- cacheControl: CacheControl,
- ): Request {
- if (cacheControl == CacheControl.FORCE_NETWORK) {
- tokenTracker[tokenRequestUrl] = Date().time
- }
-
- return GET(tokenRequestUrl, headers, cacheControl)
- }
-
- /**
- * Create a [SManga] from the JSON element with only basic attributes filled.
- */
- fun createBasicManga(
- mangaDataDto: MangaDataDto,
- coverFileName: String?,
- coverSuffix: String?,
- lang: String,
- ): SManga = SManga.create().apply {
- url = "/manga/${mangaDataDto.id}"
- val titleMap = mangaDataDto.attributes!!.title
- val dirtyTitle =
- titleMap.values.firstOrNull() // use literally anything from title as first resort
- ?: mangaDataDto.attributes.altTitles
- .find { (it[lang] ?: it["en"]) !== null }
- ?.values?.singleOrNull() // find something else from alt titles
- title = dirtyTitle?.removeEntities().orEmpty()
-
- coverFileName?.let {
- thumbnail_url = when (!coverSuffix.isNullOrEmpty()) {
- true -> "${MDConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName$coverSuffix"
- else -> "${MDConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName"
- }
- }
- }
-
- /**
- * Create an [SManga] from the JSON element with all attributes filled.
- */
- fun createManga(
- mangaDataDto: MangaDataDto,
- chapters: Map,
- firstVolumeCover: String?,
- lang: String,
- coverSuffix: String?,
- altTitlesInDesc: Boolean,
- ): SManga {
- val attr = mangaDataDto.attributes!!
-
- // Things that will go with the genre tags but aren't actually genre
- val dexLocale = Locale.forLanguageTag(lang)
-
- val nonGenres = listOfNotNull(
- attr.publicationDemographic
- ?.let { intl["publication_demographic_${it.name.lowercase()}"] },
- attr.contentRating
- .takeIf { it != ContentRatingDto.SAFE }
- ?.let { intl.format("content_rating_genre", intl["content_rating_${it.name.lowercase()}"]) },
- attr.originalLanguage
- ?.let { Locale.forLanguageTag(it) }
- ?.getDisplayName(dexLocale)
- ?.replaceFirstChar { it.uppercase(dexLocale) },
- )
-
- val authors = mangaDataDto.relationships
- .filterIsInstance()
- .mapNotNull { it.attributes?.name }
- .distinct()
-
- val artists = mangaDataDto.relationships
- .filterIsInstance()
- .mapNotNull { it.attributes?.name }
- .distinct()
-
- val coverFileName = firstVolumeCover ?: mangaDataDto.relationships
- .filterIsInstance()
- .firstOrNull()
- ?.attributes?.fileName
-
- val tags = mdFilters.getTags(intl).associate { it.id to it.name }
-
- val genresMap = attr.tags
- .groupBy({ it.attributes!!.group }) { tagDto -> tags[tagDto.id] }
- .mapValues { it.value.filterNotNull().sortedWith(intl.collator) }
-
- val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
-
- var desc = (attr.description[lang] ?: attr.description["en"])
- ?.removeEntitiesAndMarkdown()
- .orEmpty()
-
- if (altTitlesInDesc) {
- val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty()
- val altTitles = attr.altTitles
- .filter { it.containsKey(lang) || it.containsKey(romanizedOriginalLang) }
- .mapNotNull { it.values.singleOrNull() }
- .filter(String::isNotEmpty)
-
- if (altTitles.isNotEmpty()) {
- val altTitlesDesc = altTitles
- .joinToString("\n", "${intl["alternative_titles"]}\n") { "• $it" }
- desc += (if (desc.isBlank()) "" else "\n\n") + altTitlesDesc.removeEntities()
- }
- }
-
- return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
- description = desc
- author = authors.joinToString()
- artist = artists.joinToString()
- status = getPublicationStatus(attr, chapters)
- genre = genreList
- .filter(String::isNotEmpty)
- .joinToString()
- }
- }
-
- /**
- * Create the [SChapter] from the JSON element.
- */
- fun createChapter(chapterDataDto: ChapterDataDto): SChapter {
- val attr = chapterDataDto.attributes!!
-
- val groups = chapterDataDto.relationships
- .filterIsInstance()
- .filterNot { it.id == MDConstants.legacyNoGroupId } // 'no group' left over from MDv3
- .mapNotNull { it.attributes?.name }
- .joinToString(" & ")
- .ifEmpty {
- // Fallback to uploader name if no group is set.
- val users = chapterDataDto.relationships
- .filterIsInstance()
- .mapNotNull { it.attributes?.username }
- if (users.isNotEmpty()) intl.format("uploaded_by", users.joinToString(" & ")) else ""
- }
- .ifEmpty { intl["no_group"] } // "No Group" as final resort
-
- val chapterName = mutableListOf()
- // Build chapter name
-
- attr.volume?.let {
- if (it.isNotEmpty()) {
- chapterName.add("Vol.$it")
- }
- }
-
- attr.chapter?.let {
- if (it.isNotEmpty()) {
- chapterName.add("Ch.$it")
- }
- }
-
- attr.title?.let {
- if (it.isNotEmpty()) {
- if (chapterName.isNotEmpty()) {
- chapterName.add("-")
- }
- chapterName.add(it)
- }
- }
-
- // if volume, chapter and title is empty its a oneshot
- if (chapterName.isEmpty()) {
- chapterName.add("Oneshot")
- }
-
- // In future calculate [END] if non mvp api doesn't provide it
-
- return SChapter.create().apply {
- url = "/chapter/${chapterDataDto.id}"
- name = chapterName.joinToString(" ").removeEntities()
- date_upload = parseDate(attr.publishAt)
- scanlator = groups
- }
- }
-
- fun titleToSlug(title: String) = title.trim()
- .lowercase(Locale.US)
- .replace(titleSpecialCharactersRegex, "-")
- .replace(trailingHyphenRegex, "")
- .split("-")
- .reduce { accumulator, element ->
- val currentSlug = "$accumulator-$element"
- if (currentSlug.length > 100) {
- accumulator
- } else {
- currentSlug
- }
- }
-
- /**
- * Adds a custom [TextWatcher] to the preference's [EditText] that show an
- * error if the input value contains invalid UUIDs. If the validation fails,
- * the Ok button is disabled to prevent the user from saving the value.
- *
- * This will likely need to be removed or revisited when the app migrates the
- * extension preferences screen to Compose.
- */
- fun setupEditTextUuidValidator(editText: EditText) {
- editText.addTextChangedListener(
- object : TextWatcher {
-
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
-
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
-
- override fun afterTextChanged(editable: Editable?) {
- requireNotNull(editable)
-
- val text = editable.toString()
-
- val isValid = text.isBlank() || text
- .split(",")
- .map(String::trim)
- .all(::isUuid)
-
- editText.error = if (!isValid) intl["invalid_uuids"] else null
- editText.rootView.findViewById