diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt index d332e9b03..447ce061a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt @@ -22,7 +22,10 @@ import java.util.* import exh.ui.login.LoginActivity import exh.util.UriFilter import exh.util.UriGroup +import okhttp3.CacheControl +import okhttp3.Headers import okhttp3.Request +import org.jsoup.nodes.Document class EHentai(override val id: Long, val exh: Boolean, @@ -38,7 +41,7 @@ class EHentai(override val id: Long, get() = if(exh) "$schema://exhentai.org" else - "http://e-hentai.org" + "$schema://e-hentai.org" override val lang = "all" override val supportsLatest = true @@ -52,11 +55,8 @@ class EHentai(override val id: Long, */ data class ParsedManga(val fav: String?, val manga: Manga) - /** - * Parse a list of galleries - */ - fun genericMangaParse(response: Response) - = with(response.asJsoup()) { + fun extendedGenericMangaParse(doc: Document) + = with(doc) { //Parse mangas val parsedMangas = select(".gtr0,.gtr1").map { ParsedManga( @@ -84,7 +84,15 @@ class EHentai(override val id: Long, val hasNextPage = select("a[onclick=return false]").last()?.let { it.text() == ">" } ?: false - MangasPage(parsedMangas.map { it.manga }, hasNextPage) + Pair(parsedMangas, hasNextPage) + } + + /** + * Parse a list of galleries + */ + fun genericMangaParse(response: Response) + = extendedGenericMangaParse(response.asJsoup()).let { + MangasPage(it.first.map { it.manga }, it.second) } override fun fetchChapterList(manga: SManga): Observable> @@ -143,16 +151,29 @@ class EHentai(override val id: Long, return exGet(uri.toString(), page) } - override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page) + override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page)!! override fun popularMangaParse(response: Response) = genericMangaParse(response) override fun searchMangaParse(response: Response) = genericMangaParse(response) override fun latestUpdatesParse(response: Response) = genericMangaParse(response) - fun exGet(url: String, page: Int? = null) + fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true) = GET(page?.let { addParam(url, "page", Integer.toString(page - 1)) - } ?: url, headers) + } ?: url, additionalHeaders?.let { + val headers = headers.newBuilder() + it.toMultimap().forEach { t, u -> + u.forEach { + headers.add(t, it) + } + } + headers.build() + } ?: headers).let { + if(!cache) + it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build() + else + it + }!! /** * Parse gallery page to metadata model @@ -266,6 +287,37 @@ class EHentai(override val id: Long, throw UnsupportedOperationException("Unused method was called somehow!") } + //Too lazy to write return type + fun fetchFavorites() = { + //Used to get "s" cookie + val favoriteUrl = "$baseUrl/favorites.php" + val result = mutableListOf() + var page = 1 + + var favNames: List? = null + + do { + val response2 = client.newCall(exGet(favoriteUrl, + page = page, + cache = false)).execute() + val doc = response2.asJsoup() + + //Parse favorites + val parsed = extendedGenericMangaParse(doc) + result += parsed.first + + //Parse fav names + if (favNames == null) + favNames = doc.getElementsByClass("nosel").first().children().filter { + it.children().size >= 3 + }.map { it.child(2).text() }.filterNotNull() + + //Next page + page++ + } while (parsed.second) + Pair(result as List, favNames!!) + }() + val cookiesHeader by lazy { val cookies: MutableMap = mutableMapOf() if(prefs.enableExhentai().getOrDefault()) { @@ -403,5 +455,20 @@ class EHentai(override val id: Long, companion object { val QUERY_PREFIX = "?f_apply=Apply+Filter" val TR_SUFFIX = "TR" + + fun getCookies(cookies: String): Map? { + val foundCookies = HashMap() + for (cookie in cookies.split(";".toRegex()).dropLastWhile(String::isEmpty).toTypedArray()) { + val splitCookie = cookie.split("=".toRegex()).dropLastWhile(String::isEmpty).toTypedArray() + if (splitCookie.size < 2) { + return null + } + val trimmedKey = splitCookie[0].trim { it <= ' ' } + if (!foundCookies.containsKey(trimmedKey)) { + foundCookies.put(trimmedKey, splitCookie[1].trim { it <= ' ' }) + } + } + return foundCookies + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt index 916022439..15abdffac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt @@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.DialogCheckboxView +import exh.FavoritesSyncHelper import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_library.* import nucleus.factory.RequiresPresenter @@ -267,6 +268,11 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback val intent = CategoryActivity.newIntent(activity) startActivity(intent) } + R.id.action_sync -> { + FavoritesSyncHelper(this.activity).guiSyncFavorites({ + //Do we even need stuff in here? + }) + } else -> return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/exh/FavoritesSyncHelper.kt b/app/src/main/java/exh/FavoritesSyncHelper.kt new file mode 100644 index 000000000..35a41df44 --- /dev/null +++ b/app/src/main/java/exh/FavoritesSyncHelper.kt @@ -0,0 +1,135 @@ +package exh + +import android.app.Activity +import android.support.v7.app.AlertDialog +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.online.all.EHentai +import eu.kanade.tachiyomi.util.syncChaptersWithSource +import timber.log.Timber +import uy.kohesive.injekt.injectLazy +import kotlin.concurrent.thread + +class FavoritesSyncHelper(val activity: Activity) { + + val db: DatabaseHelper by injectLazy() + + val sourceManager: SourceManager by injectLazy() + + val prefs: PreferencesHelper by injectLazy() + + fun guiSyncFavorites(onComplete: () -> Unit) { + //ExHentai must be enabled/user must be logged in + if (!prefs.enableExhentai().getOrDefault()) { + AlertDialog.Builder(activity).setTitle("Error") + .setMessage("You are not logged in! Please log in and try again!") + .setPositiveButton("Ok") { dialog, _ -> dialog.dismiss() }.show() + return + } + val dialog = MaterialDialog.Builder(activity) + .progress(true, 0) + .title("Downloading favorites") + .content("Please wait...") + .cancelable(false) + .show() + thread { + var error = false + try { + syncFavorites() + } catch (e: Exception) { + error = true + Timber.e(e, "Could not sync favorites!") + } + + dialog.dismiss() + + activity.runOnUiThread { + if (error) + MaterialDialog.Builder(activity) + .title("Error") + .content("There was an error downloading your favorites, please try again later!") + .positiveText("Ok") + .show() + onComplete() + } + } + } + + fun syncFavorites() { + val onlineSources = sourceManager.getOnlineSources() + var ehSource: EHentai? = null + var exSource: EHentai? = null + onlineSources.forEach { + if(it.id == EH_SOURCE_ID) + ehSource = it as EHentai + else if(it.id == EXH_SOURCE_ID) + exSource = it as EHentai + } + + (exSource ?: ehSource)?.let { source -> + val favResponse = source.fetchFavorites() + val ourCategories = ArrayList(db.getCategories().executeAsBlocking()) + val ourMangas = ArrayList(db.getMangas().executeAsBlocking()) + //Add required categories (categories do not sync upwards) + favResponse.second.filter { theirCategory -> + ourCategories.find { + it.name.endsWith(theirCategory) + } == null + }.map { + Category.create(it) + }.let { + db.inTransaction { + //Insert new categories + db.insertCategories(it).executeAsBlocking().results().entries.filter { + it.value.wasInserted() + }.forEach { it.key.id = it.value.insertedId()!!.toInt() } + + val categoryMap = (it + ourCategories).associateBy { it.name } + + //Insert new mangas + val mangaToInsert = java.util.ArrayList() + favResponse.first.map { + val category = categoryMap[it.fav]!! + var manga = it.manga + val alreadyHaveManga = ourMangas.find { + it.url == manga.url + }?.apply { + manga = this + } != null + if (!alreadyHaveManga) { + ourMangas.add(manga) + mangaToInsert.add(manga) + } + manga.favorite = true + Pair(manga, category) + }.apply { + //Insert mangas + db.insertMangas(mangaToInsert).executeAsBlocking().results().entries.filter { + it.value.wasInserted() + }.forEach { manga -> + manga.key.id = manga.value.insertedId() + try { + source.fetchChapterList(manga.key).map { + syncChaptersWithSource(db, it, manga.key, source) + }.toBlocking().first() + } catch (e: Exception) { + Timber.w(e, "Failed to update chapters for gallery: ${manga.key.title}!") + } + } + + //Set categories + val categories = map { MangaCategory.create(it.first, it.second) } + val mangas = map { it.first } + db.setMangaCategories(categories, mangas) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/FavoritesSyncManager.java b/app/src/main/java/exh/FavoritesSyncManager.java index 9766da5a6..badc70704 100644 --- a/app/src/main/java/exh/FavoritesSyncManager.java +++ b/app/src/main/java/exh/FavoritesSyncManager.java @@ -20,11 +20,12 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper; import eu.kanade.tachiyomi.data.database.models.Category; import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.MangaCategory; +import eu.kanade.tachiyomi.source.online.all.EHentai; +import kotlin.Pair; //import eu.kanade.tachiyomi.data.source.online.english.EHentai; public class FavoritesSyncManager { - /* - Context context; + /*Context context; DatabaseHelper db; public FavoritesSyncManager(Context context, DatabaseHelper db) { @@ -72,10 +73,10 @@ public class FavoritesSyncManager { mainLooper.post(onComplete); } }).start(); - } - + }*/ +/* public void syncFavorites() throws IOException { - EHentai.FavoritesResponse favResponse = EHentai.fetchFavorites(context); + Pair favResponse = EHentai.fetchFavorites(context); Map> favorites = favResponse.favs; List ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking()); List ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking()); @@ -136,7 +137,7 @@ public class FavoritesSyncManager { for(Map.Entry entry : mangaToSetCategories.entrySet()) { db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())), Collections.singletonList(entry.getKey())); - } + }*/ //Determines what /*Map> toUpload = new HashMap<>(); for (Manga manga : ourMangas) { diff --git a/app/src/main/java/exh/ui/login/LoginActivity.kt b/app/src/main/java/exh/ui/login/LoginActivity.kt index 31cec32bf..a205b50ec 100644 --- a/app/src/main/java/exh/ui/login/LoginActivity.kt +++ b/app/src/main/java/exh/ui/login/LoginActivity.kt @@ -7,11 +7,18 @@ import android.view.MenuItem import android.webkit.CookieManager import android.webkit.WebView import android.webkit.WebViewClient +import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.R 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.activity.BaseActivity +import exh.EXH_SOURCE_ID import kotlinx.android.synthetic.main.eh_activity_login.* import kotlinx.android.synthetic.main.toolbar.* +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.net.HttpCookie @@ -24,6 +31,8 @@ class LoginActivity : BaseActivity() { val preferenceManager: PreferencesHelper by injectLazy() + val sourceManager: SourceManager by injectLazy() + override fun onCreate(savedInstanceState: Bundle?) { setAppTheme() super.onCreate(savedInstanceState) @@ -73,13 +82,40 @@ class LoginActivity : BaseActivity() { //At ExHentai, check that everything worked out... if(applyExHentaiCookies(url)) { preferenceManager.enableExhentai().set(true) - onBackPressed() + finishLogin() } } } }) } + fun finishLogin() { + val progressDialog = MaterialDialog.Builder(this) + .title("Finalizing login") + .progress(true, 0) + .content("Please wait...") + .cancelable(false) + .show() + + val eh = sourceManager + .getOnlineSources() + .find { it.id == EXH_SOURCE_ID } as EHentai + Observable.fromCallable { + //I honestly have no idea why we need to call this twice, but it works, so whatever + try { + eh.fetchFavorites() + } catch(ignored: Exception) {} + try { + eh.fetchFavorites() + } catch(ignored: Exception) {} + }.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + progressDialog.dismiss() + onBackPressed() + } + } + /** * Check if we are logged in */ @@ -127,7 +163,7 @@ class LoginActivity : BaseActivity() { fun getCookies(url: String): List? = CookieManager.getInstance().getCookie(url)?.let { - it.split("; ").flatMap { + it.split("; ").flatMap { HttpCookie.parse(it) } } diff --git a/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml b/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml new file mode 100644 index 000000000..0f56a12d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/library.xml b/app/src/main/res/menu/library.xml index 5e7bf7ad5..c6bdd721f 100644 --- a/app/src/main/res/menu/library.xml +++ b/app/src/main/res/menu/library.xml @@ -22,9 +22,14 @@ android:title="@string/action_update_library" app:showAsAction="ifRoom"/> + + - diff --git a/app/src/main/res/xml/eh_pref_eh.xml b/app/src/main/res/xml/eh_pref_eh.xml index 5ec63237a..aaf50e998 100644 --- a/app/src/main/res/xml/eh_pref_eh.xml +++ b/app/src/main/res/xml/eh_pref_eh.xml @@ -31,11 +31,10 @@ android:defaultValue="false" /> + android:title="Secure ExHentai/E-Hentai" + android:summary="Use the HTTPS version of ExHentai/E-Hentai." />