diff --git a/src/all/hentaihand/build.gradle b/src/all/hentaihand/build.gradle index c205ecbf1..8dd081b07 100644 --- a/src/all/hentaihand/build.gradle +++ b/src/all/hentaihand/build.gradle @@ -5,7 +5,7 @@ ext { extName = 'HentaiHand' pkgNameSuffix = 'all.hentaihand' extClass = '.HentaiHandFactory' - extVersionCode = 1 + extVersionCode = 2 libVersion = '1.2' containsNsfw = true } diff --git a/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHand.kt b/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHand.kt index d37b36559..9bd165f46 100644 --- a/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHand.kt +++ b/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHand.kt @@ -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(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().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>) : Filter.Select("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 = "" } } diff --git a/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHandFactory.kt b/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHandFactory.kt index eee5d1542..cc8309969 100644 --- a/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHandFactory.kt +++ b/src/all/hentaihand/src/eu/kanade/tachiyomi/extension/all/hentaihand/HentaiHandFactory.kt @@ -8,8 +8,44 @@ import eu.kanade.tachiyomi.source.SourceFactory class HentaiHandFactory : SourceFactory { override fun createSources(): List = 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), ) }