Migrate to realm for metadata

This commit is contained in:
NerdNumber9 2017-08-25 17:31:38 -04:00
parent bb6b88a703
commit cd291f0a27
31 changed files with 1394 additions and 588 deletions

View File

@ -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'

View File

@ -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.**

View File

@ -139,6 +139,14 @@
android:host="exhentai.org"
android:pathPrefix="/g/"
android:scheme="https"/>
<data
android:host="nhentai.net"
android:pathPrefix="/g/"
android:scheme="http"/>
<data
android:host="nhentai.net"
android:pathPrefix="/g/"
android:scheme="https"/>
</intent-filter>
</activity>

View File

@ -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()
}
}
}
}
}

View File

@ -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)

View File

@ -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<Source> {
val exSrcs = mutableListOf(
EHentai(EH_SOURCE_ID, false, context),
EHentaiMetadata(EH_METADATA_SOURCE_ID, false, context)
val exSrcs = mutableListOf<HttpSource>(
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")

View File

@ -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()
/**
@ -188,10 +185,20 @@ class EHentai(override val id: Long,
/**
* Parse gallery page to metadata model
*/
override fun mangaDetailsParse(response: Response) = with(response.asJsoup()) {
val metdata = ExGalleryMetadata()
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)
val metdata = (realm.loadEh(gId, gToken, exh)
?: realm.createUUIDObj(ExGalleryMetadata::class.java))
with(metdata) {
url = response.request().url().encodedPath()
this.url = url
this.gId = gId
this.gToken = gToken
exh = this@EHentai.exh
title = select("#gn").text().nullIfBlank()?.trim()
@ -200,7 +207,6 @@ class EHentai(override val id: Long,
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('/')
uploader = select("#gdn").text().nullIfBlank()?.trim()
@ -254,20 +260,15 @@ class EHentai(override val id: Long,
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.addAll(it.select("div").map {
Tag(namespace, it.text().trim(), it.hasClass("gtl"))
})
}
tags.put(namespace, ArrayList(currentTags))
}
//Save metadata
metadataHelper.writeGallery(this, id)
//Copy metadata to manga
SManga.create().let {
copyTo(it)
it
SManga.create().apply {
copyTo(this)
}
}
}
}

View File

@ -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<List<SChapter>>
= 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<ExGalleryMetadata>.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"
}

View File

@ -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,7 +113,13 @@ class NHentai(context: Context) : HttpSource() {
return MangasPage(emptyList(), false)
}
fun rawParseGallery(obj: JsonObject) = NHentaiMetadata().apply {
fun rawParseGallery(obj: JsonObject) = realmTrans { realm ->
val nhId = obj.get("id").asLong
(realm.loadNhentai(nhId)
?: realm.createUUIDObj(NHentaiMetadata::class.java)).apply {
this.nhId = nhId
uploadDate = obj.get("upload_date")?.notNull()?.long
favoritesCount = obj.get("num_favorites")?.notNull()?.long
@ -125,7 +136,9 @@ class NHentai(context: Context) : HttpSource() {
coverImageType = it.get("cover")?.get("t")?.notNull()?.asString
it.get("pages")?.asJsonArray?.map {
it?.asJsonObject?.get("t")?.notNull()?.asString
}?.filterNotNull()?.let {
}?.filterNotNull()?.map {
PageImageType(it)
}?.let {
pageImageTypes.clear()
pageImageTypes.addAll(it)
}
@ -134,8 +147,6 @@ class NHentai(context: Context) : HttpSource() {
scanlator = obj.get("scanlator")?.notNull()?.asString
id = obj.get("id")?.asLong
obj.get("tags")?.asJsonArray?.map {
val asObj = it.asJsonObject
Pair(asObj.get("type")?.string, asObj.get("name")?.string)
@ -143,27 +154,32 @@ class NHentai(context: Context) : HttpSource() {
tags.clear()
}?.forEach {
if(it.first != null && it.second != null)
tags.getOrPut(it.first!!, { ArrayList() }).add(Tag(it.second!!, false))
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))
defRealm { realm ->
realm.loadNhentaiAsync(NHentaiMetadata.nhIdFromUrl(url))
.flatMap {
if(it == null)
client.newCall(urlToDetailsRequest(url))
.asObservableSuccess()
.map {
rawParseGallery(jsonParser.parse(it.body()!!.string()).asJsonObject)
}.toBlocking().first()
}!!
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,10 +247,6 @@ class NHentai(context: Context) : HttpSource() {
val jsonParser by lazy {
JsonParser()
}
val metadataHelper by lazy {
MetadataHelper()
}
}
fun JsonElement.notNull() =

View File

@ -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,9 +100,12 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou
}
override fun mangaDetailsParse(document: Document): SManga {
val metadata = PervEdenGalleryMetadata()
realmTrans { realm ->
val url = document.location()
val metadata = (realm.loadPervEden(PervEdenGalleryMetadata.pvIdFromUrl(url), id)
?: realm.createUUIDObj(PervEdenGalleryMetadata::class.java))
with(metadata) {
url = document.location()
this.url = url
lang = this@PervEden.lang
@ -122,22 +126,18 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou
if(it is TextNode) {
val text = it.text().trim()
if(!text.isBlank())
altTitles.add(text)
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))
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))
tags.add(Tag("genre", it.text().toLowerCase(), false))
}
"Type" -> {
if(it is TextNode) {
@ -159,15 +159,12 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou
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)
}
}
}
}
override fun latestUpdatesRequest(page: Int): Request {
val num = if(lang == "en") "0"

View File

@ -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<LibraryItem> = 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)
}
}
}

View File

@ -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)
}

View File

@ -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<LibraryHolder>(), 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<LibraryHolder>(), 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)
}

View File

@ -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)
}
}

View File

@ -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) {
defRealm { realm ->
when (source) {
EH_SOURCE_ID, EXH_SOURCE_ID ->
metadataHelper.fetchEhMetadata(realUrl, isExSource(source))?.copyTo(manga)
realm.loadEh(ExGalleryMetadata.galleryId(realUrl),
ExGalleryMetadata.galleryToken(realUrl),
isExSource(source))?.copyTo(manga)
NHENTAI_SOURCE_ID ->
metadataHelper.fetchNhentaiMetadata(realUrl)?.copyTo(manga)
realm.loadNhentai(NHentaiMetadata.nhIdFromUrl(realUrl))
?.copyTo(manga)
else -> return GalleryAddEvent.Fail.UnknownType(url)
}
}
if (fav) manga.favorite = true

View File

@ -1,5 +0,0 @@
package exh
import ru.lanwen.verbalregex.VerbalExpression
fun VerbalExpression.Builder.anyChar() = add(".")!!

View File

@ -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<ExGalleryMetadata>? = 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<ExGalleryMetadata>? = 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<ExGalleryMetadata>(it.galleryUniqueIdentifier())
}
fun Realm.loadEh(gId: String, gToken: String, exh: Boolean): ExGalleryMetadata?
= ehMetadataQuery(gId, gToken, exh)
.findFirst()
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<PervEdenGalleryMetadata>(it.galleryUniqueIdentifier())
}
fun Realm.loadEhAsync(gId: String, gToken: String, exh: Boolean): Observable<ExGalleryMetadata?>
= ehMetadataQuery(gId, gToken, exh)
.findFirstAsync()
.asObservable()
fun fetchNhentaiMetadata(url: String) = NHentaiMetadata().let {
it.url = url
nhentaiGalleryBook().read<NHentaiMetadata>(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<ExGalleryMetadata>(it)
}
fun exGalleryBook() = Paper.book("gallery-ex")!!
fun pervEdenGalleryBook() = Paper.book("gallery-perveden")!!
fun nhentaiGalleryBook() = Paper.book("gallery-nhentai")!!
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<PervEdenGalleryMetadata>?) =
pervEdenMetadataQuery(
PervEdenGalleryMetadata.pvIdFromUrl(url),
source,
meta
)
fun Realm.pervEdenMetadataQuery(pvId: String,
source: Long,
meta: RealmQuery<PervEdenGalleryMetadata>? = 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<PervEdenGalleryMetadata?>
= pervEdenMetadataQuery(pvId, source)
.findFirstAsync()
.asObservable()
fun Realm.nhentaiMetaQueryFromUrl(url: String,
meta: RealmQuery<NHentaiMetadata>?) =
nhentaiMetadataQuery(
NHentaiMetadata.nhIdFromUrl(url),
meta
)
fun Realm.nhentaiMetadataQuery(nhId: Long,
meta: RealmQuery<NHentaiMetadata>? = 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<NHentaiMetadata?>
= nhentaiMetadataQuery(nhId)
.findFirstAsync()
.asObservable()
fun Realm.loadAllMetadata(): Map<KClass<out SearchableGalleryMetadata>, RealmResults<out SearchableGalleryMetadata>> =
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<out SearchableGalleryMetadata>? = null): RealmQuery<out SearchableGalleryMetadata> =
when(manga.source) {
EH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, false, meta as? RealmQuery<ExGalleryMetadata>)
EXH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, true, meta as? RealmQuery<ExGalleryMetadata>)
PERV_EDEN_EN_SOURCE_ID,
PERV_EDEN_IT_SOURCE_ID ->
pervEdenMetaQueryFromUrl(manga.url, manga.source, meta as? RealmQuery<PervEdenGalleryMetadata>)
NHENTAI_SOURCE_ID -> nhentaiMetaQueryFromUrl(manga.url, meta as? RealmQuery<NHentaiMetadata>)
else -> throw IllegalArgumentException("Unknown source type!")
}

View File

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

View File

@ -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<Tag> = 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
)
companion object {
private fun splitGalleryUrl(url: String)
= url.let {
Uri.parse(it).pathSegments
.filterNot(String::isNullOrBlank)
}
fun galleryId() = splitGalleryUrl()?.let { it[it.size - 2] }
fun galleryId(url: String) = splitGalleryUrl(url).let { it[it.size - 2] }
fun galleryToken() =
splitGalleryUrl()?.last()
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()
}
}

View File

@ -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<String> = mutableListOf()
var pageImageTypes: RealmList<PageImageType> = RealmList()
var thumbnailImageType: String? = null
var scanlator: String? = null
override fun galleryUniqueIdentifier(): String? = "NHENTAI-$id"
override var tags: RealmList<Tag> = 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)"
}

View File

@ -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<String> = mutableListOf()
var altTitles: RealmList<PervEdenTitle> = 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<Tag> = RealmList()
private fun splitGalleryUrl()
= url?.let {
override fun getTitles() = listOf(title).plus(altTitles.map {
it.title
}).filterNotNull()
@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)
}
override fun galleryUniqueIdentifier() = splitGalleryUrl()?.let {
"PERVEDEN-${lang?.toUpperCase()}-${it.last()}"
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)"
}

View File

@ -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<String, ArrayList<Tag>> = HashMap()
var tags: RealmList<Tag>
abstract fun galleryUniqueIdentifier(): String?
fun getTitles(): List<String>
abstract fun getTitles(): List<String>
val titleFields: List<String>
}

View File

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

View File

@ -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<String, List<QueryComponent>>()
fun matches(metadata: SearchableGalleryMetadata, query: List<QueryComponent>): Boolean {
fun filterResults(metadata: RealmResults<out SearchableGalleryMetadata>, query: List<QueryComponent>):
RealmResults<out SearchableGalleryMetadata> {
val first = metadata.firstOrNull() ?: return metadata
val rQuery = metadata.where()//.beginLog(SearchableGalleryMetadata::class.java)
var queryEmpty = true
fun matchTagList(tags: Sequence<Tag>,
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, {

View File

@ -1,36 +1,51 @@
package exh.search
import exh.anyChar
import ru.lanwen.verbalregex.VerbalExpression
import exh.plusAssign
class Text: QueryComponent() {
val components = mutableListOf<TextComponent>()
private var regex: VerbalExpression? = null
private var lenientRegex: VerbalExpression? = null
private var query: String? = null
private var lenientTitleQuery: String? = null
private var lenientTagQueries: List<String>? = 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<String> {
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

View File

@ -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,13 +40,14 @@ class MetadataFetchDialog {
.show()
thread {
defRealm { realm ->
db.deleteMangasNotInLibrary().executeAsBlocking()
val libraryMangas = db.getLibraryMangas()
.executeAsBlocking()
.filter {
isLewdSource(it.source)
&& metadataHelper.fetchMetadata(it.url, it.source) == null
&& realm.queryMetadataFromManga(it).findFirst() == null
}
context.runOnUiThread {
@ -65,9 +64,9 @@ class MetadataFetchDialog {
val source = sourceManager.get(manga.source)
source?.let {
manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
metadataHelper.fetchMetadata(manga.url, manga.source)?.genericCopyTo(manga)
realm.queryMetadataFromManga(manga).findFirst()?.genericCopyTo(manga)
}
} catch(t: Throwable) {
} catch (t: Throwable) {
Timber.e(t, "Could not migrate manga!")
}
}
@ -82,6 +81,7 @@ class MetadataFetchDialog {
}
}
}
}
fun askMigration(activity: Activity) {
var extra = ""
@ -106,7 +106,7 @@ class MetadataFetchDialog {
.cancelable(false)
.canceledOnTouchOutside(false)
.dismissListener {
preferenceHelper.migrateLibraryAsked().set(true)
preferenceHelper.migrateLibraryAsked2().set(true)
}.show()
}
}

View File

@ -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,22 +38,22 @@ class UrlMigrator {
//Sort possible dups so we can use binary search on it
possibleDups.sortBy { it.url }
realmTrans { realm ->
badMangas.forEach { manga ->
//Build fixed URL
val urlWithSlash = "/" + manga.url
//Fix metadata if required
val metadata = metadataHelper.fetchEhMetadata(manga.url, isExSource(manga.source))
val metadata = realm.ehMetaQueryFromUrl(manga.url, isExSource(manga.source)).findFirst()
metadata?.url?.let {
if(it.startsWith("g/")) { //Check if metadata URL has no slash
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
}
}
//If we have a dup (with the fixed url), use the dup instead
val possibleDup = possibleDups.binarySearchBy(urlWithSlash, selector = { it.url })
if(possibleDup >= 0) {
if (possibleDup >= 0) {
//Make sure it is favorited if we are
if(manga.favorite) {
if (manga.favorite) {
val dup = possibleDups[possibleDup]
dup.favorite = true
db.insertManga(dup).executeAsBlocking() //Update DB with changes
@ -69,6 +68,7 @@ class UrlMigrator {
}
}
}
}
fun tryMigration() {
if(!prefs.hasPerformedURLMigration().getOrDefault()) {

View File

@ -0,0 +1,550 @@
package exh.util
import io.realm.*
import java.util.*
/**
* Realm query with logging
*
* @author nulldev
*/
inline fun <reified E : RealmModel> RealmQuery<out E>.beginLog(clazz: Class<out E>? =
E::class.java): LoggingRealmQuery<out E>
= LoggingRealmQuery.fromQuery(this, clazz)
class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
companion object {
fun <E : RealmModel> fromQuery(q: RealmQuery<out E>, clazz: Class<out E>?)
= LoggingRealmQuery(q).apply {
log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE"
}
}
private val log = mutableListOf<String>()
private fun sec(section: String) = "{$section}"
fun log() = log.joinToString(separator = " ")
fun isValid(): Boolean {
return query.isValid
}
fun isNull(fieldName: String): RealmQuery<E> {
log += sec("\"$fieldName\" IS NULL")
return query.isNull(fieldName)
}
fun isNotNull(fieldName: String): RealmQuery<E> {
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<E> {
appendEqualTo(fieldName, value)
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: String, casing: Case): RealmQuery<E> {
appendEqualTo(fieldName, value, casing)
return query.equalTo(fieldName, value, casing)
}
fun equalTo(fieldName: String, value: Byte?): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: ByteArray): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: Short?): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: Int?): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: Long?): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: Double?): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: Float?): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: Boolean?): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun equalTo(fieldName: String, value: Date): RealmQuery<E> {
appendEqualTo(fieldName, value.toString())
return query.equalTo(fieldName, value)
}
fun appendIn(fieldName: String, values: Array<out Any?>, casing: Case? = null) {
log += sec("[${values.joinToString(separator = ", ", transform = {
"\"$it\""
})}] IN \"$fieldName\"" + (casing?.let {
" CASE ${casing.name}"
} ?: ""))
}
fun `in`(fieldName: String, values: Array<String>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<String>, casing: Case): RealmQuery<E> {
appendIn(fieldName, values, casing)
return query.`in`(fieldName, values, casing)
}
fun `in`(fieldName: String, values: Array<Byte>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<Short>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<Int>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<Long>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<Double>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<Float>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<Boolean>): RealmQuery<E> {
appendIn(fieldName, values)
return query.`in`(fieldName, values)
}
fun `in`(fieldName: String, values: Array<Date>): RealmQuery<E> {
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<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: String, casing: Case): RealmQuery<E> {
appendNotEqualTo(fieldName, value, casing)
return query.notEqualTo(fieldName, value, casing)
}
fun notEqualTo(fieldName: String, value: Byte?): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: ByteArray): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: Short?): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: Int?): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: Long?): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: Double?): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: Float?): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: Boolean?): RealmQuery<E> {
appendNotEqualTo(fieldName, value)
return query.notEqualTo(fieldName, value)
}
fun notEqualTo(fieldName: String, value: Date): RealmQuery<E> {
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<E> {
appendGreaterThan(fieldName, value)
return query.greaterThan(fieldName, value)
}
fun greaterThan(fieldName: String, value: Long): RealmQuery<E> {
appendGreaterThan(fieldName, value)
return query.greaterThan(fieldName, value)
}
fun greaterThan(fieldName: String, value: Double): RealmQuery<E> {
appendGreaterThan(fieldName, value)
return query.greaterThan(fieldName, value)
}
fun greaterThan(fieldName: String, value: Float): RealmQuery<E> {
appendGreaterThan(fieldName, value)
return query.greaterThan(fieldName, value)
}
fun greaterThan(fieldName: String, value: Date): RealmQuery<E> {
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<E> {
appendGreaterThanOrEqualTo(fieldName, value)
return query.greaterThanOrEqualTo(fieldName, value)
}
fun greaterThanOrEqualTo(fieldName: String, value: Long): RealmQuery<E> {
appendGreaterThanOrEqualTo(fieldName, value)
return query.greaterThanOrEqualTo(fieldName, value)
}
fun greaterThanOrEqualTo(fieldName: String, value: Double): RealmQuery<E> {
appendGreaterThanOrEqualTo(fieldName, value)
return query.greaterThanOrEqualTo(fieldName, value)
}
fun greaterThanOrEqualTo(fieldName: String, value: Float): RealmQuery<E> {
appendGreaterThanOrEqualTo(fieldName, value)
return query.greaterThanOrEqualTo(fieldName, value)
}
fun greaterThanOrEqualTo(fieldName: String, value: Date): RealmQuery<E> {
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<E> {
appendLessThan(fieldName, value)
return query.lessThan(fieldName, value)
}
fun lessThan(fieldName: String, value: Long): RealmQuery<E> {
appendLessThan(fieldName, value)
return query.lessThan(fieldName, value)
}
fun lessThan(fieldName: String, value: Double): RealmQuery<E> {
appendLessThan(fieldName, value)
return query.lessThan(fieldName, value)
}
fun lessThan(fieldName: String, value: Float): RealmQuery<E> {
appendLessThan(fieldName, value)
return query.lessThan(fieldName, value)
}
fun lessThan(fieldName: String, value: Date): RealmQuery<E> {
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<E> {
appendLessThanOrEqualTo(fieldName, value)
return query.lessThanOrEqualTo(fieldName, value)
}
fun lessThanOrEqualTo(fieldName: String, value: Long): RealmQuery<E> {
appendLessThanOrEqualTo(fieldName, value)
return query.lessThanOrEqualTo(fieldName, value)
}
fun lessThanOrEqualTo(fieldName: String, value: Double): RealmQuery<E> {
appendLessThanOrEqualTo(fieldName, value)
return query.lessThanOrEqualTo(fieldName, value)
}
fun lessThanOrEqualTo(fieldName: String, value: Float): RealmQuery<E> {
appendLessThanOrEqualTo(fieldName, value)
return query.lessThanOrEqualTo(fieldName, value)
}
fun lessThanOrEqualTo(fieldName: String, value: Date): RealmQuery<E> {
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<E> {
appendBetween(fieldName, from, to)
return query.between(fieldName, from, to)
}
fun between(fieldName: String, from: Long, to: Long): RealmQuery<E> {
appendBetween(fieldName, from, to)
return query.between(fieldName, from, to)
}
fun between(fieldName: String, from: Double, to: Double): RealmQuery<E> {
appendBetween(fieldName, from, to)
return query.between(fieldName, from, to)
}
fun between(fieldName: String, from: Float, to: Float): RealmQuery<E> {
appendBetween(fieldName, from, to)
return query.between(fieldName, from, to)
}
fun between(fieldName: String, from: Date, to: Date): RealmQuery<E> {
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<E> {
appendContains(fieldName, value)
return query.contains(fieldName, value)
}
fun contains(fieldName: String, value: String, casing: Case): RealmQuery<E> {
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<E> {
appendBeginsWith(fieldName, value)
return query.beginsWith(fieldName, value)
}
fun beginsWith(fieldName: String, value: String, casing: Case): RealmQuery<E> {
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<E> {
appendEndsWith(fieldName, value)
return query.endsWith(fieldName, value)
}
fun endsWith(fieldName: String, value: String, casing: Case): RealmQuery<E> {
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<E> {
appendLike(fieldName, value)
return query.like(fieldName, value)
}
fun like(fieldName: String, value: String, casing: Case): RealmQuery<E> {
appendLike(fieldName, value, casing)
return query.like(fieldName, value, casing)
}
fun beginGroup(): RealmQuery<E> {
log += "("
return query.beginGroup()
}
fun endGroup(): RealmQuery<E> {
log += ")"
return query.endGroup()
}
fun or(): RealmQuery<E> {
log += "OR"
return query.or()
}
operator fun not(): RealmQuery<E> {
log += "NOT"
return query.not()
}
fun isEmpty(fieldName: String): RealmQuery<E> {
log += "\"$fieldName\" IS EMPTY"
return query.isEmpty(fieldName)
}
fun isNotEmpty(fieldName: String): RealmQuery<E> {
log += "\"$fieldName\" IS NOT EMPTY"
return query.isNotEmpty(fieldName)
}
fun distinct(fieldName: String): RealmResults<E> {
return query.distinct(fieldName)
}
fun distinctAsync(fieldName: String): RealmResults<E> {
return query.distinctAsync(fieldName)
}
fun distinct(firstFieldName: String, vararg remainingFieldNames: String): RealmResults<E> {
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<E> {
return query.findAll()
}
fun findAllAsync(): RealmResults<E> {
return query.findAllAsync()
}
fun findAllSorted(fieldName: String, sortOrder: Sort): RealmResults<E> {
return query.findAllSorted(fieldName, sortOrder)
}
fun findAllSortedAsync(fieldName: String, sortOrder: Sort): RealmResults<E> {
return query.findAllSortedAsync(fieldName, sortOrder)
}
fun findAllSorted(fieldName: String): RealmResults<E> {
return query.findAllSorted(fieldName)
}
fun findAllSortedAsync(fieldName: String): RealmResults<E> {
return query.findAllSortedAsync(fieldName)
}
fun findAllSorted(fieldNames: Array<String>, sortOrders: Array<Sort>): RealmResults<E> {
return query.findAllSorted(fieldNames, sortOrders)
}
fun findAllSortedAsync(fieldNames: Array<String>, sortOrders: Array<Sort>): RealmResults<E> {
return query.findAllSortedAsync(fieldNames, sortOrders)
}
fun findAllSorted(fieldName1: String, sortOrder1: Sort, fieldName2: String, sortOrder2: Sort): RealmResults<E> {
return query.findAllSorted(fieldName1, sortOrder1, fieldName2, sortOrder2)
}
fun findAllSortedAsync(fieldName1: String, sortOrder1: Sort, fieldName2: String, sortOrder2: Sort): RealmResults<E> {
return query.findAllSortedAsync(fieldName1, sortOrder1, fieldName2, sortOrder2)
}
fun findFirst(): E {
return query.findFirst()
}
fun findFirstAsync(): E {
return query.findFirstAsync()
}
}

View File

@ -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 <T> 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 <T> defRealm(block: (Realm) -> T): T {
return Realm.getDefaultInstance().use {
block(it)
}
}
fun <T : RealmModel> Realm.createUUIDObj(clazz: Class<T>)
= createObject(clazz, UUID.randomUUID().toString())

View File

@ -1,6 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog bulletedList="true">
<changelogversion versionName="v6.1.1-EH" changeDate="">
<changelogtext>EH - Rewrite batch add screen</changelogtext>
<changelogtext>EH - Add nhentai link import support</changelogtext>
<changelogtext>EH - Add the ability to import links by searching them in the catalogues</changelogtext>
<changelogtext>EH - Increase library tag search speed</changelogtext>
<changelogtext>EH - Rewrite app lock UI</changelogtext>
<changelogtext>EH - Add fingerprint support to app lock</changelogtext>
</changelogversion>
<changelogversion versionName="v0.6.1" changeDate="">
<changelogtext>Bugfix release.</changelogtext>
</changelogversion>

View File

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