HentaiHand: add authorization, add more languages (#6337)

* Add logging in using username and password in settings

* Remove logs

* Fix warning

* Add more languages

* Bump ext version
This commit is contained in:
scb261 2021-03-29 14:01:54 +03:00 committed by GitHub
parent d604f06811
commit 21cf5dac2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 184 additions and 8 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'HentaiHand'
pkgNameSuffix = 'all.hentaihand'
extClass = '.HentaiHandFactory'
extVersionCode = 1
extVersionCode = 2
libVersion = '1.2'
containsNsfw = true
}

View File

@ -1,6 +1,12 @@
package eu.kanade.tachiyomi.extension.all.hentaihand
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.EditTextPreference
import android.support.v7.preference.PreferenceScreen
import android.text.InputType
import android.widget.Toast
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.nullObj
@ -10,7 +16,9 @@ import com.google.gson.JsonArray
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -19,23 +27,36 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
@Nsfw
class HentaiHand(override val lang: String, val hhLangId: Int) : HttpSource() {
class HentaiHand(
override val lang: String,
private val hhLangId: Int? = null,
extraName: String = ""
) : ConfigurableSource, HttpSource() {
override val baseUrl: String = "https://hentaihand.com"
override val name: String = "HentaiHand"
override val name: String = "HentaiHand$extraName"
override val supportsLatest = true
private val gson = Gson()
override val client: OkHttpClient = network.cloudflareClient
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor { authIntercept(it) }
.build()
private fun parseGenericResponse(response: Response): MangasPage {
val data = gson.fromJson<JsonObject>(response.body()!!.string())
@ -56,7 +77,8 @@ class HentaiHand(override val lang: String, val hhLangId: Int) : HttpSource() {
override fun popularMangaParse(response: Response): MangasPage = parseGenericResponse(response)
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/api/comics?page=$page&sort=popularity&order=desc&duration=all&languages=$hhLangId")
val url = "$baseUrl/api/comics?page=$page&sort=popularity&order=desc&duration=all"
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId"))
}
// Latest
@ -64,7 +86,8 @@ class HentaiHand(override val lang: String, val hhLangId: Int) : HttpSource() {
override fun latestUpdatesParse(response: Response): MangasPage = parseGenericResponse(response)
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/api/comics?page=$page&sort=uploaded_at&order=desc&duration=week&languages=$hhLangId")
val url = "$baseUrl/api/comics?page=$page&sort=uploaded_at&order=desc&duration=week"
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId"))
}
// Search
@ -88,7 +111,9 @@ class HentaiHand(override val lang: String, val hhLangId: Int) : HttpSource() {
val url = HttpUrl.parse("$baseUrl/api/comics")!!.newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("q", query)
.addQueryParameter("languages", hhLangId.toString())
if (hhLangId != null)
url.addQueryParameter("languages", hhLangId.toString())
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
@ -190,6 +215,116 @@ class HentaiHand(override val lang: String, val hhLangId: Int) : HttpSource() {
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
// Authorization
private fun authIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (username.isEmpty() or password.isEmpty()) {
return chain.proceed(request)
}
if (token.isEmpty()) {
token = this.login(chain, username, password)
}
val authRequest = request.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(authRequest)
}
private fun login(chain: Interceptor.Chain, username: String, password: String): String {
val jsonObject = JSONObject().apply {
this.put("username", username)
this.put("password", password)
this.put("remember_me", true)
}
val body = RequestBody.create(MEDIA_TYPE, jsonObject.toString())
val response = chain.proceed(POST("$baseUrl/api/login", headers, body))
if (response.code() == 401) {
throw Exception("Failed to login, check if username and password are correct")
}
if (response.body() == null)
throw Exception("Login response body is empty")
try {
return JSONObject(response.body()!!.string())
.getJSONObject("auth")
.getString("access_token")
} catch (e: JSONException) {
throw Exception("Cannot parse login response body")
}
}
private var token: String = ""
private val username by lazy { getPrefUsername() }
private val password by lazy { getPrefPassword() }
// Preferences
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
screen.addPreference(screen.editTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password, true))
}
private fun androidx.preference.PreferenceScreen.editTextPreference(title: String, default: String, value: String, isPassword: Boolean = false): androidx.preference.EditTextPreference {
return androidx.preference.EditTextPreference(context).apply {
key = title
this.title = title
summary = value
this.setDefaultValue(default)
dialogTitle = title
if (isPassword) {
setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
}
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(title, newValue as String).commit()
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
screen.addPreference(screen.supportEditTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
screen.addPreference(screen.supportEditTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password))
}
private fun PreferenceScreen.supportEditTextPreference(title: String, default: String, value: String): EditTextPreference {
return EditTextPreference(context).apply {
key = title
this.title = title
summary = value
this.setDefaultValue(default)
dialogTitle = title
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(title, newValue as String).commit()
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
}
private fun getPrefUsername(): String = preferences.getString(USERNAME_TITLE, USERNAME_DEFAULT)!!
private fun getPrefPassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
// Filters
private class SortFilter(sortPairs: List<Pair<String, String>>) : Filter.Select<String>("Sort By", sortPairs.map { it.first }.toTypedArray())
@ -252,5 +387,10 @@ class HentaiHand(override val lang: String, val hhLangId: Int) : HttpSource() {
companion object {
@SuppressLint("SimpleDateFormat")
private val DATE_FORMAT = SimpleDateFormat("yyyy-dd-MM")
private val MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8")
private const val USERNAME_TITLE = "Username"
private const val USERNAME_DEFAULT = ""
private const val PASSWORD_TITLE = "Password"
private const val PASSWORD_DEFAULT = ""
}
}

View File

@ -8,8 +8,44 @@ import eu.kanade.tachiyomi.source.SourceFactory
class HentaiHandFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
// https://hentaihand.com/api/languages?per_page=50
HentaiHand("other", extraName = " (Unfiltered)"),
HentaiHand("en", 1),
HentaiHand("zh", 2),
HentaiHand("ja", 3)
HentaiHand("ja", 3),
HentaiHand("other", 4, extraName = " (Text Cleaned)"),
HentaiHand("eo", 5),
HentaiHand("ceb", 6),
HentaiHand("cs", 7),
HentaiHand("ar", 8),
HentaiHand("sk", 9),
HentaiHand("mn", 10),
HentaiHand("uk", 11),
HentaiHand("la", 12),
HentaiHand("tl", 13),
HentaiHand("es", 14),
HentaiHand("it", 15),
HentaiHand("ko", 16),
HentaiHand("th", 17),
HentaiHand("pl", 18),
HentaiHand("fr", 19),
HentaiHand("pt", 20),
HentaiHand("de", 21),
HentaiHand("fi", 22),
HentaiHand("ru", 23),
HentaiHand("sv", 24),
HentaiHand("hu", 25),
HentaiHand("id", 26),
HentaiHand("vi", 27),
HentaiHand("da", 28),
HentaiHand("ro", 29),
HentaiHand("et", 30),
HentaiHand("nl", 31),
HentaiHand("ca", 32),
HentaiHand("tr", 33),
HentaiHand("el", 34),
HentaiHand("no", 35),
HentaiHand("sq", 1501),
HentaiHand("bg", 1502),
)
}