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:
parent
d604f06811
commit
21cf5dac2d
|
@ -5,7 +5,7 @@ ext {
|
||||||
extName = 'HentaiHand'
|
extName = 'HentaiHand'
|
||||||
pkgNameSuffix = 'all.hentaihand'
|
pkgNameSuffix = 'all.hentaihand'
|
||||||
extClass = '.HentaiHandFactory'
|
extClass = '.HentaiHandFactory'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
containsNsfw = true
|
containsNsfw = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.hentaihand
|
package eu.kanade.tachiyomi.extension.all.hentaihand
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
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.fromJson
|
||||||
import com.github.salomonbrys.kotson.get
|
import com.github.salomonbrys.kotson.get
|
||||||
import com.github.salomonbrys.kotson.nullObj
|
import com.github.salomonbrys.kotson.nullObj
|
||||||
|
@ -10,7 +16,9 @@ import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||||
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
|
||||||
|
@ -19,23 +27,36 @@ 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 okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
@Nsfw
|
@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 baseUrl: String = "https://hentaihand.com"
|
||||||
override val name: String = "HentaiHand"
|
override val name: String = "HentaiHand$extraName"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
private val gson = Gson()
|
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 {
|
private fun parseGenericResponse(response: Response): MangasPage {
|
||||||
val data = gson.fromJson<JsonObject>(response.body()!!.string())
|
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 popularMangaParse(response: Response): MangasPage = parseGenericResponse(response)
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
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
|
// 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 latestUpdatesParse(response: Response): MangasPage = parseGenericResponse(response)
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
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
|
// Search
|
||||||
|
@ -88,7 +111,9 @@ class HentaiHand(override val lang: String, val hhLangId: Int) : HttpSource() {
|
||||||
val url = HttpUrl.parse("$baseUrl/api/comics")!!.newBuilder()
|
val url = HttpUrl.parse("$baseUrl/api/comics")!!.newBuilder()
|
||||||
.addQueryParameter("page", page.toString())
|
.addQueryParameter("page", page.toString())
|
||||||
.addQueryParameter("q", query)
|
.addQueryParameter("q", query)
|
||||||
.addQueryParameter("languages", hhLangId.toString())
|
|
||||||
|
if (hhLangId != null)
|
||||||
|
url.addQueryParameter("languages", hhLangId.toString())
|
||||||
|
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
when (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")
|
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
|
// Filters
|
||||||
|
|
||||||
private class SortFilter(sortPairs: List<Pair<String, String>>) : Filter.Select<String>("Sort By", sortPairs.map { it.first }.toTypedArray())
|
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 {
|
companion object {
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-dd-MM")
|
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 = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,44 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
class HentaiHandFactory : SourceFactory {
|
class HentaiHandFactory : SourceFactory {
|
||||||
|
|
||||||
override fun createSources(): List<Source> = listOf(
|
override fun createSources(): List<Source> = listOf(
|
||||||
|
// https://hentaihand.com/api/languages?per_page=50
|
||||||
|
HentaiHand("other", extraName = " (Unfiltered)"),
|
||||||
HentaiHand("en", 1),
|
HentaiHand("en", 1),
|
||||||
HentaiHand("zh", 2),
|
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue