Initial commit

This commit is contained in:
FourTOne5
2024-01-09 04:12:39 +06:00
commit 600c345dfe
8593 changed files with 150590 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -0,0 +1,233 @@
package eu.kanade.tachiyomi.extension.en.comickiba
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
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.nodes.TextNode
import org.jsoup.select.Evaluator
import rx.Observable
class Manhuagold : MangaReader() {
override val name = "Manhuagold"
override val lang = "en"
override val baseUrl = "https://manhuagold.com"
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// Popular
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter/$page/?sort=views&sex=All&chapter_count=0", headers)
// Latest
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter/$page/?sort=latest-updated&sex=All&chapter_count=0", headers)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
urlBuilder.addPathSegment("search").apply {
addQueryParameter("keyword", query)
}
} else {
urlBuilder.addPathSegment("filter").apply {
filters.ifEmpty(::getFilterList).forEach { filter ->
when (filter) {
is Select -> {
addQueryParameter(filter.param, filter.selection)
}
is GenresFilter -> {
addQueryParameter(filter.param, filter.selection)
}
else -> {}
}
}
}
}
urlBuilder.addPathSegment(page.toString())
urlBuilder.addPathSegment("")
return GET(urlBuilder.build(), headers)
}
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
override fun searchMangaFromElement(element: Element) =
SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst(Evaluator.Tag("img"))!!.let {
title = it.attr("alt")
thumbnail_url = it.imgAttr()
}
}
override fun searchMangaNextPageSelector() = "ul.pagination > li.active + li"
// Filters
override fun getFilterList() =
FilterList(
Note,
StatusFilter(),
SortFilter(),
GenresFilter(),
)
// Details
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
val mangaTitle = root.selectFirst(Evaluator.Class("manga-name"))!!.ownText()
title = mangaTitle
description = root.run {
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
"", mangaTitle -> description
else -> "$description\n\nAlternative Title: $altTitle"
}
}
thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.imgAttr()
genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() }
for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) {
if (item.hasClass("item").not()) continue
when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) {
"Authors:" -> item.parseAuthorsTo(this)
"Status:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText().lowercase()) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"on-hold" -> SManga.ON_HIATUS
"canceled" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
}
}
private fun Element.parseAuthorsTo(manga: SManga) {
val authors = select(Evaluator.Tag("a"))
val text = authors.map { it.ownText().replace(",", "") }
val count = authors.size
when (count) {
0 -> return
1 -> {
manga.author = text[0]
return
}
}
val authorList = ArrayList<String>(count)
val artistList = ArrayList<String>(count)
for ((index, author) in authors.withIndex()) {
val textNode = author.nextSibling() as? TextNode
val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList
list.add(text[index])
}
if (authorList.isEmpty().not()) manga.author = authorList.joinToString()
if (artistList.isEmpty().not()) manga.artist = artistList.joinToString()
}
// Chapters
override fun chapterListRequest(mangaUrl: String, type: String): Request =
GET(baseUrl + mangaUrl, headers)
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
TODO("Not yet implemented")
}
override val chapterType = ""
override val volumeType = ""
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map(::parseChapterList)
}
private fun parseChapterList(response: Response): List<SChapter> {
val document = response.use { it.asJsoup() }
return document.select(chapterListSelector())
.map(::chapterFromElement)
}
private fun chapterListSelector(): String = "#chapters-list > li"
private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
element.selectFirst("a")!!.run {
setUrlWithoutDomain(attr("href"))
name = selectFirst(".name")?.text() ?: text()
}
}
// Images
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable {
val document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
val ajaxHeaders = super.headersBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", baseUrl + chapter.url)
add("X-Requested-With", "XMLHttpRequest")
}.build()
val ajaxUrl = "$baseUrl/ajax/image/list/chap/$id"
client.newCall(GET(ajaxUrl, ajaxHeaders)).execute().let(::pageListParse)
}
override fun pageListParse(response: Response): List<Page> {
val document = response.use { it.parseHtmlProperty() }
val pageList = document.select("div").map {
val index = it.attr("data-number").toInt()
val imgUrl = it.imgAttr().ifEmpty { it.selectFirst("img")!!.imgAttr() }
Page(index, "", imgUrl)
}
return pageList
}
// Utilities
// From mangathemesia
private fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
else -> attr("abs:src")
}
private fun Response.parseHtmlProperty(): Document {
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
return Jsoup.parseBodyFragment(html)
}
}

View File

@@ -0,0 +1,142 @@
package eu.kanade.tachiyomi.extension.en.comickiba
import eu.kanade.tachiyomi.source.model.Filter
object Note : Filter.Header("NOTE: Ignored if using text search!")
sealed class Select(
name: String,
val param: String,
values: Array<String>,
) : Filter.Select<String>(name, values) {
open val selection: String
get() = if (state == 0) "" else state.toString()
}
class StatusFilter(
values: Array<String> = statuses.keys.toTypedArray(),
) : Select("Status", "status", values) {
override val selection: String
get() = statuses[values[state]]!!
companion object {
private val statuses = mapOf(
"All" to "",
"Completed" to "completed",
"OnGoing" to "on-going",
"On-Hold" to "on-hold",
"Canceled" to "canceled",
)
}
}
class SortFilter(
values: Array<String> = orders.keys.toTypedArray(),
) : Select("Sort", "sort", values) {
override val selection: String
get() = orders[values[state]]!!
companion object {
private val orders = mapOf(
"Default" to "default",
"Latest Updated" to "latest-updated",
"Most Viewed" to "views",
"Most Viewed Month" to "views_month",
"Most Viewed Week" to "views_week",
"Most Viewed Day" to "views_day",
"Score" to "score",
"Name A-Z" to "az",
"Name Z-A" to "za",
"The highest chapter count" to "chapters",
"Newest" to "new",
"Oldest" to "old",
)
}
}
class Genre(name: String, val id: String) : Filter.CheckBox(name)
class GenresFilter(
values: List<Genre> = genres,
) : Filter.Group<Genre>("Genres", values) {
val param = "genres"
val selection: String
get() = state.filter { it.state }.joinToString(",") { it.id }
companion object {
private val genres: List<Genre>
get() = listOf(
Genre("Action", "37"),
Genre("Adaptation", "19"),
Genre("Adult", "5310"),
Genre("Adventure", "38"),
Genre("Aliens", "5436"),
Genre("Animals", "1552"),
Genre("Award Winning", "39"),
Genre("Comedy", "202"),
Genre("Comic", "287"),
Genre("Cooking", "277"),
Genre("Crime", "2723"),
Genre("Delinquents", "4438"),
Genre("Demons", "379"),
Genre("Drama", "3"),
Genre("Ecchi", "17"),
Genre("Fantasy", "197"),
Genre("Full Color", "13"),
Genre("Gender Bender", "221"),
Genre("Genderswap", "2290"),
Genre("Ghosts", "2866"),
Genre("Gore", "42"),
Genre("Harem", "222"),
Genre("Historical", "4"),
Genre("Horror", "5"),
Genre("Isekai", "259"),
Genre("Josei", "292"),
Genre("Loli", "5449"),
Genre("Long Strip", "7"),
Genre("Magic", "272"),
Genre("Manhwa", "266"),
Genre("Martial Arts", "40"),
Genre("Mature", "5311"),
Genre("Mecha", "2830"),
Genre("Medical", "1598"),
Genre("Military", "43"),
Genre("Monster Girls", "2307"),
Genre("Monsters", "298"),
Genre("Music", "3182"),
Genre("Mystery", "6"),
Genre("Office Workers", "14"),
Genre("Official Colored", "1046"),
Genre("Philosophical", "2776"),
Genre("Post-Apocalyptic", "1059"),
Genre("Psychological", "493"),
Genre("Reincarnation", "204"),
Genre("Reverse", "280"),
Genre("Reverse Harem", "199"),
Genre("Romance", "186"),
Genre("School Life", "601"),
Genre("Sci-Fi", "1845"),
Genre("Sexual Violence", "731"),
Genre("Shoujo", "254"),
Genre("Slice of Life", "10"),
Genre("Sports", "4066"),
Genre("Superhero", "481"),
Genre("Supernatural", "198"),
Genre("Survival", "44"),
Genre("Thriller", "1058"),
Genre("Time Travel", "299"),
Genre("Tragedy", "41"),
Genre("Video Games", "1846"),
Genre("Villainess", "278"),
Genre("Virtual Reality", "1847"),
Genre("Web Comic", "12"),
Genre("Webtoon", "279"),
Genre("Webtoons", "267"),
Genre("Wuxia", "203"),
Genre("Yaoi", "18"),
Genre("Yuri", "11"),
Genre("Zombies", "1060"),
)
}
}

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,231 @@
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
import java.text.SimpleDateFormat
import java.util.Locale
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.cloudflareClient.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 searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
override fun searchMangaSelector() = ".original.card-lg .unit .inner"
override fun searchMangaFromElement(element: Element) =
SManga.create().apply {
element.selectFirst(".info > a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.ownText()
}
element.selectFirst(Evaluator.Tag("img"))!!.let {
thumbnail_url = it.attr("src")
}
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val root = document.selectFirst(".info")!!
val mangaTitle = root.child(1).ownText()
title = mangaTitle
description = document.run {
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
when (val altTitle = root.child(2).ownText()) {
"", mangaTitle -> description
else -> "$description\n\nAlternative Title: $altTitle"
}
}
thumbnail_url = document.selectFirst(".poster")!!
.selectFirst("img")!!.attr("src")
status = when (root.child(0).ownText()) {
"Completed" -> SManga.COMPLETED
"Releasing" -> SManga.ONGOING
"On_hiatus" -> SManga.ON_HIATUS
"Discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
with(document.selectFirst(Evaluator.Class("meta"))!!) {
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/manga/$id/$type/$langCode", headers)
}
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
val elements = document.select("ul li")
if (elements.size > 0) {
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
val request = GET("$baseUrl/ajax/read/$mangaId/chapter/$langCode", headers)
val response = client.newCall(request).execute()
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
val chapterInfoDocument = Jsoup.parse(res)
val chapters = chapterInfoDocument.select("ul li")
for ((i, it) in elements.withIndex()) {
it.attr("data-id", chapters[i].select("a").attr("data-id"))
}
}
return elements.toList()
}
@Serializable
class ChapterIdsDto(
val html: String,
val title_format: String,
)
override fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
val request = chapterListRequest(manga.url, chapterType)
val response = client.newCall(request).execute()
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
val elements = document.selectFirst(".scroll-sm")!!.children()
val chapterCount = chapters.size
if (elements.size != chapterCount) throw Exception("Chapter count doesn't match. Try updating again.")
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
for (i in 0 until chapterCount) {
val chapter = chapters[i]
val element = elements[i]
val number = element.attr("data-number").toFloatOrNull() ?: -1f
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
val date = element.select(Evaluator.Tag("span"))[1].ownText()
chapter.date_upload = try {
dateFormat.parse(date)!!.time
} catch (_: Throwable) {
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)
}
}
@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,
val status: Int,
)
override fun getFilterList() =
FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
TypeFilter(),
GenresFilter(),
StatusFilter(),
YearFilter(),
ChapterCountFilter(),
SortFilter(),
)
}

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

@@ -0,0 +1,36 @@
## 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
- Replaced dependencies
- `android.net.Uri``okhttp3.HttpUrl`
- `org.json``kotlinx.serialization`
- Refactored some code to separate files
- Image quality preference: added prompt to summary and made it take effect without restart, fixes [#12504](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/12504)
- Added preference to show additional entries in volumes in list results and added code to support volumes, fixes [#12573](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/12573)
- Improved parsing
- Added code to parse authors and artists
- Improved chapter list parsing
- Other improvements
- Performance boosts in selectors
- Added French, Korean and Chinese languages
- Corrected filter note type (Text → Header)
- Rewrote image descrambler
- Used fragment in URL instead of appending error-prone query parameter, hopefully fixes [#12722](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/12722)
- Made interceptor singleton to be shared across languages
- Simplified code logic to make it a lot more readable, thanks to Vetle in [#9325 (comment)](https://github.com/tachiyomiorg/tachiyomi-extensions/pull/9325#issuecomment-1100950110) for code reference
- Used `javax.crypto.Cipher` for ARC4
- Memoize permutation result to reduce calculation
- Save as compressed JPG instead of PNG to avoid size bloat (original image is already compressed)
## 1.2.2
- Fixes filters causing manga list to fail to load.
## 1.2.1
- Builds on original PR and unscrambles the images.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -0,0 +1,247 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto
import eu.kanade.tachiyomi.source.model.Filter
import java.util.Calendar
object Note : Filter.Header("NOTE: Ignored if using text search!")
sealed class Select(
name: String,
val param: String,
values: Array<String>,
) : Filter.Select<String>(name, values) {
open val selection: String
get() = if (state == 0) "" else state.toString()
}
class TypeFilter(
values: Array<String> = types,
) : Select("Type", "type", values) {
companion object {
private val types: Array<String>
get() = arrayOf(
"All",
"Manga",
"One-Shot",
"Doujinshi",
"Light Novel",
"Manhwa",
"Manhua",
"Comic",
)
}
}
class StatusFilter(
values: Array<String> = statuses,
) : Select("Status", "status", values) {
companion object {
private val statuses: Array<String>
get() = arrayOf(
"All",
"Finished",
"Publishing",
"On Hiatus",
"Discontinued",
"Not yet published",
)
}
}
class RatingFilter(
values: Array<String> = ratings,
) : Select("Rating Type", "rating_type", values) {
companion object {
private val ratings: Array<String>
get() = arrayOf(
"All",
"G - All Ages",
"PG - Children",
"PG-13 - Teens 13 or older",
"R - 17+ (violence & profanity)",
"R+ - Mild Nudity",
"Rx - Hentai",
)
}
}
class ScoreFilter(
values: Array<String> = scores,
) : Select("Score", "score", values) {
companion object {
private val scores: Array<String>
get() = arrayOf(
"All",
"(1) Appalling",
"(2) Horrible",
"(3) Very Bad",
"(4) Bad",
"(5) Average",
"(6) Fine",
"(7) Good",
"(8) Very Good",
"(9) Great",
"(10) Masterpiece",
)
}
}
sealed class DateSelect(
name: String,
param: String,
values: Array<String>,
) : Select(name, param, values) {
override val selection: String
get() = if (state == 0) "" else values[state]
}
class YearFilter(
param: String,
values: Array<String> = years,
) : DateSelect("Year", param, values) {
companion object {
private val nextYear by lazy {
Calendar.getInstance()[Calendar.YEAR] + 1
}
private val years: Array<String>
get() = Array(nextYear - 1916) {
if (it == 0) "Any" else (nextYear - it).toString()
}
}
}
class MonthFilter(
param: String,
values: Array<String> = months,
) : DateSelect("Month", param, values) {
companion object {
private val months: Array<String>
get() = Array(13) {
if (it == 0) "Any" else "%02d".format(it)
}
}
}
class DayFilter(
param: String,
values: Array<String> = days,
) : DateSelect("Day", param, values) {
companion object {
private val days: Array<String>
get() = Array(32) {
if (it == 0) "Any" else "%02d".format(it)
}
}
}
sealed class DateFilter(
type: String,
values: List<DateSelect>,
) : Filter.Group<DateSelect>("$type Date", values)
class StartDateFilter(
values: List<DateSelect> = parts,
) : DateFilter("Start", values) {
companion object {
private val parts: List<DateSelect>
get() = listOf(
YearFilter("sy"),
MonthFilter("sm"),
DayFilter("sd"),
)
}
}
class EndDateFilter(
values: List<DateSelect> = parts,
) : DateFilter("End", values) {
companion object {
private val parts: List<DateSelect>
get() = listOf(
YearFilter("ey"),
MonthFilter("em"),
DayFilter("ed"),
)
}
}
class SortFilter(
values: Array<String> = orders.keys.toTypedArray(),
) : Select("Sort", "sort", values) {
override val selection: String
get() = orders[values[state]]!!
companion object {
private val orders = mapOf(
"Default" to "default",
"Latest Updated" to "latest-updated",
"Score" to "score",
"Name A-Z" to "name-az",
"Release Date" to "release-date",
"Most Viewed" to "most-viewed",
)
}
}
class Genre(name: String, val id: String) : Filter.CheckBox(name)
class GenresFilter(
values: List<Genre> = genres,
) : Filter.Group<Genre>("Genres", values) {
val param = "genres"
val selection: String
get() = state.filter { it.state }.joinToString(",") { it.id }
companion object {
private val genres: List<Genre>
get() = listOf(
Genre("Action", "1"),
Genre("Adventure", "2"),
Genre("Cars", "3"),
Genre("Comedy", "4"),
Genre("Dementia", "5"),
Genre("Demons", "6"),
Genre("Doujinshi", "7"),
Genre("Drama", "8"),
Genre("Ecchi", "9"),
Genre("Fantasy", "10"),
Genre("Game", "11"),
Genre("Gender Bender", "12"),
Genre("Harem", "13"),
Genre("Hentai", "14"),
Genre("Historical", "15"),
Genre("Horror", "16"),
Genre("Josei", "17"),
Genre("Kids", "18"),
Genre("Magic", "19"),
Genre("Martial Arts", "20"),
Genre("Mecha", "21"),
Genre("Military", "22"),
Genre("Music", "23"),
Genre("Mystery", "24"),
Genre("Parody", "25"),
Genre("Police", "26"),
Genre("Psychological", "27"),
Genre("Romance", "28"),
Genre("Samurai", "29"),
Genre("School", "30"),
Genre("Sci-Fi", "31"),
Genre("Seinen", "32"),
Genre("Shoujo", "33"),
Genre("Shoujo Ai", "34"),
Genre("Shounen", "35"),
Genre("Shounen Ai", "36"),
Genre("Slice of Life", "37"),
Genre("Space", "38"),
Genre("Sports", "39"),
Genre("Super Power", "40"),
Genre("Supernatural", "41"),
Genre("Thriller", "42"),
Genre("Vampire", "43"),
Genre("Yaoi", "44"),
Genre("Yuri", "45"),
)
}
}

View File

@@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto
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 javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import kotlin.math.min
object ImageInterceptor : Interceptor {
private val memo = hashMapOf<Int, IntArray>()
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (request.url.fragment != SCRAMBLED) return response
val image = response.body.byteStream().use(::descramble)
val body = image.toResponseBody("image/jpeg".toMediaType())
return response.newBuilder()
.body(body)
.build()
}
private fun descramble(image: InputStream): ByteArray {
// obfuscated code (imgReverser function): https://mangareader.to/js/read.min.js
// essentially, it shuffles arrays of the image slices using the key 'stay'
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 pieces = ArrayList<Piece>()
for (y in 0 until height step PIECE_SIZE) {
for (x in 0 until width step PIECE_SIZE) {
val w = min(PIECE_SIZE, width - x)
val h = min(PIECE_SIZE, height - y)
pieces.add(Piece(x, y, w, h))
}
}
val groups = pieces.groupBy { it.w shl 16 or it.h }
for (group in groups.values) {
val size = group.size
val permutation = memo.getOrPut(size) {
// The key is actually "stay", but it's padded here in case the code is run in
// Oracle's JDK, where RC4 key is required to be at least 5 bytes
val random = SeedRandom("staystay")
// https://github.com/webcaetano/shuffle-seed
val indices = (0 until size).toMutableList()
IntArray(size) { indices.removeAt((random.nextDouble() * indices.size).toInt()) }
}
for ((i, original) in permutation.withIndex()) {
val src = group[i]
val dst = group[original]
val srcRect = Rect(src.x, src.y, src.x + src.w, src.y + src.h)
val dstRect = Rect(dst.x, dst.y, dst.x + dst.w, dst.y + dst.h)
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
}
}
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
return output.toByteArray()
}
private class Piece(val x: Int, val y: Int, val w: Int, val h: Int)
// https://github.com/davidbau/seedrandom
private class SeedRandom(key: String) {
private val input = ByteArray(RC4_WIDTH)
private val buffer = ByteArray(RC4_WIDTH)
private var pos = RC4_WIDTH
private val rc4 = Cipher.getInstance("RC4").apply {
init(Cipher.ENCRYPT_MODE, SecretKeySpec(key.toByteArray(), "RC4"))
update(input, 0, RC4_WIDTH, buffer) // RC4-drop[256]
}
fun nextDouble(): Double {
var num = nextByte()
var exp = 8
while (num < 1L shl 52) {
num = num shl 8 or nextByte()
exp += 8
}
while (num >= 1L shl 53) {
num = num ushr 1
exp--
}
return Math.scalb(num.toDouble(), -exp)
}
private fun nextByte(): Long {
if (pos == RC4_WIDTH) {
rc4.update(input, 0, RC4_WIDTH, buffer)
pos = 0
}
return buffer[pos++].toLong() and 0xFF
}
}
private const val RC4_WIDTH = 256
private const val PIECE_SIZE = 200
const val SCRAMBLED = "scrambled"
}

View File

@@ -0,0 +1,192 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
import eu.kanade.tachiyomi.network.GET
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
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.nodes.TextNode
import org.jsoup.select.Evaluator
import rx.Observable
open class MangaReader(
override val lang: String,
) : MangaReader() {
override val name = "MangaReader"
override val baseUrl = "https://mangareader.to"
override val client = network.client.newBuilder()
.addInterceptor(ImageInterceptor)
.build()
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
urlBuilder.addPathSegment("search").apply {
addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
}
} else {
urlBuilder.addPathSegment("filter").apply {
addQueryParameter("language", lang)
addQueryParameter("page", page.toString())
filters.ifEmpty(::getFilterList).forEach { filter ->
when (filter) {
is Select -> {
addQueryParameter(filter.param, filter.selection)
}
is DateFilter -> {
filter.state.forEach {
addQueryParameter(it.param, it.selection)
}
}
is GenresFilter -> {
addQueryParameter(filter.param, filter.selection)
}
else -> {}
}
}
}
}
return GET(urlBuilder.build(), headers)
}
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
override fun searchMangaNextPageSelector() = ".page-link[title=Next]"
override fun searchMangaFromElement(element: Element) =
SManga.create().apply {
url = element.attr("href")
element.selectFirst(Evaluator.Tag("img"))!!.let {
title = it.attr("alt")
thumbnail_url = it.attr("src")
}
}
private fun Element.parseAuthorsTo(manga: SManga) {
val authors = select(Evaluator.Tag("a"))
val text = authors.map { it.ownText().replace(",", "") }
val count = authors.size
when (count) {
0 -> return
1 -> {
manga.author = text[0]
return
}
}
val authorList = ArrayList<String>(count)
val artistList = ArrayList<String>(count)
for ((index, author) in authors.withIndex()) {
val textNode = author.nextSibling() as? TextNode
val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList
list.add(text[index])
}
if (authorList.isEmpty().not()) manga.author = authorList.joinToString()
if (artistList.isEmpty().not()) manga.artist = artistList.joinToString()
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
val mangaTitle = root.selectFirst(Evaluator.Tag("h2"))!!.ownText()
title = mangaTitle
description = root.run {
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
"", mangaTitle -> description
else -> "$description\n\nAlternative Title: $altTitle"
}
}
thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.attr("src")
genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() }
for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) {
if (item.hasClass("item").not()) continue
when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) {
"Authors:" -> item.parseAuthorsTo(this)
"Status:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText()) {
"Finished" -> SManga.COMPLETED
"Publishing" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
}
}
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 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()
}
return container.children()
}
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(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#${ImageInterceptor.SCRAMBLED}" else url
Page(index, imageUrl = imageUrl)
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
getPreferences(screen.context).forEach(screen::addPreference)
super.setupPreferenceScreen(screen)
}
override fun getFilterList() =
FilterList(
Note,
TypeFilter(),
StatusFilter(),
RatingFilter(),
ScoreFilter(),
StartDateFilter(),
EndDateFilter(),
SortFilter(),
GenresFilter(),
)
private fun Response.parseHtmlProperty(): Document {
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
return Jsoup.parseBodyFragment(html)
}
}

View File

@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto
import eu.kanade.tachiyomi.source.SourceFactory
class MangaReaderFactory : SourceFactory {
override fun createSources() =
arrayOf("en", "fr", "ja", "ko", "zh").map(::MangaReader)
}

View File

@@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.ListPreference
fun getPreferences(context: Context) = arrayOf(
ListPreference(context).apply {
key = QUALITY_PREF
title = "Image quality"
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)
},
)
val SharedPreferences.quality
get() =
getString(QUALITY_PREF, QUALITY_MEDIUM)!!
private const val QUALITY_PREF = "quality"
private const val QUALITY_MEDIUM = "medium"