Remove SocialComics source. (#7466)
This commit is contained in:
parent
940d1ec934
commit
462e270824
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
|
@ -1,16 +0,0 @@
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'Social Comics'
|
|
||||||
pkgNameSuffix = 'pt.socialcomics'
|
|
||||||
extClass = '.SocialComics'
|
|
||||||
extVersionCode = 2
|
|
||||||
libVersion = '1.2'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(':lib-ratelimit')
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
|
@ -1,396 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.pt.socialcomics
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.text.InputType
|
|
||||||
import android.widget.Toast
|
|
||||||
import com.github.salomonbrys.kotson.array
|
|
||||||
import com.github.salomonbrys.kotson.get
|
|
||||||
import com.github.salomonbrys.kotson.int
|
|
||||||
import com.github.salomonbrys.kotson.jsonObject
|
|
||||||
import com.github.salomonbrys.kotson.obj
|
|
||||||
import com.github.salomonbrys.kotson.string
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
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 okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import androidx.preference.EditTextPreference as AndroidXEditTextPreference
|
|
||||||
import androidx.preference.PreferenceScreen as AndroidXPreferenceScreen
|
|
||||||
|
|
||||||
class SocialComics : HttpSource(), ConfigurableSource {
|
|
||||||
|
|
||||||
override val name = "Social Comics"
|
|
||||||
|
|
||||||
override val baseUrl = "https://socialcomics.com.br"
|
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
|
||||||
|
|
||||||
override val supportsLatest = false
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
||||||
.addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS))
|
|
||||||
.addInterceptor(::authIntercept)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
|
||||||
.add("Referer", "$baseUrl/home")
|
|
||||||
.add("User-Agent", USER_AGENT)
|
|
||||||
|
|
||||||
private fun sourceHeadersBuilder(): Headers.Builder = headersBuilder()
|
|
||||||
.add("Accept", ACCEPT_JSON)
|
|
||||||
.add("Origin", baseUrl)
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val email: String
|
|
||||||
get() = preferences.getString(EMAIL_PREF_KEY, "")!!
|
|
||||||
|
|
||||||
private val password: String
|
|
||||||
get() = preferences.getString(PASSWORD_PREF_KEY, "")!!
|
|
||||||
|
|
||||||
private var apiToken: String
|
|
||||||
get() = preferences.getString(API_TOKEN_PREF_KEY, "")!!
|
|
||||||
set(value) { preferences.edit().putString(API_TOKEN_PREF_KEY, value).apply() }
|
|
||||||
|
|
||||||
private var userHash: String = ""
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
val newHeaders = sourceHeadersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/home")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET("$SERVICE_URL/api/mobile/home/list/web", newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val result = response.asJson().obj
|
|
||||||
|
|
||||||
val freeTrack = result["tracks"].array
|
|
||||||
.firstOrNull {
|
|
||||||
it.obj["name"].string.contains("Grátis")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freeTrack != null) {
|
|
||||||
val popularMangas = freeTrack.obj["comics"]["items"].array
|
|
||||||
.map { popularMangaItemParse(it.obj) }
|
|
||||||
|
|
||||||
return MangasPage(popularMangas, hasNextPage = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(emptyList(), hasNextPage = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
|
||||||
title = obj["name"].string
|
|
||||||
thumbnail_url = obj["thumb"].string
|
|
||||||
url = "/quadrinho/${obj["hash"].string}?monetize=${obj["monetize"].int}"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val newHeaders = sourceHeadersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/pesquisa")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val endpointUrl = "$SERVICE_URL/api/mobile/search/".toHttpUrlOrNull()!!.newBuilder()
|
|
||||||
.addEncodedPathSegment(query)
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(endpointUrl, newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val result = response.asJson().obj
|
|
||||||
|
|
||||||
val searchMangas = result["comics"]["items"].array
|
|
||||||
.filter { it.obj["monetize"].int == 0 }
|
|
||||||
.map { searchMangaItemParse(it.obj) }
|
|
||||||
|
|
||||||
return MangasPage(searchMangas, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
|
||||||
title = obj["name"].string
|
|
||||||
thumbnail_url = obj["thumb"].string
|
|
||||||
url = "/quadrinho/${obj["hash"].string}?monetize=${obj["monetize"].int}"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
val hash = manga.url
|
|
||||||
.substringAfterLast("/")
|
|
||||||
.substringBefore("?")
|
|
||||||
|
|
||||||
val newHeaders = sourceHeadersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/quadrinho/$hash")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET("$SERVICE_URL/api/mobile/comic/detail/$hash", newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
|
||||||
val apiResult = response.asJson().obj
|
|
||||||
|
|
||||||
title = apiResult["name"].string
|
|
||||||
author = apiResult["script"].string
|
|
||||||
description = apiResult["description"].string +
|
|
||||||
"\n\nEdição: #${apiResult["edition"].int}"
|
|
||||||
genre = apiResult["tags"].array
|
|
||||||
.joinToString { it.obj["name"].string }
|
|
||||||
status = SManga.COMPLETED
|
|
||||||
thumbnail_url = apiResult["thumb"].string
|
|
||||||
|
|
||||||
if (apiResult["art"].string != "Não Informado") {
|
|
||||||
artist = apiResult["art"].string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
if (manga.url.substringAfter("?monetize=") == "1") {
|
|
||||||
return Observable.error(Exception(ERROR_PAID_CONTENT))
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.fetchChapterList(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val apiResult = response.asJson().obj
|
|
||||||
|
|
||||||
val chapter = SChapter.create().apply {
|
|
||||||
name = "Quadrinho"
|
|
||||||
scanlator = apiResult["publisher"]["name"].string
|
|
||||||
url = "/leitor/${apiResult["hash"].string}"
|
|
||||||
}
|
|
||||||
|
|
||||||
return listOf(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val newHeaders = sourceHeadersBuilder()
|
|
||||||
.set("Referer", baseUrl + chapter.url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val hash = chapter.url.substringAfterLast("/")
|
|
||||||
|
|
||||||
return GET("$SERVICE_URL/api/mobile/comic/pages/$hash", newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val apiResult = response.asJson().obj
|
|
||||||
val hash = apiResult["hash"].string
|
|
||||||
val comicUrl = "$baseUrl/leitor/$hash"
|
|
||||||
|
|
||||||
return apiResult["pages"].array
|
|
||||||
.mapIndexed { i, el -> Page(i, comicUrl, el.obj["page"].string) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Accept", ACCEPT_IMAGE)
|
|
||||||
.add("Referer", page.url)
|
|
||||||
.removeAll("Origin")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(page.imageUrl!!, newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: AndroidXPreferenceScreen) {
|
|
||||||
val emailPref = AndroidXEditTextPreference(screen.context).apply {
|
|
||||||
key = EMAIL_PREF_KEY
|
|
||||||
title = EMAIL_PREF_TITLE
|
|
||||||
setDefaultValue("")
|
|
||||||
summary = EMAIL_PREF_SUMMARY
|
|
||||||
dialogTitle = EMAIL_PREF_TITLE
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val res = preferences.edit()
|
|
||||||
.putString(EMAIL_PREF_KEY, newValue as String)
|
|
||||||
.putString(API_TOKEN_PREF_KEY, "")
|
|
||||||
.commit()
|
|
||||||
|
|
||||||
Toast.makeText(screen.context, TOAST_RESTART_TO_APPLY, Toast.LENGTH_LONG).show()
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val passwordPref = AndroidXEditTextPreference(screen.context).apply {
|
|
||||||
key = PASSWORD_PREF_KEY
|
|
||||||
title = PASSWORD_PREF_TITLE
|
|
||||||
setDefaultValue("")
|
|
||||||
summary = PASSWORD_PREF_SUMMARY
|
|
||||||
dialogTitle = PASSWORD_PREF_TITLE
|
|
||||||
|
|
||||||
setOnBindEditTextListener {
|
|
||||||
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val res = preferences.edit()
|
|
||||||
.putString(PASSWORD_PREF_KEY, newValue as String)
|
|
||||||
.putString(API_TOKEN_PREF_KEY, "")
|
|
||||||
.commit()
|
|
||||||
|
|
||||||
Toast.makeText(screen.context, TOAST_RESTART_TO_APPLY, Toast.LENGTH_LONG).show()
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.addPreference(emailPref)
|
|
||||||
screen.addPreference(passwordPref)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
|
||||||
throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
private fun authIntercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
|
|
||||||
if (request.url.toString().contains(THUMBNAIL_CDN)) {
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (email.isEmpty() || password.isEmpty()) {
|
|
||||||
throw IOException(ERROR_CREDENTIALS_MISSING)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always do logout at the first time to reset the token.
|
|
||||||
if (userHash.isEmpty() && apiToken.isNotEmpty()) {
|
|
||||||
doLogout(chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apiToken.isEmpty()) {
|
|
||||||
doLogin(chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
val authRequest = request.newBuilder()
|
|
||||||
.addHeader("Authorization", "Bearer $apiToken")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return chain.proceed(authRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doLogin(chain: Interceptor.Chain) {
|
|
||||||
val loginPayload = jsonObject(
|
|
||||||
"device" to jsonObject(
|
|
||||||
"device" to USER_AGENT,
|
|
||||||
"device_id" to "window.navigator.userAgent.replace(/\\d+/g, '')",
|
|
||||||
"platform" to "web"
|
|
||||||
),
|
|
||||||
"email" to email,
|
|
||||||
"facebook_id" to null,
|
|
||||||
"password" to password
|
|
||||||
)
|
|
||||||
|
|
||||||
val loginBody = RequestBody.create(JSON_MEDIA_TYPE, loginPayload.toString())
|
|
||||||
|
|
||||||
val loginHeaders = sourceHeadersBuilder()
|
|
||||||
.add("Content-Type", loginBody.contentType().toString())
|
|
||||||
.add("Content-Length", loginBody.contentLength().toString())
|
|
||||||
.add("Referer", "$baseUrl/login")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val loginRequest = POST("$SERVICE_URL/api/mobile/login", loginHeaders, loginBody)
|
|
||||||
val response = chain.proceed(loginRequest)
|
|
||||||
|
|
||||||
if (response.code != 200) {
|
|
||||||
throw IOException(ERROR_CANNOT_LOGIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
val apiResult = response.asJson().obj
|
|
||||||
|
|
||||||
apiToken = apiResult["api_token"].string
|
|
||||||
userHash = apiResult["hash"].string
|
|
||||||
|
|
||||||
response.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doLogout(chain: Interceptor.Chain) {
|
|
||||||
val logoutPayload = jsonObject(
|
|
||||||
"device" to jsonObject(
|
|
||||||
"device_id" to "window.navigator.userAgent.replace(/\\d+/g, '')"
|
|
||||||
),
|
|
||||||
"hash_master" to userHash
|
|
||||||
)
|
|
||||||
|
|
||||||
val logoutBody = logoutPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val logoutHeaders = sourceHeadersBuilder()
|
|
||||||
.add("Content-Type", logoutBody.contentType().toString())
|
|
||||||
.add("Content-Length", logoutBody.contentLength().toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val logoutRequest = POST("$SERVICE_URL/api/mobile/logout", logoutHeaders, logoutBody)
|
|
||||||
val response = chain.proceed(logoutRequest)
|
|
||||||
|
|
||||||
if (response.code != 200) {
|
|
||||||
throw IOException(ERROR_CANNOT_RENEW_TOKEN)
|
|
||||||
}
|
|
||||||
|
|
||||||
apiToken = ""
|
|
||||||
userHash = ""
|
|
||||||
|
|
||||||
response.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.asJson(): JsonElement = JsonParser.parseString(body!!.string())
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SERVICE_URL = "https://service.socialcomics.com.br"
|
|
||||||
private const val THUMBNAIL_CDN = "amazonaws.com"
|
|
||||||
|
|
||||||
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
|
||||||
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"
|
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36"
|
|
||||||
|
|
||||||
private const val EMAIL_PREF_KEY = "email"
|
|
||||||
private const val EMAIL_PREF_TITLE = "E-mail"
|
|
||||||
private const val EMAIL_PREF_SUMMARY = "Defina aqui o seu e-mail para o login."
|
|
||||||
|
|
||||||
private const val PASSWORD_PREF_KEY = "password"
|
|
||||||
private const val PASSWORD_PREF_TITLE = "Senha"
|
|
||||||
private const val PASSWORD_PREF_SUMMARY = "Defina aqui a sua senha para o login."
|
|
||||||
|
|
||||||
private const val API_TOKEN_PREF_KEY = "api_token"
|
|
||||||
|
|
||||||
private const val ERROR_CANNOT_LOGIN = "Não foi possível realizar o login. Verifique suas informações."
|
|
||||||
private const val ERROR_CANNOT_RENEW_TOKEN = "Não foi possível renovar o token de autenticação."
|
|
||||||
private const val ERROR_CREDENTIALS_MISSING = "Informações de login incompletas. Revise as configurações."
|
|
||||||
private const val ERROR_PAID_CONTENT = "O quadrinho não está disponível no pacote gratuito."
|
|
||||||
|
|
||||||
private const val TOAST_RESTART_TO_APPLY = "Reinicie o Tachiyomi para aplicar as novas configurações."
|
|
||||||
|
|
||||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue