diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dacd5db0c..54cfcceb8 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -179,6 +179,7 @@
android:scheme="https"/>
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
index 3a6f36d07..c9abc2c0c 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
@@ -141,5 +141,7 @@ object PreferenceKeys {
const val eh_enableExHentai = "enable_exhentai"
+ const val eh_ts_aspNetCookie = "eh_ts_aspNetCookie"
+
const val eh_showSettingsUploadWarning = "eh_showSettingsUploadWarning1"
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
index 7c8810866..5a4e61839 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
@@ -219,6 +219,8 @@ class PreferencesHelper(val context: Context) {
fun eh_lenientSync() = rxPrefs.getBoolean(Keys.eh_lenientSync, false)
+ fun eh_ts_aspNetCookie() = rxPrefs.getString(Keys.eh_ts_aspNetCookie, "")
+
fun eh_showSettingsUploadWarning() = rxPrefs.getBoolean(Keys.eh_showSettingsUploadWarning, true)
// <-- EH
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
index c999aabcf..dd3d35681 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
@@ -28,6 +28,7 @@ open class SourceManager(private val context: Context) {
init {
createInternalSources().forEach { registerSource(it) }
+
//Recreate sources when they change
val prefEntries = arrayOf(
prefs.enableExhentai(),
@@ -39,8 +40,7 @@ open class SourceManager(private val context: Context) {
).map { it.asObservable() }
Observable.merge(prefEntries).skip(prefEntries.size - 1).subscribe {
- sourcesMap.clear()
- createSources()
+ createEHSources().forEach { registerSource(it) }
}
}
@@ -87,7 +87,7 @@ open class SourceManager(private val context: Context) {
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
exSrcs += NHentai(context)
exSrcs += HentaiCafe()
- exSrcs += Tsumino()
+ exSrcs += Tsumino(context)
return exSrcs
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt
index 0a7795568..8480ca29f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Tsumino.kt
@@ -1,8 +1,12 @@
package eu.kanade.tachiyomi.source.online.english
+import android.content.Context
import android.net.Uri
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonParser
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.*
@@ -10,21 +14,25 @@ import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import exh.TSUMINO_SOURCE_ID
+import exh.captcha.CaptchaCompletionVerifier
+import exh.captcha.SolveCaptchaActivity
import exh.metadata.EMULATED_TAG_NAMESPACE
import exh.metadata.models.Tag
import exh.metadata.models.TsuminoMetadata
import exh.metadata.models.TsuminoMetadata.Companion.BASE_URL
import exh.util.urlImportFetchSearchManga
-import okhttp3.FormBody
-import okhttp3.Request
-import okhttp3.Response
+import okhttp3.*
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.*
-class Tsumino: ParsedHttpSource(), LewdSource {
+class Tsumino(private val context: Context): ParsedHttpSource(), LewdSource, CaptchaCompletionVerifier {
+ private val preferences: PreferencesHelper by injectLazy()
+
override val id = TSUMINO_SOURCE_ID
override val lang = "en"
@@ -228,6 +236,8 @@ class Tsumino: ParsedHttpSource(), LewdSource {
override fun fetchChapterList(manga: SManga) = lazyLoadMeta(queryFromUrl(manga.url),
client.newCall(mangaDetailsRequest(manga)).asObservableSuccess().map { it.asJsoup() }
).map {
+ trickTsumino(it.tmId)
+
listOf(
SChapter.create().apply {
url = "/Read/View/${it.tmId}"
@@ -239,27 +249,97 @@ class Tsumino: ParsedHttpSource(), LewdSource {
}
)
}
-
+
+ fun trickTsumino(id: String?) {
+ if(id == null) return
+
+ //Make one call to /Read/View (ASP session cookie)
+ val rvReq = GET("$BASE_URL/Read/View/$id")
+ val resp = client.newCall(rvReq).execute()
+
+ // Make 5 requests to the first 5 pages of the book in reader process
+ var chain: Observable = Observable.just(0)
+ for(i in 1 .. 5) {
+ chain = chain.flatMap {
+ val req = GET("$BASE_URL/Read/Process/$id/$i")
+ client.newCall(req).asObservableSuccess()
+ }
+ }
+
+ chain.observeOn(Schedulers.io())
+ .subscribeOn(Schedulers.io())
+ .subscribe()
+ }
+
+ override val client: OkHttpClient
+ get() = super.client.newBuilder()
+ .cookieJar(CookieJar.NO_COOKIES)
+ .addNetworkInterceptor {
+ val cAspNetCookie = preferences.eh_ts_aspNetCookie().getOrDefault()
+
+ var request = it.request()
+
+ if(cAspNetCookie.isNotBlank()) {
+ request = it.request()
+ .newBuilder()
+ .header("Cookie", "ASP.NET_SessionId=$cAspNetCookie")
+ .build()
+ }
+
+ val response = it.proceed(request)
+
+ val newCookie = response.headers("Set-Cookie").map(String::trim).find {
+ it.startsWith(ASP_NET_COOKIE_NAME)
+ }
+
+ if(newCookie != null) {
+ val res = newCookie.substringAfter('=')
+ .substringBefore(';')
+ .trim()
+
+ preferences.eh_ts_aspNetCookie().set(res)
+ }
+
+ response
+ }.build()
+
override fun fetchPageList(chapter: SChapter): Observable> {
val id = chapter.url.substringAfterLast('/')
val call = POST("$BASE_URL/Read/Load", body = FormBody.Builder().add("q", id).build())
return client.newCall(call).asObservableSuccess().map {
val parsed = jsonParser.parse(it.body()!!.string()).obj
val pageUrls = parsed["reader_page_urls"].array
-
+
val imageUrl = Uri.parse("$BASE_URL/Image/Object")
pageUrls.mapIndexed { index, obj ->
val newImageUrl = imageUrl.buildUpon().appendQueryParameter("name", obj.string)
Page(index, chapter.url + "#${index + 1}", newImageUrl.toString())
}
- }
+ }.doOnError {
+ val aspNetCookie = preferences.eh_ts_aspNetCookie().getOrDefault()
+
+ val cookiesMap = if(aspNetCookie.isNotBlank())
+ mapOf(ASP_NET_COOKIE_NAME to aspNetCookie)
+ else
+ emptyMap()
+
+ SolveCaptchaActivity.launch(context,
+ this,
+ cookiesMap,
+ CAPTCHA_SCRIPT,
+ "$BASE_URL/Read/Auth/$id")
+ }
}
-
+
+ override fun verify(url: String): Boolean {
+ return Uri.parse(url).pathSegments.getOrNull(1) == "View"
+ }
+
override fun pageListParse(document: Document) = throw UnsupportedOperationException("Unused method called!")
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Unused method called!")
-
+
data class AdvSearchEntry(val type: Int, val text: String, val exclude: Boolean)
-
+
override fun getFilterList() = FilterList(
Filter.Header("Separate tags with commas"),
Filter.Header("Prepend with dash to exclude"),
@@ -271,15 +351,15 @@ class Tsumino: ParsedHttpSource(), LewdSource {
ParodyFilter(),
CharactersFilter(),
UploaderFilter(),
-
+
Filter.Separator(),
-
+
SortFilter(),
LengthFilter(),
MinimumRatingFilter(),
ExcludeParodiesFilter()
)
-
+
class TagFilter : AdvSearchEntryFilter("Tags", 1)
class CategoryFilter : AdvSearchEntryFilter("Categories", 2)
class CollectionFilter : AdvSearchEntryFilter("Collections", 3)
@@ -289,17 +369,25 @@ class Tsumino: ParsedHttpSource(), LewdSource {
class CharactersFilter : AdvSearchEntryFilter("Characters", 7)
class UploaderFilter : AdvSearchEntryFilter("Uploaders", 8)
open class AdvSearchEntryFilter(name: String, val type: Int) : Filter.Text(name)
-
+
class SortFilter : Filter.Select("Sort by", SortType.values())
class LengthFilter : Filter.Select("Length", LengthType.values())
class MinimumRatingFilter : Filter.Select("Minimum rating", (0 .. 5).map { "$it stars" }.toTypedArray())
class ExcludeParodiesFilter : Filter.CheckBox("Exclude parodies")
-
+
companion object {
val jsonParser by lazy {
JsonParser()
}
-
+
val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US)
+
+ private val ASP_NET_COOKIE_NAME = "ASP.NET_SessionId"
+
+ private val CAPTCHA_SCRIPT = """
+ |try{ document.querySelector('.tsumino-nav-btn').remove(); } catch(e) {}
+ |try{ document.querySelector('.tsumino-nav-title').href = '#' ;} catch(e) {}
+ |try{ document.querySelector('.tsumino-nav-items').remove() ;} catch(e) {}
+ """.trimMargin()
}
}
diff --git a/app/src/main/java/exh/captcha/SolveCaptchaActivity.kt b/app/src/main/java/exh/captcha/SolveCaptchaActivity.kt
index 113f94d64..26b3357a1 100644
--- a/app/src/main/java/exh/captcha/SolveCaptchaActivity.kt
+++ b/app/src/main/java/exh/captcha/SolveCaptchaActivity.kt
@@ -1,23 +1,120 @@
package exh.captcha
+import android.content.Context
+import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
+import android.webkit.CookieManager
+import android.webkit.CookieSyncManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import kotlinx.android.synthetic.main.eh_activity_captcha.*
+import uy.kohesive.injekt.injectLazy
+import java.net.URL
class SolveCaptchaActivity : AppCompatActivity() {
+ private val sourceManager: SourceManager by injectLazy()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val sourceId = intent.getIntExtra(SOURCE_ID_EXTRA, -1)
- val source = sourc
+ setContentView(R.layout.eh_activity_captcha)
- if(sourceId == -1) {
+ val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
+ val source = if(sourceId != -1L)
+ sourceManager.get(sourceId) as? CaptchaCompletionVerifier
+ else null
+
+ val cookies: HashMap?
+ = intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap
+
+ val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
+
+ val url: String? = intent.getStringExtra(URL_EXTRA)
+
+ if(source == null || cookies == null || url == null) {
finish()
return
}
+
+ toolbar.title = source.name + ": Solve captcha"
+
+ val parsedUrl = URL(url)
+
+ val cm = CookieManager.getInstance()
+
+ fun continueLoading() {
+ cookies.forEach { (t, u) ->
+ val cookieString = t + "=" + u + "; domain=" + parsedUrl.host
+ cm.setCookie(url, cookieString)
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ CookieSyncManager.createInstance(this).sync()
+
+ webview.settings.javaScriptEnabled = true
+ webview.settings.domStorageEnabled = true
+
+ webview.webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+
+ if(source.verify(url)) {
+ finish()
+ } else {
+ view.loadUrl("javascript:(function() {$script})();")
+ }
+ }
+ }
+
+ webview.loadUrl(url)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ cm.removeAllCookies { continueLoading() }
+ } else {
+ cm.removeAllCookie()
+ continueLoading()
+ }
+
+ setSupportActionBar(toolbar)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ finish()
+ return true
}
companion object {
const val SOURCE_ID_EXTRA = "source_id_extra"
+ const val COOKIES_EXTRA = "cookies_extra"
+ const val SCRIPT_EXTRA = "script_extra"
+ const val URL_EXTRA = "url_extra"
+
+ fun launch(context: Context,
+ source: CaptchaCompletionVerifier,
+ cookies: Map,
+ script: String,
+ url: String) {
+ val intent = Intent(context, SolveCaptchaActivity::class.java).apply {
+ putExtra(SOURCE_ID_EXTRA, source.id)
+ putExtra(COOKIES_EXTRA, HashMap(cookies))
+ putExtra(SCRIPT_EXTRA, script)
+ putExtra(URL_EXTRA, url)
+ }
+
+ context.startActivity(intent)
+ }
}
}
+interface CaptchaCompletionVerifier : Source {
+ fun verify(url: String): Boolean
+}
+
diff --git a/app/src/main/java/exh/metadata/models/TsuminoMetadata.kt b/app/src/main/java/exh/metadata/models/TsuminoMetadata.kt
index 12276b3fb..2210464f5 100644
--- a/app/src/main/java/exh/metadata/models/TsuminoMetadata.kt
+++ b/app/src/main/java/exh/metadata/models/TsuminoMetadata.kt
@@ -121,7 +121,7 @@ open class TsuminoMetadata : RealmObject(), SearchableGalleryMetadata {
}
companion object {
- val BASE_URL = "https://www.tsumino.com"
+ val BASE_URL = "http://www.tsumino.com"
fun tmIdFromUrl(url: String)
= Uri.parse(url).pathSegments[2]
diff --git a/app/src/main/java/exh/ui/login/LoginController.kt b/app/src/main/java/exh/ui/login/LoginController.kt
index 910d1a1a5..245745eaa 100755
--- a/app/src/main/java/exh/ui/login/LoginController.kt
+++ b/app/src/main/java/exh/ui/login/LoginController.kt
@@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.util.launchUI
import exh.EXH_SOURCE_ID
import exh.uconfig.WarnConfigureDialogController
import kotlinx.android.synthetic.main.eh_activity_login.view.*
@@ -49,9 +50,9 @@ class LoginController : NucleusController() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().removeAllCookies {
- Observable.fromCallable {
+ launchUI {
startWebview(view)
- }.subscribeOn(AndroidSchedulers.mainThread()).subscribe()
+ }
}
} else {
CookieManager.getInstance().removeAllCookie()
@@ -67,7 +68,7 @@ class LoginController : NucleusController() {
webview.loadUrl("https://forums.e-hentai.org/index.php?act=Login")
- webview.setWebViewClient(object : WebViewClient() {
+ webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
Timber.d(url)
@@ -88,7 +89,7 @@ class LoginController : NucleusController() {
}
}
}
- })
+ }
}
}
diff --git a/app/src/main/res/layout/eh_activity_captcha.xml b/app/src/main/res/layout/eh_activity_captcha.xml
index 26fa8d825..86b1b56c2 100644
--- a/app/src/main/res/layout/eh_activity_captcha.xml
+++ b/app/src/main/res/layout/eh_activity_captcha.xml
@@ -1,22 +1,27 @@
-
-
-
+
+ android:layout_height="match_parent">
-
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file