From e37f81eba2ad9db986121a80a12950d30c80e6db Mon Sep 17 00:00:00 2001 From: Mike <51273546+SnakeDoc83@users.noreply.github.com> Date: Sun, 21 Jan 2024 01:42:02 -0500 Subject: [PATCH] Pururin refactor (#438) --- src/{en => all}/pururin/AndroidManifest.xml | 0 src/{en => all}/pururin/build.gradle | 4 +- .../pururin/res/mipmap-hdpi/ic_launcher.png | Bin .../pururin/res/mipmap-mdpi/ic_launcher.png | Bin .../pururin/res/mipmap-xhdpi/ic_launcher.png | Bin .../pururin/res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher.png | Bin .../extension/all/pururin/Pururin.kt | 163 ++++++++++++++++++ .../extension/all/pururin/PururinFactory.kt | 24 +++ .../extension/all}/pururin/PururinFilters.kt | 36 ++-- .../tachiyomi/extension/en/pururin/Pururin.kt | 163 ------------------ .../extension/en/pururin/PururinAPI.kt | 66 ------- .../tachiyomi/extension/en/pururin/Search.kt | 76 -------- 13 files changed, 200 insertions(+), 332 deletions(-) rename src/{en => all}/pururin/AndroidManifest.xml (100%) rename src/{en => all}/pururin/build.gradle (61%) rename src/{en => all}/pururin/res/mipmap-hdpi/ic_launcher.png (100%) rename src/{en => all}/pururin/res/mipmap-mdpi/ic_launcher.png (100%) rename src/{en => all}/pururin/res/mipmap-xhdpi/ic_launcher.png (100%) rename src/{en => all}/pururin/res/mipmap-xxhdpi/ic_launcher.png (100%) rename src/{en => all}/pururin/res/mipmap-xxxhdpi/ic_launcher.png (100%) create mode 100644 src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt create mode 100644 src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt rename src/{en/pururin/src/eu/kanade/tachiyomi/extension/en => all/pururin/src/eu/kanade/tachiyomi/extension/all}/pururin/PururinFilters.kt (54%) delete mode 100644 src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Pururin.kt delete mode 100644 src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/PururinAPI.kt delete mode 100644 src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Search.kt diff --git a/src/en/pururin/AndroidManifest.xml b/src/all/pururin/AndroidManifest.xml similarity index 100% rename from src/en/pururin/AndroidManifest.xml rename to src/all/pururin/AndroidManifest.xml diff --git a/src/en/pururin/build.gradle b/src/all/pururin/build.gradle similarity index 61% rename from src/en/pururin/build.gradle rename to src/all/pururin/build.gradle index 5c741393f..5a3502592 100644 --- a/src/en/pururin/build.gradle +++ b/src/all/pururin/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Pururin' - extClass = '.Pururin' - extVersionCode = 6 + extClass = '.PururinFactory' + extVersionCode = 7 isNsfw = true } diff --git a/src/en/pururin/res/mipmap-hdpi/ic_launcher.png b/src/all/pururin/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/en/pururin/res/mipmap-hdpi/ic_launcher.png rename to src/all/pururin/res/mipmap-hdpi/ic_launcher.png diff --git a/src/en/pururin/res/mipmap-mdpi/ic_launcher.png b/src/all/pururin/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/en/pururin/res/mipmap-mdpi/ic_launcher.png rename to src/all/pururin/res/mipmap-mdpi/ic_launcher.png diff --git a/src/en/pururin/res/mipmap-xhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/en/pururin/res/mipmap-xhdpi/ic_launcher.png rename to src/all/pururin/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/en/pururin/res/mipmap-xxhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/en/pururin/res/mipmap-xxhdpi/ic_launcher.png rename to src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/en/pururin/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/en/pururin/res/mipmap-xxxhdpi/ic_launcher.png rename to src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png 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 new file mode 100644 index 000000000..dd31936a2 --- /dev/null +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt @@ -0,0 +1,163 @@ +package eu.kanade.tachiyomi.extension.all.pururin + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.Page +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.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +abstract class Pururin( + override val lang: String = "all", + private val searchLang: String? = null, + private val langPath: String = "", +) : ParsedHttpSource() { + override val name = "Pururin" + + override val baseUrl = "https://pururin.to" + + override val supportsLatest = true + + override val client = network.cloudflareClient + + // Popular + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers) + } + + override fun popularMangaSelector(): String = "a.card" + + override fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + title = element.attr("title") + setUrlWithoutDomain(element.attr("abs:href")) + thumbnail_url = element.select("img").attr("abs:src") + } + } + + override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]" + + // Latest + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/browse$langPath?page=$page", headers) + } + + override fun latestUpdatesSelector(): String = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector() + + // Search + + private fun List.toValue(): String { + return "[${this.joinToString(",")}]" + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val includeTags = mutableListOf() + val excludeTags = mutableListOf() + var pagesMin: Int + var pagesMax: Int + + 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.find().range.let { + pagesMin = it.first + pagesMax = it.last + } + + val url = baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("search") + addQueryParameter("q", query) + 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) + } + + override fun searchMangaSelector(): String = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector() + + // Details + + override fun mangaDetailsParse(document: Document): SManga { + return SManga.create().apply { + document.select(".box-gallery").let { e -> + initialized = true + title = e.select(".title").text() + author = e.select("[itemprop=author]").text() + description = e.select(".box-gallery .table-info tr") + .joinToString("\n") { tr -> + tr.select("td") + .joinToString(": ") { it.text() } + } + thumbnail_url = e.select("img").attr("abs:src") + } + } + } + + // Chapters + + override fun chapterListSelector(): String = ".table-collection tbody tr a" + + override fun chapterFromElement(element: Element): SChapter { + return SChapter.create().apply { + name = element.text() + setUrlWithoutDomain(element.attr("abs:href")) + } + } + + override fun chapterListParse(response: Response): List { + return response.asJsoup().select(chapterListSelector()) + .map { chapterFromElement(it) } + .reversed() + .let { list -> + list.ifEmpty { + listOf( + SChapter.create().apply { + setUrlWithoutDomain(response.request.url.toString()) + name = "Chapter" + }, + ) + } + } + } + + // Pages + + override fun pageListParse(document: Document): List { + return document.select(".gallery-preview a img") + .mapIndexed { i, img -> + Page(i, "", img.attr("abs:src").replace("t.", ".")) + } + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() + + override fun getFilterList() = FilterList( + CategoryGroup(), + PagesGroup(), + ) +} 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 new file mode 100644 index 000000000..4eecafa35 --- /dev/null +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.extension.all.pururin + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class PururinFactory : SourceFactory { + override fun createSources(): List = listOf( + PururinAll(), + PururinEN(), + PururinJA(), + ) +} + +class PururinAll : Pururin() +class PururinEN : Pururin( + "en", + "{\"id\":13010,\"name\":\"English [Language]\"}", + "/tags/language/13010/english", +) +class PururinJA : Pururin( + "ja", + "{\"id\":13011,\"name\":\"Japanese [Language]\"}", + "/tags/language/13011/japanese", +) diff --git a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/PururinFilters.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt similarity index 54% rename from src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/PururinFilters.kt rename to src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt index 3de7c90c3..47a79c8ff 100644 --- a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/PururinFilters.kt +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt @@ -1,16 +1,10 @@ -package eu.kanade.tachiyomi.extension.en.pururin +package eu.kanade.tachiyomi.extension.all.pururin import eu.kanade.tachiyomi.source.model.Filter -class SortFilter( - values: Array = Search.Sort.values(), -) : Filter.Select("Sort by", values) { - inline val sort get() = values[state] -} - sealed class TagFilter( name: String, - val id: Int, + val id: String, ) : Filter.TriState(name) sealed class TagGroup( @@ -18,38 +12,30 @@ sealed class TagGroup( values: List, ) : Filter.Group(name, values) -// TODO: Artist, Circle, Contents, Parody, Character, Convention - -class Category(name: String, id: Int) : TagFilter(name, id) +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", 13003), - Category("Manga", 13004), - Category("Artist CG", 13006), - Category("Game CG", 13008), - Category("Artbook", 17783), - Category("Webtoon", 27939), + 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]\"}"), ) } } -class TagModeFilter( - values: Array = Search.TagMode.values(), -) : Filter.Select("Tag mode", values) { - inline val mode get() = values[state] -} - class PagesFilter( name: String, default: Int, values: Array = range, ) : Filter.Select(name, values, default) { companion object { - private val range get() = Array(1001) { it } + private val range get() = Array(301) { it } } } @@ -63,7 +49,7 @@ class PagesGroup( companion object { private val minmax get() = listOf( PagesFilter("Minimum", 0), - PagesFilter("Maximum", 100), + PagesFilter("Maximum", 300), ) } } diff --git a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Pururin.kt b/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Pururin.kt deleted file mode 100644 index 973923f29..000000000 --- a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Pururin.kt +++ /dev/null @@ -1,163 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.pururin - -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservable -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import okhttp3.Request -import okhttp3.Response -import uy.kohesive.injekt.injectLazy - -class Pururin : HttpSource() { - override val name = "Pururin" - - override val baseUrl = "https://pururin.to" - - override val lang = "en" - - override val supportsLatest = true - - override val client = network.cloudflareClient - - private val searchUrl = "$baseUrl/api/search/advance" - - private val galleryUrl = "$baseUrl/api/contribute/gallery/info" - - private val json by injectLazy() - - override fun headersBuilder() = super.headersBuilder() - .set("Origin", baseUrl).set("X-Requested-With", "XMLHttpRequest") - - override fun latestUpdatesRequest(page: Int) = - POST(searchUrl, headers, Search(Search.Sort.NEWEST, page)) - - override fun latestUpdatesParse(response: Response) = - searchMangaParse(response) - - override fun popularMangaRequest(page: Int) = - POST(searchUrl, headers, Search(Search.Sort.POPULAR, page)) - - override fun popularMangaParse(response: Response) = - searchMangaParse(response) - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = - filters.ifEmpty(::getFilterList).run { - val whitelist = mutableListOf() - val blacklist = mutableListOf() - filterIsInstance>().forEach { group -> - group.state.forEach { - when { - it.isIncluded() -> whitelist += it.id - it.isExcluded() -> blacklist += it.id - } - } - } - val body = Search( - find().sort, - page, - query, - whitelist, - blacklist, - find().mode, - find().range, - ) - POST(searchUrl, headers, body) - } - - override fun searchMangaParse(response: Response): MangasPage { - val results = json.decodeFromString( - response.jsonObject["results"]!!.jsonPrimitive.content, - ) - val mp = results.map { - SManga.create().apply { - url = it.path - title = it.title - thumbnail_url = CDN_URL + it.cover - } - } - return MangasPage(mp, results.hasNext) - } - - override fun mangaDetailsParse(response: Response): SManga { - val gallery = json.decodeFromJsonElement( - response.jsonObject["gallery"]!!, - ) - return SManga.create().apply { - description = gallery.description - artist = gallery.artists.joinToString() - author = gallery.authors.joinToString() - genre = gallery.genres.joinToString() - } - } - - override fun fetchMangaDetails(manga: SManga) = - client.newCall(chapterListRequest(manga)) - .asObservableSuccess().map(::mangaDetailsParse)!! - - override fun chapterListRequest(manga: SManga) = - POST(galleryUrl, headers, Search.info(manga.id)) - - override fun chapterListParse(response: Response): List { - val gallery = json.decodeFromJsonElement( - response.jsonObject["gallery"]!!, - ) - val chapter = SChapter.create().apply { - name = "Chapter" - url = gallery.id.toString() - scanlator = gallery.scanlators.joinToString() - } - return listOf(chapter) - } - - override fun pageListRequest(chapter: SChapter) = - POST(galleryUrl, headers, Search.info(chapter.url)) - - override fun pageListParse(response: Response): List { - val pages = json.decodeFromJsonElement( - response.jsonObject["gallery"]!!, - ).pages - return pages.mapIndexed { idx, img -> - Page(idx + 1, CDN_URL + img) - } - } - - override fun imageUrlParse(response: Response) = - throw UnsupportedOperationException() - - override fun fetchImageUrl(page: Page) = - Request.Builder().url(page.url).head().build() - .run(client::newCall).asObservable().map { - when (it.code) { - 200 -> page.url - // try to fix images that are broken even on the site - 404 -> page.url.replaceAfterLast('.', "png") - else -> throw Error("HTTP error ${it.code}") - } - }!! - - override fun getFilterList() = FilterList( - SortFilter(), - CategoryGroup(), - TagModeFilter(), - PagesGroup(), - ) - - private inline val Response.jsonObject - get() = json.parseToJsonElement(body.string()).jsonObject - - private inline val SManga.id get() = url.split('/')[2] - - companion object { - private const val CDN_URL = "https://cdn.pururin.to/assets/images/data" - } -} diff --git a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/PururinAPI.kt b/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/PururinAPI.kt deleted file mode 100644 index c874aefa5..000000000 --- a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/PururinAPI.kt +++ /dev/null @@ -1,66 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.pururin - -import kotlinx.serialization.Serializable - -@Serializable -data class Results( - private val current_page: Int, - private val data: List, - private val last_page: Int, -) : Iterable by data { - val hasNext get() = current_page != last_page -} - -@Serializable -data class Data( - private val id: Int, - val title: String, - private val slug: String, -) { - val path get() = "/gallery/$id/$slug" - - val cover get() = "/$id/cover.jpg" -} - -@Serializable -data class Gallery( - val id: Int, - private val j_title: String, - private val alt_title: String?, - private val total_pages: Int, - private val image_extension: String, - private val tags: TagList, -) { - val description get() = "$j_title\n${alt_title ?: ""}".trim() - - val pages get() = (1..total_pages).map { "/$id/$it.$image_extension" } - - val genres get() = tags.Parody + - tags.Contents + - tags.Category + - tags.Character + - tags.Convention - - val artists get() = tags.Artist - - val authors get() = tags.Circle.ifEmpty { tags.Artist } - - val scanlators get() = tags.Scanlator -} - -@Serializable -data class TagList( - val Artist: List, - val Circle: List, - val Parody: List, - val Contents: List, - val Category: List, - val Character: List, - val Scanlator: List, - val Convention: List, -) - -@Serializable -data class Tag(private val name: String) { - override fun toString() = name -} diff --git a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Search.kt b/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Search.kt deleted file mode 100644 index dbfc2bea2..000000000 --- a/src/en/pururin/src/eu/kanade/tachiyomi/extension/en/pururin/Search.kt +++ /dev/null @@ -1,76 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.pururin - -import kotlinx.serialization.json.add -import kotlinx.serialization.json.addJsonObject -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray -import kotlinx.serialization.json.putJsonObject -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.RequestBody.Companion.toRequestBody - -object Search { - private val jsonMime = "application/json".toMediaType() - - enum class Sort(private val label: String, val id: String) { - NEWEST("Newest", "newest"), - POPULAR("Most Popular", "most-popular"), - RATING("Highest Rated", "highest-rated"), - VIEWS("Most Viewed", "most-viewed"), - TITLE("Title", "title"), - ; - - override fun toString() = label - } - - enum class TagMode(val id: String) { - AND("1"), OR("2"); - - override fun toString() = name - } - - operator fun invoke( - sort: Sort, - page: Int = 1, - query: String = "", - whitelist: List = emptyList(), - blacklist: List = emptyList(), - mode: TagMode = TagMode.AND, - range: IntRange = 0..100, - ) = buildJsonObject { - putJsonObject("search") { - put("sort", sort.id) - put("PageNumber", page) - putJsonObject("manga") { - put("string", query) - put("sort", "1") - } - putJsonObject("tag") { - putJsonObject("items") { - putJsonArray("whitelisted") { - whitelist.forEach { - addJsonObject { put("id", it) } - } - } - putJsonArray("blacklisted") { - blacklist.forEach { - addJsonObject { put("id", it) } - } - } - } - put("sort", mode.id) - } - putJsonObject("page") { - putJsonArray("range") { - add(range.first) - add(range.last) - } - } - } - }.toString().toRequestBody(jsonMime) - - fun info(id: String) = buildJsonObject { - put("id", id) - put("type", "1") - }.toString().toRequestBody(jsonMime) -}