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

* chore: move mangafire away from mangareader multisrc

* chore: rework mangareader multisrc theme

* lint(lint): lint

* fix: apply recommended fixes

* lint(lint): lint

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

View File

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

View File

@ -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(),
)
}

View File

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

View File

@ -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] "
}
}

View File

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

View File

@ -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"),
),
",",
)

View File

@ -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(),
)
}

View File

@ -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"),
)
}
}

View File

@ -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"),
),
)