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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 3
|
||||||
|
|
|
@ -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(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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] "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"),
|
||||||
|
),
|
||||||
|
",",
|
||||||
|
)
|
||||||
|
|
|
@ -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(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue