ArgosScan: Change CMS (#6931)

* Change CMS

* Use sortedByDescend

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
Chopper 2025-01-03 20:53:30 -03:00 committed by Draff
parent 06dd598b45
commit f72e042cce
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
2 changed files with 72 additions and 312 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Argos Scan' extName = 'Argos Scan'
extClass = '.ArgosScan' extClass = '.ArgosScan'
extVersionCode = 23 extVersionCode = 24
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,374 +1,134 @@
package eu.kanade.tachiyomi.extension.pt.argosscan package eu.kanade.tachiyomi.extension.pt.argosscan
import android.app.Application
import android.content.SharedPreferences
import android.text.InputType
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.decodeFromString import okhttp3.HttpUrl.Companion.toHttpUrl
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.Injekt import org.jsoup.nodes.Document
import uy.kohesive.injekt.api.get import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
class ArgosScan : HttpSource(), ConfigurableSource { class ArgosScan : ParsedHttpSource() {
// Website changed from Madara to a custom CMS.
override val versionId = 2
override val name = "Argos Scan" override val name = "Argos Scan"
override val baseUrl = "http://argosscan.com" override val baseUrl = "https://argoscomics.online"
override val lang = "pt-BR" override val lang = "pt-BR"
override val supportsLatest = false override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(::loginIntercept) .addInterceptor { chain ->
.rateLimit(1, 2, TimeUnit.SECONDS) val response = chain.proceed(chain.request())
if (response.request.url.pathSegments.any { it.equals("pagina-de-login", true) }) {
throw IOException("Faça login na WebView")
}
response
}
.build() .build()
private val json: Json by injectLazy() // Website changed custom CMS.
override val versionId = 3
private val preferences: SharedPreferences by lazy { // ============================ Popular ======================================
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
}
private var token: String? = null override fun popularMangaSelector() = ".card__main._grid:not(:has(a[href*=novel]))"
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun popularMangaFromElement(element: Element) = SManga.create().apply {
.add("Token", "") with(element.selectFirst("h3.card__title")!!) {
title = text()
private fun genericMangaFromObject(project: ArgosProjectDto): SManga = SManga.create().apply { setUrlWithoutDomain(selectFirst("a")!!.absUrl("href"))
title = project.name!!
url = "/obras/${project.id}"
thumbnail_url = if (project.cover!! == "default.jpg") {
"$baseUrl/img/default.jpg"
} else {
"$baseUrl/images/${project.id}/${project.cover}"
} }
thumbnail_url = element.selectFirst("img")?.absUrl("src")
} }
override fun popularMangaRequest(page: Int): Request { override fun popularMangaNextPageSelector() = null
val payload = buildPopularQueryPayload(page)
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE) // ============================ Latest ======================================
val newHeaders = headersBuilder() override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body) override fun latestUpdatesSelector() = throw UnsupportedOperationException()
}
override fun popularMangaParse(response: Response): MangasPage { override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
val result = response.parseAs<ArgosResponseDto<ArgosProjectListDto>>()
if (result.data == null) { override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
throw Exception(REQUEST_ERROR)
}
val projectList = result.data["getProjects"]!! // ============================ Search ======================================
val mangaList = projectList.projects
.map(::genericMangaFromObject)
val hasNextPage = projectList.currentPage < projectList.totalPages
return MangasPage(mangaList, hasNextPage)
}
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val payload = buildSearchQueryPayload(query, page) val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("s", query)
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build() .build()
return GET(url, headers)
return POST(GRAPHQL_URL, newHeaders, body)
} }
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) override fun searchMangaSelector() = popularMangaSelector()
override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun mangaDetailsRequest(manga: SManga): Request { override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
val mangaId = manga.url.substringAfter("obras/").toInt()
val payload = buildMangaDetailsQueryPayload(mangaId) // ============================ Details =====================================
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE) override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text()
val newHeaders = headersBuilder() thumbnail_url = document.selectFirst("img.story__thumbnail-image")?.absUrl("src")
.add("Content-Length", body.contentLength().toString()) description = document.selectFirst(".story__summary p")?.text()
.add("Content-Type", body.contentType().toString()) document.selectFirst(".story__status")?.let {
.build() status = when (it.text().trim().lowercase()) {
"em andamento" -> SManga.ONGOING
return POST(GRAPHQL_URL, newHeaders, body) else -> SManga.UNKNOWN
} }
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val result = response.parseAs<ArgosResponseDto<ArgosProjectDto>>()
if (result.data == null) {
throw Exception(REQUEST_ERROR)
} }
setUrlWithoutDomain(document.location())
val project = result.data["project"]!!
title = project.name!!
thumbnail_url = if (project.cover!! == "default.jpg") {
"$baseUrl/img/default.jpg"
} else {
"$baseUrl/images/${project.id}/${project.cover}"
}
description = project.description.orEmpty()
author = project.authors.orEmpty().joinToString()
status = SManga.ONGOING
genre = project.tags.orEmpty().sortedBy(ArgosTagDto::name).joinToString { it.name }
} }
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) // ============================ Chapter =====================================
override fun chapterListSelector() = ".chapter-group__list li:has(a)"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
with(element.selectFirst("a")!!) {
name = text()
setUrlWithoutDomain(absUrl("href"))
}
element.selectFirst(".chapter-group__list-item-date")?.attr("datetime")?.let {
date_upload = it.parseDate()
}
}
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<ArgosResponseDto<ArgosProjectDto>>() return super.chapterListParse(response).sortedByDescending(SChapter::chapter_number)
if (result.data == null) {
throw Exception(REQUEST_ERROR)
}
return result.data["project"]!!.chapters.map(::chapterFromObject)
} }
private fun chapterFromObject(chapter: ArgosChapterDto): SChapter = SChapter.create().apply { // ============================ Pages =======================================
name = chapter.title!!
chapter_number = chapter.number?.toFloat() ?: -1f
scanlator = this@ArgosScan.name
date_upload = chapter.createAt!!.toDate()
url = "/leitor/${chapter.id}"
}
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url override fun pageListParse(document: Document): List<Page> {
return document.select("#chapter-content img").mapIndexed { index, element ->
override fun pageListRequest(chapter: SChapter): Request { Page(index, imageUrl = element.absUrl("src"))
if (chapter.url.removePrefix("/leitor/").toIntOrNull() != null) {
throw Exception(REFRESH_WARNING)
}
val chapterId = chapter.url.substringAfter("leitor/")
val payload = buildPagesQueryPayload(chapterId)
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<JsonElement>().jsonObject
if (result["errors"] != null) {
throw Exception(REQUEST_ERROR)
}
val chapterDto = result["data"]!!.jsonObject["getChapters"]!!.jsonObject["chapters"]!!.jsonArray[0]
.let { json.decodeFromJsonElement<ArgosChapterDto>(it) }
val referer = "$baseUrl/leitor/${chapterDto.id}"
return chapterDto.images.orEmpty().mapIndexed { i, page ->
Page(i, referer, "$baseUrl/images/${chapterDto.project!!.id}/$page")
} }
} }
override fun imageUrlParse(response: Response): String = "" override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request { // ============================== Utilities ==================================
val newHeaders = headersBuilder()
.set("Referer", page.url)
.build()
return GET(page.imageUrl!!, newHeaders) private fun String.parseDate(): Long {
return try { dateFormat.parse(this.trim())!!.time } catch (_: Exception) { 0L }
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val emailPref = EditTextPreference(screen.context).apply {
key = EMAIL_PREF_KEY
title = EMAIL_PREF_TITLE
summary = EMAIL_PREF_SUMMARY
setDefaultValue("")
dialogTitle = EMAIL_PREF_TITLE
dialogMessage = EMAIL_PREF_DIALOG
setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
}
setOnPreferenceChangeListener { _, newValue ->
token = null
preferences.edit()
.putString(EMAIL_PREF_KEY, newValue as String)
.commit()
}
}
val passwordPref = EditTextPreference(screen.context).apply {
key = PASSWORD_PREF_KEY
title = PASSWORD_PREF_TITLE
summary = PASSWORD_PREF_SUMMARY
setDefaultValue("")
dialogTitle = PASSWORD_PREF_TITLE
dialogMessage = PASSWORD_PREF_DIALOG
setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
setOnPreferenceChangeListener { _, newValue ->
token = null
preferences.edit()
.putString(PASSWORD_PREF_KEY, newValue as String)
.commit()
}
}
screen.addPreference(emailPref)
screen.addPreference(passwordPref)
}
private fun loginIntercept(chain: Interceptor.Chain): Response {
if (chain.request().url.toString().contains("graphql").not()) {
return chain.proceed(chain.request())
}
val email = preferences.getString(EMAIL_PREF_KEY, "")
val password = preferences.getString(PASSWORD_PREF_KEY, "")
if (!email.isNullOrEmpty() && !password.isNullOrEmpty() && token.isNullOrEmpty()) {
val loginResponse = chain.proceed(loginRequest(email, password))
if (!loginResponse.headers["Content-Type"].orEmpty().contains("application/json")) {
loginResponse.close()
throw IOException(CLOUDFLARE_ERROR)
}
val loginResult = json.parseToJsonElement(loginResponse.body.string()).jsonObject
if (loginResult["errors"] != null) {
loginResponse.close()
val errorMessage = runCatching {
loginResult["errors"]!!.jsonArray[0].jsonObject["message"]?.jsonPrimitive?.content
}
throw IOException(errorMessage.getOrNull() ?: REQUEST_ERROR)
}
token = loginResult["data"]!!
.jsonObject["login"]!!
.jsonObject["token"]!!
.jsonPrimitive.content
loginResponse.close()
}
if (!token.isNullOrEmpty()) {
val authorizedRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(authorizedRequest)
}
return chain.proceed(chain.request())
}
private fun loginRequest(email: String, password: String): Request {
val payload = buildLoginMutationQueryPayload(email, password)
val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", body.contentLength().toString())
.add("Content-Type", body.contentType().toString())
.build()
return POST(GRAPHQL_URL, newHeaders, body)
}
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromString(it.body.string())
}
private fun String.toDate(): Long {
return runCatching { DATE_PARSER.parse(this)?.time }
.getOrNull() ?: 0L
}
companion object { companion object {
private const val GRAPHQL_URL = "https://argosscan.com/graphql" val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
private const val EMAIL_PREF_KEY = "email"
private const val EMAIL_PREF_TITLE = "E-mail"
private const val EMAIL_PREF_SUMMARY = "Defina o e-mail de sua conta no site."
private const val EMAIL_PREF_DIALOG = "Deixe em branco caso o site torne o login opcional."
private const val PASSWORD_PREF_KEY = "password"
private const val PASSWORD_PREF_TITLE = "Senha"
private const val PASSWORD_PREF_SUMMARY = "Defina a senha de sua conta no site."
private const val PASSWORD_PREF_DIALOG = EMAIL_PREF_DIALOG
private const val CLOUDFLARE_ERROR = "Falha ao contornar o Cloudflare."
private const val REQUEST_ERROR = "Erro na requisição. Tente novamente mais tarde."
private const val REFRESH_WARNING = "Atualize a lista de capítulos para atualizar os IDs."
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private val DATE_PARSER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale("pt", "BR"))
}
} }
} }