From 1467ce33995c7ab222b722b606eabe28eb47373b Mon Sep 17 00:00:00 2001 From: dngonz Date: Mon, 6 Jan 2025 14:16:23 +0100 Subject: [PATCH] ReaperScans Unoriginal: Moved to ParsedHttpSource (#7002) * move to ParsedHttpSource * remove old code * clean up --- src/en/reaperscansunoriginal/build.gradle | 4 +- .../en/reaperscansunoriginal/Filters.kt | 107 +++++++++++ .../ReaperScansUnoriginal.kt | 172 +++++++++++++++++- 3 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/Filters.kt diff --git a/src/en/reaperscansunoriginal/build.gradle b/src/en/reaperscansunoriginal/build.gradle index cd214b4a9fc..69cf528e321 100644 --- a/src/en/reaperscansunoriginal/build.gradle +++ b/src/en/reaperscansunoriginal/build.gradle @@ -1,9 +1,7 @@ ext { extName = 'Reaper Scans (unoriginal)' extClass = '.ReaperScansUnoriginal' - themePkg = 'mangathemesia' - baseUrl = 'https://reaper-scans.com' - overrideVersionCode = 0 + extVersionCode = 31 } apply from: "$rootDir/common.gradle" diff --git a/src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/Filters.kt b/src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/Filters.kt new file mode 100644 index 00000000000..20083dab0f0 --- /dev/null +++ b/src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/Filters.kt @@ -0,0 +1,107 @@ +package eu.kanade.tachiyomi.extension.en.reaperscansunoriginal + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import okhttp3.HttpUrl + +interface UrlPartFilter { + fun addUrlParameter(url: HttpUrl.Builder) +} + +abstract class SelectFilter( + name: String, + private val urlParameter: String, + private val options: List>, + defaultValue: String? = null, +) : UrlPartFilter, Filter.Select( + name, + options.map { it.first }.toTypedArray(), + options.indexOfFirst { it.second == defaultValue }.coerceAtLeast(0), +) { + override fun addUrlParameter(url: HttpUrl.Builder) { + url.addQueryParameter(urlParameter, options[state].second) + } +} + +class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name) + +open class CheckBoxGroup( + name: String, + private val urlParameter: String, + options: List>, +) : UrlPartFilter, Filter.Group( + name, + options.map { CheckBoxFilter(it.first, it.second) }, +) { + override fun addUrlParameter(url: HttpUrl.Builder) { + val checked = state.filter { it.state }.map { it.value } + + if (checked.isNotEmpty()) { + checked.forEach { genre -> + url.addQueryParameter(urlParameter, genre) + } + } + } +} + +class TypeFilter : CheckBoxGroup( + "Status", + "type[]", + listOf( + Pair("Action", "action"), + Pair("Adventure", "adventure"), + Pair("Fantasy", "fantasy"), + Pair("Manga", "manga"), + Pair("Manhua", "manhua"), + Pair("Manhwa", "manhwa"), + Pair("Seinen", "seinen"), + ), +) + +class GenreFilter(genres: List>) : CheckBoxGroup( + "Genres", + "genre[]", + genres, +) + +class YearFilter : CheckBoxGroup( + "Status", + "release[]", + listOf( + Pair("2024", "2024"), + Pair("2023", "2023"), + Pair("2022", "2022"), + Pair("2021", "2021"), + Pair("2020", "2020"), + Pair("2019", "2019"), + Pair("2018", "2018"), + Pair("2017", "2017"), + Pair("2016", "2016"), + Pair("2015", "2015"), + ), +) + +class StatusFilter : CheckBoxGroup( + "Status", + "status[]", + listOf( + Pair("Releasing", "on-going"), + Pair("Completed", "end"), + ), +) + +class OrderFilter(default: String? = null) : SelectFilter( + "Sort by", + "sort", + listOf( + Pair("", ""), + Pair("Popular", "most_viewed"), + Pair("Latest", "recently_added"), + ), + default, +) { + companion object { + val POPULAR = FilterList(OrderFilter("most_viewed")) + val LATEST = FilterList(OrderFilter("recently_added")) + } +} diff --git a/src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/ReaperScansUnoriginal.kt b/src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/ReaperScansUnoriginal.kt index 786ab5d8555..d223300790e 100644 --- a/src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/ReaperScansUnoriginal.kt +++ b/src/en/reaperscansunoriginal/src/eu/kanade/tachiyomi/extension/en/reaperscansunoriginal/ReaperScansUnoriginal.kt @@ -1,14 +1,174 @@ package eu.kanade.tachiyomi.extension.en.reaperscansunoriginal -import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia +import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.Filter +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.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 +import java.util.Calendar + +class ReaperScansUnoriginal : ParsedHttpSource() { + override val baseUrl = "https://reaper-scans.com" + + override val name = "Reaper Scans (unoriginal)" + + override val lang = "en" + + override val supportsLatest = true -class ReaperScansUnoriginal : MangaThemesia( - "Reaper Scans (unoriginal)", - "https://reaper-scans.com", - "en", -) { override val client = super.client.newBuilder() .rateLimit(3) .build() + + // Popular + override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException() + + override fun popularMangaNextPageSelector(): String = throw UnsupportedOperationException() + + override fun popularMangaRequest(page: Int) = + searchMangaRequest(page, "", OrderFilter.POPULAR) + + override fun popularMangaSelector() = throw UnsupportedOperationException() + + override fun popularMangaParse(response: Response) = searchMangaParse(response) + + // Latest + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException() + + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException() + + override fun latestUpdatesRequest(page: Int) = + searchMangaRequest(page, "", OrderFilter.LATEST) + + override fun latestUpdatesSelector() = throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response) = searchMangaParse(response) + + // Search + override fun searchMangaFromElement(element: Element) = SManga.create().apply { + thumbnail_url = element.select(".poster-image-wrapper > img").attr("src") + title = element.select(".info > a").text() + setUrlWithoutDomain(element.selectFirst(".info a")!!.attr("href")) + } + + override fun searchMangaNextPageSelector() = "a[rel=\"next\"]" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + addQueryParameter("post_type", "wp-manga") + addQueryParameter("s", query) + filters.filterIsInstance().forEach { + it.addUrlParameter(this) + } + if (page > 1) { + addPathSegment("page") + addPathSegment(page.toString()) + } + }.build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = ".inner" + + override fun searchMangaParse(response: Response): MangasPage { + if (genres.isEmpty()) { + genres = parseGenres(response.asJsoup(response.peekBody(Long.MAX_VALUE).string())) + } + + return super.searchMangaParse(response) + } + + // Chapter + override fun chapterFromElement(element: Element) = SChapter.create().apply { + setUrlWithoutDomain(element.attr("href")) + name = element.attr("title") + date_upload = parseRelativeDate(element.selectFirst("span + span")?.text()) + } + + private fun parseRelativeDate(date: String?): Long { + if (date == null) { + return 0L + } + + val trimmedDate = date.split(" ") + if (trimmedDate.size != 3 && trimmedDate[2] != "ago") return 0L + val number = trimmedDate[0].toIntOrNull() ?: return 0L + val unit = trimmedDate[1].removeSuffix("s") // Remove 's' suffix + val now = Calendar.getInstance() + + val javaUnit = when (unit) { + "year", "yr" -> Calendar.YEAR + "month" -> Calendar.MONTH + "week", "wk" -> Calendar.WEEK_OF_MONTH + "day" -> Calendar.DAY_OF_MONTH + "hour", "hr" -> Calendar.HOUR + "minute", "min" -> Calendar.MINUTE + "second", "sec" -> Calendar.SECOND + else -> return 0L + } + + now.add(javaUnit, -number) + + return now.timeInMillis + } + + override fun chapterListSelector() = "a.cairo" + + // Details + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + document.selectFirst("div.serie-info")?.let { info -> + description = info.selectFirst("div.description-content")?.text() + author = info.selectFirst("span:containsOwn(Author) + span")?.text() + artist = info.selectFirst("span:containsOwn(Artist) + span")?.text() + status = info.selectFirst("span:containsOwn(Status) + span")?.text().toStatus() + genre = info.select("div.genre-link").joinToString { it.text() } + } + } + + private fun String?.toStatus() = when { + this == null -> SManga.UNKNOWN + this.contains("Ongoing") -> SManga.ONGOING + this.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + // Pages + override fun pageListParse(document: Document): List { + val chapterUrl = document.location() + return document.select("div.image-skeleton img") + .filterNot { it.attr("data-src").isEmpty() } + .mapIndexed { i, img -> Page(i, chapterUrl, img.attr("data-src")) } + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + + // Filter + override fun getFilterList() = FilterList( + TypeFilter(), + Filter.Header("Press \"Reset\" to attempt to load genres"), + GenreFilter(genres), + YearFilter(), + StatusFilter(), + OrderFilter(), + ) + + private var genres = emptyList>() + + private fun parseGenres(document: Document): List> { + return document.select("li:has(input[name=\"genre[]\"])") + .map { + Pair(it.selectFirst("label")!!.text(), it.selectFirst("input")!!.attr("value")) + } + } }