mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-22 02:12:42 +01:00
HeanCMS: Add login preference (#2071)
* Add login * Bump * Remove authHeaders from imageRequest * Make token nullable * Use /login api endpoint * Review changes * Throw error * Throw api error message * Reduce one day to prevent timezone issues * Fix no scheme found * Double parenthesis
This commit is contained in:
parent
c6763f5f62
commit
6901357ba1
@ -13,5 +13,9 @@ pref_show_paid_chapter_title=Display paid chapters
|
||||
pref_show_paid_chapter_summary_on=Paid chapters will appear.
|
||||
pref_show_paid_chapter_summary_off=Only free chapters will be displayed.
|
||||
url_changed_error=The URL of the series has changed. Migrate from %s to %s to update the URL
|
||||
pref_username_title=Username/Email
|
||||
pref_password_title=Password
|
||||
pref_credentials_summary=Ignored if empty.
|
||||
login_failed_unknown_error=Unknown error occurred while logging in
|
||||
paid_chapter_error=Paid chapter unavailable.
|
||||
id_not_found_error=Failed to get the ID for slug: %s
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 21
|
||||
baseVersionCode = 22
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.multisrc.heancms
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
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
|
||||
@ -14,7 +16,9 @@ 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.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
@ -43,6 +47,8 @@ abstract class HeanCms(
|
||||
|
||||
protected open val useNewChapterEndpoint = false
|
||||
|
||||
protected open val enableLogin = false
|
||||
|
||||
/**
|
||||
* Custom Json instance to make usage of `encodeDefaults`,
|
||||
* which is not enabled on the injected instance of the app.
|
||||
@ -70,6 +76,44 @@ abstract class HeanCms(
|
||||
.add("Origin", baseUrl)
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private fun authHeaders(): Headers {
|
||||
val builder = headersBuilder()
|
||||
if (enableLogin && preferences.user.isNotEmpty() && preferences.password.isNotEmpty()) {
|
||||
val tokenData = preferences.tokenData
|
||||
val token = if (tokenData.isExpired(tokenExpiredAtDateFormat)) {
|
||||
getToken()
|
||||
} else {
|
||||
tokenData.token
|
||||
}
|
||||
if (token != null) {
|
||||
builder.add("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun getToken(): String? {
|
||||
val body = FormBody.Builder()
|
||||
.add("email", preferences.user)
|
||||
.add("password", preferences.password)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(POST("$apiUrl/login", headers, body)).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
val result = response.parseAs<HeanCmsErrorsDto>()
|
||||
val message = result.errors?.firstOrNull()?.message ?: intl["login_failed_unknown_error"]
|
||||
|
||||
throw Exception(message)
|
||||
}
|
||||
|
||||
val result = response.parseAs<HeanCmsTokenPayloadDto>()
|
||||
|
||||
preferences.tokenData = result
|
||||
|
||||
return result.token
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = "$apiUrl/query".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("query_string", "")
|
||||
@ -277,24 +321,30 @@ abstract class HeanCms(
|
||||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), headers)
|
||||
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), authHeaders())
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = response.parseAs<HeanCmsPagePayloadDto>()
|
||||
|
||||
if (result.isPaywalled()) throw Exception(intl["paid_chapter_error"])
|
||||
if (result.isPaywalled() && result.chapter.chapterData == null) {
|
||||
throw Exception(intl["paid_chapter_error"])
|
||||
}
|
||||
|
||||
return if (useNewChapterEndpoint) {
|
||||
result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img ->
|
||||
Page(i, imageUrl = img)
|
||||
Page(i, imageUrl = img.toAbsoluteUrl())
|
||||
}
|
||||
} else {
|
||||
result.data.orEmpty().mapIndexed { i, img ->
|
||||
Page(i, imageUrl = img)
|
||||
Page(i, imageUrl = img.toAbsoluteUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toAbsoluteUrl(): String {
|
||||
return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$this"
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||
|
||||
override fun imageUrlParse(response: Response): String = ""
|
||||
@ -343,6 +393,32 @@ abstract class HeanCms(
|
||||
summaryOff = intl["pref_show_paid_chapter_summary_off"]
|
||||
setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT)
|
||||
}.also(screen::addPreference)
|
||||
|
||||
if (enableLogin) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = USER_PREF
|
||||
title = intl["pref_username_title"]
|
||||
summary = intl["pref_credentials_summary"]
|
||||
setDefaultValue("")
|
||||
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
preferences.tokenData = HeanCmsTokenPayloadDto()
|
||||
true
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = PASSWORD_PREF
|
||||
title = intl["pref_password_title"]
|
||||
summary = intl["pref_credentials_summary"]
|
||||
setDefaultValue("")
|
||||
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
preferences.tokenData = HeanCmsTokenPayloadDto()
|
||||
true
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
||||
protected inline fun <reified T> Response.parseAs(): T = use {
|
||||
@ -357,6 +433,21 @@ abstract class HeanCms(
|
||||
private val SharedPreferences.showPaidChapters: Boolean
|
||||
get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT)
|
||||
|
||||
private val SharedPreferences.user: String
|
||||
get() = getString(USER_PREF, "") ?: ""
|
||||
|
||||
private val SharedPreferences.password: String
|
||||
get() = getString(PASSWORD_PREF, "") ?: ""
|
||||
|
||||
private var SharedPreferences.tokenData: HeanCmsTokenPayloadDto
|
||||
get() {
|
||||
val jsonString = getString(TOKEN_PREF, "{}")!!
|
||||
return json.decodeFromString(jsonString)
|
||||
}
|
||||
set(data) {
|
||||
edit().putString(TOKEN_PREF, json.encodeToString(data)).apply()
|
||||
}
|
||||
|
||||
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, */*"
|
||||
@ -367,5 +458,12 @@ abstract class HeanCms(
|
||||
|
||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
||||
|
||||
private const val USER_PREF = "pref_user"
|
||||
private const val PASSWORD_PREF = "pref_password"
|
||||
|
||||
private const val TOKEN_PREF = "pref_token"
|
||||
|
||||
private val tokenExpiredAtDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,33 @@ import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@Serializable
|
||||
class HeanCmsTokenPayloadDto(
|
||||
val token: String? = null,
|
||||
private val expiresAt: String? = null,
|
||||
) {
|
||||
fun isExpired(dateFormat: SimpleDateFormat): Boolean {
|
||||
val expiredTime = try {
|
||||
// Reduce one day to prevent timezone issues
|
||||
expiresAt?.let { dateFormat.parse(it)?.time?.minus(1000 * 60 * 60 * 24) } ?: 0L
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
|
||||
return System.currentTimeMillis() > expiredTime
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class HeanCmsErrorsDto(
|
||||
val errors: List<HeanCmsErrorMessageDto>? = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class HeanCmsErrorMessageDto(
|
||||
val message: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class HeanCmsQuerySearchDto(
|
||||
val data: List<HeanCmsSeriesDto> = emptyList(),
|
||||
@ -129,7 +156,7 @@ class HeanCmsPageDataDto(
|
||||
)
|
||||
|
||||
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
|
||||
return if (startsWith("https://")) this else "$apiUrl/$coverPath$this"
|
||||
return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$coverPath$this"
|
||||
}
|
||||
|
||||
fun String.toStatus(): Int = when (this) {
|
||||
|
@ -15,6 +15,7 @@ class OmegaScans : HeanCms("Omega Scans", "https://omegascans.org", "en") {
|
||||
override val versionId = 2
|
||||
|
||||
override val useNewChapterEndpoint = true
|
||||
override val enableLogin = true
|
||||
|
||||
override fun getGenreList() = listOf(
|
||||
Genre("Romance", 1),
|
||||
|
Loading…
Reference in New Issue
Block a user