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" } }