Add MangaFire and create MangaReader multisrc (#16138)

* Add MangaFire.to

* Fixes
Updated 'searchMangaNextPageSelector'
Added throwing 'UnsupportedOperationException' for unused selector

* Fixed naming of filters and fixed parameter's name of minimal chapters' filter

* Fixed language parameter in query

* Clean up MangaFire filters

* Clean up MangaFire descrambler

* Create MangaReader multisrc theme

* Move MangaFire to overrides

* Refactor MangaFire for multisrc

* Update MangaReader changelog

* Remove duplicate filter entry

Co-authored-by: Druzai <70586473+Druzai@users.noreply.github.com>

---------

Co-authored-by: Druzai <g9code@yandex.ru>
Co-authored-by: Druzai <70586473+Druzai@users.noreply.github.com>
This commit is contained in:
stevenyomi 2023-04-24 07:12:26 +08:00 committed by GitHub
parent a6983f0ac1
commit 2ba79a0094
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 653 additions and 127 deletions

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="MangaReaderGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="tachiyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.mangareader.MangaReaderGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangareader" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangareader" />
</method>
</configuration>
</component>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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<Entry>,
) : Filter.Group<Entry>(name, values)
sealed class Select(
name: String,
val param: String,
private val valuesMap: Map<String, String>,
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
open val selection: String
get() = valuesMap[values[state]]!!
}
class TypeFilter : Group("Type", "type[]", types)
private val types: List<Entry>
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>("Genre", genres) {
val param = "genre[]"
val combineMode: Boolean
get() = state.filter { !it.isIgnored() }.size > 1
}
private val genres: List<Genre>
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<Entry>
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<Entry>
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",
)

View File

@ -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
}

View File

@ -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<Element> {
val result = json.decodeFromString<ResponseDto<ChapterListDto>>(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<Page> {
val result = json.decodeFromString<ResponseDto<PageListDto>>(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<List<JsonPrimitive>>) {
val pages get() = images.map { Image(it[0].content, it[2].int) }
}
class Image(val url: String, val offset: Int)
@Serializable
class ResponseDto<T>(val result: T)
}

View File

@ -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"),
)
}

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -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<Int, IntArray>()
@ -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)

View File

@ -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<SManga>(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<SChapter> {
val isVolume = response.request.url.queryParameter("readingBy") == "vol"
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
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<List<Page>> = 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<Page> {
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<Page> {
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<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
getPreferences(screen.context).forEach(screen::addPreference)
super.setupPreferenceScreen(screen)
}
override fun getFilterList() =

View File

@ -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] "

View File

@ -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<Element>
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
final override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = 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<Application>().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] "
}
}

View File

@ -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<String>) {
MangaReaderGenerator().createAll()
}
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -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"