mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Migrate to realm for metadata
This commit is contained in:
		| @@ -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() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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(), | ||||
|   | ||||
| @@ -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) | ||||
|                 } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|         } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
|   | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1,5 +0,0 @@ | ||||
| package exh | ||||
|  | ||||
| import ru.lanwen.verbalregex.VerbalExpression | ||||
|  | ||||
| fun VerbalExpression.Builder.anyChar() = add(".")!! | ||||
| @@ -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 Realm.loadEhAsync(gId: String, gToken: String, exh: Boolean): Observable<ExGalleryMetadata?> | ||||
|         = 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<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!") | ||||
|     } | ||||
|  | ||||
|     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 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")!! | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
|     ) | ||||
|  | ||||
|     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() | ||||
|     } | ||||
| } | ||||
| @@ -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)" | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|         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)" | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
| } | ||||
| @@ -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)" | ||||
| } | ||||
|   | ||||
| @@ -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, { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										550
									
								
								app/src/main/java/exh/util/LoggingRealmQuery.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										550
									
								
								app/src/main/java/exh/util/LoggingRealmQuery.kt
									
									
									
									
									
										Normal 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() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										39
									
								
								app/src/main/java/exh/util/RealmUtil.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/src/main/java/exh/util/RealmUtil.kt
									
									
									
									
									
										Normal 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()) | ||||
		Reference in New Issue
	
	Block a user