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:
parent
2a44209a2f
commit
56d872d023
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -1,129 +1,355 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mangareader
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
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.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import org.jsoup.nodes.TextNode
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
|
||||
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 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 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
|
||||
// =============================== Latest ===============================
|
||||
|
||||
protected open val sortLatestValue = "latest-updated"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return searchMangaRequest(
|
||||
page,
|
||||
"",
|
||||
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
|
||||
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
|
||||
|
||||
final override fun mangaDetailsParse(response: Response): SManga {
|
||||
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 SManga.create().apply {
|
||||
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
|
||||
}
|
||||
|
||||
abstract val chapterType: String
|
||||
abstract val volumeType: String
|
||||
private fun Element.parseStatus(manga: SManga): SManga {
|
||||
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 {
|
||||
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()
|
||||
open fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst("a")!!.run {
|
||||
setUrlWithoutDomain(attr("href") + "#${element.attr("data-id")}")
|
||||
name = selectFirst(".name")?.text() ?: text()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select("#$chapterIdSelect > li.chapter-item").map(::chapterFromElement)
|
||||
}
|
||||
|
||||
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"))
|
||||
// =============================== Pages ================================
|
||||
|
||||
open fun getChapterId(chapter: SChapter): String {
|
||||
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
|
||||
return document.selectFirst("div[data-reading-id]")
|
||||
?.attr("data-reading-id")
|
||||
.orEmpty()
|
||||
.ifEmpty {
|
||||
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('#')
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||
open fun getAjaxUrl(id: String): String {
|
||||
return "$baseUrl//ajax/image/list/$id?mode=vertical"
|
||||
}
|
||||
|
||||
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)
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterId = chapter.url.substringAfterLast('#').ifEmpty {
|
||||
getChapterId(chapter)
|
||||
}
|
||||
|
||||
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 {
|
||||
private const val SHOW_VOLUME_PREF = "show_volume"
|
||||
open fun pageListParseSelector(): String = ".container-reader-chapter > div > img"
|
||||
|
||||
private const val VOLUME_URL_FRAGMENT = "vol"
|
||||
private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
|
||||
private const val VOLUME_TITLE_PREFIX = "[VOL] "
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.parseHtmlProperty()
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
ext {
|
||||
extName = 'MangaFire'
|
||||
extClass = '.MangaFireFactory'
|
||||
themePkg = 'mangareader'
|
||||
baseUrl = 'https://mangafire.to'
|
||||
overrideVersionCode = 5
|
||||
extVersionCode = 8
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
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.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -19,29 +25,40 @@ import org.jsoup.Jsoup
|
|||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class MangaFire(
|
||||
class MangaFire(
|
||||
override val lang: String,
|
||||
private val langCode: String = lang,
|
||||
) : MangaReader() {
|
||||
) : ConfigurableSource, HttpSource() {
|
||||
override val name = "MangaFire"
|
||||
|
||||
override val baseUrl = "https://mangafire.to"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||
}
|
||||
|
||||
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) =
|
||||
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 latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
|
@ -63,9 +80,11 @@ open class MangaFire(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Select -> {
|
||||
addQueryParameter(filter.param, filter.selection)
|
||||
}
|
||||
|
||||
is GenresFilter -> {
|
||||
filter.state.forEach {
|
||||
if (it.state != 0) {
|
||||
|
@ -76,6 +95,7 @@ open class MangaFire(
|
|||
addQueryParameter("genre_mode", "and")
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -84,22 +104,49 @@ open class MangaFire(
|
|||
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) =
|
||||
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")
|
||||
private 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 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 mangaTitle = root.child(1).ownText()
|
||||
title = mangaTitle
|
||||
|
@ -110,8 +157,7 @@ open class MangaFire(
|
|||
else -> "$description\n\nAlternative Title: $altTitle"
|
||||
}
|
||||
}
|
||||
thumbnail_url = document.selectFirst(".poster")!!
|
||||
.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = document.selectFirst(".poster")!!.selectFirst("img")!!.attr("src")
|
||||
status = when (root.child(0).ownText()) {
|
||||
"Completed" -> SManga.COMPLETED
|
||||
"Releasing" -> SManga.ONGOING
|
||||
|
@ -127,15 +173,48 @@ open class MangaFire(
|
|||
}
|
||||
}
|
||||
|
||||
override val chapterType get() = "chapter"
|
||||
override val volumeType get() = "volume"
|
||||
private val chapterType get() = "chapter"
|
||||
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('.')
|
||||
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 document = Jsoup.parse(result)
|
||||
val selector = if (isVolume) "div.unit" else "ul li"
|
||||
|
@ -146,7 +225,8 @@ open class MangaFire(
|
|||
val type = if (isVolume) volumeType else chapterType
|
||||
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
|
||||
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 chapters = chapterInfoDocument.select("ul li")
|
||||
for ((i, it) in elements.withIndex()) {
|
||||
|
@ -162,7 +242,7 @@ open class MangaFire(
|
|||
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 response = client.newCall(request).execute()
|
||||
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 {
|
||||
val typeAndId = chapter.url.substringAfterLast('#')
|
||||
return GET("$baseUrl/ajax/read/$typeAndId", headers)
|
||||
|
@ -206,10 +290,12 @@ open class MangaFire(
|
|||
|
||||
@Serializable
|
||||
class PageListDto(private val images: List<List<JsonPrimitive>>) {
|
||||
val pages get() = images.map {
|
||||
Image(it[0].content, it[2].int)
|
||||
}
|
||||
val pages
|
||||
get() = images.map {
|
||||
Image(it[0].content, it[2].int)
|
||||
}
|
||||
}
|
||||
|
||||
class Image(val url: String, val offset: Int)
|
||||
|
||||
@Serializable
|
||||
|
@ -218,15 +304,30 @@ open class MangaFire(
|
|||
val status: Int,
|
||||
)
|
||||
|
||||
override fun getFilterList() =
|
||||
FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
TypeFilter(),
|
||||
GenresFilter(),
|
||||
StatusFilter(),
|
||||
YearFilter(),
|
||||
ChapterCountFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
TypeFilter(),
|
||||
GenresFilter(),
|
||||
StatusFilter(),
|
||||
YearFilter(),
|
||||
ChapterCountFilter(),
|
||||
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] "
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
## 1.4.7
|
||||
|
||||
- Reworked the lib-multisrc theme
|
||||
|
||||
## 1.3.4
|
||||
|
||||
- Refactor and make multisrc
|
||||
|
|
|
@ -1,247 +1,208 @@
|
|||
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 okhttp3.HttpUrl
|
||||
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(
|
||||
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 : UriPartFilter(
|
||||
"Status",
|
||||
"status",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Finished", "1"),
|
||||
Pair("Publishing", "2"),
|
||||
Pair("On Hiatus", "3"),
|
||||
Pair("Discontinued", "4"),
|
||||
Pair("Not yet published", "5"),
|
||||
),
|
||||
)
|
||||
|
||||
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 RatingFilter : UriPartFilter(
|
||||
"Rating Type",
|
||||
"rating_type",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("G - All Ages", "1"),
|
||||
Pair("PG - Children", "2"),
|
||||
Pair("PG-13 - Teens 13 or older", "3"),
|
||||
Pair("R - 17+ (violence & profanity)", "4"),
|
||||
Pair("R+ - Mild Nudity", "5"),
|
||||
Pair("Rx - Hentai", "6"),
|
||||
),
|
||||
)
|
||||
|
||||
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 ScoreFilter : UriPartFilter(
|
||||
"Score",
|
||||
"score",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("(1) Appalling", "1"),
|
||||
Pair("(2) Horrible", "2"),
|
||||
Pair("(3) Very Bad", "3"),
|
||||
Pair("(4) Bad", "4"),
|
||||
Pair("(5) Average", "5"),
|
||||
Pair("(6) Fine", "6"),
|
||||
Pair("(7) Good", "7"),
|
||||
Pair("(8) Very Good", "8"),
|
||||
Pair("(9) Great", "9"),
|
||||
Pair("(10) Masterpiece", "10"),
|
||||
),
|
||||
)
|
||||
|
||||
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) {
|
||||
class YearFilter(name: String, param: String) : UriPartFilter(
|
||||
name,
|
||||
param,
|
||||
years,
|
||||
) {
|
||||
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()
|
||||
private val years = Array(nextYear - 1916) { year ->
|
||||
if (year == 0) {
|
||||
Pair("Any", "")
|
||||
} else {
|
||||
(nextYear - year).toString().let { Pair(it, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MonthFilter(
|
||||
param: String,
|
||||
values: Array<String> = months,
|
||||
) : DateSelect("Month", param, values) {
|
||||
class MonthFilter(name: String, param: String) : UriPartFilter(
|
||||
name,
|
||||
param,
|
||||
months,
|
||||
) {
|
||||
companion object {
|
||||
private val months: Array<String>
|
||||
get() = Array(13) {
|
||||
if (it == 0) "Any" else "%02d".format(it)
|
||||
private val months = Array(13) { months ->
|
||||
if (months == 0) {
|
||||
Pair("Any", "")
|
||||
} else {
|
||||
Pair("%02d".format(months), months.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DayFilter(
|
||||
param: String,
|
||||
values: Array<String> = days,
|
||||
) : DateSelect("Day", param, values) {
|
||||
class DayFilter(name: String, param: String) : UriPartFilter(
|
||||
name,
|
||||
param,
|
||||
days,
|
||||
) {
|
||||
companion object {
|
||||
private val days: Array<String>
|
||||
get() = Array(32) {
|
||||
if (it == 0) "Any" else "%02d".format(it)
|
||||
private val days = Array(32) { day ->
|
||||
if (day == 0) {
|
||||
Pair("Any", "")
|
||||
} else {
|
||||
Pair("%02d".format(day), day.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DateFilter(
|
||||
type: String,
|
||||
values: List<DateSelect>,
|
||||
) : Filter.Group<DateSelect>("$type Date", values)
|
||||
private val values: List<UriPartFilter>,
|
||||
) : Filter.Group<UriPartFilter>("$type Date", values), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
values.forEach {
|
||||
it.addToUri(builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StartDateFilter(
|
||||
values: List<DateSelect> = parts,
|
||||
values: List<UriPartFilter> = 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",
|
||||
private val parts = listOf(
|
||||
YearFilter("Year", "sy"),
|
||||
MonthFilter("Month", "sm"),
|
||||
DayFilter("Day", "sd"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
class EndDateFilter(
|
||||
values: List<UriPartFilter> = parts,
|
||||
) : DateFilter("End", values) {
|
||||
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"),
|
||||
)
|
||||
private val parts = listOf(
|
||||
YearFilter("Year", "ey"),
|
||||
MonthFilter("Month", "em"),
|
||||
DayFilter("Day", "ed"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
),
|
||||
",",
|
||||
)
|
||||
|
|
|
@ -1,163 +1,105 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.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 uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
open class MangaReader(
|
||||
val language: Language,
|
||||
) : MangaReader() {
|
||||
|
||||
override val lang = language.code
|
||||
|
||||
override val name = "MangaReader"
|
||||
|
||||
override val baseUrl = "https://mangareader.to"
|
||||
class MangaReader(
|
||||
language: Language,
|
||||
) : MangaReader(
|
||||
"MangaReader",
|
||||
"https://mangareader.to",
|
||||
language.code,
|
||||
),
|
||||
ConfigurableSource {
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=latest-updated&language=${language.infix}&page=$page", headers)
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=most-viewed&language=${language.infix}&page=$page", 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)
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
} else {
|
||||
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 -> {}
|
||||
}
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
var (entries, hasNextPage) = super.searchMangaParse(response)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) =
|
||||
SManga.create().apply {
|
||||
url = element.attr("href")
|
||||
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
||||
title = it.attr("alt")
|
||||
thumbnail_url = it.attr("src")
|
||||
}
|
||||
}
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val path = manga.url
|
||||
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
||||
val type = if (isVolume) volumeType else chapterType
|
||||
|
||||
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()
|
||||
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
return Observable.just(chapterListParse(response, isVolume))
|
||||
}
|
||||
|
||||
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 {
|
||||
private 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> {
|
||||
private fun chapterListParse(response: Response, isVolume: Boolean): List<SChapter> {
|
||||
val container = response.parseHtmlProperty().run {
|
||||
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 document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
|
||||
val wrapper = document.selectFirst(Evaluator.Id("wrapper"))!!
|
||||
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
|
||||
val wrapper = document.selectFirst("#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)
|
||||
return GET(ajaxUrl, headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
|
@ -170,26 +112,38 @@ open class MangaReader(
|
|||
}
|
||||
}
|
||||
|
||||
// ============================ Preferences =============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
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() =
|
||||
FilterList(
|
||||
Note,
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
RatingFilter(),
|
||||
ScoreFilter(),
|
||||
StartDateFilter(),
|
||||
EndDateFilter(),
|
||||
SortFilter(),
|
||||
GenresFilter(),
|
||||
)
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun Response.parseHtmlProperty(): Document {
|
||||
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
|
||||
return Jsoup.parseBodyFragment(html)
|
||||
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] "
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Note,
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
RatingFilter(),
|
||||
ScoreFilter(),
|
||||
StartDateFilter(),
|
||||
EndDateFilter(),
|
||||
getSortFilter(),
|
||||
GenreFilter(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,36 +1,16 @@
|
|||
package eu.kanade.tachiyomi.extension.ja.rawotaku
|
||||
|
||||
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.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 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
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class RawOtaku : MangaReader() {
|
||||
|
||||
override val name = "Raw Otaku"
|
||||
|
||||
override val lang = "ja"
|
||||
|
||||
override val baseUrl = "https://rawotaku.com"
|
||||
class RawOtaku : MangaReader(
|
||||
"Raw Otaku",
|
||||
"https://rawotaku.com",
|
||||
"ja",
|
||||
) {
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
|
@ -39,213 +19,44 @@ class RawOtaku : MangaReader() {
|
|||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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)
|
||||
override fun addPage(page: Int, builder: HttpUrl.Builder) {
|
||||
builder.addQueryParameter("p", page.toString())
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
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()
|
||||
}
|
||||
override val searchPathSegment = ""
|
||||
override val searchKeyword = "q"
|
||||
|
||||
// ============================== 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 = "#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()
|
||||
}
|
||||
}
|
||||
override val chapterIdSelect = "ja-chaps"
|
||||
|
||||
// =============================== Pages ================================
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable {
|
||||
val id = chapter.url.substringAfterLast("#")
|
||||
|
||||
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 getAjaxUrl(id: String): String {
|
||||
return "$baseUrl/json/chapter?mode=vertical&id=$id"
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.use { it.parseHtmlProperty() }
|
||||
// =============================== Filters ==============================
|
||||
|
||||
val pageList = document.select(".container-reader-chapter > div > img").map {
|
||||
val index = it.attr("alt").toInt()
|
||||
val imgUrl = it.imgAttr()
|
||||
override fun getFilterList() = FilterList(
|
||||
Note,
|
||||
Filter.Separator(),
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
LanguageFilter(),
|
||||
getSortFilter(),
|
||||
GenreFilter(),
|
||||
)
|
||||
|
||||
Page(index, imageUrl = imgUrl)
|
||||
}
|
||||
|
||||
return pageList
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
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)
|
||||
override fun sortFilterValues(): Array<Pair<String, String>> {
|
||||
return arrayOf(
|
||||
Pair("デフォルト", "default"),
|
||||
Pair("最新の更新", "latest-updated"),
|
||||
Pair("最も見られました", "most-viewed"),
|
||||
Pair("Title [A-Z]", "title-az"),
|
||||
Pair("Title [Z-A]", "title-za"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,110 +1,61 @@
|
|||
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(
|
||||
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 : UriPartFilter(
|
||||
"地位",
|
||||
"status",
|
||||
arrayOf(
|
||||
Pair("全て", "all"),
|
||||
Pair("Publishing", "Publishing"),
|
||||
Pair("Finished", "Finished"),
|
||||
),
|
||||
)
|
||||
|
||||
class TypeFilter(
|
||||
values: Array<String> = types.keys.toTypedArray(),
|
||||
) : Select("タイプ", "type", values) {
|
||||
override val selection: String
|
||||
get() = types[values[state]]!!
|
||||
class LanguageFilter : UriPartFilter(
|
||||
"言語",
|
||||
"language",
|
||||
arrayOf(
|
||||
Pair("全て", "all"),
|
||||
Pair("Japanese", "ja"),
|
||||
Pair("English", "en"),
|
||||
),
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val types = mapOf(
|
||||
"全て" to "all",
|
||||
"Raw Manga" to "Raw Manga",
|
||||
"BLコミック" to "BLコミック",
|
||||
"TLコミック" to "TLコミック",
|
||||
"オトナコミック" to "オトナコミック",
|
||||
"女性マンガ" to "女性マンガ",
|
||||
"少女マンガ" to "少女マンガ",
|
||||
"少年マンガ" to "少年マンガ",
|
||||
"青年マンガ" to "青年マンガ",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StatusFilter(
|
||||
values: Array<String> = statuses.keys.toTypedArray(),
|
||||
) : Select("地位", "status", values) {
|
||||
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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
class GenreFilter : UriMultiSelectFilter(
|
||||
"ジャンル",
|
||||
"genre[]",
|
||||
arrayOf(
|
||||
Pair("アクション", "55"),
|
||||
Pair("エッチ", "15706"),
|
||||
Pair("コメディ", "91"),
|
||||
Pair("ドラマ", "56"),
|
||||
Pair("ハーレム", "20"),
|
||||
Pair("ファンタジー", "1"),
|
||||
Pair("冒険", "54"),
|
||||
Pair("悪魔", "6820"),
|
||||
Pair("武道", "1064"),
|
||||
Pair("歴史的", "9600"),
|
||||
Pair("警察・特殊部隊", "6089"),
|
||||
Pair("車・バイク", "4329"),
|
||||
Pair("音楽", "473"),
|
||||
Pair("魔法", "1416"),
|
||||
),
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue