Add favorites downloader.

This commit is contained in:
NerdNumber9 2017-03-15 14:39:09 -04:00
parent 492adb7035
commit d981c75600
8 changed files with 280 additions and 22 deletions

View File

@ -22,7 +22,10 @@ import java.util.*
import exh.ui.login.LoginActivity import exh.ui.login.LoginActivity
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document
class EHentai(override val id: Long, class EHentai(override val id: Long,
val exh: Boolean, val exh: Boolean,
@ -38,7 +41,7 @@ class EHentai(override val id: Long,
get() = if(exh) get() = if(exh)
"$schema://exhentai.org" "$schema://exhentai.org"
else else
"http://e-hentai.org" "$schema://e-hentai.org"
override val lang = "all" override val lang = "all"
override val supportsLatest = true override val supportsLatest = true
@ -52,11 +55,8 @@ class EHentai(override val id: Long,
*/ */
data class ParsedManga(val fav: String?, val manga: Manga) data class ParsedManga(val fav: String?, val manga: Manga)
/** fun extendedGenericMangaParse(doc: Document)
* Parse a list of galleries = with(doc) {
*/
fun genericMangaParse(response: Response)
= with(response.asJsoup()) {
//Parse mangas //Parse mangas
val parsedMangas = select(".gtr0,.gtr1").map { val parsedMangas = select(".gtr0,.gtr1").map {
ParsedManga( ParsedManga(
@ -84,7 +84,15 @@ class EHentai(override val id: Long,
val hasNextPage = select("a[onclick=return false]").last()?.let { val hasNextPage = select("a[onclick=return false]").last()?.let {
it.text() == ">" it.text() == ">"
} ?: false } ?: 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<List<SChapter>> override fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
@ -143,16 +151,29 @@ class EHentai(override val id: Long,
return exGet(uri.toString(), page) 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 popularMangaParse(response: Response) = genericMangaParse(response)
override fun searchMangaParse(response: Response) = genericMangaParse(response) override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesParse(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 { = GET(page?.let {
addParam(url, "page", Integer.toString(page - 1)) 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 * Parse gallery page to metadata model
@ -266,6 +287,37 @@ class EHentai(override val id: Long,
throw UnsupportedOperationException("Unused method was called somehow!") 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<ParsedManga>()
var page = 1
var favNames: List<String>? = 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<ParsedManga>, favNames!!)
}()
val cookiesHeader by lazy { val cookiesHeader by lazy {
val cookies: MutableMap<String, String> = mutableMapOf() val cookies: MutableMap<String, String> = mutableMapOf()
if(prefs.enableExhentai().getOrDefault()) { if(prefs.enableExhentai().getOrDefault()) {
@ -403,5 +455,20 @@ class EHentai(override val id: Long,
companion object { companion object {
val QUERY_PREFIX = "?f_apply=Apply+Filter" val QUERY_PREFIX = "?f_apply=Apply+Filter"
val TR_SUFFIX = "TR" val TR_SUFFIX = "TR"
fun getCookies(cookies: String): Map<String, String>? {
val foundCookies = HashMap<String, String>()
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
}
} }
} }

View File

@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DialogCheckboxView import eu.kanade.tachiyomi.widget.DialogCheckboxView
import exh.FavoritesSyncHelper
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_library.* import kotlinx.android.synthetic.main.fragment_library.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
@ -267,6 +268,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
val intent = CategoryActivity.newIntent(activity) val intent = CategoryActivity.newIntent(activity)
startActivity(intent) startActivity(intent)
} }
R.id.action_sync -> {
FavoritesSyncHelper(this.activity).guiSyncFavorites({
//Do we even need stuff in here?
})
}
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }

View File

@ -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<Category>(db.getCategories().executeAsBlocking())
val ourMangas = ArrayList<Manga>(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<Manga>()
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)
}
}
}
}
}
}

View File

@ -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.Category;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaCategory; 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; //import eu.kanade.tachiyomi.data.source.online.english.EHentai;
public class FavoritesSyncManager { public class FavoritesSyncManager {
/* /*Context context;
Context context;
DatabaseHelper db; DatabaseHelper db;
public FavoritesSyncManager(Context context, DatabaseHelper db) { public FavoritesSyncManager(Context context, DatabaseHelper db) {
@ -72,10 +73,10 @@ public class FavoritesSyncManager {
mainLooper.post(onComplete); mainLooper.post(onComplete);
} }
}).start(); }).start();
} }*/
/*
public void syncFavorites() throws IOException { public void syncFavorites() throws IOException {
EHentai.FavoritesResponse favResponse = EHentai.fetchFavorites(context); Pair favResponse = EHentai.fetchFavorites(context);
Map<String, List<Manga>> favorites = favResponse.favs; Map<String, List<Manga>> favorites = favResponse.favs;
List<Category> ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking()); List<Category> ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking());
List<Manga> ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking()); List<Manga> ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking());
@ -136,7 +137,7 @@ public class FavoritesSyncManager {
for(Map.Entry<Manga, Category> entry : mangaToSetCategories.entrySet()) { for(Map.Entry<Manga, Category> entry : mangaToSetCategories.entrySet()) {
db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())), db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())),
Collections.singletonList(entry.getKey())); Collections.singletonList(entry.getKey()));
} }*/
//Determines what //Determines what
/*Map<Integer, List<Manga>> toUpload = new HashMap<>(); /*Map<Integer, List<Manga>> toUpload = new HashMap<>();
for (Manga manga : ourMangas) { for (Manga manga : ourMangas) {

View File

@ -7,11 +7,18 @@ import android.view.MenuItem
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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 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.eh_activity_login.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.HttpCookie import java.net.HttpCookie
@ -24,6 +31,8 @@ class LoginActivity : BaseActivity() {
val preferenceManager: PreferencesHelper by injectLazy() val preferenceManager: PreferencesHelper by injectLazy()
val sourceManager: SourceManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setAppTheme() setAppTheme()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -73,13 +82,40 @@ class LoginActivity : BaseActivity() {
//At ExHentai, check that everything worked out... //At ExHentai, check that everything worked out...
if(applyExHentaiCookies(url)) { if(applyExHentaiCookies(url)) {
preferenceManager.enableExhentai().set(true) 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 * Check if we are logged in
*/ */
@ -127,7 +163,7 @@ class LoginActivity : BaseActivity() {
fun getCookies(url: String): List<HttpCookie>? fun getCookies(url: String): List<HttpCookie>?
= CookieManager.getInstance().getCookie(url)?.let { = CookieManager.getInstance().getCookie(url)?.let {
it.split("; ").flatMap { it.split("; ").flatMap {
HttpCookie.parse(it) HttpCookie.parse(it)
} }
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
</vector>

View File

@ -22,9 +22,14 @@
android:title="@string/action_update_library" android:title="@string/action_update_library"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_sync"
android:icon="@drawable/ic_cloud_download_white_24dp"
android:title="Download favorites"
app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_edit_categories" android:id="@+id/action_edit_categories"
android:title="@string/action_edit_categories" android:title="@string/action_edit_categories"
app:showAsAction="never"/> app:showAsAction="never"/>
</menu> </menu>

View File

@ -31,11 +31,10 @@
android:defaultValue="false" /> android:defaultValue="false" />
<SwitchPreference <SwitchPreference
android:dependency="enable_exhentai"
android:defaultValue="true" android:defaultValue="true"
android:key="secure_exh" android:key="secure_exh"
android:title="Secure ExHentai" android:title="Secure ExHentai/E-Hentai"
android:summary="Use the HTTPS version of ExHentai. Uncheck if ExHentai is not working" /> android:summary="Use the HTTPS version of ExHentai/E-Hentai." />
<ListPreference <ListPreference
android:defaultValue="auto" android:defaultValue="auto"