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 { ext {
extName = 'Pururin' extName = 'Pururin'
extClass = '.PururinFactory' extClass = '.PururinFactory'
extVersionCode = 8 extVersionCode = 9
isNsfw = true 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.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()
} }

View File

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

View File

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