Migrate Brazilian RS from Madara to individual extension (#13577)
* Migrate Brazilian RS from Madara to individual extension. * Remove unused imports.
This commit is contained in:
parent
b414c82fe1
commit
6def50a7c4
|
@ -1,20 +1,16 @@
|
|||
package eu.kanade.tachiyomi.extension.all.reaperscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ReaperScansFactory : SourceFactory {
|
||||
override fun createSources() = listOf(
|
||||
ReaperScansEn(),
|
||||
ReaperScansBr(),
|
||||
ReaperScansTr(),
|
||||
ReaperScansId(),
|
||||
ReaperScansFr()
|
||||
|
@ -53,19 +49,6 @@ class ReaperScansEn : ReaperScans(
|
|||
override val versionId = 2
|
||||
}
|
||||
|
||||
class ReaperScansBr : ReaperScans(
|
||||
"https://reaperscans.com.br",
|
||||
"pt-BR",
|
||||
SimpleDateFormat("dd/MM/yyyy", Locale.US)
|
||||
) {
|
||||
|
||||
override val id = 7767018058145795388
|
||||
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.rateLimit(1, 2, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
class ReaperScansTr : ReaperScans(
|
||||
"https://reaperscanstr.com",
|
||||
"tr",
|
||||
|
|
|
@ -17,7 +17,7 @@ class MadaraGenerator : ThemeSourceGenerator {
|
|||
MultiLang("MangaForFree.net", "https://mangaforfree.net", listOf("en", "ko", "all"), isNsfw = true, className = "MangaForFreeFactory", pkgName = "mangaforfree", overrideVersionCode = 1),
|
||||
MultiLang("Manhwa18.cc", "https://manhwa18.cc", listOf("en", "ko", "all"), isNsfw = true, className = "Manhwa18CcFactory", pkgName = "manhwa18cc", overrideVersionCode = 2),
|
||||
MultiLang("Olympus Scanlation", "https://olympusscanlation.com", listOf("es", "pt-BR")),
|
||||
MultiLang("Reaper Scans", "https://reaperscans.com", listOf("en", "pt-BR", "fr", "id", "tr"), className = "ReaperScansFactory", pkgName = "reaperscans", overrideVersionCode = 6),
|
||||
MultiLang("Reaper Scans", "https://reaperscans.com", listOf("en", "fr", "id", "tr"), className = "ReaperScansFactory", pkgName = "reaperscans", overrideVersionCode = 6),
|
||||
MultiLang("Seven King Scanlation", "https://sksubs.net", listOf("es", "en"), isNsfw = true),
|
||||
MultiLang("YugenMangas", "https://yugenmangas.com", listOf("es", "pt-BR"), overrideVersionCode = 3),
|
||||
SingleLang("1st Kiss Manga.love", "https://1stkissmanga.love", "en", className = "FirstKissMangaLove", overrideVersionCode = 1),
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Reaper Scans'
|
||||
pkgNameSuffix = 'pt.reaperscans'
|
||||
extClass = '.ReaperScans'
|
||||
extVersionCode = 31
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
|
@ -0,0 +1,233 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.reaperscans
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
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.JsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ReaperScans : HttpSource() {
|
||||
|
||||
override val name = "Reaper Scans"
|
||||
|
||||
override val baseUrl = "https://reaperscans.com.br"
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// Migrated from Madara to a custom CMS.
|
||||
override val versionId = 2
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(API_URL.toHttpUrl(), 1, 2)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Origin", baseUrl)
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val payloadObj = buildJsonObject {
|
||||
put("order", "desc")
|
||||
put("order_by", "total_views")
|
||||
put("series_status", "Ongoing")
|
||||
put("series_type", "Comic")
|
||||
put("tag_ids", JsonArray(emptyList()))
|
||||
}
|
||||
|
||||
val payload = payloadObj.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
val apiHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.add("Content-Type", payload.contentType().toString())
|
||||
.build()
|
||||
|
||||
return POST("$API_URL/series/querysearch", apiHeaders, payload)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val mangaList = response.parseAs<List<ReaperSeriesDto>>()
|
||||
.map(ReaperSeriesDto::toSManga)
|
||||
|
||||
return MangasPage(mangaList, hasNextPage = false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val payloadObj = buildJsonObject {
|
||||
put("order", "desc")
|
||||
put("order_by", "latest")
|
||||
put("series_status", "Ongoing")
|
||||
put("series_type", "Comic")
|
||||
put("tag_ids", JsonArray(emptyList()))
|
||||
}
|
||||
|
||||
val payload = payloadObj.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
val apiHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.add("Content-Type", payload.contentType().toString())
|
||||
.build()
|
||||
|
||||
return POST("$API_URL/series/querysearch", apiHeaders, payload)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val sortByFilter = filters.filterIsInstance<SortByFilter>().firstOrNull()
|
||||
val sortAscending = sortByFilter?.state?.ascending ?: false
|
||||
val sortProperty = sortByFilter?.selected ?: "total_views"
|
||||
|
||||
val status = filters.filterIsInstance<StatusFilter>()
|
||||
.firstOrNull()?.selected?.value ?: "Ongoing"
|
||||
|
||||
val payloadObj = buildJsonObject {
|
||||
put("order", if (sortAscending) "asc" else "desc")
|
||||
put("order_by", sortProperty)
|
||||
put("series_status", status)
|
||||
put("series_type", "Comic")
|
||||
put("tag_ids", JsonArray(emptyList()))
|
||||
}
|
||||
|
||||
val payload = payloadObj.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
val apiHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.add("Content-Type", payload.contentType().toString())
|
||||
.build()
|
||||
|
||||
val apiUrl = "$API_URL/series/querysearch".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
.toString()
|
||||
|
||||
return POST(apiUrl, apiHeaders, payload)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val query = response.request.url.queryParameter("q").orEmpty()
|
||||
|
||||
var mangaList = response.parseAs<List<ReaperSeriesDto>>()
|
||||
.map(ReaperSeriesDto::toSManga)
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
mangaList = mangaList.filter { it.title.contains(query, ignoreCase = true) }
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, hasNextPage = false)
|
||||
}
|
||||
|
||||
// Workaround to allow "Open in browser" use the real URL.
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(seriesDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
private fun seriesDetailsRequest(manga: SManga): Request {
|
||||
val seriesSlug = manga.url.substringAfterLast("/")
|
||||
|
||||
val apiHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.build()
|
||||
|
||||
return GET("$API_URL/series/$seriesSlug#${manga.status}", apiHeaders)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return response.parseAs<ReaperSeriesDto>().toSManga().apply {
|
||||
status = response.request.url.fragment?.toIntOrNull() ?: SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = seriesDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val result = response.parseAs<ReaperSeriesDto>()
|
||||
val seriesSlug = response.request.url.pathSegments.last()
|
||||
|
||||
return result.chapters.orEmpty()
|
||||
.map { it.toSChapter(seriesSlug) }
|
||||
.reversed()
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterId = chapter.url.substringAfterLast("#")
|
||||
|
||||
val apiHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.build()
|
||||
|
||||
return GET("$API_URL/series/chapter/$chapterId", apiHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return response.parseAs<ReaperReaderDto>().content?.images.orEmpty()
|
||||
.mapIndexed { i, url -> Page(i, "", "$API_URL/$url") }
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||
|
||||
override fun imageUrlParse(response: Response): String = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imageHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_IMAGE)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, imageHeaders)
|
||||
}
|
||||
|
||||
private fun getStatusList(): List<Status> = listOf(
|
||||
Status("Em andamento", "Ongoing"),
|
||||
Status("Em hiato", "Hiatus"),
|
||||
Status("Cancelado", "Dropped"),
|
||||
)
|
||||
|
||||
private fun getSortProperties(): List<SortProperty> = listOf(
|
||||
SortProperty("Título", "title"),
|
||||
SortProperty("Visualizações", "total_views"),
|
||||
SortProperty("Data de criação", "latest")
|
||||
)
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
StatusFilter(getStatusList()),
|
||||
SortByFilter(getSortProperties()),
|
||||
)
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
json.decodeFromString(it.body?.string().orEmpty())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
||||
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
||||
|
||||
const val API_URL = "https://api.reaperscans.com.br"
|
||||
|
||||
private val JSON_MEDIA_TYPE = "application/json".toMediaType()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.reaperscans
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class ReaperSeriesDto(
|
||||
val id: Int,
|
||||
@SerialName("series_slug") val slug: String,
|
||||
val author: String? = null,
|
||||
val description: String? = null,
|
||||
val studio: String? = null,
|
||||
val status: String? = null,
|
||||
val thumbnail: String,
|
||||
val title: String,
|
||||
val chapters: List<ReaperChapterDto>? = emptyList()
|
||||
) {
|
||||
|
||||
fun toSManga(): SManga = SManga.create().apply {
|
||||
title = this@ReaperSeriesDto.title
|
||||
author = this@ReaperSeriesDto.author
|
||||
artist = this@ReaperSeriesDto.studio
|
||||
description = this@ReaperSeriesDto.description?.let { Jsoup.parseBodyFragment(it).text() }
|
||||
thumbnail_url = "${ReaperScans.API_URL}/cover/$thumbnail"
|
||||
status = when (this@ReaperSeriesDto.status) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Hiatus" -> SManga.ON_HIATUS
|
||||
"Dropped" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
url = "/series/$slug"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ReaperChapterDto(
|
||||
val id: Int,
|
||||
@SerialName("chapter_name") val name: String,
|
||||
@SerialName("chapter_slug") val slug: String,
|
||||
val index: String,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
) {
|
||||
|
||||
fun toSChapter(seriesSlug: String): SChapter = SChapter.create().apply {
|
||||
name = this@ReaperChapterDto.name
|
||||
date_upload = runCatching { DATE_FORMAT.parse(createdAt.substringBefore("."))?.time }
|
||||
.getOrNull() ?: 0L
|
||||
url = "/series/$seriesSlug/$slug#$id"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale("pt", "BR"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ReaperReaderDto(
|
||||
val content: ReaperReaderContentDto? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReaperReaderContentDto(
|
||||
val images: List<String>? = emptyList()
|
||||
)
|
|
@ -0,0 +1,30 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.reaperscans
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
open class EnhancedSelect<T>(name: String, values: Array<T>) : Filter.Select<T>(name, values) {
|
||||
val selected: T
|
||||
get() = values[state]
|
||||
}
|
||||
|
||||
data class Status(val name: String, val value: String) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
class StatusFilter(statuses: List<Status>) : EnhancedSelect<Status>(
|
||||
"Status",
|
||||
statuses.toTypedArray()
|
||||
)
|
||||
|
||||
data class SortProperty(val name: String, val value: String) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
class SortByFilter(private val sortProperties: List<SortProperty>) : Filter.Sort(
|
||||
"Ordenar por",
|
||||
sortProperties.map { it.name }.toTypedArray(),
|
||||
Selection(1, ascending = false)
|
||||
) {
|
||||
val selected: String
|
||||
get() = sortProperties[state!!.index].value
|
||||
}
|
Loading…
Reference in New Issue