Add support to login at Tsuki. (#7715)
This commit is contained in:
parent
7916260467
commit
9ea926b8a8
|
@ -6,7 +6,7 @@ ext {
|
||||||
extName = 'Tsuki Mangás'
|
extName = 'Tsuki Mangás'
|
||||||
pkgNameSuffix = 'pt.tsukimangas'
|
pkgNameSuffix = 'pt.tsukimangas'
|
||||||
extClass = '.TsukiMangas'
|
extClass = '.TsukiMangas'
|
||||||
extVersionCode = 19
|
extVersionCode = 20
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
containsNsfw = true
|
containsNsfw = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
package eu.kanade.tachiyomi.extension.pt.tsukimangas
|
package eu.kanade.tachiyomi.extension.pt.tsukimangas
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.text.InputType
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||||
import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
|
import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
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.MangasPage
|
||||||
|
@ -11,16 +18,22 @@ 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.HttpSource
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
|
@ -28,7 +41,7 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Nsfw
|
@Nsfw
|
||||||
class TsukiMangas : HttpSource() {
|
class TsukiMangas : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val name = "Tsuki Mangás"
|
override val name = "Tsuki Mangás"
|
||||||
|
|
||||||
|
@ -42,17 +55,29 @@ class TsukiMangas : HttpSource() {
|
||||||
.addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1))
|
.addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1))
|
||||||
.addInterceptor(SpecificHostRateLimitInterceptor(CDN_1_URL, 1, period = 2))
|
.addInterceptor(SpecificHostRateLimitInterceptor(CDN_1_URL, 1, period = 2))
|
||||||
.addInterceptor(SpecificHostRateLimitInterceptor(CDN_2_URL, 1, period = 2))
|
.addInterceptor(SpecificHostRateLimitInterceptor(CDN_2_URL, 1, period = 2))
|
||||||
.addInterceptor(::tsukiPermissionIntercept)
|
.addInterceptor(::tsukiAuthIntercept)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val usernameOrEmail: String
|
||||||
|
get() = preferences.getString(USERNAME_OR_EMAIL_PREF_KEY, "")!!
|
||||||
|
|
||||||
|
private val password: String
|
||||||
|
get() = preferences.getString(PASSWORD_PREF_KEY, "")!!
|
||||||
|
|
||||||
|
private var apiToken: String? = null
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
.add("Accept", ACCEPT)
|
.add("Accept", ACCEPT)
|
||||||
.add("Accept-Language", ACCEPT_LANGUAGE)
|
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||||
.add("User-Agent", USER_AGENT)
|
.add("User-Agent", USER_AGENT)
|
||||||
.add("Referer", baseUrl)
|
.add("Referer", baseUrl)
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/api/v2/mangas?page=$page&title=&adult_content=false&filter=0", headers)
|
return GET("$baseUrl/api/v2/mangas?page=$page&title=&adult_content=false&filter=0", headers)
|
||||||
}
|
}
|
||||||
|
@ -295,17 +320,98 @@ class TsukiMangas : HttpSource() {
|
||||||
return GET(page.imageUrl!!, newHeaders)
|
return GET(page.imageUrl!!, newHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tsukiPermissionIntercept(chain: Interceptor.Chain): Response {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
val response = chain.proceed(chain.request())
|
val usernameOrEmailPref = EditTextPreference(screen.context).apply {
|
||||||
|
key = USERNAME_OR_EMAIL_PREF_KEY
|
||||||
|
title = USERNAME_OR_EMAIL_PREF_TITLE
|
||||||
|
setDefaultValue("")
|
||||||
|
summary = USERNAME_OR_EMAIL_PREF_SUMMARY
|
||||||
|
dialogTitle = USERNAME_OR_EMAIL_PREF_TITLE
|
||||||
|
|
||||||
if (response.code == 403) {
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
apiToken = null
|
||||||
|
|
||||||
|
preferences.edit()
|
||||||
|
.putString(USERNAME_OR_EMAIL_PREF_KEY, newValue as String)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val passwordPref = EditTextPreference(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 ->
|
||||||
|
apiToken = null
|
||||||
|
|
||||||
|
preferences.edit()
|
||||||
|
.putString(PASSWORD_PREF_KEY, newValue as String)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.addPreference(usernameOrEmailPref)
|
||||||
|
screen.addPreference(passwordPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tsukiAuthIntercept(chain: Interceptor.Chain): Response {
|
||||||
|
if (apiToken.isNullOrEmpty()) {
|
||||||
|
if (usernameOrEmail.isEmpty() || password.isEmpty()) {
|
||||||
|
throw IOException(ERROR_CREDENTIALS_MISSING)
|
||||||
|
}
|
||||||
|
|
||||||
|
val loginRequest = loginRequest(usernameOrEmail, password)
|
||||||
|
val loginResponse = chain.proceed(loginRequest)
|
||||||
|
|
||||||
|
// API returns 422 when the credentials are invalid.
|
||||||
|
if (loginResponse.code == 422) {
|
||||||
|
loginResponse.close()
|
||||||
|
throw IOException(ERROR_CANNOT_LOGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val loginResponseBody = loginResponse.body?.string().orEmpty()
|
||||||
|
val authResult = json.decodeFromString<TsukiAuthResultDto>(loginResponseBody)
|
||||||
|
|
||||||
|
apiToken = authResult.token
|
||||||
|
|
||||||
|
loginResponse.close()
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
loginResponse.close()
|
||||||
|
throw IOException(ERROR_LOGIN_FAILED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val authorizedRequest = chain.request().newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer $apiToken")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = chain.proceed(authorizedRequest)
|
||||||
|
|
||||||
|
// API returns 403 when User-Agent permission is disabled
|
||||||
|
// and returns 401 when the token is invalid.
|
||||||
|
if (response.code == 403 || response.code == 401) {
|
||||||
response.close()
|
response.close()
|
||||||
throw IOException(UA_DISABLED_MESSAGE)
|
throw IOException(if (response.code == 403) UA_DISABLED_MESSAGE else ERROR_INVALID_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loginRequest(usernameOrEmail: String, password: String): Request {
|
||||||
|
val authInfo = TsukiAuthRequestDto(usernameOrEmail, password)
|
||||||
|
val payload = json.encodeToString(authInfo).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
return POST("$baseUrl/api/v2/login", headers, payload)
|
||||||
|
}
|
||||||
|
|
||||||
private class Genre(name: String) : Filter.CheckBox(name)
|
private class Genre(name: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
private class DemographyFilter(demographies: List<String>) : Filter.Select<String>("Demografia", demographies.toTypedArray())
|
private class DemographyFilter(demographies: List<String>) : Filter.Select<String>("Demografia", demographies.toTypedArray())
|
||||||
|
@ -470,5 +576,24 @@ class TsukiMangas : HttpSource() {
|
||||||
|
|
||||||
const val PREFIX_ID_SEARCH = "id:"
|
const val PREFIX_ID_SEARCH = "id:"
|
||||||
private val ID_SEARCH_PATTERN = "^id:(\\d+)$".toRegex()
|
private val ID_SEARCH_PATTERN = "^id:(\\d+)$".toRegex()
|
||||||
|
|
||||||
|
private val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType()
|
||||||
|
|
||||||
|
private const val USERNAME_OR_EMAIL_PREF_KEY = "username_or_email"
|
||||||
|
private const val USERNAME_OR_EMAIL_PREF_TITLE = "Usuário ou E-mail"
|
||||||
|
private const val USERNAME_OR_EMAIL_PREF_SUMMARY = "Defina aqui o seu usuário ou 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 ERROR_CANNOT_LOGIN = "Não foi possível realizar o login. " +
|
||||||
|
"Revise suas informações nas configurações."
|
||||||
|
private const val ERROR_CREDENTIALS_MISSING = "Login necessário. " +
|
||||||
|
"Defina suas credenciais de acesso nas configurações."
|
||||||
|
private const val ERROR_LOGIN_FAILED = "Não foi possível realizar o login devido " +
|
||||||
|
"a um erro inesperado. Tente novamente mais tarde."
|
||||||
|
private const val ERROR_INVALID_TOKEN = "Token inválido. " +
|
||||||
|
"Revise suas informações nas configurações."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,17 @@ package eu.kanade.tachiyomi.extension.pt.tsukimangas
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TsukiAuthRequestDto(
|
||||||
|
val username: String,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TsukiAuthResultDto(
|
||||||
|
val token: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TsukiPaginatedDto(
|
data class TsukiPaginatedDto(
|
||||||
val data: List<TsukiMangaDto> = emptyList(),
|
val data: List<TsukiMangaDto> = emptyList(),
|
||||||
|
|
Loading…
Reference in New Issue