diff --git a/app/build.gradle b/app/build.gradle
index 5dd61f9d4..8664b6ef4 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -3,6 +3,8 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
+//Realm (EH)
+apply plugin: 'realm-android'
if (file("custom.gradle").exists()) {
apply from: "custom.gradle"
@@ -207,18 +209,6 @@ dependencies {
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
- //Firebase (EH)
- final firebase_version = '10.0.1'
- releaseCompile "com.google.firebase:firebase-core:$firebase_version"
- releaseCompile "com.google.firebase:firebase-messaging:$firebase_version"
- releaseCompile "com.google.firebase:firebase-crash:$firebase_version"
-
- //SnappyDB (EH)
- compile 'io.paperdb:paperdb:2.1'
-
- //JVE (Regex) (EH)
- compile 'ru.lanwen.verbalregex:java-verbal-expressions:1.4'
-
//Pin lock view (EXH)
compile 'com.andrognito.pinlockview:pinlockview:1.0.1'
@@ -303,5 +293,3 @@ afterEvaluate {
}
}
}
-//Firebase (EH)
-apply plugin: 'com.google.gms.google-services'
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 4bab22771..5cd188a7c 100755
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -89,6 +89,16 @@
# [EH]
-keep class exh.** { *; }
+# Realm
+-dontnote rx.internal.util.PlatformDependent
+-keep * public class * extends io.realm.RealmObject
+-keep * public class * implements io.realm.RealmModel
+-keep class io.realm.annotations.RealmModule
+-keep @io.realm.annotations.RealmModule class *
+-keep class io.realm.internal.Keep
+-keep @io.realm.internal.Keep class *
+-dontwarn io.realm.**
+
# Keep google stuff
-dontwarn com.google.android.gms.**
--dontwarn com.google.firebase.**
\ No newline at end of file
+-dontwarn com.google.firebase.**
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7b723e391..c715fd93f 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -139,6 +139,14 @@
android:host="exhentai.org"
android:pathPrefix="/g/"
android:scheme="https"/>
+
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index 4997f2587..fe6850a76 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -10,11 +10,14 @@ import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.util.LocaleHelper
-import io.paperdb.Paper
+import io.realm.Realm
+import io.realm.RealmConfiguration
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar
+import java.io.File
+import kotlin.concurrent.thread
open class App : Application() {
@@ -26,7 +29,7 @@ open class App : Application() {
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupJobManager()
- Paper.init(this) //Setup metadata DB (EH)
+ setupRealm() //Setup metadata DB (EH)
Reprint.initialize(this) //Setup fingerprint (EH)
LocaleHelper.updateConfiguration(this, resources.configuration)
@@ -55,4 +58,25 @@ open class App : Application() {
}
}
+ private fun setupRealm() {
+ Realm.init(this)
+ val config = RealmConfiguration.Builder()
+ .name("gallery-metadata.realm")
+ .schemaVersion(1)
+ .build()
+ Realm.setDefaultConfiguration(config)
+
+ //Delete old paper db files
+ listOf(
+ File(filesDir, "gallery-ex"),
+ File(filesDir, "gallery-perveden"),
+ File(filesDir, "gallery-nhentai")
+ ).forEach {
+ if(it.exists()) {
+ thread {
+ it.deleteRecursively()
+ }
+ }
+ }
+ }
}
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 586a4c0c4..bb51eb850 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
@@ -180,14 +180,12 @@ class PreferencesHelper(val context: Context) {
fun thumbnailRows() = rxPrefs.getString("ex_thumb_rows", "tr_2")
- fun migrateLibraryAsked() = rxPrefs.getBoolean("ex_migrate_library", false)
+ fun migrateLibraryAsked2() = rxPrefs.getBoolean("ex_migrate_library2", false)
fun migrationStatus() = rxPrefs.getInteger("migration_status", MigrationStatus.NOT_INITIALIZED)
fun hasPerformedURLMigration() = rxPrefs.getBoolean("performed_url_migration", false)
- fun hasPerformedSourceMigration() = rxPrefs.getBoolean("performed_source_migration", false)
-
//EH Cookies
fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", null)
fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", null)
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 497d43b8f..30813d069 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.online.all.EHentai
-import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.YamlHttpSource
import eu.kanade.tachiyomi.source.online.all.NHentai
@@ -88,13 +87,11 @@ open class SourceManager(private val context: Context) {
)
private fun createEHSources(): List {
- val exSrcs = mutableListOf(
- EHentai(EH_SOURCE_ID, false, context),
- EHentaiMetadata(EH_METADATA_SOURCE_ID, false, context)
+ val exSrcs = mutableListOf(
+ EHentai(EH_SOURCE_ID, false, context)
)
if(prefs.enableExhentai().getOrDefault()) {
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
- exSrcs += EHentaiMetadata(EXH_METADATA_SOURCE_ID, true, context)
}
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, "en")
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, "it")
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 f6657889f..edec28b86 100755
--- 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
@@ -20,14 +20,13 @@ import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.util.*
import exh.ui.login.LoginController
-import exh.util.UriFilter
-import exh.util.UriGroup
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.Request
import org.jsoup.nodes.Document
import exh.GalleryAdder
-import exh.util.urlImportFetchSearchManga
+import exh.util.*
+import io.realm.Realm
class EHentai(override val id: Long,
val exh: Boolean,
@@ -50,8 +49,6 @@ class EHentai(override val id: Long,
val prefs: PreferencesHelper by injectLazy()
- val metadataHelper = MetadataHelper()
-
val galleryAdder = GalleryAdder()
/**
@@ -168,10 +165,10 @@ class EHentai(override val id: Long,
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true)
- = GET(page?.let {
- addParam(url, "page", Integer.toString(page - 1))
- } ?: url, additionalHeaders?.let {
- val headers = headers.newBuilder()
+ = GET(page?.let {
+ addParam(url, "page", Integer.toString(page - 1))
+ } ?: url, additionalHeaders?.let {
+ val headers = headers.newBuilder()
it.toMultimap().forEach { (t, u) ->
u.forEach {
headers.add(t, it)
@@ -188,86 +185,90 @@ class EHentai(override val id: Long,
/**
* Parse gallery page to metadata model
*/
- override fun mangaDetailsParse(response: Response) = with(response.asJsoup()) {
- val metdata = ExGalleryMetadata()
- with(metdata) {
- url = response.request().url().encodedPath()
- exh = this@EHentai.exh
- title = select("#gn").text().nullIfBlank()?.trim()
+ override fun mangaDetailsParse(response: Response)
+ = with(response.asJsoup()) {
+ realmTrans { realm ->
+ val url = response.request().url().encodedPath()!!
+ val gId = ExGalleryMetadata.galleryId(url)
+ val gToken = ExGalleryMetadata.galleryToken(url)
- altTitle = select("#gj").text().nullIfBlank()?.trim()
+ val metdata = (realm.loadEh(gId, gToken, exh)
+ ?: realm.createUUIDObj(ExGalleryMetadata::class.java))
+ with(metdata) {
+ this.url = url
+ this.gId = gId
+ this.gToken = gToken
- thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
- it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
- }
+ exh = this@EHentai.exh
+ title = select("#gn").text().nullIfBlank()?.trim()
- genre = select(".ic").parents().attr("href").nullIfBlank()?.trim()?.substringAfterLast('/')
+ altTitle = select("#gj").text().nullIfBlank()?.trim()
- uploader = select("#gdn").text().nullIfBlank()?.trim()
+ thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
+ it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
+ }
+ genre = select(".ic").parents().attr("href").nullIfBlank()?.trim()?.substringAfterLast('/')
- //Parse the table
- select("#gdd tr").forEach {
- it.select(".gdt1")
- .text()
- .nullIfBlank()
- ?.trim()
- ?.let { left ->
- it.select(".gdt2")
- .text()
- .nullIfBlank()
- ?.trim()
- ?.let { right ->
- ignore {
- when (left.removeSuffix(":")
- .toLowerCase()) {
- "posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
- "visible" -> visible = right.nullIfBlank()
- "language" -> {
- language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
- translated = right.endsWith(TR_SUFFIX, true)
+ uploader = select("#gdn").text().nullIfBlank()?.trim()
+
+ //Parse the table
+ select("#gdd tr").forEach {
+ it.select(".gdt1")
+ .text()
+ .nullIfBlank()
+ ?.trim()
+ ?.let { left ->
+ it.select(".gdt2")
+ .text()
+ .nullIfBlank()
+ ?.trim()
+ ?.let { right ->
+ ignore {
+ when (left.removeSuffix(":")
+ .toLowerCase()) {
+ "posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
+ "visible" -> visible = right.nullIfBlank()
+ "language" -> {
+ language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
+ translated = right.endsWith(TR_SUFFIX, true)
+ }
+ "file size" -> size = parseHumanReadableByteCount(right)?.toLong()
+ "length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
+ "favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
}
- "file size" -> size = parseHumanReadableByteCount(right)?.toLong()
- "length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
- "favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
}
}
- }
- }
- }
-
- //Parse ratings
- ignore {
- averageRating = select("#rating_label")
- .text()
- .removePrefix("Average:")
- .trim()
- .nullIfBlank()
- ?.toDouble()
- ratingCount = select("#rating_count")
- .text()
- .trim()
- .nullIfBlank()
- ?.toInt()
- }
-
- //Parse tags
- tags.clear()
- select("#taglist tr").forEach {
- val namespace = it.select(".tc").text().removeSuffix(":")
- val currentTags = it.select("div").map {
- Tag(it.text().trim(),
- it.hasClass("gtl"))
+ }
}
- tags.put(namespace, ArrayList(currentTags))
- }
- //Save metadata
- metadataHelper.writeGallery(this, id)
+ //Parse ratings
+ ignore {
+ averageRating = select("#rating_label")
+ .text()
+ .removePrefix("Average:")
+ .trim()
+ .nullIfBlank()
+ ?.toDouble()
+ ratingCount = select("#rating_count")
+ .text()
+ .trim()
+ .nullIfBlank()
+ ?.toInt()
+ }
- //Copy metadata to manga
- SManga.create().let {
- copyTo(it)
- it
+ //Parse tags
+ tags.clear()
+ select("#taglist tr").forEach {
+ val namespace = it.select(".tc").text().removeSuffix(":")
+ tags.addAll(it.select("div").map {
+ Tag(namespace, it.text().trim(), it.hasClass("gtl"))
+ })
+ }
+
+ //Copy metadata to manga
+ SManga.create().apply {
+ copyTo(this)
+ }
}
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentaiMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentaiMetadata.kt
deleted file mode 100755
index d053dc9d4..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentaiMetadata.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-package eu.kanade.tachiyomi.source.online.all
-
-import android.content.Context
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.source.model.*
-import eu.kanade.tachiyomi.source.online.HttpSource
-import exh.metadata.MetadataHelper
-import exh.metadata.copyTo
-import exh.metadata.models.ExGalleryMetadata
-import exh.search.SearchEngine
-import okhttp3.Response
-import rx.Observable
-
-/**
- * Offline metadata store source
- *
- * TODO This no longer fakes an online source because of technical reasons.
- * If we still want offline search, we must find out a way to rearchitecture the source system so it supports
- * online source faking again.
- */
-
-class EHentaiMetadata(override val id: Long,
- val exh: Boolean,
- val context: Context) : HttpSource() {
- override fun popularMangaRequest(page: Int)
- = throw UnsupportedOperationException("Unused method called!")
- override fun popularMangaParse(response: Response)
- = throw UnsupportedOperationException("Unused method called!")
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
- = throw UnsupportedOperationException("Unused method called!")
- override fun searchMangaParse(response: Response)
- = throw UnsupportedOperationException("Unused method called!")
- override fun latestUpdatesRequest(page: Int)
- = throw UnsupportedOperationException("Unused method called!")
- override fun latestUpdatesParse(response: Response)
- = throw UnsupportedOperationException("Unused method called!")
- override fun mangaDetailsParse(response: Response)
- = throw UnsupportedOperationException("Unused method called!")
- override fun chapterListParse(response: Response)
- = throw UnsupportedOperationException("Unused method called!")
- override fun pageListParse(response: Response)
- = throw UnsupportedOperationException("Unused method called!")
- override fun imageUrlParse(response: Response)
- = throw UnsupportedOperationException("Unused method called!")
-
- val metadataHelper = MetadataHelper()
-
- val internalEx = EHentai(id - 2, exh, context)
-
- val searchEngine = SearchEngine()
-
- override val baseUrl: String
- get() = throw UnsupportedOperationException()
- override val lang: String
- get() = "advanced"
- override val supportsLatest: Boolean
- get() = true
-
- override fun fetchChapterList(manga: SManga): Observable>
- = Observable.just(listOf(Chapter.create().apply {
- url = manga.url
- name = "ONLINE - Chapter"
- chapter_number = 1f
- }))
-
- override fun fetchPageList(chapter: SChapter) = internalEx.fetchPageList(chapter)
-
- override fun fetchImageUrl(page: Page) = internalEx.fetchImageUrl(page)
-
- fun List.mapToManga() = filter { it.exh == exh }
- .map {
- Manga.create(id).apply {
- it.copyTo(this)
- source = this@EHentaiMetadata.id
- }
- }
-
- fun sortedByTimeGalleries() = metadataHelper.getAllGalleries().sortedByDescending {
- it.datePosted ?: 0
- }
-
- override fun fetchPopularManga(page: Int)
- = Observable.fromCallable {
- MangasPage(metadataHelper.getAllGalleries().sortedByDescending {
- it.ratingCount ?: 0
- }.mapToManga(), false)
- }!!
-
- override fun fetchSearchManga(page: Int, query: String, filters: FilterList)
- = Observable.fromCallable {
- val genreGroup = filters.find {
- it is EHentai.GenreGroup
- }!! as EHentai.GenreGroup
- val disableGenreFilter = genreGroup.state.find(EHentai.GenreOption::state) == null
-
- val parsed = searchEngine.parseQuery(query)
- MangasPage(sortedByTimeGalleries().filter { manga ->
- disableGenreFilter || genreGroup.state.find {
- it.state && it.genreId == manga.genre
- } != null
- }.filter {
- searchEngine.matches(it, parsed)
- }.mapToManga(), false)
- }!!
-
- override fun fetchLatestUpdates(page: Int)
- = Observable.fromCallable {
- MangasPage(sortedByTimeGalleries().mapToManga(), false)
- }!!
-
- override fun fetchMangaDetails(manga: SManga) = Observable.fromCallable {
- //Hack to convert the gallery into an online gallery when favoriting it or reading it
- metadataHelper.fetchEhMetadata(manga.url, exh)?.copyTo(manga)
- manga
- }!!
-
- override fun getFilterList() = FilterList(EHentai.GenreGroup())
-
- override val name: String
- get() = if(exh) {
- "ExHentai"
- } else {
- "E-Hentai"
- } + " - METADATA"
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt
index b6fe91991..df412103b 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt
@@ -17,10 +17,15 @@ import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource
import exh.NHENTAI_SOURCE_ID
-import exh.metadata.MetadataHelper
import exh.metadata.copyTo
+import exh.metadata.loadNhentai
+import exh.metadata.loadNhentaiAsync
import exh.metadata.models.NHentaiMetadata
+import exh.metadata.models.PageImageType
import exh.metadata.models.Tag
+import exh.util.createUUIDObj
+import exh.util.defRealm
+import exh.util.realmTrans
import exh.util.urlImportFetchSearchManga
import okhttp3.Request
import okhttp3.Response
@@ -108,62 +113,73 @@ class NHentai(context: Context) : HttpSource() {
return MangasPage(emptyList(), false)
}
- fun rawParseGallery(obj: JsonObject) = NHentaiMetadata().apply {
- uploadDate = obj.get("upload_date")?.notNull()?.long
+ fun rawParseGallery(obj: JsonObject) = realmTrans { realm ->
+ val nhId = obj.get("id").asLong
- favoritesCount = obj.get("num_favorites")?.notNull()?.long
+ (realm.loadNhentai(nhId)
+ ?: realm.createUUIDObj(NHentaiMetadata::class.java)).apply {
+ this.nhId = nhId
- mediaId = obj.get("media_id")?.notNull()?.string
+ uploadDate = obj.get("upload_date")?.notNull()?.long
- obj.get("title")?.asJsonObject?.let {
- japaneseTitle = it.get("japanese")?.notNull()?.string
- shortTitle = it.get("pretty")?.notNull()?.string
- englishTitle = it.get("english")?.notNull()?.string
- }
+ favoritesCount = obj.get("num_favorites")?.notNull()?.long
- obj.get("images")?.asJsonObject?.let {
- coverImageType = it.get("cover")?.get("t")?.notNull()?.asString
- it.get("pages")?.asJsonArray?.map {
- it?.asJsonObject?.get("t")?.notNull()?.asString
- }?.filterNotNull()?.let {
- pageImageTypes.clear()
- pageImageTypes.addAll(it)
+ mediaId = obj.get("media_id")?.notNull()?.string
+
+ obj.get("title")?.asJsonObject?.let {
+ japaneseTitle = it.get("japanese")?.notNull()?.string
+ shortTitle = it.get("pretty")?.notNull()?.string
+ englishTitle = it.get("english")?.notNull()?.string
}
- thumbnailImageType = it.get("thumbnail")?.get("t")?.notNull()?.asString
- }
- scanlator = obj.get("scanlator")?.notNull()?.asString
+ obj.get("images")?.asJsonObject?.let {
+ coverImageType = it.get("cover")?.get("t")?.notNull()?.asString
+ it.get("pages")?.asJsonArray?.map {
+ it?.asJsonObject?.get("t")?.notNull()?.asString
+ }?.filterNotNull()?.map {
+ PageImageType(it)
+ }?.let {
+ pageImageTypes.clear()
+ pageImageTypes.addAll(it)
+ }
+ thumbnailImageType = it.get("thumbnail")?.get("t")?.notNull()?.asString
+ }
- id = obj.get("id")?.asLong
+ scanlator = obj.get("scanlator")?.notNull()?.asString
- obj.get("tags")?.asJsonArray?.map {
- val asObj = it.asJsonObject
- Pair(asObj.get("type")?.string, asObj.get("name")?.string)
- }?.apply {
- tags.clear()
- }?.forEach {
- if(it.first != null && it.second != null)
- tags.getOrPut(it.first!!, { ArrayList() }).add(Tag(it.second!!, false))
+ obj.get("tags")?.asJsonArray?.map {
+ val asObj = it.asJsonObject
+ Pair(asObj.get("type")?.string, asObj.get("name")?.string)
+ }?.apply {
+ tags.clear()
+ }?.forEach {
+ if(it.first != null && it.second != null)
+ tags.add(Tag(it.first!!, it.second!!, false))
+ }
}
}
fun parseGallery(obj: JsonObject) = rawParseGallery(obj).let {
- metadataHelper.writeGallery(it, id)
-
SManga.create().apply {
it.copyTo(this)
}
}
fun lazyLoadMetadata(url: String) =
- Observable.fromCallable {
- metadataHelper.fetchNhentaiMetadata(url)
- ?: client.newCall(urlToDetailsRequest(url))
- .asObservableSuccess()
- .map {
- rawParseGallery(jsonParser.parse(it.body()!!.string()).asJsonObject)
- }.toBlocking().first()
- }!!
+ defRealm { realm ->
+ realm.loadNhentaiAsync(NHentaiMetadata.nhIdFromUrl(url))
+ .flatMap {
+ if(it == null)
+ client.newCall(urlToDetailsRequest(url))
+ .asObservableSuccess()
+ .map {
+ rawParseGallery(jsonParser.parse(it.body()!!.string())
+ .asJsonObject)
+ }.first()
+ else
+ Observable.just(it)
+ }.map { realm.copyFromRealm(it) }
+ }
override fun fetchChapterList(manga: SManga)
= lazyLoadMetadata(manga.url).map {
@@ -181,7 +197,7 @@ class NHentai(context: Context) : HttpSource() {
if(metadata.mediaId == null) emptyList()
else
metadata.pageImageTypes.mapIndexed { index, s ->
- val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
+ val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s.type!!)
Page(index, imageUrl!!, imageUrl)
}
}!!
@@ -231,14 +247,10 @@ class NHentai(context: Context) : HttpSource() {
val jsonParser by lazy {
JsonParser()
}
-
- val metadataHelper by lazy {
- MetadataHelper()
- }
}
fun JsonElement.notNull() =
- if(this is JsonNull)
- null
- else this
+ if(this is JsonNull)
+ null
+ else this
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt
index c0448207b..a867a40e7 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt
@@ -6,12 +6,15 @@ import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.ChapterRecognition
import eu.kanade.tachiyomi.util.asJsoup
-import exh.metadata.MetadataHelper
import exh.metadata.copyTo
+import exh.metadata.loadPervEden
import exh.metadata.models.PervEdenGalleryMetadata
+import exh.metadata.models.PervEdenTitle
import exh.metadata.models.Tag
import exh.util.UriFilter
import exh.util.UriGroup
+import exh.util.createUUIDObj
+import exh.util.realmTrans
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@@ -27,8 +30,6 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou
override val name = "Perv Eden"
override val baseUrl = "http://www.perveden.com"
- val metadataHelper by lazy { MetadataHelper() }
-
override fun popularMangaSelector() = "#topManga > ul > li"
override fun popularMangaFromElement(element: Element): SManga {
@@ -99,72 +100,68 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou
}
override fun mangaDetailsParse(document: Document): SManga {
- val metadata = PervEdenGalleryMetadata()
- with(metadata) {
- url = document.location()
+ realmTrans { realm ->
+ val url = document.location()
+ val metadata = (realm.loadPervEden(PervEdenGalleryMetadata.pvIdFromUrl(url), id)
+ ?: realm.createUUIDObj(PervEdenGalleryMetadata::class.java))
+ with(metadata) {
+ this.url = url
- lang = this@PervEden.lang
+ lang = this@PervEden.lang
- title = document.getElementsByClass("manga-title").first()?.text()
+ title = document.getElementsByClass("manga-title").first()?.text()
- thumbnailUrl = "http:" + document.getElementsByClass("mangaImage2").first()?.child(0)?.attr("src")
+ thumbnailUrl = "http:" + document.getElementsByClass("mangaImage2").first()?.child(0)?.attr("src")
- val rightBoxElement = document.select(".rightBox:not(.info)").first()
+ val rightBoxElement = document.select(".rightBox:not(.info)").first()
- tags.clear()
- var inStatus: String? = null
- rightBoxElement.childNodes().forEach {
- if(it is Element && it.tagName().toLowerCase() == "h4") {
- inStatus = it.text().trim()
- } else {
- when(inStatus) {
- "Alternative name(s)" -> {
- if(it is TextNode) {
- val text = it.text().trim()
- if(!text.isBlank())
- altTitles.add(text)
+ tags.clear()
+ var inStatus: String? = null
+ rightBoxElement.childNodes().forEach {
+ if(it is Element && it.tagName().toLowerCase() == "h4") {
+ inStatus = it.text().trim()
+ } else {
+ when(inStatus) {
+ "Alternative name(s)" -> {
+ if(it is TextNode) {
+ val text = it.text().trim()
+ if(!text.isBlank())
+ altTitles.add(PervEdenTitle(this, text))
+ }
}
- }
- "Artist" -> {
- if(it is Element && it.tagName() == "a") {
- artist = it.text()
- tags.getOrPut("artist", {
- ArrayList()
- }).add(Tag(it.text().toLowerCase(), false))
+ "Artist" -> {
+ if(it is Element && it.tagName() == "a") {
+ artist = it.text()
+ tags.add(Tag("artist", it.text().toLowerCase(), false))
+ }
}
- }
- "Genres" -> {
- if(it is Element && it.tagName() == "a")
- tags.getOrPut("genre", {
- ArrayList()
- }).add(Tag(it.text().toLowerCase(), false))
- }
- "Type" -> {
- if(it is TextNode) {
- val text = it.text().trim()
- if(!text.isBlank())
- type = text
+ "Genres" -> {
+ if(it is Element && it.tagName() == "a")
+ tags.add(Tag("genre", it.text().toLowerCase(), false))
}
- }
- "Status" -> {
- if(it is TextNode) {
- val text = it.text().trim()
- if(!text.isBlank())
- status = text
+ "Type" -> {
+ if(it is TextNode) {
+ val text = it.text().trim()
+ if(!text.isBlank())
+ type = text
+ }
+ }
+ "Status" -> {
+ if(it is TextNode) {
+ val text = it.text().trim()
+ if(!text.isBlank())
+ status = text
+ }
}
}
}
}
- }
- rating = document.getElementById("rating-score")?.attr("value")?.toFloat()
+ rating = document.getElementById("rating-score")?.attr("value")?.toFloat()
- //Save metadata
- Timber.d("LNG: " + metadata.lang)
- metadataHelper.writeGallery(this, id)
-
- return SManga.create().apply {
- copyTo(this)
+ return SManga.create().apply {
+ copyTo(this)
+ }
}
}
}
@@ -197,12 +194,12 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou
}
override fun pageListParse(document: Document)
- = document.getElementById("pageSelect").getElementsByTag("option").map {
- Page(it.attr("data-page").toInt() - 1, baseUrl + it.attr("value"))
- }
+ = document.getElementById("pageSelect").getElementsByTag("option").map {
+ Page(it.attr("data-page").toInt() - 1, baseUrl + it.attr("value"))
+ }
override fun imageUrlParse(document: Document)
- = "http:" + document.getElementById("mainImg").attr("src")!!
+ = "http:" + document.getElementById("mainImg").attr("src")!!
override fun getFilterList() = FilterList (
AuthorFilter(),
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
index 9dd87e1af..714d5cf24 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
@@ -2,6 +2,18 @@ package eu.kanade.tachiyomi.ui.library
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
+import exh.*
+import exh.metadata.loadAllMetadata
+import exh.metadata.models.ExGalleryMetadata
+import exh.metadata.models.NHentaiMetadata
+import exh.metadata.models.PervEdenGalleryMetadata
+import exh.metadata.queryMetadataFromManga
+import exh.search.SearchEngine
+import exh.util.defRealm
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import kotlin.concurrent.thread
/**
* Adapter storing a list of manga in a certain category.
@@ -16,7 +28,9 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
*/
private var mangas: List = emptyList()
- var asyncSearchText: String? = null
+ // --> EH
+ private val searchEngine = SearchEngine()
+ // <-- EH
/**
* Sets a list of manga in the adapter.
@@ -40,9 +54,42 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
}
fun performFilter() {
- updateDataSet(mangas.filter {
- it.filter(searchText)
- })
+ Observable.fromCallable {
+ defRealm { realm ->
+ val parsedQuery = searchEngine.parseQuery(searchText)
+ val metadata = realm.loadAllMetadata().map {
+ Pair(it.key, searchEngine.filterResults(it.value, parsedQuery))
+ }
+ mangas.filter { manga ->
+ // --> EH
+ if (isLewdSource(manga.manga.source)) {
+ metadata.any {
+ when (manga.manga.source) {
+ EH_SOURCE_ID,
+ EXH_SOURCE_ID ->
+ if (it.first != ExGalleryMetadata::class)
+ return@any false
+ PERV_EDEN_IT_SOURCE_ID,
+ PERV_EDEN_EN_SOURCE_ID ->
+ if (it.first != PervEdenGalleryMetadata::class)
+ return@any false
+ NHENTAI_SOURCE_ID ->
+ if (it.first != NHentaiMetadata::class)
+ return@any false
+ }
+ realm.queryMetadataFromManga(manga.manga, it.second.where()).count() > 0
+ }
+ } else {
+ manga.filter(searchText)
+ }
+ // <-- EH
+ }
+ }
+ }.subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ updateDataSet(it)
+ }
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
index 34b190d2d..b687ac92a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
@@ -39,10 +39,12 @@ import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.library_controller.view.*
import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
+import java.util.concurrent.TimeUnit
class LibraryController(
@@ -342,7 +344,11 @@ class LibraryController(
// Mutate the filter icon because it needs to be tinted and the resource is shared.
menu.findItem(R.id.action_filter).icon.mutate()
- searchView.queryTextChanges().subscribeUntilDestroy {
+ // Debounce search (EH)
+ searchView.queryTextChanges()
+ .debounce(200, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribeUntilDestroy {
query = it.toString()
searchRelay.call(query)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
index d73478491..2e78c001d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
@@ -12,18 +12,10 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
-import exh.isLewdSource
-import exh.metadata.MetadataHelper
-import exh.search.SearchEngine
import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFilterable {
- // --> EH
- private val searchEngine = SearchEngine()
- private val metadataHelper = MetadataHelper()
- // <-- EH
-
override fun getLayoutRes(): Int {
return R.layout.catalogue_grid_item
}
@@ -61,15 +53,6 @@ class LibraryItem(val manga: Manga) : AbstractFlexibleItem(), IFi
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
- // --> EH
- if(!isLewdSource(manga.source)) {
- //Use gallery search engine for EH manga
- metadataHelper.fetchMetadata(manga.url, manga.source)?.let {
- return searchEngine.matches(it, searchEngine.parseQuery(constraint))
- }
- }
- // <-- EH
-
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index 71ef08ce8..28e574064 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -17,6 +17,7 @@ import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
@@ -35,6 +36,7 @@ import exh.ui.lock.LockChangeHandler
import exh.ui.lock.LockController
import exh.ui.lock.lockEnabled
import exh.ui.lock.notifyLockSecurity
+import exh.ui.migration.MetadataFetchDialog
import kotlinx.android.synthetic.main.main_activity.*
import uy.kohesive.injekt.injectLazy
@@ -165,6 +167,10 @@ class MainActivity : BaseActivity() {
if (Migrations.upgrade(preferences)) {
ChangelogDialogController().showDialog(router)
}
+
+ // Migrate metadata to Realm (EH)
+ if(!preferences.migrateLibraryAsked2().getOrDefault())
+ MetadataFetchDialog().askMigration(this)
}
}
diff --git a/app/src/main/java/exh/GalleryAdder.kt b/app/src/main/java/exh/GalleryAdder.kt
index 4dc07af9b..71c8f77ad 100755
--- a/app/src/main/java/exh/GalleryAdder.kt
+++ b/app/src/main/java/exh/GalleryAdder.kt
@@ -10,8 +10,13 @@ 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.MetadataHelper
import exh.metadata.copyTo
+import exh.metadata.loadEh
+import exh.metadata.loadNhentai
+import exh.metadata.models.ExGalleryMetadata
+import exh.metadata.models.NHentaiMetadata
+import exh.util.defRealm
+import io.realm.Realm
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
@@ -27,8 +32,6 @@ class GalleryAdder {
private val sourceManager: SourceManager by injectLazy()
- private val metadataHelper = MetadataHelper()
-
private val networkHelper: NetworkHelper by injectLazy()
companion object {
@@ -119,11 +122,17 @@ class GalleryAdder {
manga.copyFrom(sourceObj.fetchMangaDetails(manga).toBlocking().first())
//Apply metadata
- when(source) {
- EH_SOURCE_ID, EXH_SOURCE_ID ->
- metadataHelper.fetchEhMetadata(realUrl, isExSource(source))?.copyTo(manga)
- NHENTAI_SOURCE_ID ->
- metadataHelper.fetchNhentaiMetadata(realUrl)?.copyTo(manga)
+ defRealm { realm ->
+ when (source) {
+ EH_SOURCE_ID, EXH_SOURCE_ID ->
+ realm.loadEh(ExGalleryMetadata.galleryId(realUrl),
+ ExGalleryMetadata.galleryToken(realUrl),
+ isExSource(source))?.copyTo(manga)
+ NHENTAI_SOURCE_ID ->
+ realm.loadNhentai(NHentaiMetadata.nhIdFromUrl(realUrl))
+ ?.copyTo(manga)
+ else -> return GalleryAddEvent.Fail.UnknownType(url)
+ }
}
if (fav) manga.favorite = true
diff --git a/app/src/main/java/exh/VerbelExpressionExtensions.kt b/app/src/main/java/exh/VerbelExpressionExtensions.kt
deleted file mode 100755
index 12f060076..000000000
--- a/app/src/main/java/exh/VerbelExpressionExtensions.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package exh
-
-import ru.lanwen.verbalregex.VerbalExpression
-
-fun VerbalExpression.Builder.anyChar() = add(".")!!
diff --git a/app/src/main/java/exh/metadata/MetadataHelper.kt b/app/src/main/java/exh/metadata/MetadataHelper.kt
index 81150bf02..0ba324a66 100755
--- a/app/src/main/java/exh/metadata/MetadataHelper.kt
+++ b/app/src/main/java/exh/metadata/MetadataHelper.kt
@@ -1,62 +1,114 @@
package exh.metadata
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.source.model.SManga
import exh.*
import exh.metadata.models.ExGalleryMetadata
import exh.metadata.models.NHentaiMetadata
import exh.metadata.models.PervEdenGalleryMetadata
import exh.metadata.models.SearchableGalleryMetadata
-import io.paperdb.Paper
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.RealmResults
+import rx.Observable
+import kotlin.reflect.KClass
-class MetadataHelper {
+fun Realm.ehMetaQueryFromUrl(url: String,
+ exh: Boolean,
+ meta: RealmQuery? = null) =
+ ehMetadataQuery(
+ ExGalleryMetadata.galleryId(url),
+ ExGalleryMetadata.galleryToken(url),
+ exh,
+ meta
+ )
- fun writeGallery(galleryMetadata: SearchableGalleryMetadata, source: Long)
- = (if(isExSource(source) || isEhSource(source)) exGalleryBook()
- else if(isPervEdenSource(source)) pervEdenGalleryBook()
- else if(isNhentaiSource(source)) nhentaiGalleryBook()
- else null)?.write(galleryMetadata.galleryUniqueIdentifier(), galleryMetadata)!!
+fun Realm.ehMetadataQuery(gId: String,
+ gToken: String,
+ exh: Boolean,
+ meta: RealmQuery? = null)
+ = (meta ?: where(ExGalleryMetadata::class.java))
+ .equalTo(ExGalleryMetadata::gId.name, gId)
+ .equalTo(ExGalleryMetadata::gToken.name, gToken)
+ .equalTo(ExGalleryMetadata::exh.name, exh)
- fun fetchEhMetadata(url: String, exh: Boolean): ExGalleryMetadata?
- = ExGalleryMetadata().let {
- it.url = url
- it.exh = exh
- return exGalleryBook().read(it.galleryUniqueIdentifier())
+fun Realm.loadEh(gId: String, gToken: String, exh: Boolean): ExGalleryMetadata?
+ = ehMetadataQuery(gId, gToken, exh)
+ .findFirst()
+
+fun Realm.loadEhAsync(gId: String, gToken: String, exh: Boolean): Observable
+ = ehMetadataQuery(gId, gToken, exh)
+ .findFirstAsync()
+ .asObservable()
+
+private fun pervEdenSourceToLang(source: Long)
+ = when (source) {
+ PERV_EDEN_EN_SOURCE_ID -> "en"
+ PERV_EDEN_IT_SOURCE_ID -> "it"
+ else -> throw IllegalArgumentException()
+}
+
+fun Realm.pervEdenMetaQueryFromUrl(url: String,
+ source: Long,
+ meta: RealmQuery?) =
+ pervEdenMetadataQuery(
+ PervEdenGalleryMetadata.pvIdFromUrl(url),
+ source,
+ meta
+ )
+
+fun Realm.pervEdenMetadataQuery(pvId: String,
+ source: Long,
+ meta: RealmQuery? = null)
+ = (meta ?: where(PervEdenGalleryMetadata::class.java))
+ .equalTo(PervEdenGalleryMetadata::lang.name, pervEdenSourceToLang(source))
+ .equalTo(PervEdenGalleryMetadata::pvId.name, pvId)
+
+fun Realm.loadPervEden(pvId: String, source: Long): PervEdenGalleryMetadata?
+ = pervEdenMetadataQuery(pvId, source)
+ .findFirst()
+
+fun Realm.loadPervEdenAsync(pvId: String, source: Long): Observable
+ = pervEdenMetadataQuery(pvId, source)
+ .findFirstAsync()
+ .asObservable()
+
+fun Realm.nhentaiMetaQueryFromUrl(url: String,
+ meta: RealmQuery?) =
+ nhentaiMetadataQuery(
+ NHentaiMetadata.nhIdFromUrl(url),
+ meta
+ )
+
+fun Realm.nhentaiMetadataQuery(nhId: Long,
+ meta: RealmQuery? = null)
+ = (meta ?: where(NHentaiMetadata::class.java))
+ .equalTo(NHentaiMetadata::nhId.name, nhId)
+
+fun Realm.loadNhentai(nhId: Long): NHentaiMetadata?
+ = nhentaiMetadataQuery(nhId)
+ .findFirst()
+
+fun Realm.loadNhentaiAsync(nhId: Long): Observable
+ = nhentaiMetadataQuery(nhId)
+ .findFirstAsync()
+ .asObservable()
+
+fun Realm.loadAllMetadata(): Map, RealmResults> =
+ mapOf(
+ Pair(ExGalleryMetadata::class, where(ExGalleryMetadata::class.java).findAll()),
+ Pair(NHentaiMetadata::class, where(NHentaiMetadata::class.java).findAll()),
+ Pair(PervEdenGalleryMetadata::class, where(PervEdenGalleryMetadata::class.java).findAll())
+ )
+
+fun Realm.queryMetadataFromManga(manga: Manga,
+ meta: RealmQuery? = null): RealmQuery =
+ when(manga.source) {
+ EH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, false, meta as? RealmQuery)
+ EXH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, true, meta as? RealmQuery)
+ PERV_EDEN_EN_SOURCE_ID,
+ PERV_EDEN_IT_SOURCE_ID ->
+ pervEdenMetaQueryFromUrl(manga.url, manga.source, meta as? RealmQuery)
+ NHENTAI_SOURCE_ID -> nhentaiMetaQueryFromUrl(manga.url, meta as? RealmQuery)
+ else -> throw IllegalArgumentException("Unknown source type!")
}
-
- fun fetchPervEdenMetadata(url: String, source: Long): PervEdenGalleryMetadata?
- = PervEdenGalleryMetadata().let {
- it.url = url
- if(source == PERV_EDEN_EN_SOURCE_ID)
- it.lang = "en"
- else if(source == PERV_EDEN_IT_SOURCE_ID)
- it.lang = "it"
- else throw IllegalArgumentException("Invalid source id!")
- return pervEdenGalleryBook().read(it.galleryUniqueIdentifier())
- }
-
- fun fetchNhentaiMetadata(url: String) = NHentaiMetadata().let {
- it.url = url
- nhentaiGalleryBook().read(it.galleryUniqueIdentifier())
- }
-
- fun fetchMetadata(url: String, source: Long): SearchableGalleryMetadata? {
- if(isExSource(source) || isEhSource(source)) {
- return fetchEhMetadata(url, isExSource(source))
- } else if(isPervEdenSource(source)) {
- return fetchPervEdenMetadata(url, source)
- } else if(isNhentaiSource(source)) {
- return fetchNhentaiMetadata(url)
- } else {
- return null
- }
- }
-
- fun getAllGalleries() = exGalleryBook().allKeys.map {
- exGalleryBook().read(it)
- }
-
- fun exGalleryBook() = Paper.book("gallery-ex")!!
-
- fun pervEdenGalleryBook() = Paper.book("gallery-perveden")!!
-
- fun nhentaiGalleryBook() = Paper.book("gallery-nhentai")!!
-}
\ No newline at end of file
diff --git a/app/src/main/java/exh/metadata/MetdataCopier.kt b/app/src/main/java/exh/metadata/MetdataCopier.kt
index ed5744983..02e93b935 100755
--- a/app/src/main/java/exh/metadata/MetdataCopier.kt
+++ b/app/src/main/java/exh/metadata/MetdataCopier.kt
@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.EHentai
-import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata
import eu.kanade.tachiyomi.source.online.all.PervEden
import exh.metadata.models.*
import exh.plusAssign
@@ -51,12 +50,12 @@ fun ExGalleryMetadata.copyTo(manga: SManga) {
titleObj?.let { manga.title = it }
//Set artist (if we can find one)
- tags[EH_ARTIST_NAMESPACE]?.let {
- if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
+ tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let {
+ if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name!! })
}
//Set author (if we can find one)
- tags[EH_AUTHOR_NAMESPACE]?.let {
- if(it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
+ tags.filter { it.namespace == EH_AUTHOR_NAMESPACE }.let {
+ if(it.isNotEmpty()) manga.author = it.joinToString(transform = { it.name!! })
}
//Set genre
genre?.let { manga.genre = it }
@@ -159,12 +158,12 @@ fun NHentaiMetadata.copyTo(manga: SManga) {
manga.title = englishTitle ?: japaneseTitle ?: shortTitle!!
//Set artist (if we can find one)
- tags[NHENTAI_ARTIST_NAMESPACE]?.let {
- if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
+ tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let {
+ if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name!! })
}
- tags[NHENTAI_CATEGORIES_NAMESPACE]?.let {
- if(it.isNotEmpty()) manga.genre = it.joinToString(transform = Tag::name)
+ tags.filter { it.namespace == NHENTAI_CATEGORIES_NAMESPACE }.let {
+ if(it.isNotEmpty()) manga.genre = it.joinToString(transform = { it.name!! })
}
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
@@ -209,7 +208,9 @@ fun SearchableGalleryMetadata.genericCopyTo(manga: SManga): Boolean {
private fun buildTagsDescription(metadata: SearchableGalleryMetadata)
= StringBuilder("Tags:\n").apply {
//BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
- metadata.tags.entries.forEach { namespace, tags ->
+ metadata.tags.groupBy {
+ it.namespace
+ }.entries.forEach { namespace, tags ->
if (tags.isNotEmpty()) {
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
this += "▪ $namespace: $joinedTags\n"
diff --git a/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt b/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
index 2ce181523..3b5f95b16 100755
--- a/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
+++ b/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
@@ -1,22 +1,43 @@
package exh.metadata.models
import android.net.Uri
+import io.realm.RealmList
+import io.realm.RealmObject
+import io.realm.annotations.Ignore
+import io.realm.annotations.Index
+import io.realm.annotations.PrimaryKey
+import io.realm.annotations.RealmClass
import java.util.*
/**
* Gallery metadata storage model
*/
-class ExGalleryMetadata : SearchableGalleryMetadata() {
+@RealmClass
+open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata {
+ @PrimaryKey
+ override var uuid: String = UUID.randomUUID().toString()
+
var url: String? = null
+ @Index
+ var gId: String? = null
+ @Index
+ var gToken: String? = null
+
+ @Index
var exh: Boolean? = null
var thumbnailUrl: String? = null
+ @Index
var title: String? = null
+ @Index
var altTitle: String? = null
+ @Index
+ override var uploader: String? = null
+
var genre: String? = null
var datePosted: Long? = null
@@ -30,22 +51,26 @@ class ExGalleryMetadata : SearchableGalleryMetadata() {
var ratingCount: Int? = null
var averageRating: Double? = null
+ override var tags: RealmList = RealmList()
+
override fun getTitles() = listOf(title, altTitle).filterNotNull()
- private fun splitGalleryUrl()
- = url?.let {
- Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
- }
+ @Ignore
+ override val titleFields = listOf(
+ ExGalleryMetadata::title.name,
+ ExGalleryMetadata::altTitle.name
+ )
- fun galleryId() = splitGalleryUrl()?.let { it[it.size - 2] }
+ companion object {
+ private fun splitGalleryUrl(url: String)
+ = url.let {
+ Uri.parse(it).pathSegments
+ .filterNot(String::isNullOrBlank)
+ }
- fun galleryToken() =
- splitGalleryUrl()?.last()
+ fun galleryId(url: String) = splitGalleryUrl(url).let { it[it.size - 2] }
- override fun galleryUniqueIdentifier() = exh?.let { exh ->
- url?.let {
- //Fuck, this should be EXH and EH but it's too late to change it now...
- "${if(exh) "EXH" else "EX"}-${galleryId()}-${galleryToken()}"
- }
+ fun galleryToken(url: String) =
+ splitGalleryUrl(url).last()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt b/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt
index a9b267e3b..e57b187b2 100755
--- a/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt
+++ b/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt
@@ -1,40 +1,64 @@
package exh.metadata.models
+import io.realm.RealmList
+import io.realm.RealmObject
+import io.realm.annotations.Ignore
+import io.realm.annotations.Index
+import io.realm.annotations.PrimaryKey
+import io.realm.annotations.RealmClass
+import java.util.*
+
/**
* NHentai metadata
*/
-class NHentaiMetadata : SearchableGalleryMetadata() {
+@RealmClass
+open class NHentaiMetadata : RealmObject(), SearchableGalleryMetadata {
+ @PrimaryKey
+ override var uuid: String = UUID.randomUUID().toString()
- var id: Long? = null
+ var nhId: Long? = null
- var url get() = id?.let { "$BASE_URL/g/$it" }
+ var url get() = nhId?.let { "$BASE_URL/g/$it" }
set(a) {
a?.let {
- id = a.split("/").last { it.isNotBlank() }.toLong()
+ nhId = nhIdFromUrl(a)
}
}
+ @Index
+ override var uploader: String? = null
+
var uploadDate: Long? = null
var favoritesCount: Long? = null
var mediaId: String? = null
+ @Index
var japaneseTitle: String? = null
+ @Index
var englishTitle: String? = null
+ @Index
var shortTitle: String? = null
var coverImageType: String? = null
- var pageImageTypes: MutableList = mutableListOf()
+ var pageImageTypes: RealmList = RealmList()
var thumbnailImageType: String? = null
var scanlator: String? = null
- override fun galleryUniqueIdentifier(): String? = "NHENTAI-$id"
+ override var tags: RealmList = RealmList()
override fun getTitles() = listOf(japaneseTitle, englishTitle, shortTitle).filterNotNull()
+ @Ignore
+ override val titleFields = listOf(
+ NHentaiMetadata::japaneseTitle.name,
+ NHentaiMetadata::englishTitle.name,
+ NHentaiMetadata::shortTitle.name
+ )
+
companion object {
val BASE_URL = "https://nhentai.net"
@@ -44,5 +68,27 @@ class NHentaiMetadata : SearchableGalleryMetadata() {
"j" -> "jpg"
else -> null
}
+
+ fun nhIdFromUrl(url: String)
+ = url.split("/").last { it.isNotBlank() }.toLong()
}
}
+
+@RealmClass
+open class PageImageType(var type: String? = null): RealmObject() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PageImageType
+
+ if (type != other.type) return false
+
+ return true
+ }
+
+
+ override fun hashCode() = type?.hashCode() ?: 0
+
+ override fun toString() = "PageImageType(type=$type)"
+}
diff --git a/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt b/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt
index b9770aa7a..dabcd1137 100755
--- a/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt
+++ b/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt
@@ -1,14 +1,33 @@
package exh.metadata.models
import android.net.Uri
+import io.realm.RealmList
+import io.realm.RealmObject
+import io.realm.annotations.Ignore
+import io.realm.annotations.Index
+import io.realm.annotations.PrimaryKey
+import io.realm.annotations.RealmClass
+import java.util.*
+
+@RealmClass
+open class PervEdenGalleryMetadata : RealmObject(), SearchableGalleryMetadata {
+ @PrimaryKey
+ override var uuid: String = UUID.randomUUID().toString()
+
+ @Index
+ var pvId: String? = null
-class PervEdenGalleryMetadata : SearchableGalleryMetadata() {
var url: String? = null
var thumbnailUrl: String? = null
+ @Index
var title: String? = null
- var altTitles: MutableList = mutableListOf()
+ var altTitles: RealmList = RealmList()
+ @Index
+ override var uploader: String? = null
+
+ @Index
var artist: String? = null
var type: String? = null
@@ -19,14 +38,48 @@ class PervEdenGalleryMetadata : SearchableGalleryMetadata() {
var lang: String? = null
- override fun getTitles() = listOf(title).plus(altTitles).filterNotNull()
+ override var tags: RealmList = RealmList()
- private fun splitGalleryUrl()
- = url?.let {
- Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
- }
+ override fun getTitles() = listOf(title).plus(altTitles.map {
+ it.title
+ }).filterNotNull()
- override fun galleryUniqueIdentifier() = splitGalleryUrl()?.let {
- "PERVEDEN-${lang?.toUpperCase()}-${it.last()}"
+ @Ignore
+ override val titleFields = listOf(
+ //TODO Somehow include altTitles
+ PervEdenGalleryMetadata::title.name
+ )
+
+ companion object {
+ private fun splitGalleryUrl(url: String)
+ = url.let {
+ Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
+ }
+
+ fun pvIdFromUrl(url: String) = splitGalleryUrl(url).last()
}
}
+
+@RealmClass
+open class PervEdenTitle(var metadata: PervEdenGalleryMetadata? = null,
+ @Index var title: String? = null): RealmObject() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PervEdenTitle
+
+ if (metadata != other.metadata) return false
+ if (title != other.title) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = metadata?.hashCode() ?: 0
+ result = 31 * result + (title?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString() = "PervEdenTitle(metadata=$metadata, title=$title)"
+}
diff --git a/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt b/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt
index bc33eef24..6d1c0d804 100755
--- a/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt
+++ b/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt
@@ -1,18 +1,24 @@
package exh.metadata.models
+import io.realm.RealmList
+import io.realm.RealmModel
+import io.realm.annotations.Index
import java.util.ArrayList
import java.util.HashMap
+import kotlin.reflect.KCallable
/**
* A gallery that can be searched using the EH search engine
*/
-abstract class SearchableGalleryMetadata {
- var uploader: String? = null
+interface SearchableGalleryMetadata: RealmModel {
+ var uuid: String
+
+ var uploader: String?
//Being specific about which classes are used in generics to make deserialization easier
- val tags: HashMap> = HashMap()
+ var tags: RealmList
- abstract fun galleryUniqueIdentifier(): String?
+ fun getTitles(): List
- abstract fun getTitles(): List
+ val titleFields: List
}
\ No newline at end of file
diff --git a/app/src/main/java/exh/metadata/models/Tag.kt b/app/src/main/java/exh/metadata/models/Tag.kt
index 9b78b0c35..0c294432e 100755
--- a/app/src/main/java/exh/metadata/models/Tag.kt
+++ b/app/src/main/java/exh/metadata/models/Tag.kt
@@ -1,7 +1,36 @@
package exh.metadata.models
+import io.realm.RealmObject
+import io.realm.annotations.Index
+import io.realm.annotations.RealmClass
+
/**
* Simple tag model
*/
-data class Tag(var name: String, var light: Boolean)
+@RealmClass
+open class Tag(@Index var namespace: String? = null,
+ @Index var name: String? = null,
+ var light: Boolean? = null): RealmObject() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Tag
+
+ if (namespace != other.namespace) return false
+ if (name != other.name) return false
+ if (light != other.light) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = namespace?.hashCode() ?: 0
+ result = 31 * result + (name?.hashCode() ?: 0)
+ result = 31 * result + (light?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString() = "Tag(namespace=$namespace, name=$name, light=$light)"
+}
diff --git a/app/src/main/java/exh/search/SearchEngine.kt b/app/src/main/java/exh/search/SearchEngine.kt
index eb4d6f5a2..1a484467e 100755
--- a/app/src/main/java/exh/search/SearchEngine.kt
+++ b/app/src/main/java/exh/search/SearchEngine.kt
@@ -2,74 +2,94 @@ package exh.search
import exh.metadata.models.SearchableGalleryMetadata
import exh.metadata.models.Tag
+import exh.util.beginLog
+import io.realm.Case
+import io.realm.RealmResults
class SearchEngine {
private val queryCache = mutableMapOf>()
- fun matches(metadata: SearchableGalleryMetadata, query: List): Boolean {
+ fun filterResults(metadata: RealmResults, query: List):
+ RealmResults {
+ val first = metadata.firstOrNull() ?: return metadata
+ val rQuery = metadata.where()//.beginLog(SearchableGalleryMetadata::class.java)
+ var queryEmpty = true
- fun matchTagList(tags: Sequence,
- component: Text): Boolean {
- //Match tags
- val tagMatcher = if(!component.exact)
- component.asLenientRegex()
+ fun matchTagList(namespace: String?,
+ component: Text?,
+ excluded: Boolean) {
+ if(excluded)
+ rQuery.not()
+ else if (queryEmpty)
+ queryEmpty = false
else
- component.asRegex()
- //Match beginning of tag
- if (tags.find {
- tagMatcher.testExact(it.name)
- } != null) {
- if(component.excluded) return false
- } else {
- //No tag matched for this component
- return false
- }
- return true
- }
+ rQuery.or()
- val cachedTitles = metadata.getTitles().map(String::toLowerCase)
+ rQuery.beginGroup()
+ //Match namespace if specified
+ namespace?.let {
+ rQuery.equalTo("${SearchableGalleryMetadata::tags.name}.${Tag::namespace.name}",
+ it,
+ Case.INSENSITIVE)
+ }
+ //Match tag name if specified
+ component?.let {
+ rQuery.beginGroup()
+ val q = if (!it.exact)
+ it.asLenientTagQueries()
+ else
+ listOf(it.asQuery())
+ q.forEachIndexed { index, s ->
+ if(index > 0)
+ rQuery.or()
+
+ rQuery.like("${SearchableGalleryMetadata::tags.name}.${Tag::name.name}", s, Case.INSENSITIVE)
+ }
+ rQuery.endGroup()
+ }
+ rQuery.endGroup()
+ }
for(component in query) {
if(component is Text) {
+ if(component.excluded)
+ rQuery.not()
+
+ rQuery.beginGroup()
+
//Match title
- if (cachedTitles.find { component.asRegex().test(it) } != null) {
- continue
- }
+ first.titleFields
+ .forEachIndexed { index, s ->
+ queryEmpty = false
+ if(index > 0)
+ rQuery.or()
+
+ rQuery.like(s, component.asLenientTitleQuery(), Case.INSENSITIVE)
+ }
+
//Match tags
- if(!matchTagList(metadata.tags.entries.asSequence().flatMap { it.value.asSequence() },
- component)) return false
+ matchTagList(null, component, false) //We already deal with exclusions here
+ rQuery.endGroup()
} else if(component is Namespace) {
if(component.namespace == "uploader") {
+ queryEmpty = false
//Match uploader
- if(!component.tag?.rawTextOnly().equals(metadata.uploader,
- ignoreCase = true)) {
- return false
- }
+ rQuery.equalTo(SearchableGalleryMetadata::uploader.name,
+ component.tag!!.rawTextOnly(),
+ Case.INSENSITIVE)
} else {
if(component.tag!!.components.size > 0) {
- //Match namespace
- val ns = metadata.tags.entries.asSequence().filter {
- it.key == component.namespace
- }.flatMap { it.value.asSequence() }
- //Match tags
- if (!matchTagList(ns, component.tag!!))
- return false
+ //Match namespace + tags
+ matchTagList(component.namespace, component.tag!!, component.tag!!.excluded)
} else {
//Perform namespace search
- val hasNs = metadata.tags.entries.find {
- it.key == component.namespace
- } != null
-
- if(hasNs && component.excluded)
- return false
- else if(!hasNs && !component.excluded)
- return false
+ matchTagList(component.namespace, null, component.excluded)
}
}
}
}
- return true
+ return rQuery.findAll()
}
fun parseQuery(query: String) = queryCache.getOrPut(query, {
diff --git a/app/src/main/java/exh/search/Text.kt b/app/src/main/java/exh/search/Text.kt
index 5bccb2671..98a51fd72 100755
--- a/app/src/main/java/exh/search/Text.kt
+++ b/app/src/main/java/exh/search/Text.kt
@@ -1,36 +1,51 @@
package exh.search
-import exh.anyChar
-import ru.lanwen.verbalregex.VerbalExpression
+import exh.plusAssign
class Text: QueryComponent() {
val components = mutableListOf()
- private var regex: VerbalExpression? = null
- private var lenientRegex: VerbalExpression? = null
+ private var query: String? = null
+ private var lenientTitleQuery: String? = null
+ private var lenientTagQueries: List? = null
private var rawText: String? = null
- fun asRegex(): VerbalExpression {
- if(regex == null) {
- regex = baseBuilder().build()
+ fun asQuery(): String {
+ if(query == null) {
+ query = rBaseBuilder().toString()
}
- return regex!!
+ return query!!
}
- fun asLenientRegex(): VerbalExpression {
- if(lenientRegex == null) {
- lenientRegex = baseBuilder().anything().build()
+ fun asLenientTitleQuery(): String {
+ if(lenientTitleQuery == null) {
+ lenientTitleQuery = StringBuilder("*").append(rBaseBuilder()).append("*").toString()
}
- return lenientRegex!!
+ return lenientTitleQuery!!
}
- fun baseBuilder(): VerbalExpression.Builder {
- val builder = VerbalExpression.regex()
+ fun asLenientTagQueries(): List {
+ if(lenientTagQueries == null) {
+ lenientTagQueries = listOf(
+ //Match beginning of tag
+ rBaseBuilder().append("*").toString(),
+ //Tag word matcher (that matches multiple words)
+ //Can't make it match a single word in Realm :(
+ StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
+ StringBuilder(" ").append(rBaseBuilder()).toString(),
+ rBaseBuilder().append(" ").toString()
+ )
+ }
+ return lenientTagQueries!!
+ }
+
+ fun rBaseBuilder(): StringBuilder {
+ val builder = StringBuilder()
for(component in components) {
when(component) {
- is StringTextComponent -> builder.then(component.value)
- is SingleWildcard -> builder.anyChar()
- is MultiWildcard -> builder.anything()
+ is StringTextComponent -> builder += component.value
+ is SingleWildcard -> builder += "?"
+ is MultiWildcard -> builder += "*"
}
}
return builder
diff --git a/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt b/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt
index 75b72e165..c8edfec84 100755
--- a/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt
+++ b/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt
@@ -9,20 +9,18 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.isExSource
import exh.isLewdSource
-import exh.metadata.MetadataHelper
-import exh.metadata.copyTo
import exh.metadata.genericCopyTo
+import exh.metadata.queryMetadataFromManga
+import exh.util.defRealm
+import exh.util.realmTrans
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import kotlin.concurrent.thread
class MetadataFetchDialog {
- val metadataHelper by lazy { MetadataHelper() }
-
val db: DatabaseHelper by injectLazy()
val sourceManager: SourceManager by injectLazy()
@@ -42,43 +40,45 @@ class MetadataFetchDialog {
.show()
thread {
- db.deleteMangasNotInLibrary().executeAsBlocking()
+ defRealm { realm ->
+ db.deleteMangasNotInLibrary().executeAsBlocking()
- val libraryMangas = db.getLibraryMangas()
- .executeAsBlocking()
- .filter {
- isLewdSource(it.source)
- && metadataHelper.fetchMetadata(it.url, it.source) == null
- }
+ val libraryMangas = db.getLibraryMangas()
+ .executeAsBlocking()
+ .filter {
+ isLewdSource(it.source)
+ && realm.queryMetadataFromManga(it).findFirst() == null
+ }
- context.runOnUiThread {
- progressDialog.maxProgress = libraryMangas.size
- }
-
- //Actual metadata fetch code
- libraryMangas.forEachIndexed { i, manga ->
context.runOnUiThread {
- progressDialog.setContent("Processing: ${manga.title}")
- progressDialog.setProgress(i + 1)
+ progressDialog.maxProgress = libraryMangas.size
}
- try {
- val source = sourceManager.get(manga.source)
- source?.let {
- manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
- metadataHelper.fetchMetadata(manga.url, manga.source)?.genericCopyTo(manga)
+
+ //Actual metadata fetch code
+ libraryMangas.forEachIndexed { i, manga ->
+ context.runOnUiThread {
+ progressDialog.setContent("Processing: ${manga.title}")
+ progressDialog.setProgress(i + 1)
+ }
+ try {
+ val source = sourceManager.get(manga.source)
+ source?.let {
+ manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
+ realm.queryMetadataFromManga(manga).findFirst()?.genericCopyTo(manga)
+ }
+ } catch (t: Throwable) {
+ Timber.e(t, "Could not migrate manga!")
}
- } catch(t: Throwable) {
- Timber.e(t, "Could not migrate manga!")
}
- }
- context.runOnUiThread {
- progressDialog.dismiss()
+ context.runOnUiThread {
+ progressDialog.dismiss()
- //Enable orientation changes again
- context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
+ //Enable orientation changes again
+ context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
- displayMigrationComplete(context)
+ displayMigrationComplete(context)
+ }
}
}
}
@@ -106,7 +106,7 @@ class MetadataFetchDialog {
.cancelable(false)
.canceledOnTouchOutside(false)
.dismissListener {
- preferenceHelper.migrateLibraryAsked().set(true)
+ preferenceHelper.migrateLibraryAsked2().set(true)
}.show()
}
}
diff --git a/app/src/main/java/exh/ui/migration/UrlMigrator.kt b/app/src/main/java/exh/ui/migration/UrlMigrator.kt
index 2169c555d..5719d6f56 100755
--- a/app/src/main/java/exh/ui/migration/UrlMigrator.kt
+++ b/app/src/main/java/exh/ui/migration/UrlMigrator.kt
@@ -6,7 +6,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import exh.isExSource
import exh.isLewdSource
-import exh.metadata.MetadataHelper
+import exh.metadata.ehMetaQueryFromUrl
+import exh.util.realmTrans
import uy.kohesive.injekt.injectLazy
class UrlMigrator {
@@ -14,8 +15,6 @@ class UrlMigrator {
private val prefs: PreferencesHelper by injectLazy()
- private val metadataHelper: MetadataHelper by lazy { MetadataHelper() }
-
fun perform() {
db.inTransaction {
val dbMangas = db.getMangas()
@@ -39,33 +38,34 @@ class UrlMigrator {
//Sort possible dups so we can use binary search on it
possibleDups.sortBy { it.url }
- badMangas.forEach { manga ->
- //Build fixed URL
- val urlWithSlash = "/" + manga.url
- //Fix metadata if required
- val metadata = metadataHelper.fetchEhMetadata(manga.url, isExSource(manga.source))
- metadata?.url?.let {
- if(it.startsWith("g/")) { //Check if metadata URL has no slash
- metadata.url = urlWithSlash //Fix it
- metadataHelper.writeGallery(metadata, manga.source) //Write new metadata to disk
+ realmTrans { realm ->
+ badMangas.forEach { manga ->
+ //Build fixed URL
+ val urlWithSlash = "/" + manga.url
+ //Fix metadata if required
+ val metadata = realm.ehMetaQueryFromUrl(manga.url, isExSource(manga.source)).findFirst()
+ metadata?.url?.let {
+ if (it.startsWith("g/")) { //Check if metadata URL has no slash
+ metadata.url = urlWithSlash //Fix it
+ }
}
- }
- //If we have a dup (with the fixed url), use the dup instead
- val possibleDup = possibleDups.binarySearchBy(urlWithSlash, selector = { it.url })
- if(possibleDup >= 0) {
- //Make sure it is favorited if we are
- if(manga.favorite) {
- val dup = possibleDups[possibleDup]
- dup.favorite = true
- db.insertManga(dup).executeAsBlocking() //Update DB with changes
+ //If we have a dup (with the fixed url), use the dup instead
+ val possibleDup = possibleDups.binarySearchBy(urlWithSlash, selector = { it.url })
+ if (possibleDup >= 0) {
+ //Make sure it is favorited if we are
+ if (manga.favorite) {
+ val dup = possibleDups[possibleDup]
+ dup.favorite = true
+ db.insertManga(dup).executeAsBlocking() //Update DB with changes
+ }
+ //Delete ourself (but the dup is still there)
+ db.deleteManga(manga).executeAsBlocking()
+ return@forEach
}
- //Delete ourself (but the dup is still there)
- db.deleteManga(manga).executeAsBlocking()
- return@forEach
+ //No dup, correct URL and reinsert ourselves
+ manga.url = urlWithSlash
+ db.insertManga(manga).executeAsBlocking()
}
- //No dup, correct URL and reinsert ourselves
- manga.url = urlWithSlash
- db.insertManga(manga).executeAsBlocking()
}
}
}
diff --git a/app/src/main/java/exh/util/LoggingRealmQuery.kt b/app/src/main/java/exh/util/LoggingRealmQuery.kt
new file mode 100644
index 000000000..6b88e59c9
--- /dev/null
+++ b/app/src/main/java/exh/util/LoggingRealmQuery.kt
@@ -0,0 +1,550 @@
+package exh.util
+
+import io.realm.*
+import java.util.*
+
+/**
+ * Realm query with logging
+ *
+ * @author nulldev
+ */
+
+inline fun RealmQuery.beginLog(clazz: Class? =
+ E::class.java): LoggingRealmQuery
+ = LoggingRealmQuery.fromQuery(this, clazz)
+
+class LoggingRealmQuery(val query: RealmQuery) {
+ companion object {
+ fun fromQuery(q: RealmQuery, clazz: Class?)
+ = LoggingRealmQuery(q).apply {
+ log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE"
+ }
+ }
+
+ private val log = mutableListOf()
+
+ private fun sec(section: String) = "{$section}"
+
+ fun log() = log.joinToString(separator = " ")
+
+ fun isValid(): Boolean {
+ return query.isValid
+ }
+
+ fun isNull(fieldName: String): RealmQuery {
+ log += sec("\"$fieldName\" IS NULL")
+ return query.isNull(fieldName)
+ }
+
+ fun isNotNull(fieldName: String): RealmQuery {
+ log += sec("\"$fieldName\" IS NOT NULL")
+ return query.isNotNull(fieldName)
+ }
+
+ private fun appendEqualTo(fieldName: String, value: String, casing: Case? = null) {
+ log += sec("\"$fieldName\" == \"$value\"" + (casing?.let {
+ " CASE ${casing.name}"
+ } ?: ""))
+ }
+
+ fun equalTo(fieldName: String, value: String): RealmQuery {
+ appendEqualTo(fieldName, value)
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: String, casing: Case): RealmQuery {
+ appendEqualTo(fieldName, value, casing)
+ return query.equalTo(fieldName, value, casing)
+ }
+
+ fun equalTo(fieldName: String, value: Byte?): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: ByteArray): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: Short?): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: Int?): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: Long?): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: Double?): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: Float?): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: Boolean?): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun equalTo(fieldName: String, value: Date): RealmQuery {
+ appendEqualTo(fieldName, value.toString())
+ return query.equalTo(fieldName, value)
+ }
+
+ fun appendIn(fieldName: String, values: Array, casing: Case? = null) {
+ log += sec("[${values.joinToString(separator = ", ", transform = {
+ "\"$it\""
+ })}] IN \"$fieldName\"" + (casing?.let {
+ " CASE ${casing.name}"
+ } ?: ""))
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array, casing: Case): RealmQuery {
+ appendIn(fieldName, values, casing)
+ return query.`in`(fieldName, values, casing)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ fun `in`(fieldName: String, values: Array): RealmQuery {
+ appendIn(fieldName, values)
+ return query.`in`(fieldName, values)
+ }
+
+ private fun appendNotEqualTo(fieldName: String, value: Any?, casing: Case? = null) {
+ log += sec("\"$fieldName\" != \"$value\"" + (casing?.let {
+ " CASE ${casing.name}"
+ } ?: ""))
+ }
+
+ fun notEqualTo(fieldName: String, value: String): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: String, casing: Case): RealmQuery {
+ appendNotEqualTo(fieldName, value, casing)
+ return query.notEqualTo(fieldName, value, casing)
+ }
+
+ fun notEqualTo(fieldName: String, value: Byte?): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: ByteArray): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: Short?): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: Int?): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: Long?): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: Double?): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: Float?): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: Boolean?): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ fun notEqualTo(fieldName: String, value: Date): RealmQuery {
+ appendNotEqualTo(fieldName, value)
+ return query.notEqualTo(fieldName, value)
+ }
+
+ private fun appendGreaterThan(fieldName: String, value: Any?) {
+ log += sec("\"$fieldName\" > $value")
+ }
+
+ fun greaterThan(fieldName: String, value: Int): RealmQuery {
+ appendGreaterThan(fieldName, value)
+ return query.greaterThan(fieldName, value)
+ }
+
+ fun greaterThan(fieldName: String, value: Long): RealmQuery {
+ appendGreaterThan(fieldName, value)
+ return query.greaterThan(fieldName, value)
+ }
+
+ fun greaterThan(fieldName: String, value: Double): RealmQuery {
+ appendGreaterThan(fieldName, value)
+ return query.greaterThan(fieldName, value)
+ }
+
+ fun greaterThan(fieldName: String, value: Float): RealmQuery {
+ appendGreaterThan(fieldName, value)
+ return query.greaterThan(fieldName, value)
+ }
+
+ fun greaterThan(fieldName: String, value: Date): RealmQuery {
+ appendGreaterThan(fieldName, value)
+ return query.greaterThan(fieldName, value)
+ }
+
+ private fun appendGreaterThanOrEqualTo(fieldName: String, value: Any?) {
+ log += sec("\"$fieldName\" >= $value")
+ }
+
+ fun greaterThanOrEqualTo(fieldName: String, value: Int): RealmQuery {
+ appendGreaterThanOrEqualTo(fieldName, value)
+ return query.greaterThanOrEqualTo(fieldName, value)
+ }
+
+ fun greaterThanOrEqualTo(fieldName: String, value: Long): RealmQuery {
+ appendGreaterThanOrEqualTo(fieldName, value)
+ return query.greaterThanOrEqualTo(fieldName, value)
+ }
+
+ fun greaterThanOrEqualTo(fieldName: String, value: Double): RealmQuery {
+ appendGreaterThanOrEqualTo(fieldName, value)
+ return query.greaterThanOrEqualTo(fieldName, value)
+ }
+
+ fun greaterThanOrEqualTo(fieldName: String, value: Float): RealmQuery {
+ appendGreaterThanOrEqualTo(fieldName, value)
+ return query.greaterThanOrEqualTo(fieldName, value)
+ }
+
+ fun greaterThanOrEqualTo(fieldName: String, value: Date): RealmQuery {
+ appendGreaterThanOrEqualTo(fieldName, value)
+ return query.greaterThanOrEqualTo(fieldName, value)
+ }
+
+ private fun appendLessThan(fieldName: String, value: Any?) {
+ log += sec("\"$fieldName\" < $value")
+ }
+
+ fun lessThan(fieldName: String, value: Int): RealmQuery {
+ appendLessThan(fieldName, value)
+ return query.lessThan(fieldName, value)
+ }
+
+ fun lessThan(fieldName: String, value: Long): RealmQuery {
+ appendLessThan(fieldName, value)
+ return query.lessThan(fieldName, value)
+ }
+
+ fun lessThan(fieldName: String, value: Double): RealmQuery {
+ appendLessThan(fieldName, value)
+ return query.lessThan(fieldName, value)
+ }
+
+ fun lessThan(fieldName: String, value: Float): RealmQuery {
+ appendLessThan(fieldName, value)
+ return query.lessThan(fieldName, value)
+ }
+
+ fun lessThan(fieldName: String, value: Date): RealmQuery {
+ appendLessThan(fieldName, value)
+ return query.lessThan(fieldName, value)
+ }
+
+ private fun appendLessThanOrEqualTo(fieldName: String, value: Any?) {
+ log += sec("\"$fieldName\" <= $value")
+ }
+
+ fun lessThanOrEqualTo(fieldName: String, value: Int): RealmQuery {
+ appendLessThanOrEqualTo(fieldName, value)
+ return query.lessThanOrEqualTo(fieldName, value)
+ }
+
+ fun lessThanOrEqualTo(fieldName: String, value: Long): RealmQuery {
+ appendLessThanOrEqualTo(fieldName, value)
+ return query.lessThanOrEqualTo(fieldName, value)
+ }
+
+ fun lessThanOrEqualTo(fieldName: String, value: Double): RealmQuery {
+ appendLessThanOrEqualTo(fieldName, value)
+ return query.lessThanOrEqualTo(fieldName, value)
+ }
+
+ fun lessThanOrEqualTo(fieldName: String, value: Float): RealmQuery {
+ appendLessThanOrEqualTo(fieldName, value)
+ return query.lessThanOrEqualTo(fieldName, value)
+ }
+
+ fun lessThanOrEqualTo(fieldName: String, value: Date): RealmQuery {
+ appendLessThanOrEqualTo(fieldName, value)
+ return query.lessThanOrEqualTo(fieldName, value)
+ }
+
+ private fun appendBetween(fieldName: String, from: Any?, to: Any?) {
+ log += sec("\"$fieldName\" BETWEEN $from - $to")
+ }
+
+ fun between(fieldName: String, from: Int, to: Int): RealmQuery {
+ appendBetween(fieldName, from, to)
+ return query.between(fieldName, from, to)
+ }
+
+ fun between(fieldName: String, from: Long, to: Long): RealmQuery {
+ appendBetween(fieldName, from, to)
+ return query.between(fieldName, from, to)
+ }
+
+ fun between(fieldName: String, from: Double, to: Double): RealmQuery {
+ appendBetween(fieldName, from, to)
+ return query.between(fieldName, from, to)
+ }
+
+ fun between(fieldName: String, from: Float, to: Float): RealmQuery {
+ appendBetween(fieldName, from, to)
+ return query.between(fieldName, from, to)
+ }
+
+ fun between(fieldName: String, from: Date, to: Date): RealmQuery {
+ appendBetween(fieldName, from, to)
+ return query.between(fieldName, from, to)
+ }
+
+ private fun appendContains(fieldName: String, value: Any?, casing: Case? = null) {
+ log += sec("\"$fieldName\" CONTAINS \"$value\"" + (casing?.let {
+ " CASE ${casing.name}"
+ } ?: ""))
+ }
+
+ fun contains(fieldName: String, value: String): RealmQuery {
+ appendContains(fieldName, value)
+ return query.contains(fieldName, value)
+ }
+
+ fun contains(fieldName: String, value: String, casing: Case): RealmQuery {
+ appendContains(fieldName, value, casing)
+ return query.contains(fieldName, value, casing)
+ }
+
+ private fun appendBeginsWith(fieldName: String, value: Any?, casing: Case? = null) {
+ log += sec("\"$fieldName\" BEGINS WITH \"$value\"" + (casing?.let {
+ " CASE ${casing.name}"
+ } ?: ""))
+ }
+
+ fun beginsWith(fieldName: String, value: String): RealmQuery {
+ appendBeginsWith(fieldName, value)
+ return query.beginsWith(fieldName, value)
+ }
+
+ fun beginsWith(fieldName: String, value: String, casing: Case): RealmQuery {
+ appendBeginsWith(fieldName, value, casing)
+ return query.beginsWith(fieldName, value, casing)
+ }
+
+ private fun appendEndsWith(fieldName: String, value: Any?, casing: Case? = null) {
+ log += sec("\"$fieldName\" ENDS WITH \"$value\"" + (casing?.let {
+ " CASE ${casing.name}"
+ } ?: ""))
+ }
+
+ fun endsWith(fieldName: String, value: String): RealmQuery {
+ appendEndsWith(fieldName, value)
+ return query.endsWith(fieldName, value)
+ }
+
+ fun endsWith(fieldName: String, value: String, casing: Case): RealmQuery {
+ appendEndsWith(fieldName, value, casing)
+ return query.endsWith(fieldName, value, casing)
+ }
+
+ private fun appendLike(fieldName: String, value: Any?, casing: Case? = null) {
+ log += sec("\"$fieldName\" LIKE \"$value\"" + (casing?.let {
+ " CASE ${casing.name}"
+ } ?: ""))
+ }
+
+ fun like(fieldName: String, value: String): RealmQuery {
+ appendLike(fieldName, value)
+ return query.like(fieldName, value)
+ }
+
+ fun like(fieldName: String, value: String, casing: Case): RealmQuery {
+ appendLike(fieldName, value, casing)
+ return query.like(fieldName, value, casing)
+ }
+
+ fun beginGroup(): RealmQuery {
+ log += "("
+ return query.beginGroup()
+ }
+
+ fun endGroup(): RealmQuery {
+ log += ")"
+ return query.endGroup()
+ }
+
+ fun or(): RealmQuery {
+ log += "OR"
+ return query.or()
+ }
+
+ operator fun not(): RealmQuery {
+ log += "NOT"
+ return query.not()
+ }
+
+ fun isEmpty(fieldName: String): RealmQuery {
+ log += "\"$fieldName\" IS EMPTY"
+ return query.isEmpty(fieldName)
+ }
+
+ fun isNotEmpty(fieldName: String): RealmQuery {
+ log += "\"$fieldName\" IS NOT EMPTY"
+ return query.isNotEmpty(fieldName)
+ }
+
+ fun distinct(fieldName: String): RealmResults {
+ return query.distinct(fieldName)
+ }
+
+ fun distinctAsync(fieldName: String): RealmResults {
+ return query.distinctAsync(fieldName)
+ }
+
+ fun distinct(firstFieldName: String, vararg remainingFieldNames: String): RealmResults {
+ return query.distinct(firstFieldName, *remainingFieldNames)
+ }
+
+ fun sum(fieldName: String): Number {
+ return query.sum(fieldName)
+ }
+
+ fun average(fieldName: String): Double {
+ return query.average(fieldName)
+ }
+
+ fun min(fieldName: String): Number {
+ return query.min(fieldName)
+ }
+
+ fun minimumDate(fieldName: String): Date {
+ return query.minimumDate(fieldName)
+ }
+
+ fun max(fieldName: String): Number {
+ return query.max(fieldName)
+ }
+
+ fun maximumDate(fieldName: String): Date {
+ return query.maximumDate(fieldName)
+ }
+
+ fun count(): Long {
+ return query.count()
+ }
+
+ fun findAll(): RealmResults {
+ return query.findAll()
+ }
+
+ fun findAllAsync(): RealmResults {
+ return query.findAllAsync()
+ }
+
+ fun findAllSorted(fieldName: String, sortOrder: Sort): RealmResults {
+ return query.findAllSorted(fieldName, sortOrder)
+ }
+
+ fun findAllSortedAsync(fieldName: String, sortOrder: Sort): RealmResults {
+ return query.findAllSortedAsync(fieldName, sortOrder)
+ }
+
+ fun findAllSorted(fieldName: String): RealmResults {
+ return query.findAllSorted(fieldName)
+ }
+
+ fun findAllSortedAsync(fieldName: String): RealmResults {
+ return query.findAllSortedAsync(fieldName)
+ }
+
+ fun findAllSorted(fieldNames: Array, sortOrders: Array): RealmResults {
+ return query.findAllSorted(fieldNames, sortOrders)
+ }
+
+ fun findAllSortedAsync(fieldNames: Array, sortOrders: Array): RealmResults {
+ return query.findAllSortedAsync(fieldNames, sortOrders)
+ }
+
+ fun findAllSorted(fieldName1: String, sortOrder1: Sort, fieldName2: String, sortOrder2: Sort): RealmResults {
+ return query.findAllSorted(fieldName1, sortOrder1, fieldName2, sortOrder2)
+ }
+
+ fun findAllSortedAsync(fieldName1: String, sortOrder1: Sort, fieldName2: String, sortOrder2: Sort): RealmResults {
+ return query.findAllSortedAsync(fieldName1, sortOrder1, fieldName2, sortOrder2)
+ }
+
+ fun findFirst(): E {
+ return query.findFirst()
+ }
+
+ fun findFirstAsync(): E {
+ return query.findFirstAsync()
+ }
+}
diff --git a/app/src/main/java/exh/util/RealmUtil.kt b/app/src/main/java/exh/util/RealmUtil.kt
new file mode 100644
index 000000000..f794560c3
--- /dev/null
+++ b/app/src/main/java/exh/util/RealmUtil.kt
@@ -0,0 +1,39 @@
+package exh.util
+
+import io.realm.Realm
+import io.realm.RealmModel
+import io.realm.log.RealmLog
+import java.util.*
+
+inline fun realmTrans(block: (Realm) -> T): T {
+ return defRealm {
+ it.beginTransaction()
+ try {
+ val res = block(it)
+ it.commitTransaction()
+ res
+ } catch(t: Throwable) {
+ if (it.isInTransaction) {
+ it.cancelTransaction()
+ } else {
+ RealmLog.warn("Could not cancel transaction, not currently in a transaction.")
+ }
+
+ throw t
+ } finally {
+ //Just in case
+ if (it.isInTransaction) {
+ it.cancelTransaction()
+ }
+ }
+ }
+}
+
+inline fun defRealm(block: (Realm) -> T): T {
+ return Realm.getDefaultInstance().use {
+ block(it)
+ }
+}
+
+fun Realm.createUUIDObj(clazz: Class)
+ = createObject(clazz, UUID.randomUUID().toString())
diff --git a/app/src/main/res/raw/changelog_release.xml b/app/src/main/res/raw/changelog_release.xml
index 40c74259f..49b0f0893 100755
--- a/app/src/main/res/raw/changelog_release.xml
+++ b/app/src/main/res/raw/changelog_release.xml
@@ -1,6 +1,20 @@
+
+ EH - Rewrite batch add screen
+
+ EH - Add nhentai link import support
+
+ EH - Add the ability to import links by searching them in the catalogues
+
+ EH - Increase library tag search speed
+
+ EH - Rewrite app lock UI
+
+ EH - Add fingerprint support to app lock
+
+
Bugfix release.
diff --git a/build.gradle b/build.gradle
index 479adc50d..8857b122e 100755
--- a/build.gradle
+++ b/build.gradle
@@ -13,6 +13,8 @@ buildscript {
//Firebase (EH)
classpath 'com.google.gms:google-services:3.0.0'
+ //Realm (EH)
+ classpath "io.realm:realm-gradle-plugin:3.5.0"
}
}