From fb91d67c366ff40a3493e68f95eb00569e241038 Mon Sep 17 00:00:00 2001 From: Arraiment <76941874+Arraiment@users.noreply.github.com> Date: Wed, 25 Aug 2021 18:19:17 +0800 Subject: [PATCH] Added filters (#8828) --- src/en/mangakatana/build.gradle | 2 +- .../extension/en/mangakatana/MangaKatana.kt | 209 ++++++++++++++++-- 2 files changed, 195 insertions(+), 16 deletions(-) diff --git a/src/en/mangakatana/build.gradle b/src/en/mangakatana/build.gradle index 923bfaccb..bde685bc1 100644 --- a/src/en/mangakatana/build.gradle +++ b/src/en/mangakatana/build.gradle @@ -5,7 +5,7 @@ ext { extName = 'MangaKatana' pkgNameSuffix = 'en.mangakatana' extClass = '.MangaKatana' - extVersionCode = 5 + extVersionCode = 6 libVersion = '1.2' } diff --git a/src/en/mangakatana/src/eu/kanade/tachiyomi/extension/en/mangakatana/MangaKatana.kt b/src/en/mangakatana/src/eu/kanade/tachiyomi/extension/en/mangakatana/MangaKatana.kt index 9dddfad1f..7713a83d1 100644 --- a/src/en/mangakatana/src/eu/kanade/tachiyomi/extension/en/mangakatana/MangaKatana.kt +++ b/src/en/mangakatana/src/eu/kanade/tachiyomi/extension/en/mangakatana/MangaKatana.kt @@ -6,6 +6,7 @@ import androidx.preference.ListPreference import androidx.preference.PreferenceScreen 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 @@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request @@ -53,36 +55,89 @@ class MangaKatana : ConfigurableSource, ParsedHttpSource() { } }.build() - override fun latestUpdatesSelector() = "div#book_list > div.item" + // Latest override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page", headers) + override fun latestUpdatesSelector() = "div#book_list > div.item" + override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { - setUrlWithoutDomain(element.select("div.text > h3 > a").attr("href")) - title = element.select("div.text > h3 > a").text() - thumbnail_url = element.select("img").attr("abs:src") + setUrlWithoutDomain(element.selectFirst("div.text > h3 > a").attr("href")) + title = element.selectFirst("div.text > h3 > a").ownText() + thumbnail_url = element.selectFirst("img").attr("abs:src") } - override fun latestUpdatesNextPageSelector() = ".next.page-numbers" + override fun latestUpdatesNextPageSelector() = "a.next.page-numbers" - override fun popularMangaSelector() = latestUpdatesSelector() + // Popular (is actually alphabetical) override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/page/$page", headers) + override fun popularMangaSelector() = latestUpdatesSelector() + override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element) override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector() - override fun searchMangaSelector() = latestUpdatesSelector() + // Search - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = GET("$baseUrl/page/$page?search=$query&search_by=book_name", headers) + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val filterList = if (filters.isEmpty()) getFilterList() else filters + + if (query.isNotEmpty()) { + val type = filterList.find { it is TypeFilter } as TypeFilter + val url = "$baseUrl/page/$page".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("search", query) + .addQueryParameter("search_by", type.toUriPart()) + return GET(url.toString(), headers) + } else { + val url = "$baseUrl/manga/page/$page".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("filter", "1") + for (filter in filterList) { + when (filter) { + is GenreList -> { + val includedGenres = mutableListOf() + val excludedGenres = mutableListOf() + filter.state.forEach { + if (it.isIncluded()) { + includedGenres.add(it.id) + } else if (it.isExcluded()) { + excludedGenres.add(it.id) + } + } + if (includedGenres.isNotEmpty()) url.addQueryParameter("include", includedGenres.joinToString("_")) + if (excludedGenres.isNotEmpty()) url.addQueryParameter("exclude", excludedGenres.joinToString("_")) + } + is GenreInclusionMode -> url.addQueryParameter("include_mode", filter.toUriPart()) + is SortFilter -> url.addQueryParameter("order", filter.toUriPart()) + is StatusFilter -> { + if (filter.toUriPart().isNotEmpty()) { + url.addQueryParameter("status", filter.toUriPart()) + } + } + is ChaptersFilter -> { + when (filter.state.trim()) { + "-1" -> url.addQueryParameter("chapters", "e1") + "" -> url.addQueryParameter("chapters", "1") + else -> url.addQueryParameter("chapters", filter.state.trim()) + } + } + } + } + return GET(url.toString(), headers) + } + } + + override fun searchMangaSelector() = latestUpdatesSelector() override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element) override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() override fun searchMangaParse(response: Response): MangasPage { - return if (response.request.url.toString().contains("/manga/")) { + // If search request redirects to a single manga page, use alternative parsing + val pathSegments = response.request.url.pathSegments + return if (pathSegments[0] == "manga" && pathSegments[1] != "page") { val document = response.asJsoup() val manga = SManga.create().apply { thumbnail_url = parseThumbnail(document) @@ -95,6 +150,8 @@ class MangaKatana : ConfigurableSource, ParsedHttpSource() { } } + // Details + override fun mangaDetailsParse(document: Document) = SManga.create().apply { author = document.select(".author").eachText().joinToString() description = document.select(".summary > p").text() + @@ -112,6 +169,8 @@ class MangaKatana : ConfigurableSource, ParsedHttpSource() { else -> SManga.UNKNOWN } + // Chapters + override fun chapterListSelector() = "tr:has(.chapter)" override fun chapterFromElement(element: Element) = SChapter.create().apply { @@ -120,15 +179,11 @@ class MangaKatana : ConfigurableSource, ParsedHttpSource() { date_upload = dateFormat.parse(element.select(".update_time").text())?.time ?: 0 } - companion object { - val dateFormat by lazy { - SimpleDateFormat("MMM-dd-yyyy", Locale.US) - } - } - private val imageArrayRegex = Regex("""var ytaw=\[([^\[]*)]""") private val imageUrlRegex = Regex("""'([^']*)'""") + // Page List + override fun pageListRequest(chapter: SChapter): Request { val serverSuffix = preferences.getString(serverPreference, "")?.takeIf { it.isNotBlank() }?.let { "?sv=$it" } ?: "" return GET(baseUrl + chapter.url + serverSuffix, headers) @@ -145,6 +200,8 @@ class MangaKatana : ConfigurableSource, ParsedHttpSource() { override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used") + // Preferences + override fun setupPreferenceScreen(screen: PreferenceScreen) { val serverPref = ListPreference(screen.context).apply { key = "server_preference" @@ -162,4 +219,126 @@ class MangaKatana : ConfigurableSource, ParsedHttpSource() { screen.addPreference(serverPref) } + + // Filters + + override fun getFilterList() = FilterList( + // MangaKarate does not support genre filtering and text search at the same time + Filter.Header("NOTE: Other filters ignored if using text search!"), + TypeFilter(), + Filter.Separator(), + GenreList(genres), + GenreInclusionMode(), + SortFilter(), + StatusFilter(), + Filter.Separator(), + Filter.Header("Input -1 to search for only oneshots"), + ChaptersFilter() + ) + + private class TypeFilter : UriPartFilter( + "Text search by", + arrayOf( + Pair("Title", "book_name"), + Pair("Author", "author") + ) + ) + + private class Genre(val id: String, name: String) : Filter.TriState(name) + private class GenreList(genres: List) : Filter.Group("Genres", genres) + private val genres = listOf( + Genre("4-koma", "4 koma"), + Genre("action", "Action"), + Genre("adult", "Adult"), + Genre("adventure", "Adventure"), + Genre("artbook", "Artbook"), + Genre("award-winning", "Award winning"), + Genre("comedy", "Comedy"), + Genre("cooking", "Cooking"), + Genre("doujinshi", "Doujinshi"), + Genre("drama", "Drama"), + Genre("ecchi", "Ecchi"), + Genre("fantasy", "Fantasy"), + Genre("gender-bender", "Gender Bender"), + Genre("gore", "Gore"), + Genre("harem", "Harem"), + Genre("historical", "Historical"), + Genre("horror", "Horror"), + Genre("isekai", "Isekai"), + Genre("josei", "Josei"), + Genre("loli", "Loli"), + Genre("manhua", "Manhua"), + Genre("manhwa", "Manhwa"), + Genre("martial-arts", "Martial Arts"), + Genre("mecha", "Mecha"), + Genre("medical", "Medical"), + Genre("music", "Music"), + Genre("mystery", "Mystery"), + Genre("one-shot", "One shot"), + Genre("overpowered-mc", "Overpowered MC"), + Genre("psychological", "Psychological"), + Genre("reincarnation", "Reincarnation"), + Genre("romance", "Romance"), + Genre("school-life", "School Life"), + Genre("sci-fi", "Sci-fi"), + Genre("seinen", "Seinen"), + Genre("sexual-violence", "Sexual violence"), + Genre("shota", "Shota"), + Genre("shoujo", "Shoujo"), + Genre("shoujo-ai", "Shoujo Ai"), + Genre("shounen", "Shounen"), + Genre("shounen-ai", "Shounen Ai"), + Genre("slice-of-life", "Slice of Life"), + Genre("smut", "Smut"), + Genre("sports", "Sports"), + Genre("super-power", "Super power"), + Genre("supernatural", "Supernatural"), + Genre("survival", "Survival"), + Genre("time-travel", "Time Travel"), + Genre("tragedy", "Tragedy"), + Genre("webtoon", "Webtoon"), + Genre("yaoi", "Yaoi"), + Genre("yuri", "Yuri"), + ) + + private class GenreInclusionMode : UriPartFilter( + "Genre inclusion mode", + arrayOf( + Pair("And", "and"), + Pair("Or", "or") + ) + ) + + private class ChaptersFilter : Filter.Text("Minimum Chapters") + + private class SortFilter : UriPartFilter( + "Sort by", + arrayOf( + Pair("Latest update", "latest"), + Pair("New manga", "new"), + Pair("A-Z", "az"), + Pair("Number of chapters", "numc") + ) + ) + + private class StatusFilter : UriPartFilter( + "Status", + arrayOf( + Pair("All", ""), + Pair("Cancelled", "0"), + Pair("Ongoing", "1"), + Pair("Completed", "2") + ) + ) + + private open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + companion object { + val dateFormat by lazy { + SimpleDateFormat("MMM-dd-yyyy", Locale.US) + } + } }