feat(lib-multisrc/mangareader): Rework mangareader (#7561)

* chore: move mangafire away from mangareader multisrc

* chore: rework mangareader multisrc theme

* lint(lint): lint

* fix: apply recommended fixes

* lint(lint): lint

* bump versions
This commit is contained in:
Secozzi 2025-02-11 08:42:47 +01:00 committed by Draff
parent 2a44209a2f
commit 56d872d023
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
9 changed files with 790 additions and 784 deletions

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3

View File

@ -1,129 +1,355 @@
package eu.kanade.tachiyomi.multisrc.mangareader package eu.kanade.tachiyomi.multisrc.mangareader
import android.app.Application import eu.kanade.tachiyomi.network.GET
import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.source.model.Filter
import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.MangasPage 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator import org.jsoup.nodes.TextNode
import rx.Observable import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.Injekt import java.net.URLEncoder
import uy.kohesive.injekt.api.get
abstract class MangaReader : HttpSource(), ConfigurableSource { abstract class MangaReader(
override val name: String,
override val baseUrl: String,
final override val lang: String,
) : HttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val client = network.cloudflareClient override val client = network.cloudflareClient
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response) private val json: Json by injectLazy()
open fun addPage(page: Int, builder: HttpUrl.Builder) {
builder.addQueryParameter("page", page.toString())
}
// ============================== Popular ===============================
protected open val sortPopularValue = "most-viewed"
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortPopularValue)),
)
}
final override fun popularMangaParse(response: Response) = searchMangaParse(response) final override fun popularMangaParse(response: Response) = searchMangaParse(response)
final override fun searchMangaParse(response: Response): MangasPage { // =============================== Latest ===============================
val document = response.asJsoup()
var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement) protected open val sortLatestValue = "latest-updated"
if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga -> override fun latestUpdatesRequest(page: Int): Request {
val volume = SManga.create().apply { return searchMangaRequest(
url = manga.url + VOLUME_URL_SUFFIX page,
title = VOLUME_TITLE_PREFIX + manga.title "",
thumbnail_url = manga.thumbnail_url FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortLatestValue)),
)
}
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
protected open val searchPathSegment = "search"
protected open val searchKeyword = "keyword"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotBlank()) {
addPathSegment(searchPathSegment)
addQueryParameter(searchKeyword, query)
} else {
addPathSegment("filter")
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
} }
listOf(manga, volume)
} }
addPage(page, this)
}.build()
return GET(url, headers)
}
open fun searchMangaSelector(): String = ".manga_list-sbs .manga-poster"
open fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("img")!!.let {
title = it.attr("alt")
thumbnail_url = it.imgAttr()
} }
}
open fun searchMangaNextPageSelector(): String = "ul.pagination > li.active + li"
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val entries = document.select(searchMangaSelector())
.map(::searchMangaFromElement)
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(entries, hasNextPage) return MangasPage(entries, hasNextPage)
} }
final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX) // =========================== Manga Details ============================
abstract fun searchMangaSelector(): String override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
abstract fun searchMangaNextPageSelector(): String private val authorText: String = when (lang) {
"ja" -> "著者"
else -> "Authors"
}
abstract fun searchMangaFromElement(element: Element): SManga private val statusText: String = when (lang) {
"ja" -> "地位"
else -> "Status"
}
abstract fun mangaDetailsParse(document: Document): SManga override fun mangaDetailsParse(response: Response): SManga {
final override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup() val document = response.asJsoup()
val manga = mangaDetailsParse(document)
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) { return SManga.create().apply {
manga.title = VOLUME_TITLE_PREFIX + manga.title document.selectFirst("#ani_detail")!!.run {
title = selectFirst(".manga-name")!!.ownText()
thumbnail_url = selectFirst("img")?.imgAttr()
genre = select(".genres > a").joinToString { it.ownText() }
description = buildString {
selectFirst(".description")?.ownText()?.let { append(it) }
append("\n\n")
selectFirst(".manga-name-or")?.ownText()?.let {
if (it.isNotEmpty() && it != title) {
append("Alternative Title: ")
append(it)
}
}
}.trim()
select(".anisc-info > .item").forEach { info ->
when (info.selectFirst(".item-head")?.ownText()) {
"$authorText:" -> info.parseAuthorsTo(this@apply)
"$statusText:" -> info.parseStatus(this@apply)
}
}
}
} }
}
private fun Element.parseAuthorsTo(manga: SManga): SManga {
val authors = select("a")
val text = authors.map { it.ownText().replace(",", "") }
val count = authors.size
when (count) {
0 -> return manga
1 -> {
manga.author = text.first()
return manga
}
}
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?.wholeText?.contains("(Art)") == true) artistList else authorList
list.add(text[index])
}
if (authorList.isNotEmpty()) manga.author = authorList.joinToString()
if (artistList.isNotEmpty()) manga.artist = artistList.joinToString()
return manga return manga
} }
abstract val chapterType: String private fun Element.parseStatus(manga: SManga): SManga {
abstract val volumeType: String manga.status = this.selectFirst(".name")?.ownText().getStatus()
return manga
}
abstract fun chapterListRequest(mangaUrl: String, type: String): Request open fun String?.getStatus(): Int = when (this?.lowercase()) {
"ongoing", "publishing", "releasing" -> SManga.ONGOING
"completed", "finished" -> SManga.COMPLETED
"on-hold", "on_hiatus" -> SManga.ON_HIATUS
"canceled", "discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
abstract fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> // ============================== Chapters ==============================
override fun chapterListParse(response: Response) = throw UnsupportedOperationException() override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url.substringBeforeLast('#')
}
open fun updateChapterList(manga: SManga, chapters: List<SChapter>) = Unit open val chapterIdSelect = "en-chapters"
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable { open fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
val path = manga.url element.selectFirst("a")!!.run {
val isVolume = path.endsWith(VOLUME_URL_SUFFIX) setUrlWithoutDomain(attr("href") + "#${element.attr("data-id")}")
val type = if (isVolume) volumeType else chapterType name = selectFirst(".name")?.text() ?: text()
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type) }
val response = client.newCall(request).execute() }
val abbrPrefix = if (isVolume) "Vol" else "Chap" override fun chapterListParse(response: Response): List<SChapter> {
val fullPrefix = if (isVolume) "Volume" else "Chapter" val document = response.asJsoup()
val linkSelector = Evaluator.Tag("a") return document.select("#$chapterIdSelect > li.chapter-item").map(::chapterFromElement)
parseChapterElements(response, isVolume).map { element -> }
SChapter.create().apply {
val number = element.attr("data-number")
chapter_number = number.toFloatOrNull() ?: -1f
val link = element.selectFirst(linkSelector)!! // =============================== Pages ================================
name = run {
val name = link.text() open fun getChapterId(chapter: SChapter): String {
val prefix = "$abbrPrefix $number: " val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
if (!name.startsWith(prefix)) return@run name return document.selectFirst("div[data-reading-id]")
val realName = name.removePrefix(prefix) ?.attr("data-reading-id")
if (realName.contains(number)) realName else "$fullPrefix $number: $realName" .orEmpty()
} .ifEmpty {
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id")) throw Exception("Unable to retrieve chapter id")
} }
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
} }
final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#') open fun getAjaxUrl(id: String): String {
return "$baseUrl//ajax/image/list/$id?mode=vertical"
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun pageListRequest(chapter: SChapter): Request {
SwitchPreferenceCompat(screen.context).apply { val chapterId = chapter.url.substringAfterLast('#').ifEmpty {
key = SHOW_VOLUME_PREF getChapterId(chapter)
title = "Show volume entries in search result" }
setDefaultValue(false)
}.let(screen::addPreference) val ajaxHeaders = super.headersBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", URLEncoder.encode(baseUrl + chapter.url.substringBeforeLast("#"), "utf-8"))
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET(getAjaxUrl(chapterId), ajaxHeaders)
} }
companion object { open fun pageListParseSelector(): String = ".container-reader-chapter > div > img"
private const val SHOW_VOLUME_PREF = "show_volume"
private const val VOLUME_URL_FRAGMENT = "vol" override fun pageListParse(response: Response): List<Page> {
private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT val document = response.parseHtmlProperty()
private const val VOLUME_TITLE_PREFIX = "[VOL] "
val pageList = document.select(pageListParseSelector()).mapIndexed { index, element ->
val imgUrl = element.imgAttr().ifEmpty {
element.selectFirst("img")!!.imgAttr()
}
Page(index, imageUrl = imgUrl)
}
return pageList
} }
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
open 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")
}
open fun Response.parseHtmlProperty(): Document {
val html = json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
return Jsoup.parseBodyFragment(html)
}
// =============================== Filters ==============================
object Note : Filter.Header("NOTE: Ignored if using text search!")
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
private val join: String? = null,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
if (join == null) {
checked.forEach {
builder.addQueryParameter(param, it.value)
}
} else {
builder.addQueryParameter(param, checked.joinToString(join) { it.value })
}
}
}
open class SortFilter(
title: String,
param: String,
values: Array<Pair<String, String>>,
default: String? = null,
) : UriPartFilter(title, param, values, default)
private val sortFilterName: String = when (lang) {
"ja" -> "選別"
else -> "Sort"
}
protected open val sortFilterParam: String = "sort"
protected open fun sortFilterValues(): Array<Pair<String, String>> {
return arrayOf(
Pair("Default", "default"),
Pair("Latest Updated", sortLatestValue),
Pair("Score", "score"),
Pair("Name A-Z", "name-az"),
Pair("Release Date", "release-date"),
Pair("Most Viewed", sortPopularValue),
)
}
open fun getSortFilter() = SortFilter(sortFilterName, sortFilterParam, sortFilterValues())
override fun getFilterList(): FilterList = FilterList(
getSortFilter(),
)
} }

View File

@ -1,9 +1,7 @@
ext { ext {
extName = 'MangaFire' extName = 'MangaFire'
extClass = '.MangaFireFactory' extClass = '.MangaFireFactory'
themePkg = 'mangareader' extVersionCode = 8
baseUrl = 'https://mangafire.to'
overrideVersionCode = 5
isNsfw = true isNsfw = true
} }

View File

@ -1,12 +1,18 @@
package eu.kanade.tachiyomi.extension.all.mangafire package eu.kanade.tachiyomi.extension.all.mangafire
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader import android.app.Application
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -19,29 +25,40 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator import org.jsoup.select.Evaluator
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
open class MangaFire( class MangaFire(
override val lang: String, override val lang: String,
private val langCode: String = lang, private val langCode: String = lang,
) : MangaReader() { ) : ConfigurableSource, HttpSource() {
override val name = "MangaFire" override val name = "MangaFire"
override val baseUrl = "https://mangafire.to" override val baseUrl = "https://mangafire.to"
override val supportsLatest = true
private val json: Json by injectLazy() private val json: Json by injectLazy()
override val client = super.client.newBuilder() private val preferences by lazy {
.addInterceptor(ImageInterceptor) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
.build() }
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) = override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers) GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
override fun popularMangaRequest(page: Int) = override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder() val urlBuilder = baseUrl.toHttpUrl().newBuilder()
@ -63,9 +80,11 @@ open class MangaFire(
} }
} }
} }
is Select -> { is Select -> {
addQueryParameter(filter.param, filter.selection) addQueryParameter(filter.param, filter.selection)
} }
is GenresFilter -> { is GenresFilter -> {
filter.state.forEach { filter.state.forEach {
if (it.state != 0) { if (it.state != 0) {
@ -76,6 +95,7 @@ open class MangaFire(
addQueryParameter("genre_mode", "and") addQueryParameter("genre_mode", "and")
} }
} }
else -> {} else -> {}
} }
} }
@ -84,22 +104,49 @@ open class MangaFire(
return GET(urlBuilder.build(), headers) return GET(urlBuilder.build(), headers)
} }
override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link" private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
override fun searchMangaSelector() = ".original.card-lg .unit .inner" private fun searchMangaSelector() = ".original.card-lg .unit .inner"
override fun searchMangaFromElement(element: Element) = private fun searchMangaFromElement(element: Element) = SManga.create().apply {
SManga.create().apply { element.selectFirst(".info > a")!!.let {
element.selectFirst(".info > a")!!.let { setUrlWithoutDomain(it.attr("href"))
setUrlWithoutDomain(it.attr("href")) title = it.ownText()
title = it.ownText() }
} element.selectFirst(Evaluator.Tag("img"))!!.let {
element.selectFirst(Evaluator.Tag("img"))!!.let { thumbnail_url = it.attr("src")
thumbnail_url = it.attr("src") }
}
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)
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
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
}
private fun mangaDetailsParse(document: Document) = SManga.create().apply {
val root = document.selectFirst(".info")!! val root = document.selectFirst(".info")!!
val mangaTitle = root.child(1).ownText() val mangaTitle = root.child(1).ownText()
title = mangaTitle title = mangaTitle
@ -110,8 +157,7 @@ open class MangaFire(
else -> "$description\n\nAlternative Title: $altTitle" else -> "$description\n\nAlternative Title: $altTitle"
} }
} }
thumbnail_url = document.selectFirst(".poster")!! thumbnail_url = document.selectFirst(".poster")!!.selectFirst("img")!!.attr("src")
.selectFirst("img")!!.attr("src")
status = when (root.child(0).ownText()) { status = when (root.child(0).ownText()) {
"Completed" -> SManga.COMPLETED "Completed" -> SManga.COMPLETED
"Releasing" -> SManga.ONGOING "Releasing" -> SManga.ONGOING
@ -127,15 +173,48 @@ open class MangaFire(
} }
} }
override val chapterType get() = "chapter" private val chapterType get() = "chapter"
override val volumeType get() = "volume" private val volumeType get() = "volume"
override fun chapterListRequest(mangaUrl: String, type: String): Request { override fun chapterListParse(response: Response): List<SChapter> {
throw UnsupportedOperationException()
}
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"))
}
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
}
private fun chapterListRequest(mangaUrl: String, type: String): Request {
val id = mangaUrl.substringAfterLast('.') val id = mangaUrl.substringAfterLast('.')
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers) return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
} }
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> { private fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result) val document = Jsoup.parse(result)
val selector = if (isVolume) "div.unit" else "ul li" val selector = if (isVolume) "div.unit" else "ul li"
@ -146,7 +225,8 @@ open class MangaFire(
val type = if (isVolume) volumeType else chapterType val type = if (isVolume) volumeType else chapterType
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers) val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
val response = client.newCall(request).execute() val response = client.newCall(request).execute()
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html val res =
json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
val chapterInfoDocument = Jsoup.parse(res) val chapterInfoDocument = Jsoup.parse(res)
val chapters = chapterInfoDocument.select("ul li") val chapters = chapterInfoDocument.select("ul li")
for ((i, it) in elements.withIndex()) { for ((i, it) in elements.withIndex()) {
@ -162,7 +242,7 @@ open class MangaFire(
val title_format: String, val title_format: String,
) )
override fun updateChapterList(manga: SManga, chapters: List<SChapter>) { private fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
val request = chapterListRequest(manga.url, chapterType) val request = chapterListRequest(manga.url, chapterType)
val response = client.newCall(request).execute() val response = client.newCall(request).execute()
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
@ -187,6 +267,10 @@ open class MangaFire(
} }
} }
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val typeAndId = chapter.url.substringAfterLast('#') val typeAndId = chapter.url.substringAfterLast('#')
return GET("$baseUrl/ajax/read/$typeAndId", headers) return GET("$baseUrl/ajax/read/$typeAndId", headers)
@ -206,10 +290,12 @@ open class MangaFire(
@Serializable @Serializable
class PageListDto(private val images: List<List<JsonPrimitive>>) { class PageListDto(private val images: List<List<JsonPrimitive>>) {
val pages get() = images.map { val pages
Image(it[0].content, it[2].int) get() = images.map {
} Image(it[0].content, it[2].int)
}
} }
class Image(val url: String, val offset: Int) class Image(val url: String, val offset: Int)
@Serializable @Serializable
@ -218,15 +304,30 @@ open class MangaFire(
val status: Int, val status: Int,
) )
override fun getFilterList() = override fun getFilterList() = FilterList(
FilterList( Filter.Header("NOTE: Ignored if using text search!"),
Filter.Header("NOTE: Ignored if using text search!"), Filter.Separator(),
Filter.Separator(), TypeFilter(),
TypeFilter(), GenresFilter(),
GenresFilter(), StatusFilter(),
StatusFilter(), YearFilter(),
YearFilter(), ChapterCountFilter(),
ChapterCountFilter(), SortFilter(),
SortFilter(), )
)
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

@ -1,3 +1,7 @@
## 1.4.7
- Reworked the lib-multisrc theme
## 1.3.4 ## 1.3.4
- Refactor and make multisrc - Refactor and make multisrc

View File

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

View File

@ -1,163 +1,105 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto package eu.kanade.tachiyomi.extension.all.mangareaderto
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
import eu.kanade.tachiyomi.network.GET 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup 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.Request
import okhttp3.Response 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 import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
open class MangaReader( class MangaReader(
val language: Language, language: Language,
) : MangaReader() { ) : MangaReader(
"MangaReader",
override val lang = language.code "https://mangareader.to",
language.code,
override val name = "MangaReader" ),
ConfigurableSource {
override val baseUrl = "https://mangareader.to"
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.addInterceptor(ImageInterceptor) .addInterceptor(ImageInterceptor)
.build() .build()
override fun latestUpdatesRequest(page: Int) = private val preferences: SharedPreferences by lazy {
GET("$baseUrl/filter?sort=latest-updated&language=${language.infix}&page=$page", headers) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularMangaRequest(page: Int) = // =============================== Search ===============================
GET("$baseUrl/filter?sort=most-viewed&language=${language.infix}&page=$page", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaParse(response: Response): MangasPage {
val urlBuilder = baseUrl.toHttpUrl().newBuilder() var (entries, hasNextPage) = super.searchMangaParse(response)
if (query.isNotBlank()) { if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
urlBuilder.addPathSegment("search").apply { entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
addQueryParameter("keyword", query) val volume = SManga.create().apply {
addQueryParameter("page", page.toString()) url = manga.url + VOLUME_URL_SUFFIX
} title = VOLUME_TITLE_PREFIX + manga.title
} else { thumbnail_url = manga.thumbnail_url
urlBuilder.addPathSegment("filter").apply {
addQueryParameter("language", language.infix)
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 -> {}
}
} }
listOf(manga, volume)
} }
} }
return GET(urlBuilder.build(), headers) return MangasPage(entries, hasNextPage)
} }
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster" // ============================== Chapters ==============================
override fun searchMangaNextPageSelector() = ".page-link[title=Next]" private val volumeType = "vol"
private val chapterType = "chap"
override fun searchMangaFromElement(element: Element) = override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
SManga.create().apply { val path = manga.url
url = element.attr("href") val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
element.selectFirst(Evaluator.Tag("img"))!!.let { val type = if (isVolume) volumeType else chapterType
title = it.attr("alt")
thumbnail_url = it.attr("src")
}
}
private fun Element.parseAuthorsTo(manga: SManga) { val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
val authors = select(Evaluator.Tag("a")) val response = client.newCall(request).execute()
val text = authors.map { it.ownText().replace(",", "") }
val count = authors.size return Observable.just(chapterListParse(response, isVolume))
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 { private fun chapterListRequest(mangaUrl: String, type: String): Request {
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('-') val id = mangaUrl.substringAfterLast('-')
return GET("$baseUrl/ajax/manga/reading-list/$id?readingBy=$type", headers) return GET("$baseUrl/ajax/manga/reading-list/$id?readingBy=$type", headers)
} }
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> { private fun chapterListParse(response: Response, isVolume: Boolean): List<SChapter> {
val container = response.parseHtmlProperty().run { val container = response.parseHtmlProperty().run {
val type = if (isVolume) "volumes" else "chapters" val type = if (isVolume) "volumes" else "chapters"
selectFirst(Evaluator.Id("${language.chapterInfix}-$type")) ?: return emptyList() selectFirst("#$lang-$type") ?: return emptyList()
}
return container.children().map { element ->
chapterFromElement(element).apply {
val dataId = url.substringAfterLast('#', "")
if (dataId.isNotEmpty()) {
url = "${url.substringBeforeLast('#')}#${if (isVolume) volumeType else chapterType}/$dataId"
}
}
} }
return container.children()
} }
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable { // =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
val typeAndId = chapter.url.substringAfterLast('#', "").ifEmpty { val typeAndId = chapter.url.substringAfterLast('#', "").ifEmpty {
val document = client.newCall(pageListRequest(chapter)).execute().asJsoup() val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
val wrapper = document.selectFirst(Evaluator.Id("wrapper"))!! val wrapper = document.selectFirst("#wrapper")!!
wrapper.attr("data-reading-by") + '/' + wrapper.attr("data-reading-id") wrapper.attr("data-reading-by") + '/' + wrapper.attr("data-reading-id")
} }
val ajaxUrl = "$baseUrl/ajax/image/list/$typeAndId?quality=${preferences.quality}" val ajaxUrl = "$baseUrl/ajax/image/list/$typeAndId?quality=${preferences.quality}"
client.newCall(GET(ajaxUrl, headers)).execute().let(::pageListParse) return GET(ajaxUrl, headers)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
@ -170,26 +112,38 @@ open class MangaReader(
} }
} }
// ============================ Preferences =============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
getPreferences(screen.context).forEach(screen::addPreference) getPreferences(screen.context).forEach(screen::addPreference)
super.setupPreferenceScreen(screen) SwitchPreferenceCompat(screen.context).apply {
key = SHOW_VOLUME_PREF
title = "Show volume entries in search result"
setDefaultValue(false)
}.let(screen::addPreference)
} }
override fun getFilterList() = // ============================= Utilities ==============================
FilterList(
Note,
TypeFilter(),
StatusFilter(),
RatingFilter(),
ScoreFilter(),
StartDateFilter(),
EndDateFilter(),
SortFilter(),
GenresFilter(),
)
private fun Response.parseHtmlProperty(): Document { companion object {
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content private const val SHOW_VOLUME_PREF = "show_volume"
return Jsoup.parseBodyFragment(html)
private const val VOLUME_URL_FRAGMENT = "vol"
private const val VOLUME_URL_SUFFIX = "#$VOLUME_URL_FRAGMENT"
private const val VOLUME_TITLE_PREFIX = "[VOL] "
} }
// ============================== Filters ===============================
override fun getFilterList() = FilterList(
Note,
TypeFilter(),
StatusFilter(),
RatingFilter(),
ScoreFilter(),
StartDateFilter(),
EndDateFilter(),
getSortFilter(),
GenreFilter(),
)
} }

View File

@ -1,36 +1,16 @@
package eu.kanade.tachiyomi.extension.ja.rawotaku package eu.kanade.tachiyomi.extension.ja.rawotaku
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader 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.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page import okhttp3.HttpUrl
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
import java.net.URLEncoder
class RawOtaku : MangaReader() { class RawOtaku : MangaReader(
"Raw Otaku",
override val name = "Raw Otaku" "https://rawotaku.com",
"ja",
override val lang = "ja" ) {
override val baseUrl = "https://rawotaku.com"
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.rateLimit(2) .rateLimit(2)
@ -39,213 +19,44 @@ class RawOtaku : MangaReader() {
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/") .add("Referer", "$baseUrl/")
// ============================== Popular =============================== override fun addPage(page: Int, builder: HttpUrl.Builder) {
builder.addQueryParameter("p", page.toString())
override fun popularMangaRequest(page: Int) = }
GET("$baseUrl/filter/?type=all&status=all&language=all&sort=most-viewed&p=$page", headers)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter/?type=all&status=all&language=all&sort=latest-updated&p=$page", headers)
// =============================== Search =============================== // =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override val searchPathSegment = ""
val url = baseUrl.toHttpUrl().newBuilder().apply { override val searchKeyword = "q"
if (query.isNotBlank()) {
addQueryParameter("q", query)
} else {
addPathSegment("filter")
addPathSegment("")
filters.ifEmpty(::getFilterList).forEach { filter ->
when (filter) {
is TypeFilter -> {
addQueryParameter(filter.param, filter.selection)
}
is StatusFilter -> {
addQueryParameter(filter.param, filter.selection)
}
is LanguageFilter -> {
addQueryParameter(filter.param, filter.selection)
}
is SortFilter -> {
addQueryParameter(filter.param, filter.selection)
}
is GenresFilter -> {
filter.state.forEach {
if (it.state) {
addQueryParameter(filter.param, it.id)
}
}
}
else -> { }
}
}
}
addQueryParameter("p", page.toString())
}.build()
return GET(url, 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,
Filter.Separator(),
TypeFilter(),
StatusFilter(),
LanguageFilter(),
SortFilter(),
GenresFilter(),
)
// =========================== Manga 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 = buildString {
root.selectFirst(".description")?.ownText()?.let { append(it) }
append("\n\n")
root.selectFirst(".manga-name-or")?.ownText()?.let {
if (it.isNotEmpty() && it != mangaTitle) {
append("Alternative Title: ")
append(it)
}
}
}.trim()
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()) {
"著者:" -> item.parseAuthorsTo(this)
"地位:" -> 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 ============================== // ============================== Chapters ==============================
override fun chapterListRequest(mangaUrl: String, type: String): Request = override val chapterIdSelect = "ja-chaps"
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 = "#ja-chaps > .chapter-item"
private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
val id = element.attr("data-id")
element.selectFirst("a")!!.run {
setUrlWithoutDomain(attr("href") + "#$id")
name = selectFirst(".name")?.text() ?: text()
}
}
// =============================== Pages ================================ // =============================== Pages ================================
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable { override fun getAjaxUrl(id: String): String {
val id = chapter.url.substringAfterLast("#") return "$baseUrl/json/chapter?mode=vertical&id=$id"
val ajaxHeaders = super.headersBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", URLEncoder.encode(baseUrl + chapter.url.substringBeforeLast("#"), "utf-8"))
add("X-Requested-With", "XMLHttpRequest")
}.build()
val ajaxUrl = "$baseUrl/json/chapter?mode=vertical&id=$id"
client.newCall(GET(ajaxUrl, ajaxHeaders)).execute().let(::pageListParse)
} }
override fun pageListParse(response: Response): List<Page> { // =============================== Filters ==============================
val document = response.use { it.parseHtmlProperty() }
val pageList = document.select(".container-reader-chapter > div > img").map { override fun getFilterList() = FilterList(
val index = it.attr("alt").toInt() Note,
val imgUrl = it.imgAttr() Filter.Separator(),
TypeFilter(),
StatusFilter(),
LanguageFilter(),
getSortFilter(),
GenreFilter(),
)
Page(index, imageUrl = imgUrl) override fun sortFilterValues(): Array<Pair<String, String>> {
} return arrayOf(
Pair("デフォルト", "default"),
return pageList Pair("最新の更新", "latest-updated"),
} Pair("最も見られました", "most-viewed"),
Pair("Title [A-Z]", "title-az"),
// ============================= Utilities ============================== Pair("Title [Z-A]", "title-za"),
)
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

@ -1,110 +1,61 @@
package eu.kanade.tachiyomi.extension.ja.rawotaku package eu.kanade.tachiyomi.extension.ja.rawotaku
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriMultiSelectFilter
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriPartFilter
object Note : Filter.Header("NOTE: Ignored if using text search!") class TypeFilter : UriPartFilter(
"タイプ",
"type",
arrayOf(
Pair("全て", "all"),
Pair("Raw Manga", "Raw Manga"),
Pair("BLコミック", "BLコミック"),
Pair("TLコミック", "TLコミック"),
Pair("オトナコミック", "オトナコミック"),
Pair("女性マンガ", "女性マンガ"),
Pair("少女マンガ", "少女マンガ"),
Pair("少年マンガ", "少年マンガ"),
Pair("青年マンガ", "青年マンガ"),
),
)
sealed class Select( class StatusFilter : UriPartFilter(
name: String, "地位",
val param: String, "status",
values: Array<String>, arrayOf(
) : Filter.Select<String>(name, values) { Pair("全て", "all"),
open val selection: String Pair("Publishing", "Publishing"),
get() = if (state == 0) "" else state.toString() Pair("Finished", "Finished"),
} ),
)
class TypeFilter( class LanguageFilter : UriPartFilter(
values: Array<String> = types.keys.toTypedArray(), "言語",
) : Select("タイプ", "type", values) { "language",
override val selection: String arrayOf(
get() = types[values[state]]!! Pair("全て", "all"),
Pair("Japanese", "ja"),
Pair("English", "en"),
),
)
companion object { class GenreFilter : UriMultiSelectFilter(
private val types = mapOf( "ジャンル",
"全て" to "all", "genre[]",
"Raw Manga" to "Raw Manga", arrayOf(
"BLコミック" to "BLコミック", Pair("アクション", "55"),
"TLコミック" to "TLコミック", Pair("エッチ", "15706"),
"オトナコミック" to "オトナコミック", Pair("コメディ", "91"),
"女性マンガ" to "女性マンガ", Pair("ドラマ", "56"),
"少女マンガ" to "少女マンガ", Pair("ハーレム", "20"),
"少年マンガ" to "少年マンガ", Pair("ファンタジー", "1"),
"青年マンガ" to "青年マンガ", Pair("冒険", "54"),
) Pair("悪魔", "6820"),
} Pair("武道", "1064"),
} Pair("歴史的", "9600"),
Pair("警察・特殊部隊", "6089"),
class StatusFilter( Pair("車・バイク", "4329"),
values: Array<String> = statuses.keys.toTypedArray(), Pair("音楽", "473"),
) : Select("地位", "status", values) { Pair("魔法", "1416"),
override val selection: String ),
get() = statuses[values[state]]!! )
companion object {
private val statuses = mapOf(
"全て" to "all",
"Publishing" to "Publishing",
"Finished" to "Finished",
)
}
}
class LanguageFilter(
values: Array<String> = languages.keys.toTypedArray(),
) : Select("言語", "language", values) {
override val selection: String
get() = languages[values[state]]!!
companion object {
private val languages = mapOf(
"全て" to "all",
"Japanese" to "ja",
"English" to "en",
)
}
}
class SortFilter(
values: Array<String> = sort.keys.toTypedArray(),
) : Select("選別", "sort", values) {
override val selection: String
get() = sort[values[state]]!!
companion object {
private val sort = mapOf(
"デフォルト" to "default",
"最新の更新" to "latest-updated",
"最も見られました" to "most-viewed",
"Title [A-Z]" to "title-az",
"Title [Z-A]" to "title-za",
)
}
}
class Genre(name: String, val id: String) : Filter.CheckBox(name)
class GenresFilter(
values: List<Genre> = genres,
) : Filter.Group<Genre>("ジャンル", values) {
val param = "genre[]"
companion object {
private val genres: List<Genre>
get() = listOf(
Genre("アクション", "55"),
Genre("エッチ", "15706"),
Genre("コメディ", "91"),
Genre("ドラマ", "56"),
Genre("ハーレム", "20"),
Genre("ファンタジー", "1"),
Genre("冒険", "54"),
Genre("悪魔", "6820"),
Genre("武道", "1064"),
Genre("歴史的", "9600"),
Genre("警察・特殊部隊", "6089"),
Genre("車・バイク", "4329"),
Genre("音楽", "473"),
Genre("魔法", "1416"),
)
}
}