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:
KenjieDec 2024-06-28 17:55:38 +07:00 committed by Draff
parent 82b7531d00
commit 780089af90
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
4 changed files with 178 additions and 77 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Pururin'
extClass = '.PururinFactory'
extVersionCode = 8
extVersionCode = 9
isNsfw = true
}

View File

@ -7,27 +7,33 @@ 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 kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
abstract class Pururin(
override val lang: String = "all",
private val searchLang: String? = null,
private val searchLang: Pair<String, String>? = null,
private val langPath: String = "",
) : ParsedHttpSource() {
override val name = "Pururin"
override val baseUrl = "https://pururin.to"
final override val baseUrl = "https://pururin.to"
override val supportsLatest = true
override val client = network.cloudflareClient
// Popular
private val json: Json by injectLazy()
// Popular
override fun popularMangaRequest(page: Int): Request {
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]"
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse$langPath?page=$page", headers)
}
@ -58,40 +63,131 @@ abstract class Pururin(
// Search
private fun List<String>.toValue(): String {
return "[${this.joinToString(",")}]"
private fun List<Pair<String, String>>.toValue(): String {
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 {
val includeTags = mutableListOf<String>()
val excludeTags = mutableListOf<String>()
var pagesMin: Int
var pagesMax: Int
val includeTags = mutableListOf<Pair<String, String>>()
val excludeTags = mutableListOf<Pair<String, String>>()
var pagesMin = 1
var pagesMax = 9999
var sortBy = "newest"
if (searchLang != null) includeTags.add(searchLang)
filters.filterIsInstance<TagGroup<*>>().map { group ->
group.state.map {
if (it.isIncluded()) includeTags.add(it.id)
if (it.isExcluded()) excludeTags.add(it.id)
filters.forEach {
when (it) {
is SelectFilter -> sortBy = it.getValue()
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 {
pagesMin = it.first
pagesMax = it.last
is TextFilter -> {
if (it.state.isNotEmpty()) {
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 {
addPathSegment("search")
addQueryParameter("q", query)
addQueryParameter("sort", sortBy)
addQueryParameter("start_page", pagesMin.toString())
addQueryParameter("last_page", pagesMax.toString())
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
if (page > 1) addQueryParameter("page", page.toString())
}
return GET(url.build().toString(), headers)
}.build()
return GET(url, headers)
}
override fun searchMangaSelector(): String = popularMangaSelector()
@ -107,8 +203,13 @@ abstract class Pururin(
document.select(".box-gallery").let { e ->
initialized = true
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")
.filter { tr ->
tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
}
.joinToString("\n") { tr ->
tr.select("td")
.joinToString(": ") { it.text() }
@ -156,8 +257,8 @@ abstract class Pururin(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
CategoryGroup(),
PagesGroup(),
)
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun getFilterList() = getFilters()
}

View File

@ -14,11 +14,11 @@ class PururinFactory : SourceFactory {
class PururinAll : Pururin()
class PururinEN : Pururin(
"en",
"{\"id\":13010,\"name\":\"English [Language]\"}",
Pair("13010", "english"),
"/tags/language/13010/english",
)
class PururinJA : Pururin(
"ja",
"{\"id\":13011,\"name\":\"Japanese [Language]\"}",
Pair("13011", "japanese"),
"/tags/language/13011/japanese",
)

View File

@ -1,57 +1,57 @@
package eu.kanade.tachiyomi.extension.all.pururin
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
sealed class TagFilter(
name: String,
val id: String,
) : Filter.TriState(name)
sealed class TagGroup<T : TagFilter>(
name: String,
values: List<T>,
) : Filter.Group<T>(name, values)
class Category(name: String, id: String) : TagFilter(name, id)
class CategoryGroup(
values: List<Category> = categories,
) : TagGroup<Category>("Categories", values) {
companion object {
private val categories get() = listOf(
Category("Doujinshi", "{\"id\":13003,\"name\":\"Doujinshi [Category]\"}"),
Category("Manga", "{\"id\":13004,\"name\":\"Manga [Category]\"}"),
Category("Artist CG", "{\"id\":13006,\"name\":\"Artist CG [Category]\"}"),
Category("Game CG", "{\"id\":13008,\"name\":\"Game CG [Category]\"}"),
Category("Artbook", "{\"id\":17783,\"name\":\"Artbook [Category]\"}"),
Category("Webtoon", "{\"id\":27939,\"name\":\"Webtoon [Category]\"}"),
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
TypeFilter("Types"),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Tags", "[Content]"),
TextFilter("Artists", "[Artist]"),
TextFilter("Circles", "[Circle]"),
TextFilter("Parodies", "[Parody]"),
TextFilter("Languages", "[Language]"),
TextFilter("Scanlators", "[Scanlator]"),
TextFilter("Conventions", "[Convention]"),
TextFilter("Collections", "[Collections]"),
TextFilter("Categories", "[Category]"),
TextFilter("Uploaders", "[Uploader]"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
PageFilter("Pages"),
)
}
}
class PagesFilter(
name: String,
default: Int,
values: Array<Int> = range,
) : Filter.Select<Int>(name, values, default) {
companion object {
private val range get() = Array(301) { it }
}
}
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),
internal class TypeFilter(name: String) :
Filter.Group<CheckBoxFilter>(
name,
listOf(
Pair("Artbook", "17783"),
Pair("Artist CG", "13004"),
Pair("Doujinshi", "13003"),
Pair("Game CG", "13008"),
Pair("Manga", "13004"),
Pair("Webtoon", "27939"),
).map { CheckBoxFilter(it.first, it.second, true) },
)
}
}
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"),
)