mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Initial implementation of favorites syncing
General code cleanup Fix some cases of duplicate galleries (not completely fixed)
This commit is contained in:
		| @@ -11,21 +11,26 @@ import eu.kanade.tachiyomi.source.model.* | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.source.online.LewdSource | ||||
| import eu.kanade.tachiyomi.util.asJsoup | ||||
| import exh.metadata.* | ||||
| import exh.metadata.EX_DATE_FORMAT | ||||
| import exh.metadata.ignore | ||||
| import exh.metadata.models.ExGalleryMetadata | ||||
| import exh.metadata.models.Tag | ||||
| import exh.metadata.nullIfBlank | ||||
| import exh.metadata.parseHumanReadableByteCount | ||||
| import exh.ui.login.LoginController | ||||
| import exh.util.UriFilter | ||||
| import exh.util.UriGroup | ||||
| import exh.util.urlImportFetchSearchManga | ||||
| import okhttp3.CacheControl | ||||
| import okhttp3.Headers | ||||
| import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.net.URLEncoder | ||||
| import java.util.* | ||||
| import exh.ui.login.LoginController | ||||
| import okhttp3.CacheControl | ||||
| import okhttp3.Headers | ||||
| import okhttp3.Request | ||||
| import org.jsoup.nodes.Document | ||||
| import exh.util.* | ||||
|  | ||||
| class EHentai(override val id: Long, | ||||
|               val exh: Boolean, | ||||
| @@ -51,14 +56,14 @@ class EHentai(override val id: Long, | ||||
|     /** | ||||
|      * Gallery list entry | ||||
|      */ | ||||
|     data class ParsedManga(val fav: String?, val manga: Manga) | ||||
|     data class ParsedManga(val fav: Int, val manga: Manga) | ||||
|  | ||||
|     fun extendedGenericMangaParse(doc: Document) | ||||
|             = with(doc) { | ||||
|         //Parse mangas | ||||
|         val parsedMangas = select(".gtr0,.gtr1").map { | ||||
|             ParsedManga( | ||||
|                     fav = it.select(".itd .it3 > .i[id]").first()?.attr("title"), | ||||
|                     fav = parseFavoritesStyle(it.select(".itd .it3 > .i[id]").first()?.attr("style")), | ||||
|                     manga = Manga.create(id).apply { | ||||
|                         //Get title | ||||
|                         it.select(".itd .it5 a").first()?.apply { | ||||
| @@ -85,6 +90,14 @@ class EHentai(override val id: Long, | ||||
|         Pair(parsedMangas, hasNextPage) | ||||
|     } | ||||
|  | ||||
|     fun parseFavoritesStyle(style: String?): Int { | ||||
|         val offset = style?.substringAfterLast("background-position:0px ") | ||||
|                 ?.removeSuffix("px; cursor:pointer") | ||||
|                 ?.toIntOrNull() ?: return -1 | ||||
|  | ||||
|         return (offset + 2)/-19 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse a list of galleries | ||||
|      */ | ||||
| @@ -287,9 +300,7 @@ 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 | ||||
|     fun fetchFavorites(): Pair<List<ParsedManga>, List<String>> { | ||||
|         val favoriteUrl = "$baseUrl/favorites.php" | ||||
|         val result = mutableListOf<ParsedManga>() | ||||
|         var page = 1 | ||||
| @@ -308,22 +319,23 @@ class EHentai(override val id: Long, | ||||
|  | ||||
|             //Parse fav names | ||||
|             if (favNames == null) | ||||
|                 favNames = doc.getElementsByClass("nosel").first().children().filter { | ||||
|                     it.children().size >= 3 | ||||
|                 }.mapNotNull { it.child(2).text() } | ||||
|                 favNames = doc.select(".fp:not(.fps)").mapNotNull { | ||||
|                     it.child(2).text() | ||||
|                 } | ||||
|  | ||||
|             //Next page | ||||
|             page++ | ||||
|         } while (parsed.second) | ||||
|         Pair(result as List<ParsedManga>, favNames!!) | ||||
|     }() | ||||
|  | ||||
|         return Pair(result as List<ParsedManga>, favNames!!) | ||||
|     } | ||||
|  | ||||
|     val cookiesHeader by lazy { | ||||
|         val cookies: MutableMap<String, String> = mutableMapOf() | ||||
|         if(prefs.enableExhentai().getOrDefault()) { | ||||
|             cookies.put(LoginController.MEMBER_ID_COOKIE, prefs.memberIdVal().get()!!) | ||||
|             cookies.put(LoginController.PASS_HASH_COOKIE, prefs.passHashVal().get()!!) | ||||
|             cookies.put(LoginController.IGNEOUS_COOKIE, prefs.igneousVal().get()!!) | ||||
|             cookies[LoginController.MEMBER_ID_COOKIE] = prefs.memberIdVal().get()!! | ||||
|             cookies[LoginController.PASS_HASH_COOKIE] = prefs.passHashVal().get()!! | ||||
|             cookies[LoginController.IGNEOUS_COOKIE] = prefs.igneousVal().get()!! | ||||
|         } | ||||
|  | ||||
|         //Setup settings | ||||
| @@ -458,20 +470,5 @@ class EHentai(override val id: Long, | ||||
|     companion object { | ||||
|         val QUERY_PREFIX = "?f_apply=Apply+Filter" | ||||
|         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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import android.support.v7.app.AppCompatActivity | ||||
| import android.support.v7.view.ActionMode | ||||
| import android.support.v7.widget.SearchView | ||||
| import android.view.* | ||||
| import com.afollestad.materialdialogs.MaterialDialog | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import com.f2prateek.rx.preferences.Preference | ||||
| @@ -36,7 +37,7 @@ import eu.kanade.tachiyomi.ui.migration.MigrationController | ||||
| import eu.kanade.tachiyomi.util.inflate | ||||
| import eu.kanade.tachiyomi.util.toast | ||||
| import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener | ||||
| import exh.FavoritesSyncHelper | ||||
| import exh.favorites.FavoritesSyncStatus | ||||
| import exh.metadata.loadAllMetadata | ||||
| import exh.metadata.models.SearchableGalleryMetadata | ||||
| import io.realm.Realm | ||||
| @@ -133,8 +134,12 @@ class LibraryController( | ||||
|     var realm: Realm? = null | ||||
|     //Cached metadata | ||||
|     var meta: Map<KClass<out SearchableGalleryMetadata>, RealmResults<out SearchableGalleryMetadata>>? = null | ||||
|     //Sync dialog | ||||
|     private var favSyncDialog: MaterialDialog? = null | ||||
|     //Old sync status | ||||
|     private var oldSyncStatus: FavoritesSyncStatus? = null | ||||
|     //Favorites | ||||
|     val favorites by lazy { FavoritesSyncHelper(activity!!) } | ||||
|     private var favoritesSyncSubscription: Subscription? = null | ||||
|     // <-- EH | ||||
|  | ||||
|     init { | ||||
| @@ -406,7 +411,7 @@ class LibraryController( | ||||
|                 router.pushController(MigrationController().withFadeTransaction()) | ||||
|             } | ||||
|             R.id.action_download_favorites -> { | ||||
|                 favorites.guiSyncFavorites {  } | ||||
|                 presenter.favoritesSync.runSync() | ||||
|             } | ||||
|             else -> return super.onOptionsItemSelected(item) | ||||
|         } | ||||
| @@ -531,6 +536,100 @@ class LibraryController( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onAttach(view: View) { | ||||
|         super.onAttach(view) | ||||
|  | ||||
|         // --> EXH | ||||
|         cleanupSyncState() | ||||
|         favoritesSyncSubscription = | ||||
|                 presenter.favoritesSync.status | ||||
|                         .observeOn(AndroidSchedulers.mainThread()) | ||||
|                         .subscribe { | ||||
|                     updateSyncStatus(it) | ||||
|         } | ||||
|         // <-- EXH | ||||
|     } | ||||
|  | ||||
|     override fun onDetach(view: View) { | ||||
|         super.onDetach(view) | ||||
|  | ||||
|         //EXH | ||||
|         cleanupSyncState() | ||||
|     } | ||||
|  | ||||
|     // --> EXH | ||||
|     private fun cleanupSyncState() { | ||||
|         favoritesSyncSubscription?.unsubscribe() | ||||
|         favoritesSyncSubscription = null | ||||
|         //Close sync status | ||||
|         favSyncDialog?.dismiss() | ||||
|         favSyncDialog = null | ||||
|         oldSyncStatus = null | ||||
|     } | ||||
|  | ||||
|     private fun buildDialog() = activity?.let { | ||||
|         MaterialDialog.Builder(it) | ||||
|     } | ||||
|      | ||||
|     private fun showSyncProgressDialog() { | ||||
|         favSyncDialog?.dismiss() | ||||
|         favSyncDialog = buildDialog() | ||||
|                 ?.title("Favorites syncing") | ||||
|                 ?.cancelable(false) | ||||
|                 ?.progress(true, 0) | ||||
|                 ?.show() | ||||
|     } | ||||
|  | ||||
|     private fun updateSyncStatus(status: FavoritesSyncStatus) { | ||||
|         when(status) { | ||||
|             is FavoritesSyncStatus.Idle -> { | ||||
|                 favSyncDialog?.dismiss() | ||||
|                 favSyncDialog = null | ||||
|             } | ||||
|             is FavoritesSyncStatus.Error -> { | ||||
|                 favSyncDialog?.dismiss() | ||||
|                 favSyncDialog = buildDialog() | ||||
|                         ?.title("Favorites sync error") | ||||
|                         ?.content("An error occurred during the sync process: ${status.message}") | ||||
|                         ?.cancelable(false) | ||||
|                         ?.positiveText("Ok") | ||||
|                         ?.onPositive { _, _ -> | ||||
|                             presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle()) | ||||
|                         } | ||||
|                         ?.show() | ||||
|             } | ||||
|             is FavoritesSyncStatus.Processing, | ||||
|             is FavoritesSyncStatus.Initializing -> { | ||||
|                 if(favSyncDialog == null || (oldSyncStatus != null | ||||
|                         && oldSyncStatus !is FavoritesSyncStatus.Initializing | ||||
|                         && oldSyncStatus !is FavoritesSyncStatus.Processing)) | ||||
|                     showSyncProgressDialog() | ||||
|  | ||||
|                 favSyncDialog?.setContent(status.message) | ||||
|             } | ||||
|             is FavoritesSyncStatus.Complete -> { | ||||
|                 favSyncDialog?.dismiss() | ||||
|  | ||||
|                 if(status.errors.isNotEmpty()) { | ||||
|                     favSyncDialog = buildDialog() | ||||
|                             ?.title("Favorites sync complete with errors") | ||||
|                             ?.content("Some errors occurred during the sync process:\n\n" | ||||
|                                     + status.errors.joinToString("\n")) | ||||
|                             ?.cancelable(false) | ||||
|                             ?.positiveText("Ok") | ||||
|                             ?.onPositive { _, _ -> | ||||
|                                 presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle()) | ||||
|                             } | ||||
|                             ?.show() | ||||
|                 } else { | ||||
|                     presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle()) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         oldSyncStatus = status | ||||
|     } | ||||
|     // <-- EXH | ||||
|  | ||||
|     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         if (requestCode == REQUEST_IMAGE_OPEN) { | ||||
|             if (data == null || resultCode != Activity.RESULT_OK) return | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.combineLatest | ||||
| import eu.kanade.tachiyomi.util.isNullOrUnsubscribed | ||||
| import exh.favorites.FavoritesSyncHelper | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| @@ -76,6 +77,10 @@ class LibraryPresenter( | ||||
|      */ | ||||
|     private var librarySubscription: Subscription? = null | ||||
|  | ||||
|     // --> EXH | ||||
|     val favoritesSync = FavoritesSyncHelper(context) | ||||
|     // <-- EXH | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|         subscribeLibrary() | ||||
|   | ||||
| @@ -1,137 +0,0 @@ | ||||
| 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 = db.getCategories().executeAsBlocking().toMutableList() | ||||
|             val ourMangas = db.getMangas().executeAsBlocking().filter { | ||||
|                 it.source == EH_SOURCE_ID || it.source == EXH_SOURCE_ID | ||||
|             }.toMutableList() | ||||
|             //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 = mutableListOf<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) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,192 +0,0 @@ | ||||
| package exh; | ||||
|  | ||||
| import android.app.ProgressDialog; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.support.v7.app.AlertDialog; | ||||
|  | ||||
| import com.pushtorefresh.storio.sqlite.operations.put.PutResult; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| 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; | ||||
|     DatabaseHelper db; | ||||
|  | ||||
|     public FavoritesSyncManager(Context context, DatabaseHelper db) { | ||||
|         this.context = context; | ||||
|         this.db = db; | ||||
|     } | ||||
|  | ||||
|     public void guiSyncFavorites(final Runnable onComplete) { | ||||
|         if(!DialogLogin.isLoggedIn(context, false)) { | ||||
|             new AlertDialog.Builder(context).setTitle("Error") | ||||
|                     .setMessage("You are not logged in! Please log in and try again!") | ||||
|                     .setPositiveButton("Ok", new DialogInterface.OnClickListener() { | ||||
|                         @Override | ||||
|                         public void onClick(DialogInterface dialog, int which) { | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     }).show(); | ||||
|             return; | ||||
|         } | ||||
|         final ProgressDialog dialog = ProgressDialog.show(context, "Downloading Favorites", "Please wait...", true, false); | ||||
|         new Thread(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 Handler mainLooper = new Handler(Looper.getMainLooper()); | ||||
|                 try { | ||||
|                     syncFavorites(); | ||||
|                 } catch (Exception e) { | ||||
|                     mainLooper.post(new Runnable() { | ||||
|                         @Override | ||||
|                         public void run() { | ||||
|                             new AlertDialog.Builder(context) | ||||
|                                     .setTitle("Error") | ||||
|                                     .setMessage("There was an error downloading your favorites, please try again later!") | ||||
|                                     .setPositiveButton("Ok", new DialogInterface.OnClickListener() { | ||||
|                                         @Override | ||||
|                                         public void onClick(DialogInterface dialog, int which) { | ||||
|                                             dialog.dismiss(); | ||||
|                                         } | ||||
|                                     }).show(); | ||||
|                         } | ||||
|                     }); | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|                 dialog.dismiss(); | ||||
|                 mainLooper.post(onComplete); | ||||
|             } | ||||
|         }).start(); | ||||
|     }*/ | ||||
| /* | ||||
|     public void syncFavorites() throws IOException { | ||||
|         Pair favResponse = EHentai.fetchFavorites(context); | ||||
|         Map<String, List<Manga>> favorites = favResponse.favs; | ||||
|         List<Category> ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking()); | ||||
|         List<Manga> ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking()); | ||||
|         //Add required categories (categories do not sync upwards) | ||||
|         List<Category> categoriesToInsert = new ArrayList<>(); | ||||
|         for (String theirCategory : favorites.keySet()) { | ||||
|             boolean haveCategory = false; | ||||
|             for (Category category : ourCategories) { | ||||
|                 if (category.getName().endsWith(theirCategory)) { | ||||
|                     haveCategory = true; | ||||
|                 } | ||||
|             } | ||||
|             if (!haveCategory) { | ||||
|                 Category category = Category.Companion.create(theirCategory); | ||||
|                 ourCategories.add(category); | ||||
|                 categoriesToInsert.add(category); | ||||
|             } | ||||
|         } | ||||
|         if (!categoriesToInsert.isEmpty()) { | ||||
|             for(Map.Entry<Category, PutResult> result : db.insertCategories(categoriesToInsert).executeAsBlocking().results().entrySet()) { | ||||
|                 if(result.getValue().wasInserted()) { | ||||
|                     result.getKey().setId(result.getValue().insertedId().intValue()); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         //Build category map | ||||
|         Map<String, Category> categoryMap = new HashMap<>(); | ||||
|         for (Category category : ourCategories) { | ||||
|             categoryMap.put(category.getName(), category); | ||||
|         } | ||||
|         //Insert new mangas | ||||
|         List<Manga> mangaToInsert = new ArrayList<>(); | ||||
|         Map<Manga, Category> mangaToSetCategories = new HashMap<>(); | ||||
|         for (Map.Entry<String, List<Manga>> entry : favorites.entrySet()) { | ||||
|             Category category = categoryMap.get(entry.getKey()); | ||||
|             for (Manga manga : entry.getValue()) { | ||||
|                 boolean alreadyHaveManga = false; | ||||
|                 for (Manga ourManga : ourMangas) { | ||||
|                     if (ourManga.getUrl().equals(manga.getUrl())) { | ||||
|                         alreadyHaveManga = true; | ||||
|                         manga = ourManga; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 if (!alreadyHaveManga) { | ||||
|                     ourMangas.add(manga); | ||||
|                     mangaToInsert.add(manga); | ||||
|                 } | ||||
|                 mangaToSetCategories.put(manga, category); | ||||
|                 manga.setFavorite(true); | ||||
|             } | ||||
|         } | ||||
|         for (Map.Entry<Manga, PutResult> results : db.insertMangas(mangaToInsert).executeAsBlocking().results().entrySet()) { | ||||
|             if(results.getValue().wasInserted()) { | ||||
|                 results.getKey().setId(results.getValue().insertedId()); | ||||
|             } | ||||
|         } | ||||
|         for(Map.Entry<Manga, Category> entry : mangaToSetCategories.entrySet()) { | ||||
|             db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())), | ||||
|                     Collections.singletonList(entry.getKey())); | ||||
|         }*/ | ||||
|         //Determines what | ||||
|         /*Map<Integer, List<Manga>> toUpload = new HashMap<>(); | ||||
|         for (Manga manga : ourMangas) { | ||||
|             if(manga.getFavorite()) { | ||||
|                 boolean remoteHasManga = false; | ||||
|                 for (List<Manga> remoteMangas : favorites.values()) { | ||||
|                     for (Manga remoteManga : remoteMangas) { | ||||
|                         if (remoteManga.getUrl().equals(manga.getUrl())) { | ||||
|                             remoteHasManga = true; | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (!remoteHasManga) { | ||||
|                     List<Category> mangaCategories = db.getCategoriesForManga(manga).executeAsBlocking(); | ||||
|                     for (Category category : mangaCategories) { | ||||
|                         int categoryIndex = favResponse.favCategories.indexOf(category.getName()); | ||||
|                         if (categoryIndex >= 0) { | ||||
|                             List<Manga> uploadMangas = toUpload.get(categoryIndex); | ||||
|                             if (uploadMangas == null) { | ||||
|                                 uploadMangas = new ArrayList<>(); | ||||
|                                 toUpload.put(categoryIndex, uploadMangas); | ||||
|                             } | ||||
|                             uploadMangas.add(manga); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }*/ | ||||
|         /********** NON-FUNCTIONAL, modifygids[] CANNOT ADD NEW FAVORITES! (or as of my testing it can't, maybe I'll do more testing)**/ | ||||
|         /*PreferencesHelper helper = new PreferencesHelper(context); | ||||
|         for(Map.Entry<Integer, List<Manga>> entry : toUpload.entrySet()) { | ||||
|             FormBody.Builder formBody = new FormBody.Builder() | ||||
|                     .add("ddact", "fav" + entry.getKey()); | ||||
|             for(Manga manga : entry.getValue()) { | ||||
|                 List<String> splitUrl = new ArrayList<>(Arrays.asList(manga.getUrl().split("/"))); | ||||
|                 splitUrl.removeAll(Collections.singleton("")); | ||||
|                 if(splitUrl.size() < 2) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 formBody.add("modifygids[]", splitUrl.get(1).trim()); | ||||
|             } | ||||
|             formBody.add("apply", "Apply"); | ||||
|             Request request = RequestsKt.POST(EHentai.buildFavoritesBase(context, helper.getPrefs()).favoritesBase, | ||||
|                     EHentai.getHeadersBuilder(helper).build(), | ||||
|                     formBody.build(), | ||||
|                     RequestsKt.getDEFAULT_CACHE_CONTROL()); | ||||
|             Response response = NetworkManager.getInstance().getClient().newCall(request).execute(); | ||||
|             Util.d("EHentai", response.body().string()); | ||||
|         }*/ | ||||
| //    } | ||||
| } | ||||
| @@ -10,8 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.util.syncChaptersWithSource | ||||
| import exh.metadata.models.* | ||||
| import exh.util.defRealm | ||||
| import exh.metadata.models.ExGalleryMetadata | ||||
| import okhttp3.MediaType | ||||
| import okhttp3.Request | ||||
| import okhttp3.RequestBody | ||||
| @@ -131,7 +130,7 @@ class GalleryAdder { | ||||
|                     ?: return GalleryAddEvent.Fail.Error(url, "Could not find EH source!") | ||||
|  | ||||
|             val cleanedUrl = when(source) { | ||||
|                 EH_SOURCE_ID, EXH_SOURCE_ID -> getUrlWithoutDomain(realUrl) | ||||
|                 EH_SOURCE_ID, EXH_SOURCE_ID -> ExGalleryMetadata.normalizeUrl(getUrlWithoutDomain(realUrl)) | ||||
|                 NHENTAI_SOURCE_ID -> realUrl //nhentai uses URLs directly (oops, my bad when implementing this source) | ||||
|                 PERV_EDEN_EN_SOURCE_ID, | ||||
|                 PERV_EDEN_IT_SOURCE_ID -> getUrlWithoutDomain(realUrl) | ||||
| @@ -152,19 +151,6 @@ class GalleryAdder { | ||||
|             manga.copyFrom(newManga) | ||||
|             manga.title = newManga.title //Forcibly copy title as copyFrom does not copy title | ||||
|  | ||||
|             //Apply metadata | ||||
|             defRealm { realm -> | ||||
|                 when (source) { | ||||
|                     EH_SOURCE_ID, EXH_SOURCE_ID -> ExGalleryMetadata.UrlQuery(realUrl, isExSource(source)) | ||||
|                     NHENTAI_SOURCE_ID -> NHentaiMetadata.UrlQuery(realUrl) | ||||
|                     PERV_EDEN_EN_SOURCE_ID, | ||||
|                     PERV_EDEN_IT_SOURCE_ID -> PervEdenGalleryMetadata.UrlQuery(realUrl, PervEdenLang.source(source)) | ||||
|                     HENTAI_CAFE_SOURCE_ID -> HentaiCafeMetadata.UrlQuery(realUrl) | ||||
|                     TSUMINO_SOURCE_ID -> TsuminoMetadata.UrlQuery(realUrl) | ||||
|                     else -> return GalleryAddEvent.Fail.UnknownType(url) | ||||
|                 }.query(realm).findFirst() | ||||
|             } | ||||
|  | ||||
|             if (fav) manga.favorite = true | ||||
|  | ||||
|             db.insertManga(manga).executeAsBlocking().insertedId()?.let { | ||||
|   | ||||
							
								
								
									
										23
									
								
								app/src/main/java/exh/favorites/FavoriteEntry.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/src/main/java/exh/favorites/FavoriteEntry.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| package exh.favorites | ||||
|  | ||||
| import exh.metadata.models.ExGalleryMetadata | ||||
| import io.realm.RealmObject | ||||
| import io.realm.annotations.Index | ||||
| import io.realm.annotations.PrimaryKey | ||||
| import io.realm.annotations.RealmClass | ||||
| import java.util.* | ||||
|  | ||||
| @RealmClass | ||||
| open class FavoriteEntry : RealmObject() { | ||||
|     @PrimaryKey var id: String = UUID.randomUUID().toString() | ||||
|  | ||||
|     var title: String? = null | ||||
|  | ||||
|     @Index lateinit var gid: String | ||||
|  | ||||
|     @Index lateinit var token: String | ||||
|  | ||||
|     @Index var category: Int = -1 | ||||
|  | ||||
|     fun getUrl() = ExGalleryMetadata.normalizeUrl(gid, token) | ||||
| } | ||||
							
								
								
									
										274
									
								
								app/src/main/java/exh/favorites/FavoritesSyncHelper.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								app/src/main/java/exh/favorites/FavoritesSyncHelper.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,274 @@ | ||||
| package exh.favorites | ||||
|  | ||||
| import android.content.Context | ||||
| 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 exh.EH_METADATA_SOURCE_ID | ||||
| import exh.EXH_SOURCE_ID | ||||
| import exh.GalleryAddEvent | ||||
| import exh.GalleryAdder | ||||
| import okhttp3.FormBody | ||||
| import okhttp3.Request | ||||
| import rx.subjects.BehaviorSubject | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import kotlin.concurrent.thread | ||||
|  | ||||
| class FavoritesSyncHelper(context: Context) { | ||||
|     private val db: DatabaseHelper by injectLazy() | ||||
|  | ||||
|     private val prefs: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val exh by lazy { | ||||
|         Injekt.get<SourceManager>().get(EXH_SOURCE_ID) as? EHentai | ||||
|                 ?: EHentai(0, true, context) | ||||
|     } | ||||
|  | ||||
|     private val storage = LocalFavoritesStorage() | ||||
|  | ||||
|     private val galleryAdder = GalleryAdder() | ||||
|  | ||||
|     val status = BehaviorSubject.create<FavoritesSyncStatus>(FavoritesSyncStatus.Idle()) | ||||
|  | ||||
|     @Synchronized | ||||
|     fun runSync() { | ||||
|         if(status.value !is FavoritesSyncStatus.Idle) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         status.onNext(FavoritesSyncStatus.Initializing()) | ||||
|  | ||||
|         thread { beginSync() } | ||||
|     } | ||||
|  | ||||
|     private fun beginSync() { | ||||
|         //Check if logged in | ||||
|         if(!prefs.enableExhentai().getOrDefault()) { | ||||
|             status.onNext(FavoritesSyncStatus.Error("Please log in!")) | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         //Download remote favorites | ||||
|         val favorites = try { | ||||
|             status.onNext(FavoritesSyncStatus.Processing("Downloading favorites from remote server")) | ||||
|             exh.fetchFavorites() | ||||
|         } catch(e: Exception) { | ||||
|             status.onNext(FavoritesSyncStatus.Error("Failed to fetch favorites from remote server!")) | ||||
|             Timber.e(e, "Could not fetch favorites!") | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         val errors = mutableListOf<String>() | ||||
|  | ||||
|         try { | ||||
|             db.inTransaction { | ||||
|                 val remoteChanges = storage.getChangedRemoteEntries(favorites.first) | ||||
|                 val localChanges = storage.getChangedDbEntries() | ||||
|  | ||||
|                 //Apply remote categories | ||||
|                 status.onNext(FavoritesSyncStatus.Processing("Updating category names")) | ||||
|                 applyRemoteCategories(favorites.second) | ||||
|  | ||||
|                 //Apply ChangeSets | ||||
|                 applyChangeSetToLocal(remoteChanges, errors) | ||||
|                 applyChangeSetToRemote(localChanges, errors) | ||||
|  | ||||
|                 status.onNext(FavoritesSyncStatus.Processing("Cleaning up")) | ||||
|                 storage.snapshotEntries() | ||||
|             } | ||||
|         } catch(e: IgnoredException) { | ||||
|             //Do not display error as this error has already been reported | ||||
|             Timber.w(e, "Ignoring exception!") | ||||
|             return | ||||
|         } catch (e: Exception) { | ||||
|             status.onNext(FavoritesSyncStatus.Error("Unknown error: ${e.message}")) | ||||
|             Timber.e(e, "Sync error!") | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         status.onNext(FavoritesSyncStatus.Complete(errors)) | ||||
|     } | ||||
|  | ||||
|     private fun applyRemoteCategories(categories: List<String>) { | ||||
|         val localCategories = db.getCategories().executeAsBlocking() | ||||
|  | ||||
|         val newLocalCategories = localCategories.toMutableList() | ||||
|  | ||||
|         var changed = false | ||||
|  | ||||
|         categories.forEachIndexed { index, remote -> | ||||
|             val local = localCategories.getOrElse(index) { | ||||
|                 changed = true | ||||
|  | ||||
|                 Category.create(remote).apply { | ||||
|                     order = index | ||||
|  | ||||
|                     //Going through categories list from front to back | ||||
|                     //If category does not exist, list size <= category index | ||||
|                     //Thus, we can just add it here and not worry about indexing | ||||
|                     newLocalCategories += this | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if(local.name != remote) { | ||||
|                 changed = true | ||||
|  | ||||
|                 local.name = remote | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //Ensure consistent ordering | ||||
|         newLocalCategories.forEachIndexed { index, category -> | ||||
|             if(category.order != index) { | ||||
|                 changed = true | ||||
|  | ||||
|                 category.order = index | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //Only insert categories if changed | ||||
|         if(changed) | ||||
|             db.insertCategories(newLocalCategories).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     private fun addGalleryRemote(gallery: FavoriteEntry, errors: MutableList<String>) { | ||||
|         val url = "${exh.baseUrl}/gallerypopups.php?gid=${gallery.gid}&t=${gallery.token}&act=addfav" | ||||
|  | ||||
|         val request = Request.Builder() | ||||
|                 .url(url) | ||||
|                 .post(FormBody.Builder() | ||||
|                         .add("favcat", gallery.category.toString()) | ||||
|                         .add("favnote", "") | ||||
|                         .add("apply", "Add to Favorites") | ||||
|                         .add("update", "1") | ||||
|                         .build()) | ||||
|                 .build() | ||||
|  | ||||
|         if(!explicitlyRetryExhRequest(10, request)) { | ||||
|             errors += "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun explicitlyRetryExhRequest(retryCount: Int, request: Request): Boolean { | ||||
|         var success = false | ||||
|  | ||||
|         for(i in 1 .. retryCount) { | ||||
|             try { | ||||
|                 val resp = exh.client.newCall(request).execute() | ||||
|  | ||||
|                 if (resp.isSuccessful) { | ||||
|                     success = true | ||||
|                     break | ||||
|                 } | ||||
|             } catch (e: Exception) { | ||||
|                 Timber.e(e, "Sync network error!") | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return success | ||||
|     } | ||||
|  | ||||
|     private fun applyChangeSetToRemote(changeSet: ChangeSet, errors: MutableList<String>) { | ||||
|         //Apply removals | ||||
|         if(changeSet.removed.isNotEmpty()) { | ||||
|             status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server")) | ||||
|  | ||||
|             val formBody = FormBody.Builder() | ||||
|                     .add("ddact", "delete") | ||||
|                     .add("apply", "Apply") | ||||
|  | ||||
|             //Add change set to form | ||||
|             changeSet.removed.forEach { | ||||
|                 formBody.add("modifygids[]", it.gid) | ||||
|             } | ||||
|  | ||||
|             val request = Request.Builder() | ||||
|                     .url("https://exhentai.org/favorites.php") | ||||
|                     .post(formBody.build()) | ||||
|                     .build() | ||||
|  | ||||
|             if(!explicitlyRetryExhRequest(10, request)) { | ||||
|                 status.onNext(FavoritesSyncStatus.Error("Unable to delete galleries from the remote servers!")) | ||||
|  | ||||
|                 //It is still safe to stop here so crash | ||||
|                 throw IgnoredException() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //Apply additions | ||||
|         changeSet.added.forEachIndexed { index, it -> | ||||
|             status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server")) | ||||
|  | ||||
|             addGalleryRemote(it, errors) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun applyChangeSetToLocal(changeSet: ChangeSet, errors: MutableList<String>) { | ||||
|         val removedManga = mutableListOf<Manga>() | ||||
|  | ||||
|         //Apply removals | ||||
|         changeSet.removed.forEachIndexed { index, it -> | ||||
|             status.onNext(FavoritesSyncStatus.Processing("Removing gallery ${index + 1} of ${changeSet.removed.size} from local library")) | ||||
|             val url = it.getUrl() | ||||
|  | ||||
|             //Consider both EX and EH sources | ||||
|             listOf(db.getManga(url, EXH_SOURCE_ID), | ||||
|                     db.getManga(url, EH_METADATA_SOURCE_ID)).forEach { | ||||
|                 val manga = it.executeAsBlocking() | ||||
|  | ||||
|                 if(manga?.favorite == true) { | ||||
|                     manga.favorite = false | ||||
|                     db.updateMangaFavorite(manga).executeAsBlocking() | ||||
|                     removedManga += manga | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         db.deleteOldMangasCategories(removedManga).executeAsBlocking() | ||||
|  | ||||
|         val insertedMangaCategories = mutableListOf<MangaCategory>() | ||||
|         val insertedMangaCategoriesMangas = mutableListOf<Manga>() | ||||
|         val categories = db.getCategories().executeAsBlocking() | ||||
|  | ||||
|         //Apply additions | ||||
|         changeSet.added.forEachIndexed { index, it -> | ||||
|             status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library")) | ||||
|  | ||||
|             //Import using gallery adder | ||||
|             val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}", | ||||
|                     true, | ||||
|                     EXH_SOURCE_ID) | ||||
|  | ||||
|             if(result is GalleryAddEvent.Fail) { | ||||
|                 errors += "Failed to add gallery to local database: " + when (result) { | ||||
|                     is GalleryAddEvent.Fail.Error -> "'${it.title}' ${result.logMessage}" | ||||
|                     is GalleryAddEvent.Fail.UnknownType -> "'${it.title}' (${result.galleryUrl}) is not a valid gallery!" | ||||
|                 } | ||||
|             } else if(result is GalleryAddEvent.Success) { | ||||
|                 insertedMangaCategories += MangaCategory.create(result.manga, | ||||
|                         categories[it.category]) | ||||
|                 insertedMangaCategoriesMangas += result.manga | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         db.setMangaCategories(insertedMangaCategories, insertedMangaCategoriesMangas) | ||||
|     } | ||||
|  | ||||
|     class IgnoredException : RuntimeException() | ||||
| } | ||||
|  | ||||
| sealed class FavoritesSyncStatus(val message: String) { | ||||
|     class Error(message: String) : FavoritesSyncStatus(message) | ||||
|     class Idle : FavoritesSyncStatus("Waiting for sync to start") | ||||
|     class Initializing : FavoritesSyncStatus("Initializing sync") | ||||
|     class Processing(message: String) : FavoritesSyncStatus(message) | ||||
|     class Complete(val errors: List<String>) : FavoritesSyncStatus("Sync complete!") | ||||
| } | ||||
							
								
								
									
										132
									
								
								app/src/main/java/exh/favorites/LocalFavoritesStorage.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								app/src/main/java/exh/favorites/LocalFavoritesStorage.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| package exh.favorites | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.source.online.all.EHentai | ||||
| import exh.EH_SOURCE_ID | ||||
| import exh.EXH_SOURCE_ID | ||||
| import exh.metadata.models.ExGalleryMetadata | ||||
| import exh.util.trans | ||||
| import io.realm.Realm | ||||
| import io.realm.RealmConfiguration | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class LocalFavoritesStorage { | ||||
|     private val db: DatabaseHelper by injectLazy() | ||||
|  | ||||
|     private val realmConfig = RealmConfiguration.Builder() | ||||
|             .name("fav-sync") | ||||
|             .deleteRealmIfMigrationNeeded() | ||||
|             .build() | ||||
|  | ||||
|     private val realm | ||||
|         get() = Realm.getInstance(realmConfig) | ||||
|  | ||||
|     fun getChangedDbEntries() | ||||
|             = getChangedEntries( | ||||
|             parseToFavoriteEntries( | ||||
|                     loadDbCategories( | ||||
|                             db.getFavoriteMangas() | ||||
|                                     .executeAsBlocking() | ||||
|                                     .asSequence() | ||||
|                     ) | ||||
|             ) | ||||
|     ) | ||||
|  | ||||
|     fun getChangedRemoteEntries(entries: List<EHentai.ParsedManga>) | ||||
|             = getChangedEntries( | ||||
|             parseToFavoriteEntries( | ||||
|                     entries.asSequence().map { | ||||
|                         Pair(it.fav, it.manga.apply { | ||||
|                             favorite = true | ||||
|                         }) | ||||
|                     } | ||||
|             ) | ||||
|     ) | ||||
|  | ||||
|     fun snapshotEntries() { | ||||
|         val dbMangas = parseToFavoriteEntries( | ||||
|                 loadDbCategories( | ||||
|                         db.getFavoriteMangas() | ||||
|                                 .executeAsBlocking() | ||||
|                                 .asSequence() | ||||
|                 ) | ||||
|         ) | ||||
|  | ||||
|         realm.use { realm -> | ||||
|             realm.trans { | ||||
|                 //Delete old snapshot | ||||
|                 realm.delete(FavoriteEntry::class.java) | ||||
|  | ||||
|                 //Insert new snapshots | ||||
|                 realm.copyToRealm(dbMangas.toList()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getChangedEntries(entries: Sequence<FavoriteEntry>): ChangeSet { | ||||
|         return realm.use { realm -> | ||||
|             val terminated = entries.toList() | ||||
|  | ||||
|             val added = terminated.filter { | ||||
|                 realm.queryRealmForEntry(it) == null | ||||
|             } | ||||
|  | ||||
|             val removed = realm.where(FavoriteEntry::class.java) | ||||
|                     .findAll() | ||||
|                     .filter { | ||||
|                         queryListForEntry(terminated, it) == null | ||||
|                     }.map { | ||||
|                         realm.copyFromRealm(it) | ||||
|                     } | ||||
|  | ||||
|             ChangeSet(added, removed) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun Realm.queryRealmForEntry(entry: FavoriteEntry) | ||||
|             = where(FavoriteEntry::class.java) | ||||
|             .equalTo(FavoriteEntry::gid.name, entry.gid) | ||||
|             .equalTo(FavoriteEntry::token.name, entry.token) | ||||
|             .equalTo(FavoriteEntry::category.name, entry.category) | ||||
|             .findFirst() | ||||
|  | ||||
|     private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) | ||||
|         = list.find { | ||||
|         it.gid == entry.gid | ||||
|                 && it.token == entry.token | ||||
|                 && it.category == entry.category | ||||
|     } | ||||
|  | ||||
|     private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> { | ||||
|         val dbCategories = db.getCategories().executeAsBlocking() | ||||
|  | ||||
|         return manga.filter(this::validateDbManga).mapNotNull { | ||||
|             val category = db.getCategoriesForManga(it).executeAsBlocking() | ||||
|  | ||||
|             Pair(dbCategories.indexOf(category.firstOrNull() | ||||
|                     ?: return@mapNotNull null), it) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) | ||||
|             = manga.filter { | ||||
|         validateDbManga(it.second) | ||||
|     }.mapNotNull { | ||||
|                 FavoriteEntry().apply { | ||||
|                     title = it.second.title | ||||
|                     gid = ExGalleryMetadata.galleryId(it.second.url) | ||||
|                     token = ExGalleryMetadata.galleryToken(it.second.url) | ||||
|                     category = it.first | ||||
|  | ||||
|                     if(this.category > 9) | ||||
|                         return@mapNotNull null | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|     private fun validateDbManga(manga: Manga) | ||||
|             = manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) | ||||
| } | ||||
|  | ||||
| data class ChangeSet(val added: List<FavoriteEntry>, | ||||
|                      val removed: List<FavoriteEntry>) | ||||
| @@ -26,6 +26,10 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata { | ||||
|     override var uuid: String = UUID.randomUUID().toString() | ||||
|  | ||||
|     var url: String? = null | ||||
|         set(value) { | ||||
|             //Ensure that URLs are always formatted in the same way to reduce duplicate galleries | ||||
|             field = value?.let { normalizeUrl(it) } | ||||
|         } | ||||
|  | ||||
|     @Index | ||||
|     var gId: String? = null | ||||
| @@ -60,7 +64,7 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata { | ||||
|  | ||||
|     override var tags: RealmList<Tag> = RealmList() | ||||
|  | ||||
|     override fun getTitles() = listOf(title, altTitle).filterNotNull() | ||||
|     override fun getTitles() = listOfNotNull(title, altTitle) | ||||
|  | ||||
|     @Ignore | ||||
|     override val titleFields = TITLE_FIELDS | ||||
| @@ -93,7 +97,7 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata { | ||||
|     } | ||||
|  | ||||
|     override fun copyTo(manga: SManga) { | ||||
|         url?.let { manga.url = it } | ||||
|         url?.let { manga.url = normalizeUrl(it) } | ||||
|         thumbnailUrl?.let { manga.thumbnail_url = it } | ||||
|  | ||||
|         //No title bug? | ||||
| @@ -118,8 +122,8 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata { | ||||
|             ONGOING_SUFFIX.find { | ||||
|                 t.endsWith(it, ignoreCase = true) | ||||
|             }?.let { | ||||
|                 manga.status = SManga.ONGOING | ||||
|             } | ||||
|                         manga.status = SManga.ONGOING | ||||
|                     } | ||||
|         } | ||||
|  | ||||
|         //Build a nice looking description out of what we know | ||||
| @@ -165,6 +169,12 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata { | ||||
|         fun galleryToken(url: String) = | ||||
|                 splitGalleryUrl(url).last() | ||||
|  | ||||
|         fun normalizeUrl(id: String, token: String) | ||||
|                 = "/g/$id/$token/?nw=always" | ||||
|  | ||||
|         fun normalizeUrl(url: String) | ||||
|                 = normalizeUrl(galleryId(url), galleryToken(url)) | ||||
|  | ||||
|         val TITLE_FIELDS = listOf( | ||||
|                 ExGalleryMetadata::title.name, | ||||
|                 ExGalleryMetadata::altTitle.name | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| package exh.metadata.models | ||||
|  | ||||
| import io.realm.* | ||||
| import io.realm.Case | ||||
| import io.realm.Realm | ||||
| import io.realm.RealmQuery | ||||
| import java.util.* | ||||
| import kotlin.reflect.KClass | ||||
| import kotlin.reflect.KProperty | ||||
| @@ -58,7 +60,7 @@ abstract class GalleryQuery<T : SearchableGalleryMetadata>(val clazz: KClass<T>) | ||||
|                     is Long -> newMeta.equalTo(n, v) | ||||
|                     is Short -> newMeta.equalTo(n, v) | ||||
|                     is String -> newMeta.equalTo(n, v, Case.INSENSITIVE) | ||||
|                     else -> throw IllegalArgumentException("Unknown type: ${v::class.qualifiedName}!") | ||||
|                     else -> throw IllegalArgumentException("Unknown type: ${v::class.java.name}!") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user