Pururin: Add Tags Filter & Sort Filter (#3769)
* Add Tags Filter - Similar to Pururin's Advanced Search - Some description changes - Change page filter format ( now more similar to nhentai's ) * Fix Japanese language id
This commit is contained in:
parent
82b7531d00
commit
780089af90
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Pururin'
|
extName = 'Pururin'
|
||||||
extClass = '.PururinFactory'
|
extClass = '.PururinFactory'
|
||||||
extVersionCode = 8
|
extVersionCode = 9
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,27 +7,33 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class Pururin(
|
abstract class Pururin(
|
||||||
override val lang: String = "all",
|
override val lang: String = "all",
|
||||||
private val searchLang: String? = null,
|
private val searchLang: Pair<String, String>? = null,
|
||||||
private val langPath: String = "",
|
private val langPath: String = "",
|
||||||
) : ParsedHttpSource() {
|
) : ParsedHttpSource() {
|
||||||
override val name = "Pururin"
|
override val name = "Pururin"
|
||||||
|
|
||||||
override val baseUrl = "https://pururin.to"
|
final override val baseUrl = "https://pururin.to"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
// Popular
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
// Popular
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
|
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
|
||||||
}
|
}
|
||||||
@ -45,7 +51,6 @@ abstract class Pururin(
|
|||||||
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
|
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
|
||||||
|
|
||||||
// Latest
|
// Latest
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/browse$langPath?page=$page", headers)
|
return GET("$baseUrl/browse$langPath?page=$page", headers)
|
||||||
}
|
}
|
||||||
@ -58,40 +63,131 @@ abstract class Pururin(
|
|||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|
||||||
private fun List<String>.toValue(): String {
|
private fun List<Pair<String, String>>.toValue(): String {
|
||||||
return "[${this.joinToString(",")}]"
|
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
|
||||||
|
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
|
||||||
|
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
|
||||||
|
|
||||||
|
if (num < 0) return minPages to maxPages
|
||||||
|
return when (query.firstOrNull()) {
|
||||||
|
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
|
||||||
|
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
|
||||||
|
'=' -> when (query[1]) {
|
||||||
|
'>' -> limitedNum() to maxPages
|
||||||
|
'<' -> 1 to limitedNum(maxPages)
|
||||||
|
else -> limitedNum() to limitedNum()
|
||||||
|
}
|
||||||
|
else -> limitedNum() to limitedNum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Tag(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
|
||||||
|
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
|
||||||
|
return tag?.let { Pair(tag.id.toString(), tag.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
|
||||||
|
val requestBody = FormBody.Builder()
|
||||||
|
.add("text", tag)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$baseUrl/api/get/tags/search")
|
||||||
|
.headers(headers)
|
||||||
|
.post(requestBody)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val includeTags = mutableListOf<String>()
|
val includeTags = mutableListOf<Pair<String, String>>()
|
||||||
val excludeTags = mutableListOf<String>()
|
val excludeTags = mutableListOf<Pair<String, String>>()
|
||||||
var pagesMin: Int
|
var pagesMin = 1
|
||||||
var pagesMax: Int
|
var pagesMax = 9999
|
||||||
|
var sortBy = "newest"
|
||||||
|
|
||||||
if (searchLang != null) includeTags.add(searchLang)
|
if (searchLang != null) includeTags.add(searchLang)
|
||||||
|
|
||||||
filters.filterIsInstance<TagGroup<*>>().map { group ->
|
filters.forEach {
|
||||||
group.state.map {
|
when (it) {
|
||||||
if (it.isIncluded()) includeTags.add(it.id)
|
is SelectFilter -> sortBy = it.getValue()
|
||||||
if (it.isExcluded()) excludeTags.add(it.id)
|
|
||||||
|
is TypeFilter -> {
|
||||||
|
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
|
||||||
|
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
|
||||||
|
}
|
||||||
|
|
||||||
|
is PageFilter -> {
|
||||||
|
if (it.state.isNotEmpty()) {
|
||||||
|
val (min, max) = parsePageRange(it.state)
|
||||||
|
pagesMin = min
|
||||||
|
pagesMax = max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filters.find<PagesGroup>().range.let {
|
is TextFilter -> {
|
||||||
pagesMin = it.first
|
if (it.state.isNotEmpty()) {
|
||||||
pagesMax = it.last
|
it.state.split(",").filter(String::isNotBlank).map { tag ->
|
||||||
|
val trimmed = tag.trim()
|
||||||
|
if (trimmed.startsWith('-')) {
|
||||||
|
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
|
||||||
|
excludeTags.add(tagInfo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
|
||||||
|
includeTags.add(tagInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searching with just one tag usually gives wrong results
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
when {
|
||||||
|
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
|
||||||
|
includeTags.size == 1 && excludeTags.isEmpty() -> {
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegment("browse")
|
||||||
|
addPathSegment("tags")
|
||||||
|
addPathSegment("content")
|
||||||
|
addPathSegment(includeTags[0].first)
|
||||||
|
addQueryParameter("sort", sortBy)
|
||||||
|
addQueryParameter("start_page", pagesMin.toString())
|
||||||
|
addQueryParameter("last_page", pagesMax.toString())
|
||||||
|
if (page > 1) addQueryParameter("page", page.toString())
|
||||||
|
}.build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
addPathSegment("search")
|
addPathSegment("search")
|
||||||
addQueryParameter("q", query)
|
addQueryParameter("q", query)
|
||||||
|
addQueryParameter("sort", sortBy)
|
||||||
addQueryParameter("start_page", pagesMin.toString())
|
addQueryParameter("start_page", pagesMin.toString())
|
||||||
addQueryParameter("last_page", pagesMax.toString())
|
addQueryParameter("last_page", pagesMax.toString())
|
||||||
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
|
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
|
||||||
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
|
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
if (page > 1) addQueryParameter("page", page.toString())
|
||||||
}
|
}.build()
|
||||||
return GET(url.build().toString(), headers)
|
|
||||||
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||||
@ -107,8 +203,13 @@ abstract class Pururin(
|
|||||||
document.select(".box-gallery").let { e ->
|
document.select(".box-gallery").let { e ->
|
||||||
initialized = true
|
initialized = true
|
||||||
title = e.select(".title").text()
|
title = e.select(".title").text()
|
||||||
author = e.select("[itemprop=author]").text()
|
author = e.select("a[href*=/circle/]").text().ifEmpty { e.select("[itemprop=author]").text() }
|
||||||
|
artist = e.select("[itemprop=author]").text()
|
||||||
|
genre = e.select("a[href*=/content/]").text()
|
||||||
description = e.select(".box-gallery .table-info tr")
|
description = e.select(".box-gallery .table-info tr")
|
||||||
|
.filter { tr ->
|
||||||
|
tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
|
||||||
|
}
|
||||||
.joinToString("\n") { tr ->
|
.joinToString("\n") { tr ->
|
||||||
tr.select("td")
|
tr.select("td")
|
||||||
.joinToString(": ") { it.text() }
|
.joinToString(": ") { it.text() }
|
||||||
@ -156,8 +257,8 @@ abstract class Pururin(
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
CategoryGroup(),
|
return json.decodeFromString(body.string())
|
||||||
PagesGroup(),
|
}
|
||||||
)
|
override fun getFilterList() = getFilters()
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,11 @@ class PururinFactory : SourceFactory {
|
|||||||
class PururinAll : Pururin()
|
class PururinAll : Pururin()
|
||||||
class PururinEN : Pururin(
|
class PururinEN : Pururin(
|
||||||
"en",
|
"en",
|
||||||
"{\"id\":13010,\"name\":\"English [Language]\"}",
|
Pair("13010", "english"),
|
||||||
"/tags/language/13010/english",
|
"/tags/language/13010/english",
|
||||||
)
|
)
|
||||||
class PururinJA : Pururin(
|
class PururinJA : Pururin(
|
||||||
"ja",
|
"ja",
|
||||||
"{\"id\":13011,\"name\":\"Japanese [Language]\"}",
|
Pair("13011", "japanese"),
|
||||||
"/tags/language/13011/japanese",
|
"/tags/language/13011/japanese",
|
||||||
)
|
)
|
||||||
|
@ -1,57 +1,57 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.pururin
|
package eu.kanade.tachiyomi.extension.all.pururin
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
sealed class TagFilter(
|
fun getFilters(): FilterList {
|
||||||
name: String,
|
return FilterList(
|
||||||
val id: String,
|
SelectFilter("Sort by", getSortsList),
|
||||||
) : Filter.TriState(name)
|
TypeFilter("Types"),
|
||||||
|
Filter.Separator(),
|
||||||
sealed class TagGroup<T : TagFilter>(
|
Filter.Header("Separate tags with commas (,)"),
|
||||||
name: String,
|
Filter.Header("Prepend with dash (-) to exclude"),
|
||||||
values: List<T>,
|
TextFilter("Tags", "[Content]"),
|
||||||
) : Filter.Group<T>(name, values)
|
TextFilter("Artists", "[Artist]"),
|
||||||
|
TextFilter("Circles", "[Circle]"),
|
||||||
class Category(name: String, id: String) : TagFilter(name, id)
|
TextFilter("Parodies", "[Parody]"),
|
||||||
|
TextFilter("Languages", "[Language]"),
|
||||||
class CategoryGroup(
|
TextFilter("Scanlators", "[Scanlator]"),
|
||||||
values: List<Category> = categories,
|
TextFilter("Conventions", "[Convention]"),
|
||||||
) : TagGroup<Category>("Categories", values) {
|
TextFilter("Collections", "[Collections]"),
|
||||||
companion object {
|
TextFilter("Categories", "[Category]"),
|
||||||
private val categories get() = listOf(
|
TextFilter("Uploaders", "[Uploader]"),
|
||||||
Category("Doujinshi", "{\"id\":13003,\"name\":\"Doujinshi [Category]\"}"),
|
Filter.Separator(),
|
||||||
Category("Manga", "{\"id\":13004,\"name\":\"Manga [Category]\"}"),
|
Filter.Header("Filter by pages, for example: (>20)"),
|
||||||
Category("Artist CG", "{\"id\":13006,\"name\":\"Artist CG [Category]\"}"),
|
PageFilter("Pages"),
|
||||||
Category("Game CG", "{\"id\":13008,\"name\":\"Game CG [Category]\"}"),
|
|
||||||
Category("Artbook", "{\"id\":17783,\"name\":\"Artbook [Category]\"}"),
|
|
||||||
Category("Webtoon", "{\"id\":27939,\"name\":\"Webtoon [Category]\"}"),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
internal class TypeFilter(name: String) :
|
||||||
|
Filter.Group<CheckBoxFilter>(
|
||||||
class PagesFilter(
|
name,
|
||||||
name: String,
|
listOf(
|
||||||
default: Int,
|
Pair("Artbook", "17783"),
|
||||||
values: Array<Int> = range,
|
Pair("Artist CG", "13004"),
|
||||||
) : Filter.Select<Int>(name, values, default) {
|
Pair("Doujinshi", "13003"),
|
||||||
companion object {
|
Pair("Game CG", "13008"),
|
||||||
private val range get() = Array(301) { it }
|
Pair("Manga", "13004"),
|
||||||
}
|
Pair("Webtoon", "27939"),
|
||||||
}
|
).map { CheckBoxFilter(it.first, it.second, true) },
|
||||||
|
|
||||||
class PagesGroup(
|
|
||||||
values: List<PagesFilter> = minmax,
|
|
||||||
) : Filter.Group<PagesFilter>("Pages", values) {
|
|
||||||
inline val range get() = IntRange(state[0].state, state[1].state).also {
|
|
||||||
require(it.first <= it.last) { "'Minimum' cannot exceed 'Maximum'" }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val minmax get() = listOf(
|
|
||||||
PagesFilter("Minimum", 0),
|
|
||||||
PagesFilter("Maximum", 300),
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> List<Filter<*>>.find() = find { it is T } as T
|
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
|
||||||
|
|
||||||
|
internal open class PageFilter(name: String) : Filter.Text(name)
|
||||||
|
|
||||||
|
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
||||||
|
|
||||||
|
internal open class SelectFilter(name: String, val vals: List<Pair<String, String>>, state: Int = 0) :
|
||||||
|
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
||||||
|
fun getValue() = vals[state].second
|
||||||
|
}
|
||||||
|
private val getSortsList: List<Pair<String, String>> = listOf(
|
||||||
|
Pair("Newest", "newest"),
|
||||||
|
Pair("Most Popular", "most-popular"),
|
||||||
|
Pair("Highest Rated", "highest-rated"),
|
||||||
|
Pair("Most Viewed", "most-viewed"),
|
||||||
|
Pair("Title", "title"),
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user