From 56d872d0232fb0c5071f25c383daef25603002b2 Mon Sep 17 00:00:00 2001 From: Secozzi <49240133+Secozzi@users.noreply.github.com> Date: Tue, 11 Feb 2025 08:42:47 +0100 Subject: [PATCH] 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 --- lib-multisrc/mangareader/build.gradle.kts | 2 +- .../multisrc/mangareader/MangaReader.kt | 378 ++++++++++++++---- src/all/mangafire/build.gradle | 4 +- .../extension/all/mangafire/MangaFire.kt | 183 +++++++-- src/all/mangareaderto/CHANGELOG.md | 4 + .../extension/all/mangareaderto/Filters.kt | 373 ++++++++--------- .../all/mangareaderto/MangaReader.kt | 218 ++++------ .../extension/ja/rawotaku/RawOtaku.kt | 253 ++---------- .../extension/ja/rawotaku/RawOtakuFilters.kt | 159 +++----- 9 files changed, 790 insertions(+), 784 deletions(-) diff --git a/lib-multisrc/mangareader/build.gradle.kts b/lib-multisrc/mangareader/build.gradle.kts index 9dce2478c..e2f11e9c1 100644 --- a/lib-multisrc/mangareader/build.gradle.kts +++ b/lib-multisrc/mangareader/build.gradle.kts @@ -2,4 +2,4 @@ plugins { id("lib-multisrc") } -baseVersionCode = 2 +baseVersionCode = 3 diff --git a/lib-multisrc/mangareader/src/eu/kanade/tachiyomi/multisrc/mangareader/MangaReader.kt b/lib-multisrc/mangareader/src/eu/kanade/tachiyomi/multisrc/mangareader/MangaReader.kt index 3f1bbc655..eb9e4062e 100644 --- a/lib-multisrc/mangareader/src/eu/kanade/tachiyomi/multisrc/mangareader/MangaReader.kt +++ b/lib-multisrc/mangareader/src/eu/kanade/tachiyomi/multisrc/mangareader/MangaReader.kt @@ -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().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(count) + val artistList = ArrayList(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 + // ============================== 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) = Unit + open val chapterIdSelect = "en-chapters" - override fun fetchChapterList(manga: SManga): Observable> = 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 { + 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().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 { + 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>, + defaultValue: String? = null, + ) : Filter.Select( + 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>, + private val join: String? = null, + ) : Filter.Group(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>, + 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> { + 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(), + ) } diff --git a/src/all/mangafire/build.gradle b/src/all/mangafire/build.gradle index 68375cac3..e5ccaa62d 100644 --- a/src/all/mangafire/build.gradle +++ b/src/all/mangafire/build.gradle @@ -1,9 +1,7 @@ ext { extName = 'MangaFire' extClass = '.MangaFireFactory' - themePkg = 'mangareader' - baseUrl = 'https://mangafire.to' - overrideVersionCode = 5 + extVersionCode = 8 isNsfw = true } diff --git a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt index e648a2023..7b429301f 100644 --- a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt +++ b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt @@ -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().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 { + throw UnsupportedOperationException() + } + + override fun fetchChapterList(manga: SManga): Observable> = + 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 { + private fun parseChapterElements(response: Response, isVolume: Boolean): List { val result = json.decodeFromString>(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>(response.body.string()).result.html + val res = + json.decodeFromString>(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) { + private fun updateChapterList(manga: SManga, chapters: List) { val request = chapterListRequest(manga.url, chapterType) val response = client.newCall(request).execute() val result = json.decodeFromString>(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>) { - 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] " + } } diff --git a/src/all/mangareaderto/CHANGELOG.md b/src/all/mangareaderto/CHANGELOG.md index f4fce847c..bed643ea9 100644 --- a/src/all/mangareaderto/CHANGELOG.md +++ b/src/all/mangareaderto/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.4.7 + +- Reworked the lib-multisrc theme + ## 1.3.4 - Refactor and make multisrc diff --git a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/Filters.kt b/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/Filters.kt index 002e93db0..9309372d6 100644 --- a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/Filters.kt +++ b/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/Filters.kt @@ -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, -) : Filter.Select(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 = types, -) : Select("Type", "type", values) { - companion object { - private val types: Array - 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 = statuses, -) : Select("Status", "status", values) { - companion object { - private val statuses: Array - 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 = ratings, -) : Select("Rating Type", "rating_type", values) { - companion object { - private val ratings: Array - 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 = scores, -) : Select("Score", "score", values) { - companion object { - private val scores: Array - 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, -) : Select(name, param, values) { - override val selection: String - get() = if (state == 0) "" else values[state] -} - -class YearFilter( - param: String, - values: Array = 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 - 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 = months, -) : DateSelect("Month", param, values) { +class MonthFilter(name: String, param: String) : UriPartFilter( + name, + param, + months, +) { companion object { - private val months: Array - 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 = days, -) : DateSelect("Day", param, values) { +class DayFilter(name: String, param: String) : UriPartFilter( + name, + param, + days, +) { companion object { - private val days: Array - 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, -) : Filter.Group("$type Date", values) + private val values: List, +) : Filter.Group("$type Date", values), UriFilter { + override fun addToUri(builder: HttpUrl.Builder) { + values.forEach { + it.addToUri(builder) + } + } +} class StartDateFilter( - values: List = parts, + values: List = parts, ) : DateFilter("Start", values) { companion object { - private val parts: List - get() = listOf( - YearFilter("sy"), - MonthFilter("sm"), - DayFilter("sd"), - ) - } -} - -class EndDateFilter( - values: List = parts, -) : DateFilter("End", values) { - companion object { - private val parts: List - get() = listOf( - YearFilter("ey"), - MonthFilter("em"), - DayFilter("ed"), - ) - } -} - -class SortFilter( - values: Array = 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 = genres, -) : Filter.Group("Genres", values) { - val param = "genres" - - val selection: String - get() = state.filter { it.state }.joinToString(",") { it.id } - +class EndDateFilter( + values: List = parts, +) : DateFilter("End", values) { companion object { - private val genres: List - 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"), + ), + ",", +) diff --git a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReader.kt b/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReader.kt index 5f5929236..b6e103213 100644 --- a/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReader.kt +++ b/src/all/mangareaderto/src/eu/kanade/tachiyomi/extension/all/mangareaderto/MangaReader.kt @@ -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().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> { + 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(count) - val artistList = ArrayList(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 { + private fun chapterListParse(response: Response, isVolume: Boolean): List { 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> = 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 { @@ -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(), + ) } diff --git a/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtaku.kt b/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtaku.kt index b76ba7df2..961335743 100644 --- a/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtaku.kt +++ b/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtaku.kt @@ -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(count) - val artistList = ArrayList(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 { - TODO("Not yet implemented") - } - - override val chapterType = "" - override val volumeType = "" - - override fun fetchChapterList(manga: SManga): Observable> { - return client.newCall(chapterListRequest(manga)) - .asObservableSuccess() - .map(::parseChapterList) - } - - private fun parseChapterList(response: Response): List { - 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> = 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 { - 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> { + return arrayOf( + Pair("デフォルト", "default"), + Pair("最新の更新", "latest-updated"), + Pair("最も見られました", "most-viewed"), + Pair("Title [A-Z]", "title-az"), + Pair("Title [Z-A]", "title-za"), + ) } } diff --git a/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtakuFilters.kt b/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtakuFilters.kt index cc443b5d5..ce25e0a29 100644 --- a/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtakuFilters.kt +++ b/src/ja/rawotaku/src/eu/kanade/tachiyomi/extension/ja/rawotaku/RawOtakuFilters.kt @@ -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, -) : Filter.Select(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 = 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 = 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 = 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 = 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 = genres, -) : Filter.Group("ジャンル", values) { - val param = "genre[]" - - companion object { - private val genres: List - 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"), + ), +)