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"