diff --git a/.run/MangaReaderGenerator.run.xml b/.run/MangaReaderGenerator.run.xml
new file mode 100644
index 000000000..f92f92a8c
--- /dev/null
+++ b/.run/MangaReaderGenerator.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/multisrc/overrides/mangareader/mangafire/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangafire/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..bb7c377bc
Binary files /dev/null and b/multisrc/overrides/mangareader/mangafire/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mangareader/mangafire/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangafire/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..e4ae40abf
Binary files /dev/null and b/multisrc/overrides/mangareader/mangafire/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mangareader/mangafire/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangafire/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..2afaaac46
Binary files /dev/null and b/multisrc/overrides/mangareader/mangafire/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mangareader/mangafire/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangafire/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..5c1f701f9
Binary files /dev/null and b/multisrc/overrides/mangareader/mangafire/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mangareader/mangafire/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangafire/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..c993005be
Binary files /dev/null and b/multisrc/overrides/mangareader/mangafire/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mangareader/mangafire/res/web_hi_res_512.png b/multisrc/overrides/mangareader/mangafire/res/web_hi_res_512.png
new file mode 100644
index 000000000..6ede68720
Binary files /dev/null and b/multisrc/overrides/mangareader/mangafire/res/web_hi_res_512.png differ
diff --git a/multisrc/overrides/mangareader/mangafire/src/Filters.kt b/multisrc/overrides/mangareader/mangafire/src/Filters.kt
new file mode 100644
index 000000000..a5ccc3e0e
--- /dev/null
+++ b/multisrc/overrides/mangareader/mangafire/src/Filters.kt
@@ -0,0 +1,166 @@
+package eu.kanade.tachiyomi.extension.all.mangafire
+
+import eu.kanade.tachiyomi.source.model.Filter
+
+class Entry(name: String, val id: String) : Filter.CheckBox(name) {
+ constructor(name: String) : this(name, name)
+}
+
+sealed class Group(
+ name: String,
+ val param: String,
+ values: List,
+) : Filter.Group(name, values)
+
+sealed class Select(
+ name: String,
+ val param: String,
+ private val valuesMap: Map,
+) : Filter.Select(name, valuesMap.keys.toTypedArray()) {
+ open val selection: String
+ get() = valuesMap[values[state]]!!
+}
+
+class TypeFilter : Group("Type", "type[]", types)
+
+private val types: List
+ get() = listOf(
+ Entry("Manga", "manga"),
+ Entry("One-Shot", "one_shot"),
+ Entry("Doujinshi", "doujinshi"),
+ Entry("Light-Novel", "light_novel"),
+ Entry("Novel", "novel"),
+ Entry("Manhwa", "manhwa"),
+ Entry("Manhua", "manhua"),
+ )
+
+class Genre(name: String, val id: String) : Filter.TriState(name) {
+ val selection: String
+ get() = (if (isExcluded()) "-" else "") + id
+}
+
+class GenresFilter : Filter.Group("Genre", genres) {
+ val param = "genre[]"
+
+ val combineMode: Boolean
+ get() = state.filter { !it.isIgnored() }.size > 1
+}
+
+private val genres: List
+ get() = listOf(
+ Genre("Action", "1"),
+ Genre("Adventure", "78"),
+ Genre("Avant Garde", "3"),
+ Genre("Boys Love", "4"),
+ Genre("Comedy", "5"),
+ Genre("Demons", "77"),
+ Genre("Drama", "6"),
+ Genre("Ecchi", "7"),
+ Genre("Fantasy", "79"),
+ Genre("Girls Love", "9"),
+ Genre("Gourmet", "10"),
+ Genre("Harem", "11"),
+ Genre("Horror", "530"),
+ Genre("Isekai", "13"),
+ Genre("Iyashikei", "531"),
+ Genre("Josei", "15"),
+ Genre("Kids", "532"),
+ Genre("Magic", "539"),
+ Genre("Mahou Shoujo", "533"),
+ Genre("Martial Arts", "534"),
+ Genre("Mecha", "19"),
+ Genre("Military", "535"),
+ Genre("Music", "21"),
+ Genre("Mystery", "22"),
+ Genre("Parody", "23"),
+ Genre("Psychological", "536"),
+ Genre("Reverse Harem", "25"),
+ Genre("Romance", "26"),
+ Genre("School", "73"),
+ Genre("Sci-Fi", "28"),
+ Genre("Seinen", "537"),
+ Genre("Shoujo", "30"),
+ Genre("Shounen", "31"),
+ Genre("Slice of Life", "538"),
+ Genre("Space", "33"),
+ Genre("Sports", "34"),
+ Genre("Super Power", "75"),
+ Genre("Supernatural", "76"),
+ Genre("Suspense", "37"),
+ Genre("Thriller", "38"),
+ Genre("Vampire", "39"),
+ )
+
+class StatusFilter : Group("Status", "status[]", statuses)
+
+private val statuses: List
+ get() = listOf(
+ Entry("Completed", "completed"),
+ Entry("Releasing", "releasing"),
+ Entry("On Hiatus", "on_hiatus"),
+ Entry("Discontinued", "discontinued"),
+ Entry("Not Yet Published", "info"),
+ )
+
+class YearFilter : Group("Year", "year[]", years)
+
+private val years: List
+ get() = listOf(
+ Entry("2023"),
+ Entry("2022"),
+ Entry("2021"),
+ Entry("2020"),
+ Entry("2019"),
+ Entry("2018"),
+ Entry("2017"),
+ Entry("2016"),
+ Entry("2015"),
+ Entry("2014"),
+ Entry("2013"),
+ Entry("2012"),
+ Entry("2011"),
+ Entry("2010"),
+ Entry("2009"),
+ Entry("2008"),
+ Entry("2007"),
+ Entry("2006"),
+ Entry("2005"),
+ Entry("2004"),
+ Entry("2003"),
+ Entry("2000s"),
+ Entry("1990s"),
+ Entry("1980s"),
+ Entry("1970s"),
+ Entry("1960s"),
+ Entry("1950s"),
+ Entry("1940s"),
+ )
+
+class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
+
+private val chapterCounts
+ get() = mapOf(
+ "Any" to "",
+ "At least 1 chapter" to "1",
+ "At least 3 chapters" to "3",
+ "At least 5 chapters" to "5",
+ "At least 10 chapters" to "10",
+ "At least 20 chapters" to "20",
+ "At least 30 chapters" to "30",
+ "At least 50 chapters" to "50",
+ )
+
+class SortFilter : Select("Sort", "sort", orders)
+
+private val orders
+ get() = mapOf(
+ "Trending" to "trending",
+ "Recently updated" to "recently_updated",
+ "Recently added" to "recently_added",
+ "Release date" to "release_date",
+ "Name A-Z" to "title_az",
+ "Score" to "scores",
+ "MAL score" to "mal_scores",
+ "Most viewed" to "most_viewed",
+ "Most favourited" to "most_favourited",
+ )
diff --git a/multisrc/overrides/mangareader/mangafire/src/ImageInterceptor.kt b/multisrc/overrides/mangareader/mangafire/src/ImageInterceptor.kt
new file mode 100644
index 000000000..95c18c79f
--- /dev/null
+++ b/multisrc/overrides/mangareader/mangafire/src/ImageInterceptor.kt
@@ -0,0 +1,79 @@
+package eu.kanade.tachiyomi.extension.all.mangafire
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Rect
+import okhttp3.Interceptor
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import kotlin.math.min
+
+object ImageInterceptor : Interceptor {
+
+ const val SCRAMBLED = "scrambled"
+ private const val PIECE_SIZE = 200
+ private const val MIN_SPLIT_COUNT = 5
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ val response = chain.proceed(request)
+ val fragment = request.url.fragment ?: return response
+ if (SCRAMBLED !in fragment) return response
+ val offset = fragment.substringAfterLast('_').toInt()
+
+ val image = response.body.byteStream().use { descramble(it, offset) }
+ val body = image.toResponseBody("image/jpeg".toMediaType())
+ return response.newBuilder().body(body).build()
+ }
+
+ private fun descramble(image: InputStream, offset: Int): ByteArray {
+ // obfuscated code: https://mangafire.to/assets/t1/min/all.js
+ // it shuffles arrays of the image slices
+
+ val bitmap = BitmapFactory.decodeStream(image)
+ val width = bitmap.width
+ val height = bitmap.height
+
+ val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(result)
+
+ val pieceWidth = min(PIECE_SIZE, width.ceilDiv(MIN_SPLIT_COUNT))
+ val pieceHeight = min(PIECE_SIZE, height.ceilDiv(MIN_SPLIT_COUNT))
+ val xMax = width.ceilDiv(pieceWidth) - 1
+ val yMax = height.ceilDiv(pieceHeight) - 1
+
+ for (y in 0..yMax) {
+ for (x in 0..xMax) {
+ val xDst = pieceWidth * x
+ val yDst = pieceHeight * y
+ val w = min(pieceWidth, width - xDst)
+ val h = min(pieceHeight, height - yDst)
+
+ val xSrc = pieceWidth * when (x) {
+ xMax -> x // margin
+ else -> (xMax - x + offset) % xMax
+ }
+ val ySrc = pieceHeight * when (y) {
+ yMax -> y // margin
+ else -> (yMax - y + offset) % yMax
+ }
+
+ val srcRect = Rect(xSrc, ySrc, xSrc + w, ySrc + h)
+ val dstRect = Rect(xDst, yDst, xDst + w, yDst + h)
+
+ canvas.drawBitmap(bitmap, srcRect, dstRect, null)
+ }
+ }
+
+ val output = ByteArrayOutputStream()
+ result.compress(Bitmap.CompressFormat.JPEG, 90, output)
+ return output.toByteArray()
+ }
+
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun Int.ceilDiv(other: Int) = (this + (other - 1)) / other
+}
diff --git a/multisrc/overrides/mangareader/mangafire/src/MangaFire.kt b/multisrc/overrides/mangareader/mangafire/src/MangaFire.kt
new file mode 100644
index 000000000..5459e630f
--- /dev/null
+++ b/multisrc/overrides/mangareader/mangafire/src/MangaFire.kt
@@ -0,0 +1,189 @@
+package eu.kanade.tachiyomi.extension.all.mangafire
+
+import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.int
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import org.jsoup.select.Evaluator
+import uy.kohesive.injekt.injectLazy
+
+open class MangaFire(
+ override val lang: String,
+ private val langCode: String = lang,
+) : MangaReader() {
+ override val name = "MangaFire"
+
+ override val baseUrl = "https://mangafire.to"
+
+ private val json: Json by injectLazy()
+
+ override val client = network.client.newBuilder()
+ .addInterceptor(ImageInterceptor)
+ .build()
+
+ override fun latestUpdatesRequest(page: Int) =
+ GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
+
+ override fun popularMangaRequest(page: Int) =
+ GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val urlBuilder = baseUrl.toHttpUrl().newBuilder()
+ if (query.isNotBlank()) {
+ urlBuilder.addPathSegment("filter").apply {
+ addQueryParameter("keyword", query)
+ addQueryParameter("page", page.toString())
+ }
+ } else {
+ urlBuilder.addPathSegment("filter").apply {
+ addQueryParameter("language[]", langCode)
+ addQueryParameter("page", page.toString())
+ filters.ifEmpty(::getFilterList).forEach { filter ->
+ when (filter) {
+ is Group -> {
+ filter.state.forEach {
+ if (it.state) {
+ addQueryParameter(filter.param, it.id)
+ }
+ }
+ }
+ is Select -> {
+ addQueryParameter(filter.param, filter.selection)
+ }
+ is GenresFilter -> {
+ filter.state.forEach {
+ if (it.state != 0) {
+ addQueryParameter(filter.param, it.selection)
+ }
+ }
+ if (filter.combineMode) {
+ addQueryParameter("genre_mode", "and")
+ }
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+ return GET(urlBuilder.build(), headers)
+ }
+
+ override fun searchMangaSelector() = ".mangas.items .inner"
+
+ override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
+
+ override fun searchMangaFromElement(element: Element) =
+ SManga.create().apply {
+ element.selectFirst("a.color-light")!!.let {
+ url = it.attr("href")
+ title = it.attr("title")
+ }
+ element.selectFirst(Evaluator.Tag("img"))!!.let {
+ thumbnail_url = it.attr("src")
+ }
+ }
+
+ override fun mangaDetailsParse(document: Document) = SManga.create().apply {
+ val root = document.selectFirst(".detail .top .wrapper")!!
+ val mangaTitle = root.selectFirst(Evaluator.Class("name"))!!.ownText()
+ title = mangaTitle
+ description = document.run {
+ val description = selectFirst(Evaluator.Class("summary"))!!.ownText()
+ when (val altTitle = root.selectFirst(Evaluator.Class("al-name"))!!.ownText()) {
+ "", mangaTitle -> description
+ else -> "$description\n\nAlternative Title: $altTitle"
+ }
+ }
+ thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.attr("src")
+ status = when (root.selectFirst(Evaluator.Class("status"))!!.ownText()) {
+ "Completed" -> SManga.COMPLETED
+ "Releasing" -> SManga.ONGOING
+ "On_hiatus" -> SManga.ON_HIATUS
+ "Discontinued" -> SManga.CANCELLED
+ else -> SManga.UNKNOWN
+ }
+ with(root.selectFirst(Evaluator.Class("more-info"))!!) {
+ author = selectFirst("span:contains(Author:) + span")?.text()
+ val type = selectFirst("span:contains(Type:) + span")?.text()
+ val genres = selectFirst("span:contains(Genres:) + span")?.text()
+ genre = listOfNotNull(type, genres).joinToString()
+ }
+ }
+
+ override val chapterType get() = "chapter"
+ override val volumeType get() = "volume"
+
+ override fun chapterListRequest(mangaUrl: String, type: String): Request {
+ val id = mangaUrl.substringAfterLast('.')
+ return GET("$baseUrl/ajax/read/$id/list?viewby=$type", headers)
+ }
+
+ override fun parseChapterElements(response: Response, isVolume: Boolean): List {
+ val result = json.decodeFromString>(response.body.string()).result
+ val container = result.parseHtml(if (isVolume) volumeType else chapterType)
+ ?.selectFirst(".numberlist[data-lang=$langCode]")
+ ?: return emptyList()
+ return container.children().map { it.child(0) }
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val typeAndId = chapter.url.substringAfterLast('#')
+ return GET("$baseUrl/ajax/read/$typeAndId", headers)
+ }
+
+ override fun pageListParse(response: Response): List {
+ val result = json.decodeFromString>(response.body.string()).result
+
+ return result.pages.mapIndexed { index, image ->
+ val url = image.url
+ val offset = image.offset
+ val imageUrl = if (offset > 0) "$url#${ImageInterceptor.SCRAMBLED}_$offset" else url
+
+ Page(index, imageUrl = imageUrl)
+ }
+ }
+
+ override fun getFilterList() =
+ FilterList(
+ Filter.Header("NOTE: Ignored if using text search!"),
+ Filter.Separator(),
+ TypeFilter(),
+ GenresFilter(),
+ StatusFilter(),
+ YearFilter(),
+ ChapterCountFilter(),
+ SortFilter(),
+ )
+
+ @Serializable
+ class ChapterListDto(private val html: String, private val link_format: String) {
+ fun parseHtml(type: String): Document? {
+ if ("LANG/$type-NUMBER" !in link_format) return null
+ return Jsoup.parseBodyFragment(html)
+ }
+ }
+
+ @Serializable
+ class PageListDto(private val images: List>) {
+ val pages get() = images.map { Image(it[0].content, it[2].int) }
+ }
+
+ class Image(val url: String, val offset: Int)
+
+ @Serializable
+ class ResponseDto(val result: T)
+}
diff --git a/multisrc/overrides/mangareader/mangafire/src/MangaFireFactory.kt b/multisrc/overrides/mangareader/mangafire/src/MangaFireFactory.kt
new file mode 100644
index 000000000..3fbc88e6d
--- /dev/null
+++ b/multisrc/overrides/mangareader/mangafire/src/MangaFireFactory.kt
@@ -0,0 +1,15 @@
+package eu.kanade.tachiyomi.extension.all.mangafire
+
+import eu.kanade.tachiyomi.source.SourceFactory
+
+class MangaFireFactory : SourceFactory {
+ override fun createSources() = listOf(
+ MangaFire("en"),
+ MangaFire("es"),
+ MangaFire("es-419", "es-la"),
+ MangaFire("fr"),
+ MangaFire("ja"),
+ MangaFire("pt"),
+ MangaFire("pt-BR", "pt-br"),
+ )
+}
diff --git a/src/all/mangareaderto/CHANGELOG.md b/multisrc/overrides/mangareader/mangareaderto/CHANGELOG.md
similarity index 91%
rename from src/all/mangareaderto/CHANGELOG.md
rename to multisrc/overrides/mangareader/mangareaderto/CHANGELOG.md
index 1b0c78aef..f4fce847c 100644
--- a/src/all/mangareaderto/CHANGELOG.md
+++ b/multisrc/overrides/mangareader/mangareaderto/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.3.4
+
+- Refactor and make multisrc
+- Chapter page list now requires only 1 network request (those fetched in old versions still need 2)
+
## 1.3.3
- Appended `.to` to extension name
diff --git a/src/all/mangareaderto/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangareaderto/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from src/all/mangareaderto/res/mipmap-hdpi/ic_launcher.png
rename to multisrc/overrides/mangareader/mangareaderto/res/mipmap-hdpi/ic_launcher.png
diff --git a/src/all/mangareaderto/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangareaderto/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from src/all/mangareaderto/res/mipmap-mdpi/ic_launcher.png
rename to multisrc/overrides/mangareader/mangareaderto/res/mipmap-mdpi/ic_launcher.png
diff --git a/src/all/mangareaderto/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangareaderto/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from src/all/mangareaderto/res/mipmap-xhdpi/ic_launcher.png
rename to multisrc/overrides/mangareader/mangareaderto/res/mipmap-xhdpi/ic_launcher.png
diff --git a/src/all/mangareaderto/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangareaderto/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from src/all/mangareaderto/res/mipmap-xxhdpi/ic_launcher.png
rename to multisrc/overrides/mangareader/mangareaderto/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/src/all/mangareaderto/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mangareader/mangareaderto/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from src/all/mangareaderto/res/mipmap-xxxhdpi/ic_launcher.png
rename to multisrc/overrides/mangareader/mangareaderto/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/src/all/mangareaderto/res/web_hi_res_512.png b/multisrc/overrides/mangareader/mangareaderto/res/web_hi_res_512.png
similarity index 100%
rename from src/all/mangareaderto/res/web_hi_res_512.png
rename to multisrc/overrides/mangareader/mangareaderto/res/web_hi_res_512.png
diff --git a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderFilters.kt b/multisrc/overrides/mangareader/mangareaderto/src/Filters.kt
similarity index 100%
rename from src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderFilters.kt
rename to multisrc/overrides/mangareader/mangareaderto/src/Filters.kt
diff --git a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderImageInterceptor.kt b/multisrc/overrides/mangareader/mangareaderto/src/ImageInterceptor.kt
similarity index 92%
rename from src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderImageInterceptor.kt
rename to multisrc/overrides/mangareader/mangareaderto/src/ImageInterceptor.kt
index d0f86d0ca..cdd7af521 100644
--- a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderImageInterceptor.kt
+++ b/multisrc/overrides/mangareader/mangareaderto/src/ImageInterceptor.kt
@@ -14,7 +14,7 @@ import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import kotlin.math.min
-object MangaReaderImageInterceptor : Interceptor {
+object ImageInterceptor : Interceptor {
private val memo = hashMapOf()
@@ -22,11 +22,9 @@ object MangaReaderImageInterceptor : Interceptor {
val request = chain.request()
val response = chain.proceed(request)
- val url = request.url
- // TODO: remove the query parameter check (legacy) in later versions
- if (url.fragment != SCRAMBLED && url.queryParameter("shuffled") == null) return response
+ if (request.url.fragment != SCRAMBLED) return response
- val image = descramble(response.body.byteStream())
+ val image = response.body.byteStream().use(::descramble)
val body = image.toResponseBody("image/jpeg".toMediaType())
return response.newBuilder()
.body(body)
diff --git a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReader.kt b/multisrc/overrides/mangareader/mangareaderto/src/MangaReader.kt
similarity index 59%
rename from src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReader.kt
rename to multisrc/overrides/mangareader/mangareaderto/src/MangaReader.kt
index 6c0dff429..16842a8d5 100644
--- a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReader.kt
+++ b/multisrc/overrides/mangareader/mangareaderto/src/MangaReader.kt
@@ -1,15 +1,13 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto
-import android.app.Application
import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -21,62 +19,25 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import org.jsoup.select.Evaluator
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
+import rx.Observable
open class MangaReader(
override val lang: String,
-) : ConfigurableSource, ParsedHttpSource() {
+) : MangaReader() {
override val name = "MangaReader"
override val baseUrl = "https://mangareader.to"
- override val supportsLatest = true
-
override val client = network.client.newBuilder()
- .addInterceptor(MangaReaderImageInterceptor)
+ .addInterceptor(ImageInterceptor)
.build()
- private fun MangasPage.insertVolumeEntries(): MangasPage {
- if (preferences.showVolume.not()) return this
- val list = mangas.ifEmpty { return this }
- val newList = ArrayList(list.size * 2)
- for (manga in list) {
- val volume = SManga.create().apply {
- url = manga.url + VOLUME_URL_SUFFIX
- title = VOLUME_TITLE_PREFIX + manga.title
- thumbnail_url = manga.thumbnail_url
- }
- newList.add(manga)
- newList.add(volume)
- }
- return MangasPage(newList, hasNextPage)
- }
-
- override fun latestUpdatesParse(response: Response) = super.latestUpdatesParse(response).insertVolumeEntries()
- override fun popularMangaParse(response: Response) = super.popularMangaParse(response).insertVolumeEntries()
- override fun searchMangaParse(response: Response) = super.searchMangaParse(response).insertVolumeEntries()
-
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
- override fun latestUpdatesSelector() = searchMangaSelector()
-
- override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
-
- override fun latestUpdatesFromElement(element: Element) =
- searchMangaFromElement(element)
-
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
- override fun popularMangaSelector() = searchMangaSelector()
-
- override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
-
- override fun popularMangaFromElement(element: Element) =
- searchMangaFromElement(element)
-
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
@@ -106,7 +67,7 @@ open class MangaReader(
}
}
}
- return Request.Builder().url(urlBuilder.build()).headers(headers).build()
+ return GET(urlBuilder.build(), headers)
}
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
@@ -145,10 +106,9 @@ open class MangaReader(
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
- url = document.location().removePrefix(baseUrl)
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
val mangaTitle = root.selectFirst(Evaluator.Tag("h2"))!!.ownText()
- title = if (url.endsWith(VOLUME_URL_SUFFIX)) VOLUME_TITLE_PREFIX + mangaTitle else mangaTitle
+ title = mangaTitle
description = root.run {
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
@@ -171,70 +131,45 @@ open class MangaReader(
}
}
- override fun chapterListRequest(manga: SManga): Request {
- val url = manga.url
- val id = url.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast('-')
- val type = if (url.endsWith(VOLUME_URL_SUFFIX)) "vol" else "chap"
+ override val chapterType get() = "chap"
+ override val volumeType get() = "vol"
+
+ override fun chapterListRequest(mangaUrl: String, type: String): Request {
+ val id = mangaUrl.substringAfterLast('-')
return GET("$baseUrl/ajax/manga/reading-list/$id?readingBy=$type", headers)
}
- override fun chapterListSelector() = "#$lang-chapters .item"
-
- override fun chapterListParse(response: Response): List {
- val isVolume = response.request.url.queryParameter("readingBy") == "vol"
+ override fun parseChapterElements(response: Response, isVolume: Boolean): List {
val container = response.parseHtmlProperty().run {
val type = if (isVolume) "volumes" else "chapters"
selectFirst(Evaluator.Id("$lang-$type")) ?: return emptyList()
}
- val abbrPrefix = if (isVolume) "Vol" else "Chap"
- val fullPrefix = if (isVolume) "Volume" else "Chapter"
- return container.children().map { chapterFromElement(it, abbrPrefix, fullPrefix) }
+ return container.children()
}
- override fun chapterFromElement(element: Element) =
- throw UnsupportedOperationException("Not used.")
-
- private fun chapterFromElement(element: Element, abbrPrefix: String, fullPrefix: String) =
- SChapter.create().apply {
- val number = element.attr("data-number")
- chapter_number = number.toFloatOrNull() ?: -1f
- element.selectFirst(Evaluator.Tag("a"))!!.let {
- url = it.attr("href")
- name = run {
- val name = it.attr("title")
- val prefix = "$abbrPrefix $number: "
- if (name.startsWith(prefix).not()) return@run name
- val realName = name.removePrefix(prefix)
- if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
- }
- }
+ override fun fetchPageList(chapter: SChapter): Observable> = Observable.fromCallable {
+ val typeAndId = chapter.url.substringAfterLast('#', "").ifEmpty {
+ val document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
+ val wrapper = document.selectFirst(Evaluator.Id("wrapper"))!!
+ wrapper.attr("data-reading-by") + '/' + wrapper.attr("data-reading-id")
}
+ val ajaxUrl = "$baseUrl/ajax/image/list/$typeAndId?quality=${preferences.quality}"
+ client.newCall(GET(ajaxUrl, headers)).execute().let(::pageListParse)
+ }
- override fun pageListParse(document: Document): List {
- val ajaxUrl = document.selectFirst(Evaluator.Id("wrapper"))!!.run {
- val readingBy = attr("data-reading-by")
- val readingId = attr("data-reading-id")
- "$baseUrl/ajax/image/list/$readingBy/$readingId?quality=${preferences.quality}"
- }
-
- val pageDocument = client.newCall(GET(ajaxUrl, headers)).execute().parseHtmlProperty()
+ override fun pageListParse(response: Response): List {
+ val pageDocument = response.parseHtmlProperty()
return pageDocument.getElementsByClass("iv-card").mapIndexed { index, img ->
val url = img.attr("data-url")
- val imageUrl = if (img.hasClass("shuffled")) "$url#${MangaReaderImageInterceptor.SCRAMBLED}" else url
+ val imageUrl = if (img.hasClass("shuffled")) "$url#${ImageInterceptor.SCRAMBLED}" else url
Page(index, imageUrl = imageUrl)
}
}
- override fun imageUrlParse(document: Document) =
- throw UnsupportedOperationException("Not used")
-
- private val preferences by lazy {
- Injekt.get().getSharedPreferences("source_$id", 0x0000)!!
- }
-
override fun setupPreferenceScreen(screen: PreferenceScreen) {
getPreferences(screen.context).forEach(screen::addPreference)
+ super.setupPreferenceScreen(screen)
}
override fun getFilterList() =
diff --git a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderFactory.kt b/multisrc/overrides/mangareader/mangareaderto/src/MangaReaderFactory.kt
similarity index 100%
rename from src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderFactory.kt
rename to multisrc/overrides/mangareader/mangareaderto/src/MangaReaderFactory.kt
diff --git a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderPreferences.kt b/multisrc/overrides/mangareader/mangareaderto/src/Preferences.kt
similarity index 62%
rename from src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderPreferences.kt
rename to multisrc/overrides/mangareader/mangareaderto/src/Preferences.kt
index ea17a6fc9..3d58d1cb5 100644
--- a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReaderPreferences.kt
+++ b/multisrc/overrides/mangareader/mangareaderto/src/Preferences.kt
@@ -3,39 +3,24 @@ package eu.kanade.tachiyomi.extension.all.mangareaderto
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.ListPreference
-import androidx.preference.SwitchPreferenceCompat
fun getPreferences(context: Context) = arrayOf(
ListPreference(context).apply {
key = QUALITY_PREF
title = "Image quality"
- summary = "Selected: %s\n" +
+ summary = "%s\n" +
"Changes will not be applied to chapters that are already loaded or read " +
"until you clear the chapter cache."
entries = arrayOf("Low", "Medium", "High")
entryValues = arrayOf("low", QUALITY_MEDIUM, "high")
setDefaultValue(QUALITY_MEDIUM)
},
-
- SwitchPreferenceCompat(context).apply {
- key = SHOW_VOLUME_PREF
- title = "Show manga in volumes in search result"
- setDefaultValue(false)
- },
)
val SharedPreferences.quality
get() =
getString(QUALITY_PREF, QUALITY_MEDIUM)!!
-val SharedPreferences.showVolume
- get() =
- getBoolean(SHOW_VOLUME_PREF, false)
-
private const val QUALITY_PREF = "quality"
private const val QUALITY_MEDIUM = "medium"
-private const val SHOW_VOLUME_PREF = "show_volume"
-
-const val VOLUME_URL_SUFFIX = "#vol"
-const val VOLUME_TITLE_PREFIX = "[VOL] "
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReader.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReader.kt
new file mode 100644
index 000000000..1008ac308
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReader.kt
@@ -0,0 +1,125 @@
+package eu.kanade.tachiyomi.multisrc.mangareader
+
+import android.app.Application
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import eu.kanade.tachiyomi.source.ConfigurableSource
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import org.jsoup.select.Evaluator
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+abstract class MangaReader : HttpSource(), ConfigurableSource {
+
+ override val supportsLatest = true
+
+ final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
+
+ final override fun popularMangaParse(response: Response) = searchMangaParse(response)
+
+ final override fun searchMangaParse(response: Response): MangasPage {
+ val document = response.asJsoup()
+ var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement)
+ if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
+ entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
+ val volume = SManga.create().apply {
+ url = manga.url + VOLUME_URL_SUFFIX
+ title = VOLUME_TITLE_PREFIX + manga.title
+ thumbnail_url = manga.thumbnail_url
+ }
+ listOf(manga, volume)
+ }
+ }
+ val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
+ return MangasPage(entries, hasNextPage)
+ }
+
+ final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
+
+ abstract fun searchMangaSelector(): String
+
+ abstract fun searchMangaNextPageSelector(): String
+
+ abstract fun searchMangaFromElement(element: Element): SManga
+
+ abstract fun mangaDetailsParse(document: Document): SManga
+
+ final override fun mangaDetailsParse(response: Response): SManga {
+ val document = response.asJsoup()
+ val manga = mangaDetailsParse(document)
+ if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
+ manga.title = VOLUME_TITLE_PREFIX + manga.title
+ }
+ return manga
+ }
+
+ abstract val chapterType: String
+ abstract val volumeType: String
+
+ abstract fun chapterListRequest(mangaUrl: String, type: String): Request
+
+ abstract fun parseChapterElements(response: Response, isVolume: Boolean): List
+
+ override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
+
+ final override fun fetchChapterList(manga: SManga): Observable> = Observable.fromCallable {
+ val path = manga.url
+ val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
+ val type = if (isVolume) volumeType else chapterType
+ val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
+ val response = client.newCall(request).execute()
+
+ val abbrPrefix = if (isVolume) "Vol" else "Chap"
+ val fullPrefix = if (isVolume) "Volume" else "Chapter"
+ val linkSelector = Evaluator.Tag("a")
+ parseChapterElements(response, isVolume).map { element ->
+ SChapter.create().apply {
+ val number = element.attr("data-number")
+ chapter_number = number.toFloatOrNull() ?: -1f
+
+ val link = element.selectFirst(linkSelector)!!
+ name = run {
+ val name = link.text()
+ val prefix = "$abbrPrefix $number: "
+ if (!name.startsWith(prefix)) return@run name
+ val realName = name.removePrefix(prefix)
+ if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
+ }
+ setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
+ }
+ }
+ }
+
+ final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#')
+
+ override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
+
+ val preferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)!!
+ }
+
+ override fun setupPreferenceScreen(screen: PreferenceScreen) {
+ SwitchPreferenceCompat(screen.context).apply {
+ key = SHOW_VOLUME_PREF
+ title = "Show volume entries in search result"
+ setDefaultValue(false)
+ }.let(screen::addPreference)
+ }
+
+ companion object {
+ private const val SHOW_VOLUME_PREF = "show_volume"
+
+ private const val VOLUME_URL_FRAGMENT = "vol"
+ private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
+ private const val VOLUME_TITLE_PREFIX = "[VOL] "
+ }
+}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReaderGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReaderGenerator.kt
new file mode 100644
index 000000000..2f200b9fd
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReaderGenerator.kt
@@ -0,0 +1,33 @@
+package eu.kanade.tachiyomi.multisrc.mangareader
+
+import generator.ThemeSourceData.MultiLang
+import generator.ThemeSourceGenerator
+
+class MangaReaderGenerator : ThemeSourceGenerator {
+ override val themeClass = "MangaReader"
+ override val themePkg = "mangareader"
+ override val baseVersionCode = 1
+ override val sources = listOf(
+ MultiLang(
+ name = "MangaReader",
+ baseUrl = "https://mangareader.to",
+ langs = listOf("en", "fr", "ja", "ko", "zh"),
+ isNsfw = true,
+ pkgName = "mangareaderto",
+ overrideVersionCode = 3,
+ ),
+ MultiLang(
+ name = "MangaFire",
+ baseUrl = "https://mangafire.to",
+ langs = listOf("en", "es", "es-419", "fr", "ja", "pt", "pt-BR"),
+ isNsfw = true,
+ ),
+ )
+
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ MangaReaderGenerator().createAll()
+ }
+ }
+}
diff --git a/src/all/mangareaderto/AndroidManifest.xml b/src/all/mangareaderto/AndroidManifest.xml
deleted file mode 100644
index 30deb7f79..000000000
--- a/src/all/mangareaderto/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/src/all/mangareaderto/build.gradle b/src/all/mangareaderto/build.gradle
deleted file mode 100644
index d4b4a5393..000000000
--- a/src/all/mangareaderto/build.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlinx-serialization'
-
-ext {
- extName = 'MangaReader.to'
- pkgNameSuffix = 'all.mangareaderto'
- extClass = '.MangaReaderFactory'
- extVersionCode = 3
- isNsfw = true
-}
-
-apply from: "$rootDir/common.gradle"