diff --git a/src/pt/argosscan/build.gradle b/src/pt/argosscan/build.gradle index 1f77f3df2..c88bac50f 100644 --- a/src/pt/argosscan/build.gradle +++ b/src/pt/argosscan/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Argos Scan' extClass = '.ArgosScan' - extVersionCode = 23 + extVersionCode = 24 } apply from: "$rootDir/common.gradle" diff --git a/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt b/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt index a10921ada..c65a6a5de 100644 --- a/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt +++ b/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt @@ -1,374 +1,134 @@ 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.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.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.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 eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element import java.io.IOException import java.text.SimpleDateFormat import java.util.Locale -import java.util.concurrent.TimeUnit -class ArgosScan : HttpSource(), ConfigurableSource { - - // Website changed from Madara to a custom CMS. - override val versionId = 2 +class ArgosScan : ParsedHttpSource() { override val name = "Argos Scan" - override val baseUrl = "http://argosscan.com" + override val baseUrl = "https://argoscomics.online" override val lang = "pt-BR" override val supportsLatest = false override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .addInterceptor(::loginIntercept) - .rateLimit(1, 2, TimeUnit.SECONDS) + .addInterceptor { chain -> + 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() - private val json: Json by injectLazy() + // Website changed custom CMS. + override val versionId = 3 - private val preferences: SharedPreferences by lazy { - Injekt.get().getSharedPreferences("source_$id", 0x0000) - } + // ============================ Popular ====================================== + 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() - .add("Token", "") - - private fun genericMangaFromObject(project: ArgosProjectDto): SManga = SManga.create().apply { - 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}" + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + with(element.selectFirst("h3.card__title")!!) { + title = text() + setUrlWithoutDomain(selectFirst("a")!!.absUrl("href")) } + thumbnail_url = element.selectFirst("img")?.absUrl("src") } - override fun popularMangaRequest(page: Int): Request { - val payload = buildPopularQueryPayload(page) + override fun popularMangaNextPageSelector() = null - val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE) + // ============================ Latest ====================================== - val newHeaders = headersBuilder() - .add("Content-Length", body.contentLength().toString()) - .add("Content-Type", body.contentType().toString()) - .build() + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() - return POST(GRAPHQL_URL, newHeaders, body) - } + override fun latestUpdatesSelector() = throw UnsupportedOperationException() - override fun popularMangaParse(response: Response): MangasPage { - val result = response.parseAs>() + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException() - if (result.data == null) { - throw Exception(REQUEST_ERROR) - } + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException() - val projectList = result.data["getProjects"]!! - - 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() + // ============================ Search ====================================== override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val payload = buildSearchQueryPayload(query, page) - - val body = payload.toString().toRequestBody(JSON_MEDIA_TYPE) - - val newHeaders = headersBuilder() - .add("Content-Length", body.contentLength().toString()) - .add("Content-Type", body.contentType().toString()) + val url = baseUrl.toHttpUrl().newBuilder() + .addQueryParameter("s", query) .build() - - return POST(GRAPHQL_URL, newHeaders, body) + return GET(url, headers) } - 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 { - val mangaId = manga.url.substringAfter("obras/").toInt() + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() - val payload = buildMangaDetailsQueryPayload(mangaId) + // ============================ Details ===================================== - 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 mangaDetailsParse(response: Response): SManga = SManga.create().apply { - val result = response.parseAs>() - - if (result.data == null) { - throw Exception(REQUEST_ERROR) + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.selectFirst("h1")!!.text() + thumbnail_url = document.selectFirst("img.story__thumbnail-image")?.absUrl("src") + description = document.selectFirst(".story__summary p")?.text() + document.selectFirst(".story__status")?.let { + status = when (it.text().trim().lowercase()) { + "em andamento" -> SManga.ONGOING + else -> SManga.UNKNOWN + } } - - 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 } + setUrlWithoutDomain(document.location()) } - 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 { - val result = response.parseAs>() - - if (result.data == null) { - throw Exception(REQUEST_ERROR) - } - - return result.data["project"]!!.chapters.map(::chapterFromObject) + return super.chapterListParse(response).sortedByDescending(SChapter::chapter_number) } - private fun chapterFromObject(chapter: ArgosChapterDto): SChapter = SChapter.create().apply { - name = chapter.title!! - chapter_number = chapter.number?.toFloat() ?: -1f - scanlator = this@ArgosScan.name - date_upload = chapter.createAt!!.toDate() - url = "/leitor/${chapter.id}" - } + // ============================ Pages ======================================= - override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url - - override fun pageListRequest(chapter: SChapter): Request { - 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 { - val result = response.parseAs().jsonObject - - if (result["errors"] != null) { - throw Exception(REQUEST_ERROR) - } - - val chapterDto = result["data"]!!.jsonObject["getChapters"]!!.jsonObject["chapters"]!!.jsonArray[0] - .let { json.decodeFromJsonElement(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 pageListParse(document: Document): List { + return document.select("#chapter-content img").mapIndexed { index, element -> + Page(index, imageUrl = element.absUrl("src")) } } - override fun imageUrlParse(response: Response): String = "" + override fun imageUrlParse(document: Document) = "" - override fun imageRequest(page: Page): Request { - val newHeaders = headersBuilder() - .set("Referer", page.url) - .build() + // ============================== Utilities ================================== - 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 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 { - private const val GRAPHQL_URL = "https://argosscan.com/graphql" - - 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")) - } + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) } }