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:
Alessandro Jean 2022-09-24 23:34:50 -03:00 committed by GitHub
parent b414c82fe1
commit 6def50a7c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 349 additions and 18 deletions

View File

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

View File

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

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -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

View File

@ -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()
}
}

View File

@ -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()
)

View File

@ -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
}