diff --git a/src/all/pururin/build.gradle b/src/all/pururin/build.gradle index 06c23a5fa..82c6ae636 100644 --- a/src/all/pururin/build.gradle +++ b/src/all/pururin/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Pururin' extClass = '.PururinFactory' - extVersionCode = 8 + extVersionCode = 9 isNsfw = true } diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt index 3511a7e10..68d18564e 100644 --- a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt @@ -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? = 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.toValue(): String { - return "[${this.joinToString(",")}]" + private fun List>.toValue(): String { + return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]" + } + + private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair { + 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, substring: String): Pair? { + 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? { + 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>(), type) } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val includeTags = mutableListOf() - val excludeTags = mutableListOf() - var pagesMin: Int - var pagesMax: Int + val includeTags = mutableListOf>() + val excludeTags = mutableListOf>() + var pagesMin = 1 + var pagesMax = 9999 + var sortBy = "newest" if (searchLang != null) includeTags.add(searchLang) - filters.filterIsInstance>().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 + } + } + + 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 -> {} } } - filters.find().range.let { - pagesMin = it.first - pagesMax = it.last + // 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 Response.parseAs(): T { + return json.decodeFromString(body.string()) + } + override fun getFilterList() = getFilters() } diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt index 4eecafa35..5556861dc 100644 --- a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt @@ -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", ) diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt index 47a79c8ff..ebe96b225 100644 --- a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt @@ -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( - name: String, - values: List, -) : Filter.Group(name, values) - -class Category(name: String, id: String) : TagFilter(name, id) - -class CategoryGroup( - values: List = categories, -) : TagGroup("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"), + ) } +internal class TypeFilter(name: String) : + Filter.Group( + 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) }, + ) -class PagesFilter( - name: String, - default: Int, - values: Array = range, -) : Filter.Select(name, values, default) { - companion object { - private val range get() = Array(301) { it } - } +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>, state: Int = 0) : + Filter.Select(name, vals.map { it.first }.toTypedArray(), state) { + fun getValue() = vals[state].second } - -class PagesGroup( - values: List = minmax, -) : Filter.Group("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 List>.find() = find { it is T } as T +private val getSortsList: List> = listOf( + Pair("Newest", "newest"), + Pair("Most Popular", "most-popular"), + Pair("Highest Rated", "highest-rated"), + Pair("Most Viewed", "most-viewed"), + Pair("Title", "title"), +)